mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
118 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2afd02d86f | ||
|
|
6de88bfe11 | ||
|
|
6d7236249f | ||
|
|
69fbaba8a6 | ||
|
|
6d88737505 | ||
|
|
9869cfc736 | ||
|
|
8505326a68 | ||
|
|
3a4af88384 | ||
|
|
5b2e7d0f70 | ||
|
|
ddeea6bee3 | ||
|
|
0cfe3a7634 | ||
|
|
727463e4d1 | ||
|
|
d42abfdc56 | ||
|
|
e01ea1df4c | ||
|
|
078bfac5f5 | ||
|
|
111b07b9e6 | ||
|
|
dfbc89addc | ||
|
|
bf44da9a14 | ||
|
|
d75d13965b | ||
|
|
8aedebdc94 | ||
|
|
9388c4bb0d | ||
|
|
77d4f601af | ||
|
|
7fae590848 | ||
|
|
bc41558a26 | ||
|
|
f6651face4 | ||
|
|
345f00f7f2 | ||
|
|
e876d02118 | ||
|
|
5b7018f71b | ||
|
|
f45b3fc50a | ||
|
|
01196be30d | ||
|
|
0a2999bffb | ||
|
|
8f097096e7 | ||
|
|
cd97fc046a | ||
|
|
eeb10f31a6 | ||
|
|
9df5e116e8 | ||
|
|
1228a03d39 | ||
|
|
a5e1b3096e | ||
|
|
b41ae67128 | ||
|
|
ddfbe20125 | ||
|
|
0bfe9291dd | ||
|
|
622b2e1edc | ||
|
|
4a40719534 | ||
|
|
384993d363 | ||
|
|
01b7d28154 | ||
|
|
d7c4f5577f | ||
|
|
a69d23ca64 | ||
|
|
e2f8b7a6e3 | ||
|
|
171a0b012f | ||
|
|
5c04b15433 | ||
|
|
6397feffff | ||
|
|
e73b9b7f1c | ||
|
|
0d82e40c67 | ||
|
|
b75d6d02fa | ||
|
|
76d4542716 | ||
|
|
87955de849 | ||
|
|
6df60cf5da | ||
|
|
3c23a314f0 | ||
|
|
8fda6b04a4 | ||
|
|
9fa98e6b76 | ||
|
|
deb685f39b | ||
|
|
d7851d3a18 | ||
|
|
44946fc54a | ||
|
|
a033d10adc | ||
|
|
5a3e599fe0 | ||
|
|
a9f645f389 | ||
|
|
d662f0903a | ||
|
|
beaa947eb7 | ||
|
|
48006b64d6 | ||
|
|
8f195ba66f | ||
|
|
123288e745 | ||
|
|
5866e95d49 | ||
|
|
e79f395424 | ||
|
|
999ca87fec | ||
|
|
1217266d88 | ||
|
|
bb262198be | ||
|
|
11aae77caf | ||
|
|
8212cede6e | ||
|
|
a3c51884f4 | ||
|
|
b8890aca7f | ||
|
|
014b0cce14 | ||
|
|
6d860c5cb7 | ||
|
|
d8be832858 | ||
|
|
afcb9fcf41 | ||
|
|
3c7ae0aaf0 | ||
|
|
6b7f93dbfe | ||
|
|
c40b255022 | ||
|
|
1742d265f3 | ||
|
|
3240e0bcae | ||
|
|
ff185f6505 | ||
|
|
346b517c9d | ||
|
|
80f00aba0a | ||
|
|
949905f6e2 | ||
|
|
b9e26fecfd | ||
|
|
232682f4a8 | ||
|
|
de3b690d60 | ||
|
|
de69a78a98 | ||
|
|
1c341c34a3 | ||
|
|
33beb57e9d | ||
|
|
66eeadca0b | ||
|
|
a10d1c98a8 | ||
|
|
59ead4986f | ||
|
|
09f6c18189 | ||
|
|
a5cd6d5ac0 | ||
|
|
0f3ad7c8b1 | ||
|
|
0487dea7fc | ||
|
|
a6803bf0e3 | ||
|
|
8cac1ee284 | ||
|
|
196620e1bd | ||
|
|
43d6c76873 | ||
|
|
b864c39a0d | ||
|
|
818b975111 | ||
|
|
d5fbc8393f | ||
|
|
df9a71a63d | ||
|
|
7b5e9d2344 | ||
|
|
7fc2d95886 | ||
|
|
78d3b369bb | ||
|
|
bb3620680b | ||
|
|
d4a45655ca |
21
CHANGELOG
21
CHANGELOG
@@ -1,3 +1,24 @@
|
|||||||
|
KeePassDX(2.9.19)
|
||||||
|
* Fix search slowdown #964
|
||||||
|
* Fix closing notification after lock request #965
|
||||||
|
* Better temp advanced unlocking code implementation #965
|
||||||
|
* Fix OTP token generation #967
|
||||||
|
|
||||||
|
KeePassDX(2.9.18)
|
||||||
|
* Move groups #658
|
||||||
|
* Improve autofill recognition #960
|
||||||
|
* Remove diacritical marks in search string #945
|
||||||
|
* Fix search in references #962
|
||||||
|
* Fix themes in Libre version
|
||||||
|
|
||||||
|
KeePassDX(2.9.17)
|
||||||
|
* Import / Export app properties #839
|
||||||
|
* Force twofish padding compatibility #955
|
||||||
|
* Better timeout preference #579
|
||||||
|
|
||||||
|
KeePassDX(2.9.16)
|
||||||
|
* Fix small bugs #948
|
||||||
|
|
||||||
KeePassDX(2.9.15)
|
KeePassDX(2.9.15)
|
||||||
* Fix themes #935 #926
|
* Fix themes #935 #926
|
||||||
* Decrease default clipboard time #934
|
* Decrease default clipboard time #934
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 15
|
minSdkVersion 15
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode = 68
|
versionCode = 73
|
||||||
versionName = "2.9.15"
|
versionName = "2.9.19"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
@@ -109,7 +109,7 @@ dependencies {
|
|||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.biometric:biometric:1.1.0-rc01'
|
implementation 'androidx.biometric:biometric:1.1.0'
|
||||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||||
implementation "androidx.core:core-ktx:1.3.2"
|
implementation "androidx.core:core-ktx:1.3.2"
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.kunzisoft.keepass.tests.utils
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
|
import junit.framework.TestCase
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class UUIDTest: TestCase() {
|
||||||
|
|
||||||
|
fun testUUID() {
|
||||||
|
val randomUUID = UUID.randomUUID()
|
||||||
|
val hexStringUUID = UuidUtil.toHexString(randomUUID)
|
||||||
|
val retrievedUUID = UuidUtil.fromHexString(hexStringUUID)
|
||||||
|
assertEquals(randomUUID, retrievedUUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|||||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||||
@@ -93,6 +94,8 @@ class EntryActivity : LockingActivity() {
|
|||||||
private var clipboardHelper: ClipboardHelper? = null
|
private var clipboardHelper: ClipboardHelper? = null
|
||||||
private var mFirstLaunchOfActivity: Boolean = false
|
private var mFirstLaunchOfActivity: Boolean = false
|
||||||
|
|
||||||
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var iconColor: Int = 0
|
private var iconColor: Int = 0
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -140,6 +143,9 @@ class EntryActivity : LockingActivity() {
|
|||||||
clipboardHelper = ClipboardHelper(this)
|
clipboardHelper = ClipboardHelper(this)
|
||||||
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
|
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
|
||||||
|
|
||||||
|
// Init SAF manager
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
|
||||||
// Init attachment service binder manager
|
// Init attachment service binder manager
|
||||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
|
|
||||||
@@ -344,7 +350,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
|
|
||||||
// Manage attachments
|
// Manage attachments
|
||||||
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
||||||
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
mExternalFileHelper?.createDocument(attachmentItem.name)?.let { requestCode ->
|
||||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,7 +386,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
||||||
if (createdFileUri != null) {
|
if (createdFileUri != null) {
|
||||||
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
||||||
mAttachmentFileBinderManager
|
mAttachmentFileBinderManager
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import com.kunzisoft.keepass.activities.dialogs.*
|
|||||||
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
|
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
|
||||||
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
@@ -103,7 +103,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
private var lockView: View? = null
|
private var lockView: View? = null
|
||||||
|
|
||||||
// To manage attachments
|
// To manage attachments
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||||
private var mAllowMultipleAttachments: Boolean = false
|
private var mAllowMultipleAttachments: Boolean = false
|
||||||
private var mTempAttachments = ArrayList<EntryAttachmentState>()
|
private var mTempAttachments = ArrayList<EntryAttachmentState>()
|
||||||
@@ -241,7 +241,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
// To retrieve attachment
|
// To retrieve attachment
|
||||||
mSelectFileHelper = SelectFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
|
|
||||||
// Save button
|
// Save button
|
||||||
@@ -458,8 +458,8 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
/**
|
/**
|
||||||
* Add a new attachment
|
* Add a new attachment
|
||||||
*/
|
*/
|
||||||
private fun addNewAttachment(item: MenuItem) {
|
private fun addNewAttachment() {
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.onMenuItemClick(item)
|
mExternalFileHelper?.openDocument()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) {
|
override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) {
|
||||||
@@ -505,7 +505,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
entryEditFragment?.icon = icon
|
entryEditFragment?.icon = icon
|
||||||
}
|
}
|
||||||
|
|
||||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||||
uri?.let { attachmentToUploadUri ->
|
uri?.let { attachmentToUploadUri ->
|
||||||
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
||||||
documentFile.name?.let { fileName ->
|
documentFile.name?.let { fileName ->
|
||||||
@@ -655,7 +655,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
||||||
attachmentView,
|
attachmentView,
|
||||||
{
|
{
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(attachmentView)
|
mExternalFileHelper?.openDocument()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(entryEditActivityEducation)
|
performedNextEducation(entryEditActivityEducation)
|
||||||
@@ -683,7 +683,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_add_attachment -> {
|
R.id.menu_add_attachment -> {
|
||||||
addNewAttachment(item)
|
addNewAttachment()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_add_otp -> {
|
R.id.menu_add_otp -> {
|
||||||
|
|||||||
@@ -42,8 +42,9 @@ import com.google.android.material.snackbar.Snackbar
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
@@ -82,7 +83,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
|
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||||
|
|
||||||
@@ -103,14 +104,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
||||||
|
|
||||||
// Open database button
|
// Open database button
|
||||||
mSelectFileHelper = SelectFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
||||||
openDatabaseButtonView?.apply {
|
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
|
||||||
setOnClickListener(it)
|
|
||||||
setOnLongClickListener(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// History list
|
// History list
|
||||||
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
||||||
@@ -171,8 +167,6 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
||||||
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
||||||
}
|
}
|
||||||
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
|
||||||
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
|
|
||||||
}
|
}
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
||||||
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
||||||
@@ -185,10 +179,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
databaseFilesViewModel.consumeAction()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to observe database action", e)
|
Log.e(TAG, "Unable to observe database action", e)
|
||||||
}
|
}
|
||||||
databaseFilesViewModel.consumeAction()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe default database
|
// Observe default database
|
||||||
@@ -206,6 +200,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential()
|
val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential()
|
||||||
databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri)
|
databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri)
|
||||||
}
|
}
|
||||||
|
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
||||||
|
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
|
||||||
}
|
}
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
val database = Database.getInstance()
|
val database = Database.getInstance()
|
||||||
@@ -234,7 +230,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
* Create a new file by calling the content provider
|
* Create a new file by calling the content provider
|
||||||
*/
|
*/
|
||||||
private fun createNewFile() {
|
private fun createNewFile() {
|
||||||
createDocument(this, getString(R.string.database_file_name_default) +
|
mExternalFileHelper?.createDocument( getString(R.string.database_file_name_default) +
|
||||||
getString(R.string.database_file_extension_default), "application/x-keepass")
|
getString(R.string.database_file_extension_default), "application/x-keepass")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +282,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
// Show open and create button or special mode
|
// Show open and create button or special mode
|
||||||
when (mSpecialMode) {
|
when (mSpecialMode) {
|
||||||
SpecialMode.DEFAULT -> {
|
SpecialMode.DEFAULT -> {
|
||||||
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
if (ExternalFileHelper.allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
||||||
// There is an activity which can handle this intent.
|
// There is an activity which can handle this intent.
|
||||||
createDatabaseButtonView?.visibility = View.VISIBLE
|
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||||
} else{
|
} else{
|
||||||
@@ -359,14 +355,14 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
launchPasswordActivityWithPath(uri)
|
launchPasswordActivityWithPath(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the created URI from the file manager
|
// Retrieve the created URI from the file manager
|
||||||
onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
|
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
|
||||||
mDatabaseFileUri = databaseFileCreatedUri
|
mDatabaseFileUri = databaseFileCreatedUri
|
||||||
if (mDatabaseFileUri != null) {
|
if (mDatabaseFileUri != null) {
|
||||||
AssignMasterKeyDialogFragment.getInstance(true)
|
AssignMasterKeyDialogFragment.getInstance(true)
|
||||||
@@ -412,9 +408,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
openDatabaseButtonView != null
|
openDatabaseButtonView != null
|
||||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
||||||
openDatabaseButtonView!!,
|
openDatabaseButtonView!!,
|
||||||
{tapTargetView ->
|
{ tapTargetView ->
|
||||||
tapTargetView?.let {
|
tapTargetView?.let {
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
mExternalFileHelper?.openDocument()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
|
|||||||
@@ -812,74 +812,75 @@ class GroupActivity : LockingActivity(),
|
|||||||
|
|
||||||
override fun onPasteMenuClick(pasteMode: ListNodesFragment.PasteMode?,
|
override fun onPasteMenuClick(pasteMode: ListNodesFragment.PasteMode?,
|
||||||
nodes: List<Node>): Boolean {
|
nodes: List<Node>): Boolean {
|
||||||
// Move or copy only if allowed (in root if allowed)
|
when (pasteMode) {
|
||||||
if (mCurrentGroup != mDatabase?.rootGroup
|
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
|
||||||
|| mDatabase?.rootCanContainsEntry() == true) {
|
// Copy
|
||||||
|
mCurrentGroup?.let { newParent ->
|
||||||
when (pasteMode) {
|
mProgressDatabaseTaskProvider?.startDatabaseCopyNodes(
|
||||||
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
|
nodes,
|
||||||
// Copy
|
newParent,
|
||||||
mCurrentGroup?.let { newParent ->
|
!mReadOnly && mAutoSaveEnable
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseCopyNodes(
|
)
|
||||||
nodes,
|
|
||||||
newParent,
|
|
||||||
!mReadOnly && mAutoSaveEnable
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
|
|
||||||
// Move
|
|
||||||
mCurrentGroup?.let { newParent ->
|
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseMoveNodes(
|
|
||||||
nodes,
|
|
||||||
newParent,
|
|
||||||
!mReadOnly && mAutoSaveEnable
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
|
||||||
coordinatorLayout?.let { coordinatorLayout ->
|
// Move
|
||||||
Snackbar.make(coordinatorLayout,
|
mCurrentGroup?.let { newParent ->
|
||||||
R.string.error_copy_entry_here,
|
mProgressDatabaseTaskProvider?.startDatabaseMoveNodes(
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
nodes,
|
||||||
|
newParent,
|
||||||
|
!mReadOnly && mAutoSaveEnable
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
finishNodeAction()
|
finishNodeAction()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun eachNodeRecyclable(nodes: List<Node>): Boolean {
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
return nodes.find { node ->
|
||||||
|
var cannotRecycle = true
|
||||||
|
if (node is Entry) {
|
||||||
|
cannotRecycle = !database.canRecycle(node)
|
||||||
|
} else if (node is Group) {
|
||||||
|
cannotRecycle = !database.canRecycle(node)
|
||||||
|
}
|
||||||
|
cannotRecycle
|
||||||
|
} == null
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false): Boolean {
|
private fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false): Boolean {
|
||||||
val database = mDatabase
|
mDatabase?.let { database ->
|
||||||
|
|
||||||
// If recycle bin enabled, ensure it exists
|
// If recycle bin enabled, ensure it exists
|
||||||
if (database != null && database.isRecycleBinEnabled) {
|
if (database.isRecycleBinEnabled) {
|
||||||
database.ensureRecycleBinExists(resources)
|
database.ensureRecycleBinExists(resources)
|
||||||
}
|
|
||||||
|
|
||||||
// If recycle bin enabled and not in recycle bin, move in recycle bin
|
|
||||||
if (database != null
|
|
||||||
&& database.isRecycleBinEnabled
|
|
||||||
&& database.recycleBin != mCurrentGroup) {
|
|
||||||
|
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
|
|
||||||
nodes,
|
|
||||||
!mReadOnly && mAutoSaveEnable
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// else open the dialog to confirm deletion
|
|
||||||
else {
|
|
||||||
val deleteNodesDialogFragment: DeleteNodesDialogFragment =
|
|
||||||
if (recycleBin) {
|
|
||||||
EmptyRecycleBinDialogFragment.getInstance(nodes)
|
|
||||||
} else {
|
|
||||||
DeleteNodesDialogFragment.getInstance(nodes)
|
|
||||||
}
|
}
|
||||||
deleteNodesDialogFragment.show(supportFragmentManager, "deleteNodesDialogFragment")
|
|
||||||
|
// If recycle bin enabled and not in recycle bin, move in recycle bin
|
||||||
|
if (eachNodeRecyclable(nodes)) {
|
||||||
|
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
|
||||||
|
nodes,
|
||||||
|
!mReadOnly && mAutoSaveEnable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// else open the dialog to confirm deletion
|
||||||
|
else {
|
||||||
|
val deleteNodesDialogFragment: DeleteNodesDialogFragment =
|
||||||
|
if (recycleBin) {
|
||||||
|
EmptyRecycleBinDialogFragment.getInstance(nodes)
|
||||||
|
} else {
|
||||||
|
DeleteNodesDialogFragment.getInstance(nodes)
|
||||||
|
}
|
||||||
|
deleteNodesDialogFragment.show(supportFragmentManager, "deleteNodesDialogFragment")
|
||||||
|
}
|
||||||
|
finishNodeAction()
|
||||||
}
|
}
|
||||||
finishNodeAction()
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1076,6 +1077,16 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isValidGroupName(name: String): GroupEditDialogFragment.Error {
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
return GroupEditDialogFragment.Error(true, R.string.error_no_name)
|
||||||
|
}
|
||||||
|
if (mDatabase?.groupNamesNotAllowed?.find { it.equals(name, ignoreCase = true) } != null) {
|
||||||
|
return GroupEditDialogFragment.Error(true, R.string.error_word_reserved)
|
||||||
|
}
|
||||||
|
return GroupEditDialogFragment.Error(false, null)
|
||||||
|
}
|
||||||
|
|
||||||
override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction,
|
override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction,
|
||||||
groupInfo: GroupInfo) {
|
groupInfo: GroupInfo) {
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ import androidx.fragment.app.commit
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
|
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
@@ -66,7 +67,7 @@ class IconPickerActivity : LockingActivity() {
|
|||||||
|
|
||||||
private var mDatabase: Database? = null
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -84,15 +85,11 @@ class IconPickerActivity : LockingActivity() {
|
|||||||
|
|
||||||
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
||||||
|
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
|
||||||
uploadButton = findViewById(R.id.icon_picker_upload)
|
uploadButton = findViewById(R.id.icon_picker_upload)
|
||||||
if (mDatabase?.allowCustomIcons == true) {
|
if (mDatabase?.allowCustomIcons == true) {
|
||||||
uploadButton.setOnClickListener {
|
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
|
||||||
}
|
|
||||||
uploadButton.setOnLongClickListener {
|
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.onLongClick(it)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
uploadButton.visibility = View.GONE
|
uploadButton.visibility = View.GONE
|
||||||
}
|
}
|
||||||
@@ -124,8 +121,6 @@ class IconPickerActivity : LockingActivity() {
|
|||||||
// Focus view to reinitialize timeout
|
// Focus view to reinitialize timeout
|
||||||
findViewById<ViewGroup>(R.id.icon_picker_container)?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
findViewById<ViewGroup>(R.id.icon_picker_container)?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
||||||
|
|
||||||
mSelectFileHelper = SelectFileHelper(this)
|
|
||||||
|
|
||||||
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
|
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
|
||||||
mIconImage.standard = iconStandard
|
mIconImage.standard = iconStandard
|
||||||
// Remove the custom icon if a standard one is selected
|
// Remove the custom icon if a standard one is selected
|
||||||
@@ -281,7 +276,7 @@ class IconPickerActivity : LockingActivity() {
|
|||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||||
addCustomIcon(uri)
|
addCustomIcon(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,10 +42,7 @@ import androidx.fragment.app.commit
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.*
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
@@ -95,7 +92,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
private var mDatabaseKeyFileUri: Uri? = null
|
private var mDatabaseKeyFileUri: Uri? = null
|
||||||
|
|
||||||
private var mRememberKeyFile: Boolean = false
|
private var mRememberKeyFile: Boolean = false
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mPermissionAsked = false
|
private var mPermissionAsked = false
|
||||||
private var readOnly: Boolean = false
|
private var readOnly: Boolean = false
|
||||||
@@ -138,13 +135,8 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
|
|
||||||
mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
|
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
|
||||||
keyFileSelectionView?.apply {
|
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
|
||||||
setOnClickListener(it)
|
|
||||||
setOnLongClickListener(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
||||||
passwordView?.addTextChangedListener(object : TextWatcher {
|
passwordView?.addTextChangedListener(object : TextWatcher {
|
||||||
@@ -702,8 +694,8 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
}
|
}
|
||||||
|
|
||||||
var keyFileResult = false
|
var keyFileResult = false
|
||||||
mSelectFileHelper?.let {
|
mExternalFileHelper?.let {
|
||||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data
|
||||||
) { uri ->
|
) { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
mDatabaseKeyFileUri = uri
|
mDatabaseKeyFileUri = uri
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ import android.text.SpannableStringBuilder
|
|||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
@@ -60,7 +60,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
private var mListener: AssignPasswordDialogListener? = null
|
private var mListener: AssignPasswordDialogListener? = null
|
||||||
|
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
||||||
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||||
@@ -133,11 +133,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||||
|
|
||||||
mSelectFileHelper = SelectFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
keyFileSelectionView?.apply {
|
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
|
||||||
setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
val dialog = builder.create()
|
val dialog = builder.create()
|
||||||
|
|
||||||
@@ -289,7 +286,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||||
uri?.let { pathUri ->
|
uri?.let { pathUri ->
|
||||||
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||||
keyFileSelectionView?.error = null
|
keyFileSelectionView?.error = null
|
||||||
|
|||||||
@@ -219,14 +219,19 @@ class GroupEditDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isValid(): Boolean {
|
private fun isValid(): Boolean {
|
||||||
if (nameTextView.text.toString().isEmpty()) {
|
val error = mEditGroupListener?.isValidGroupName(nameTextView.text.toString()) ?: Error(false, null)
|
||||||
nameTextLayoutView.error = getString(R.string.error_no_name)
|
error.messageId?.let { messageId ->
|
||||||
return false
|
nameTextLayoutView.error = getString(messageId)
|
||||||
|
} ?: kotlin.run {
|
||||||
|
nameTextLayoutView.error = null
|
||||||
}
|
}
|
||||||
return true
|
return !error.isError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class Error(val isError: Boolean, val messageId: Int?)
|
||||||
|
|
||||||
interface EditGroupListener {
|
interface EditGroupListener {
|
||||||
|
fun isValidGroupName(name: String): Error
|
||||||
fun approveEditGroup(action: EditGroupDialogAction,
|
fun approveEditGroup(action: EditGroupDialogAction,
|
||||||
groupInfo: GroupInfo)
|
groupInfo: GroupInfo)
|
||||||
fun cancelEditGroup(action: EditGroupDialogAction,
|
fun cancelEditGroup(action: EditGroupDialogAction,
|
||||||
|
|||||||
@@ -311,13 +311,17 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
menu?.removeItem(R.id.menu_edit)
|
menu?.removeItem(R.id.menu_edit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy and Move (not for groups)
|
// Move
|
||||||
|
if (readOnly
|
||||||
|
|| isASearchResult) {
|
||||||
|
menu?.removeItem(R.id.menu_move)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy (not allowed for group)
|
||||||
if (readOnly
|
if (readOnly
|
||||||
|| isASearchResult
|
|| isASearchResult
|
||||||
|| nodes.any { it.type == Type.GROUP }) {
|
|| nodes.any { it.type == Type.GROUP }) {
|
||||||
// TODO Copy For Group
|
|
||||||
menu?.removeItem(R.id.menu_copy)
|
menu?.removeItem(R.id.menu_copy)
|
||||||
menu?.removeItem(R.id.menu_move)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deletion
|
// Deletion
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 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.helpers
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
|
class ExternalFileHelper {
|
||||||
|
|
||||||
|
private var activity: FragmentActivity? = null
|
||||||
|
private var fragment: Fragment? = null
|
||||||
|
|
||||||
|
constructor(context: FragmentActivity) {
|
||||||
|
this.activity = context
|
||||||
|
this.fragment = null
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Fragment) {
|
||||||
|
this.activity = context.activity
|
||||||
|
this.fragment = context
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openDocument(getContent: Boolean = false,
|
||||||
|
typeString: String = "*/*") {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
try {
|
||||||
|
if (getContent) {
|
||||||
|
openActivityWithActionGetContent(typeString)
|
||||||
|
} else {
|
||||||
|
openActivityWithActionOpenDocument(typeString)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to open document", e)
|
||||||
|
showFileManagerDialogFragment()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showFileManagerDialogFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||||
|
private fun openActivityWithActionOpenDocument(typeString: String) {
|
||||||
|
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = typeString
|
||||||
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
if (fragment != null)
|
||||||
|
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||||
|
else
|
||||||
|
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||||
|
private fun openActivityWithActionGetContent(typeString: String) {
|
||||||
|
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = typeString
|
||||||
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
if (fragment != null)
|
||||||
|
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||||
|
else
|
||||||
|
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To use in onActivityResultCallback in Fragment or Activity
|
||||||
|
* @param onFileSelected Callback retrieve from data
|
||||||
|
* @return true if requestCode was captured, false elsewhere
|
||||||
|
*/
|
||||||
|
fun onOpenDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
|
||||||
|
onFileSelected: ((uri: Uri?) -> Unit)?): Boolean {
|
||||||
|
|
||||||
|
when (requestCode) {
|
||||||
|
FILE_BROWSE -> {
|
||||||
|
if (resultCode == RESULT_OK) {
|
||||||
|
val filename = data?.dataString
|
||||||
|
var keyUri: Uri? = null
|
||||||
|
if (filename != null) {
|
||||||
|
keyUri = UriUtil.parse(filename)
|
||||||
|
}
|
||||||
|
onFileSelected?.invoke(keyUri)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
GET_CONTENT, OPEN_DOC -> {
|
||||||
|
if (resultCode == RESULT_OK) {
|
||||||
|
if (data != null) {
|
||||||
|
val uri = data.data
|
||||||
|
if (uri != null) {
|
||||||
|
try {
|
||||||
|
// try to persist read and write permissions
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
activity?.contentResolver?.apply {
|
||||||
|
takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// nop
|
||||||
|
}
|
||||||
|
onFileSelected?.invoke(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show Browser dialog to select file picker app
|
||||||
|
*/
|
||||||
|
private fun showFileManagerDialogFragment() {
|
||||||
|
try {
|
||||||
|
if (fragment != null) {
|
||||||
|
fragment?.parentFragmentManager
|
||||||
|
} else {
|
||||||
|
activity?.supportFragmentManager
|
||||||
|
}?.let { fragmentManager ->
|
||||||
|
FileManagerDialogFragment().show(fragmentManager, "browserDialog")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Can't open BrowserDialog", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createDocument(titleString: String,
|
||||||
|
typeString: String = "application/octet-stream"): Int? {
|
||||||
|
val idCode = getUnusedCreateFileRequestCode()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
try {
|
||||||
|
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = typeString
|
||||||
|
putExtra(Intent.EXTRA_TITLE, titleString)
|
||||||
|
}
|
||||||
|
if (fragment != null)
|
||||||
|
fragment?.startActivityForResult(intent, idCode)
|
||||||
|
else
|
||||||
|
activity?.startActivityForResult(intent, idCode)
|
||||||
|
return idCode
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to create document", e)
|
||||||
|
showFileManagerDialogFragment()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showFileManagerDialogFragment()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To use in onActivityResultCallback in Fragment or Activity
|
||||||
|
* @param onFileCreated Callback retrieve from data
|
||||||
|
* @return true if requestCode was captured, false elsewhere
|
||||||
|
*/
|
||||||
|
fun onCreateDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
|
||||||
|
onFileCreated: (fileCreated: Uri?)->Unit) {
|
||||||
|
// Retrieve the created URI from the file manager
|
||||||
|
if (fileRequestCodes.contains(requestCode) && resultCode == RESULT_OK) {
|
||||||
|
onFileCreated.invoke(data?.data)
|
||||||
|
fileRequestCodes.remove(requestCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val TAG = "OpenFileHelper"
|
||||||
|
|
||||||
|
private const val GET_CONTENT = 25745
|
||||||
|
private const val OPEN_DOC = 25845
|
||||||
|
private const val FILE_BROWSE = 25645
|
||||||
|
|
||||||
|
private var CREATE_FILE_REQUEST_CODE_DEFAULT = 3853
|
||||||
|
private var fileRequestCodes = ArrayList<Int>()
|
||||||
|
|
||||||
|
private fun getUnusedCreateFileRequestCode(): Int {
|
||||||
|
val newCreateFileRequestCode = CREATE_FILE_REQUEST_CODE_DEFAULT++
|
||||||
|
fileRequestCodes.add(newCreateFileRequestCode)
|
||||||
|
return newCreateFileRequestCode
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager,
|
||||||
|
typeString: String = "application/octet-stream"): Boolean {
|
||||||
|
return when {
|
||||||
|
// To check if a custom file manager can manage the ACTION_CREATE_DOCUMENT
|
||||||
|
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT -> {
|
||||||
|
packageManager.queryIntentActivities(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = typeString
|
||||||
|
}, PackageManager.MATCH_DEFAULT_ONLY).isNotEmpty()
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) {
|
||||||
|
externalFileHelper?.let { fileHelper ->
|
||||||
|
setOnClickListener {
|
||||||
|
fileHelper.openDocument()
|
||||||
|
}
|
||||||
|
setOnLongClickListener {
|
||||||
|
fileHelper.openDocument(true)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} ?: kotlin.run {
|
||||||
|
setOnClickListener(null)
|
||||||
|
setOnLongClickListener(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.helpers
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Activity.RESULT_OK
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
|
||||||
|
|
||||||
class SelectFileHelper {
|
|
||||||
|
|
||||||
private var activity: Activity? = null
|
|
||||||
private var fragment: Fragment? = null
|
|
||||||
|
|
||||||
val selectFileOnClickViewListener: SelectFileOnClickViewListener
|
|
||||||
get() = SelectFileOnClickViewListener()
|
|
||||||
|
|
||||||
constructor(context: Activity) {
|
|
||||||
this.activity = context
|
|
||||||
this.fragment = null
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Fragment) {
|
|
||||||
this.activity = context.activity
|
|
||||||
this.fragment = context
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class SelectFileOnClickViewListener :
|
|
||||||
View.OnClickListener,
|
|
||||||
View.OnLongClickListener,
|
|
||||||
MenuItem.OnMenuItemClickListener {
|
|
||||||
|
|
||||||
private fun onAbstractClick(longClick: Boolean = false) {
|
|
||||||
try {
|
|
||||||
if (longClick) {
|
|
||||||
try {
|
|
||||||
openActivityWithActionGetContent()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
openActivityWithActionOpenDocument()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
openActivityWithActionOpenDocument()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
openActivityWithActionGetContent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Enable to start the file picker activity", e)
|
|
||||||
// Open browser dialog
|
|
||||||
if (lookForOpenIntentsFilePicker())
|
|
||||||
showBrowserDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
|
||||||
onAbstractClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongClick(v: View?): Boolean {
|
|
||||||
onAbstractClick(true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
|
||||||
onAbstractClick()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
private fun openActivityWithActionOpenDocument() {
|
|
||||||
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "*/*"
|
|
||||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
if (fragment != null)
|
|
||||||
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
|
||||||
else
|
|
||||||
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
private fun openActivityWithActionGetContent() {
|
|
||||||
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "*/*"
|
|
||||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
if (fragment != null)
|
|
||||||
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
|
||||||
else
|
|
||||||
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun lookForOpenIntentsFilePicker(): Boolean {
|
|
||||||
var showBrowser = false
|
|
||||||
try {
|
|
||||||
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
|
|
||||||
val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
|
|
||||||
if (fragment != null)
|
|
||||||
fragment?.startActivityForResult(intent, FILE_BROWSE)
|
|
||||||
else
|
|
||||||
activity?.startActivityForResult(intent, FILE_BROWSE)
|
|
||||||
} else {
|
|
||||||
showBrowser = true
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "Enable to start OPEN_INTENTS_FILE_BROWSE", e)
|
|
||||||
showBrowser = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return showBrowser
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether the specified action can be used as an intent. This
|
|
||||||
* method queries the package manager for installed packages that can
|
|
||||||
* respond to an intent with the specified action. If no suitable package is
|
|
||||||
* found, this method returns false.
|
|
||||||
*
|
|
||||||
* @param context The application's environment.
|
|
||||||
* @param action The Intent action to check for availability.
|
|
||||||
*
|
|
||||||
* @return True if an Intent with the specified action can be sent and
|
|
||||||
* responded to, false otherwise.
|
|
||||||
*/
|
|
||||||
private fun isIntentAvailable(context: Context, action: String): Boolean {
|
|
||||||
val packageManager = context.packageManager
|
|
||||||
val intent = Intent(action)
|
|
||||||
val list = packageManager.queryIntentActivities(intent,
|
|
||||||
PackageManager.MATCH_DEFAULT_ONLY)
|
|
||||||
return list.size > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show Browser dialog to select file picker app
|
|
||||||
*/
|
|
||||||
private fun showBrowserDialog() {
|
|
||||||
try {
|
|
||||||
val fileManagerDialogFragment = FileManagerDialogFragment()
|
|
||||||
fragment?.let {
|
|
||||||
fileManagerDialogFragment.show(it.parentFragmentManager, "browserDialog")
|
|
||||||
} ?: fileManagerDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Can't open BrowserDialog", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To use in onActivityResultCallback in Fragment or Activity
|
|
||||||
* @param keyFileCallback Callback retrieve from data
|
|
||||||
* @return true if requestCode was captured, false elsechere
|
|
||||||
*/
|
|
||||||
fun onActivityResultCallback(
|
|
||||||
requestCode: Int,
|
|
||||||
resultCode: Int,
|
|
||||||
data: Intent?,
|
|
||||||
keyFileCallback: ((uri: Uri?) -> Unit)?): Boolean {
|
|
||||||
|
|
||||||
when (requestCode) {
|
|
||||||
FILE_BROWSE -> {
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
val filename = data?.dataString
|
|
||||||
var keyUri: Uri? = null
|
|
||||||
if (filename != null) {
|
|
||||||
keyUri = UriUtil.parse(filename)
|
|
||||||
}
|
|
||||||
keyFileCallback?.invoke(keyUri)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
GET_CONTENT, OPEN_DOC -> {
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
if (data != null) {
|
|
||||||
val uri = data.data
|
|
||||||
if (uri != null) {
|
|
||||||
try {
|
|
||||||
// try to persist read and write permissions
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
|
||||||
activity?.contentResolver?.apply {
|
|
||||||
takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// nop
|
|
||||||
}
|
|
||||||
keyFileCallback?.invoke(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val TAG = "OpenFileHelper"
|
|
||||||
|
|
||||||
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
|
|
||||||
|
|
||||||
private const val GET_CONTENT = 25745
|
|
||||||
private const val OPEN_DOC = 25845
|
|
||||||
private const val FILE_BROWSE = 25645
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,12 +37,12 @@ object Stylish {
|
|||||||
* Initialize the class with a theme preference
|
* Initialize the class with a theme preference
|
||||||
* @param context Context to retrieve the theme preference
|
* @param context Context to retrieve the theme preference
|
||||||
*/
|
*/
|
||||||
fun init(context: Context) {
|
fun load(context: Context) {
|
||||||
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
||||||
themeString = PreferencesUtil.getStyle(context)
|
themeString = PreferencesUtil.getStyle(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
|
fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
|
||||||
val systemNightMode = when (PreferencesUtil.getStyleBrightness(context)) {
|
val systemNightMode = when (PreferencesUtil.getStyleBrightness(context)) {
|
||||||
context.getString(R.string.list_style_brightness_light) -> false
|
context.getString(R.string.list_style_brightness_light) -> false
|
||||||
context.getString(R.string.list_style_brightness_night) -> true
|
context.getString(R.string.list_style_brightness_night) -> true
|
||||||
@@ -84,12 +84,16 @@ object Stylish {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun defaultStyle(context: Context): String {
|
||||||
|
return context.getString(R.string.list_style_name_light)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assign the style to the class attribute
|
* Assign the style to the class attribute
|
||||||
* @param styleString Style id String
|
* @param styleString Style id String
|
||||||
*/
|
*/
|
||||||
fun assignStyle(context: Context, styleString: String) {
|
fun assignStyle(context: Context, styleString: String) {
|
||||||
themeString = retrieveEquivalentSystemStyle(context, styleString)
|
PreferencesUtil.setStyle(context, styleString)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -106,7 +106,6 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
|
|
||||||
private fun getEntryFrom(cursor: Cursor): Entry? {
|
private fun getEntryFrom(cursor: Cursor): Entry? {
|
||||||
return database.createEntry()?.apply {
|
return database.createEntry()?.apply {
|
||||||
database.startManageEntry(this)
|
|
||||||
entryKDB?.let { entryKDB ->
|
entryKDB?.let { entryKDB ->
|
||||||
(cursor as EntryCursorKDB).populateEntry(entryKDB,
|
(cursor as EntryCursorKDB).populateEntry(entryKDB,
|
||||||
{ standardIconId ->
|
{ standardIconId ->
|
||||||
@@ -127,7 +126,6 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
database.stopManageEntry(this)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,12 +148,14 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
if (searchGroup != null) {
|
if (searchGroup != null) {
|
||||||
// Search in hide entries but not meta-stream
|
// Search in hide entries but not meta-stream
|
||||||
for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) {
|
for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) {
|
||||||
|
database.startManageEntry(entry)
|
||||||
entry.entryKDB?.let {
|
entry.entryKDB?.let {
|
||||||
cursorKDB?.addEntry(it)
|
cursorKDB?.addEntry(it)
|
||||||
}
|
}
|
||||||
entry.entryKDBX?.let {
|
entry.entryKDBX?.let {
|
||||||
cursorKDBX?.addEntry(it)
|
cursorKDBX?.addEntry(it)
|
||||||
}
|
}
|
||||||
|
database.stopManageEntry(entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class App : MultiDexApplication() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
Stylish.init(this)
|
Stylish.load(this)
|
||||||
PRNGFixes.apply()
|
PRNGFixes.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.app.database
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.*
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.ServiceConnection
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -42,66 +39,95 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
// Temp DAO to easily remove content if object no longer in memory
|
// Temp DAO to easily remove content if object no longer in memory
|
||||||
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
||||||
|
|
||||||
private val mIntentAdvancedUnlockService = Intent(applicationContext,
|
|
||||||
AdvancedUnlockNotificationService::class.java)
|
|
||||||
private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null
|
private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null
|
||||||
private var mServiceConnection: ServiceConnection? = null
|
private var mServiceConnection: ServiceConnection? = null
|
||||||
|
|
||||||
private var mDatabaseListeners = LinkedList<DatabaseListener>()
|
private var mDatabaseListeners = LinkedList<CipherDatabaseListener>()
|
||||||
|
private var mAdvancedUnlockBroadcastReceiver = AdvancedUnlockNotificationService.AdvancedUnlockReceiver {
|
||||||
|
deleteAll()
|
||||||
|
removeAllDataAndDetach()
|
||||||
|
}
|
||||||
|
|
||||||
fun reloadPreferences() {
|
fun reloadPreferences() {
|
||||||
useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun attachService(performedAction: () -> Unit) {
|
private fun serviceActionTask(startService: Boolean = false, performedAction: () -> Unit) {
|
||||||
// Check if a service is currently running else do nothing
|
// Check if a service is currently running else call action without info
|
||||||
if (mBinder != null) {
|
if (startService && mServiceConnection == null) {
|
||||||
|
attachService(performedAction)
|
||||||
|
} else {
|
||||||
performedAction.invoke()
|
performedAction.invoke()
|
||||||
} else if (mServiceConnection == null) {
|
|
||||||
mServiceConnection = object : ServiceConnection {
|
|
||||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
|
||||||
mBinder = (serviceBinder as AdvancedUnlockNotificationService.AdvancedUnlockBinder)
|
|
||||||
performedAction.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
|
||||||
mBinder = null
|
|
||||||
mServiceConnection = null
|
|
||||||
mDatabaseListeners.forEach {
|
|
||||||
it.onDatabaseCleared()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
applicationContext.bindService(mIntentAdvancedUnlockService,
|
|
||||||
mServiceConnection!!,
|
|
||||||
Context.BIND_ABOVE_CLIENT)
|
|
||||||
if (mBinder == null) {
|
|
||||||
try {
|
|
||||||
applicationContext.startService(mIntentAdvancedUnlockService)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to start cipher action", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun registerDatabaseListener(listener: DatabaseListener) {
|
@Synchronized
|
||||||
mDatabaseListeners.add(listener)
|
private fun attachService(performedAction: () -> Unit) {
|
||||||
|
applicationContext.registerReceiver(mAdvancedUnlockBroadcastReceiver, IntentFilter().apply {
|
||||||
|
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION)
|
||||||
|
})
|
||||||
|
|
||||||
|
mServiceConnection = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||||
|
mBinder = (serviceBinder as AdvancedUnlockNotificationService.AdvancedUnlockBinder)
|
||||||
|
performedAction.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
onClear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
AdvancedUnlockNotificationService.bindService(applicationContext,
|
||||||
|
mServiceConnection!!,
|
||||||
|
Context.BIND_AUTO_CREATE)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to start cipher action", e)
|
||||||
|
performedAction.invoke()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregisterDatabaseListener(listener: DatabaseListener) {
|
@Synchronized
|
||||||
mDatabaseListeners.remove(listener)
|
private fun detachService() {
|
||||||
|
try {
|
||||||
|
applicationContext.unregisterReceiver(mAdvancedUnlockBroadcastReceiver)
|
||||||
|
} catch (e: Exception) {}
|
||||||
|
|
||||||
|
mServiceConnection?.let {
|
||||||
|
AdvancedUnlockNotificationService.unbindService(applicationContext, it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DatabaseListener {
|
private fun removeAllDataAndDetach() {
|
||||||
fun onDatabaseCleared()
|
detachService()
|
||||||
|
onClear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerDatabaseListener(listenerCipher: CipherDatabaseListener) {
|
||||||
|
mDatabaseListeners.add(listenerCipher)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregisterDatabaseListener(listenerCipher: CipherDatabaseListener) {
|
||||||
|
mDatabaseListeners.remove(listenerCipher)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onClear() {
|
||||||
|
mBinder = null
|
||||||
|
mServiceConnection = null
|
||||||
|
mDatabaseListeners.forEach {
|
||||||
|
it.onCipherDatabaseCleared()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CipherDatabaseListener {
|
||||||
|
fun onCipherDatabaseCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCipherDatabase(databaseUri: Uri,
|
fun getCipherDatabase(databaseUri: Uri,
|
||||||
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
|
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
|
||||||
if (useTempDao) {
|
if (useTempDao) {
|
||||||
attachService {
|
serviceActionTask {
|
||||||
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri))
|
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -126,7 +152,8 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
||||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||||
if (useTempDao) {
|
if (useTempDao) {
|
||||||
attachService {
|
// The only case to create service (not needed to get an info)
|
||||||
|
serviceActionTask(true) {
|
||||||
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
|
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
|
||||||
cipherDatabaseResultListener?.invoke()
|
cipherDatabaseResultListener?.invoke()
|
||||||
}
|
}
|
||||||
@@ -151,7 +178,7 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
fun deleteByDatabaseUri(databaseUri: Uri,
|
fun deleteByDatabaseUri(databaseUri: Uri,
|
||||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||||
if (useTempDao) {
|
if (useTempDao) {
|
||||||
attachService {
|
serviceActionTask {
|
||||||
mBinder?.deleteByDatabaseUri(databaseUri)
|
mBinder?.deleteByDatabaseUri(databaseUri)
|
||||||
cipherDatabaseResultListener?.invoke()
|
cipherDatabaseResultListener?.invoke()
|
||||||
}
|
}
|
||||||
@@ -168,14 +195,19 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAll() {
|
fun deleteAll() {
|
||||||
attachService {
|
if (useTempDao) {
|
||||||
mBinder?.deleteAll()
|
serviceActionTask {
|
||||||
|
mBinder?.deleteAll()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// To erase the residues
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
cipherDatabaseDao.deleteAll()
|
cipherDatabaseDao.deleteAll()
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
|
// Unbind
|
||||||
|
removeAllDataAndDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {
|
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {
|
||||||
|
|||||||
@@ -223,9 +223,22 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
usernameValueCandidate = node.autofillValue
|
usernameValueCandidate = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
||||||
}
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
|
||||||
|
// Some forms used visible password as username
|
||||||
|
if (usernameCandidate == null && usernameValueCandidate == null) {
|
||||||
|
usernameCandidate = autofillId
|
||||||
|
usernameValueCandidate = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill visible password android text type (as username): ${showHexInputType(inputType)}")
|
||||||
|
} else if (result?.passwordId == null && result?.passwordValue == null) {
|
||||||
|
result?.passwordId = autofillId
|
||||||
|
result?.passwordValue = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill visible password android text type (as password): ${showHexInputType(inputType)}")
|
||||||
|
usernameNeeded = false
|
||||||
|
}
|
||||||
|
}
|
||||||
inputIsVariationType(inputType,
|
inputIsVariationType(inputType,
|
||||||
InputType.TYPE_TEXT_VARIATION_PASSWORD,
|
InputType.TYPE_TEXT_VARIATION_PASSWORD,
|
||||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
|
|
||||||
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
|
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
|
||||||
result?.passwordId = autofillId
|
result?.passwordId = autofillId
|
||||||
result?.passwordValue = node.autofillValue
|
result?.passwordValue = node.autofillValue
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
|||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||||
import com.kunzisoft.keepass.database.exception.IODatabaseException
|
import com.kunzisoft.keepass.database.exception.IODatabaseException
|
||||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||||
|
|
||||||
@@ -68,7 +67,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
|
|
||||||
private lateinit var cipherDatabaseAction : CipherDatabaseAction
|
private lateinit var cipherDatabaseAction : CipherDatabaseAction
|
||||||
|
|
||||||
private var cipherDatabaseListener: CipherDatabaseAction.DatabaseListener? = null
|
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
|
||||||
|
|
||||||
// Only to fix multiple fingerprint menu #332
|
// Only to fix multiple fingerprint menu #332
|
||||||
private var mAllowAdvancedUnlockMenu = false
|
private var mAllowAdvancedUnlockMenu = false
|
||||||
@@ -402,9 +401,10 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
fun connect(databaseUri: Uri) {
|
fun connect(databaseUri: Uri) {
|
||||||
showViews(true)
|
showViews(true)
|
||||||
this.databaseFileUri = databaseUri
|
this.databaseFileUri = databaseUri
|
||||||
cipherDatabaseListener = object: CipherDatabaseAction.DatabaseListener {
|
cipherDatabaseListener = object: CipherDatabaseAction.CipherDatabaseListener {
|
||||||
override fun onDatabaseCleared() {
|
override fun onCipherDatabaseCleared() {
|
||||||
deleteEncryptedDatabaseKey()
|
advancedUnlockManager?.closeBiometricPrompt()
|
||||||
|
checkUnlockAvailability()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cipherDatabaseAction.apply {
|
cipherDatabaseAction.apply {
|
||||||
@@ -435,14 +435,12 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
fun deleteEncryptedDatabaseKey() {
|
fun deleteEncryptedDatabaseKey() {
|
||||||
allowOpenBiometricPrompt = false
|
|
||||||
mAdvancedUnlockInfoView?.setIconViewClickListener(false, null)
|
|
||||||
advancedUnlockManager?.closeBiometricPrompt()
|
advancedUnlockManager?.closeBiometricPrompt()
|
||||||
databaseFileUri?.let { databaseUri ->
|
databaseFileUri?.let { databaseUri ->
|
||||||
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
|
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
|
||||||
checkUnlockAvailability()
|
checkUnlockAvailability()
|
||||||
}
|
}
|
||||||
}
|
} ?: checkUnlockAvailability()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
@@ -479,7 +477,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
mBuilderListener?.retrieveCredentialForEncryption()?.let { credential ->
|
mBuilderListener?.retrieveCredentialForEncryption()?.let { credential ->
|
||||||
advancedUnlockManager?.encryptData(credential)
|
advancedUnlockManager?.encryptData(credential)
|
||||||
}
|
}
|
||||||
AdvancedUnlockNotificationService.startServiceForTimeout(requireContext())
|
|
||||||
}
|
}
|
||||||
Mode.EXTRACT_CREDENTIAL -> {
|
Mode.EXTRACT_CREDENTIAL -> {
|
||||||
// retrieve the encrypted value from preferences
|
// retrieve the encrypted value from preferences
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import android.util.Log
|
|||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.*
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.exception.EntryDatabaseException
|
import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException
|
||||||
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
|
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
|
||||||
|
|
||||||
class MoveNodesRunnable constructor(
|
class MoveNodesRunnable constructor(
|
||||||
@@ -47,8 +47,10 @@ class MoveNodesRunnable constructor(
|
|||||||
when (nodeToMove.type) {
|
when (nodeToMove.type) {
|
||||||
Type.GROUP -> {
|
Type.GROUP -> {
|
||||||
val groupToMove = nodeToMove as Group
|
val groupToMove = nodeToMove as Group
|
||||||
// Move group in new parent if not in the current group
|
// Move group if the parent change
|
||||||
if (groupToMove != mNewParent
|
if (mOldParent != mNewParent
|
||||||
|
// and if not in the current group
|
||||||
|
&& groupToMove != mNewParent
|
||||||
&& !mNewParent.isContainedIn(groupToMove)) {
|
&& !mNewParent.isContainedIn(groupToMove)) {
|
||||||
nodeToMove.touch(modified = true, touchParents = true)
|
nodeToMove.touch(modified = true, touchParents = true)
|
||||||
database.moveGroupTo(groupToMove, mNewParent)
|
database.moveGroupTo(groupToMove, mNewParent)
|
||||||
@@ -68,7 +70,7 @@ class MoveNodesRunnable constructor(
|
|||||||
database.moveEntryTo(entryToMove, mNewParent)
|
database.moveEntryTo(entryToMove, mNewParent)
|
||||||
} else {
|
} else {
|
||||||
// Only finish thread
|
// Only finish thread
|
||||||
setError(EntryDatabaseException())
|
setError(MoveEntryDatabaseException())
|
||||||
break@foreachNode
|
break@foreachNode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ abstract class CipherEngine {
|
|||||||
return 16
|
return 16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used only with padding workaround
|
||||||
|
var forcePaddingCompatibility = false
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||||
abstract fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher
|
abstract fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class TwofishEngine : CipherEngine() {
|
|||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||||
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
|
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
|
||||||
return CipherFactory.getTwofish(opmode, key, IV)
|
return CipherFactory.getTwofish(opmode, key, IV, forcePaddingCompatibility)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getEncryptionAlgorithm(): EncryptionAlgorithm {
|
override fun getEncryptionAlgorithm(): EncryptionAlgorithm {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import android.content.ContentResolver
|
|||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.utils.readBytes4ToUInt
|
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
@@ -55,6 +54,7 @@ import com.kunzisoft.keepass.model.MainCredential
|
|||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolder
|
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.utils.readBytes4ToUInt
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
@@ -105,10 +105,6 @@ class Database {
|
|||||||
return mDatabaseKDB?.binaryCache ?: mDatabaseKDBX?.binaryCache ?: BinaryCache()
|
return mDatabaseKDB?.binaryCache ?: mDatabaseKDBX?.binaryCache ?: BinaryCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCacheDirectory(cacheDirectory: File) {
|
|
||||||
binaryCache.cacheDirectory = cacheDirectory
|
|
||||||
}
|
|
||||||
|
|
||||||
private val iconsManager: IconsManager
|
private val iconsManager: IconsManager
|
||||||
get() {
|
get() {
|
||||||
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager(binaryCache)
|
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager(binaryCache)
|
||||||
@@ -363,15 +359,10 @@ class Database {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ensureRecycleBinExists(resources: Resources) {
|
val groupNamesNotAllowed: List<String>
|
||||||
mDatabaseKDB?.ensureBackupExists()
|
get() {
|
||||||
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
return mDatabaseKDB?.groupNamesNotAllowed ?: ArrayList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeRecycleBin() {
|
|
||||||
// Don't allow remove backup in KDB
|
|
||||||
mDatabaseKDBX?.removeRecycleBin()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setDatabaseKDB(databaseKDB: DatabaseKDB) {
|
private fun setDatabaseKDB(databaseKDB: DatabaseKDB) {
|
||||||
this.mDatabaseKDB = databaseKDB
|
this.mDatabaseKDB = databaseKDB
|
||||||
@@ -546,25 +537,27 @@ class Database {
|
|||||||
omitBackup: Boolean,
|
omitBackup: Boolean,
|
||||||
max: Int = Integer.MAX_VALUE): Group? {
|
max: Int = Integer.MAX_VALUE): Group? {
|
||||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
||||||
searchQuery, SearchParameters(), omitBackup, max)
|
SearchParameters().apply {
|
||||||
|
this.searchQuery = searchQuery
|
||||||
|
}, omitBackup, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createVirtualGroupFromSearchInfo(searchInfoString: String,
|
fun createVirtualGroupFromSearchInfo(searchInfoString: String,
|
||||||
omitBackup: Boolean,
|
omitBackup: Boolean,
|
||||||
max: Int = Integer.MAX_VALUE): Group? {
|
max: Int = Integer.MAX_VALUE): Group? {
|
||||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
||||||
searchInfoString, SearchParameters().apply {
|
SearchParameters().apply {
|
||||||
searchInTitles = true
|
searchQuery = searchInfoString
|
||||||
searchInUserNames = false
|
searchInTitles = true
|
||||||
searchInPasswords = false
|
searchInUserNames = false
|
||||||
searchInUrls = true
|
searchInPasswords = false
|
||||||
searchInNotes = true
|
searchInUrls = true
|
||||||
searchInOTP = false
|
searchInNotes = true
|
||||||
searchInOther = true
|
searchInOTP = false
|
||||||
searchInUUIDs = false
|
searchInOther = true
|
||||||
searchInTags = false
|
searchInUUIDs = false
|
||||||
ignoreCase = true
|
searchInTags = false
|
||||||
}, omitBackup, max)
|
}, omitBackup, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
val attachmentPool: AttachmentPool
|
val attachmentPool: AttachmentPool
|
||||||
@@ -794,11 +787,11 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addGroupTo(group: Group, parent: Group) {
|
fun addGroupTo(group: Group, parent: Group) {
|
||||||
group.groupKDB?.let { entryKDB ->
|
group.groupKDB?.let { groupKDB ->
|
||||||
mDatabaseKDB?.addGroupTo(entryKDB, parent.groupKDB)
|
mDatabaseKDB?.addGroupTo(groupKDB, parent.groupKDB)
|
||||||
}
|
}
|
||||||
group.groupKDBX?.let { entryKDBX ->
|
group.groupKDBX?.let { groupKDBX ->
|
||||||
mDatabaseKDBX?.addGroupTo(entryKDBX, parent.groupKDBX)
|
mDatabaseKDBX?.addGroupTo(groupKDBX, parent.groupKDBX)
|
||||||
}
|
}
|
||||||
group.afterAssignNewParent()
|
group.afterAssignNewParent()
|
||||||
}
|
}
|
||||||
@@ -813,11 +806,11 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun removeGroupFrom(group: Group, parent: Group) {
|
fun removeGroupFrom(group: Group, parent: Group) {
|
||||||
group.groupKDB?.let { entryKDB ->
|
group.groupKDB?.let { groupKDB ->
|
||||||
mDatabaseKDB?.removeGroupFrom(entryKDB, parent.groupKDB)
|
mDatabaseKDB?.removeGroupFrom(groupKDB, parent.groupKDB)
|
||||||
}
|
}
|
||||||
group.groupKDBX?.let { entryKDBX ->
|
group.groupKDBX?.let { groupKDBX ->
|
||||||
mDatabaseKDBX?.removeGroupFrom(entryKDBX, parent.groupKDBX)
|
mDatabaseKDBX?.removeGroupFrom(groupKDBX, parent.groupKDBX)
|
||||||
}
|
}
|
||||||
group.afterAssignNewParent()
|
group.afterAssignNewParent()
|
||||||
}
|
}
|
||||||
@@ -892,6 +885,16 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ensureRecycleBinExists(resources: Resources) {
|
||||||
|
mDatabaseKDB?.ensureBackupExists()
|
||||||
|
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeRecycleBin() {
|
||||||
|
// Don't allow remove backup in KDB
|
||||||
|
mDatabaseKDBX?.removeRecycleBin()
|
||||||
|
}
|
||||||
|
|
||||||
fun canRecycle(entry: Entry): Boolean {
|
fun canRecycle(entry: Entry): Boolean {
|
||||||
var canRecycle: Boolean? = null
|
var canRecycle: Boolean? = null
|
||||||
entry.entryKDB?.let {
|
entry.entryKDB?.let {
|
||||||
|
|||||||
@@ -466,16 +466,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
companion object CREATOR : Parcelable.Creator<Entry> {
|
|
||||||
override fun createFromParcel(parcel: Parcel): Entry {
|
|
||||||
return Entry(parcel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<Entry?> {
|
|
||||||
return arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
const val PMS_TAN_ENTRY = "<TAN>"
|
const val PMS_TAN_ENTRY = "<TAN>"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -484,5 +475,16 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
fun newExtraFieldNameAllowed(field: Field): Boolean {
|
fun newExtraFieldNameAllowed(field: Field): Boolean {
|
||||||
return EntryKDBX.newCustomNameAllowed(field.name)
|
return EntryKDBX.newCustomNameAllowed(field.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val CREATOR: Parcelable.Creator<Entry> = object : Parcelable.Creator<Entry> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): Entry {
|
||||||
|
return Entry(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<Entry?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -368,14 +368,6 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
groupKDB?.nodeId = id
|
groupKDB?.nodeId = id
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLevel(): Int {
|
|
||||||
return groupKDB?.level ?: -1
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setLevel(level: Int) {
|
|
||||||
groupKDB?.level = level
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
------------
|
------------
|
||||||
KDBX Methods
|
KDBX Methods
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class BinaryCache {
|
|||||||
*/
|
*/
|
||||||
var loadedCipherKey: LoadedKey = LoadedKey.generateNewCipherKey()
|
var loadedCipherKey: LoadedKey = LoadedKey.generateNewCipherKey()
|
||||||
|
|
||||||
lateinit var cacheDirectory: File
|
var cacheDirectory: File? = null
|
||||||
|
|
||||||
private val voidBinary = KeyByteArray(UNKNOWN, ByteArray(0))
|
private val voidBinary = KeyByteArray(UNKNOWN, ByteArray(0))
|
||||||
|
|
||||||
@@ -19,11 +19,12 @@ class BinaryCache {
|
|||||||
smallSize: Boolean = false,
|
smallSize: Boolean = false,
|
||||||
compression: Boolean = false,
|
compression: Boolean = false,
|
||||||
protection: Boolean = false): BinaryData {
|
protection: Boolean = false): BinaryData {
|
||||||
return if (smallSize) {
|
val cacheDir = cacheDirectory
|
||||||
|
return if (smallSize || cacheDir == null) {
|
||||||
BinaryByte(binaryId, compression, protection)
|
BinaryByte(binaryId, compression, protection)
|
||||||
} else {
|
} else {
|
||||||
val fileInCache = File(cacheDirectory, binaryId)
|
val fileInCache = File(cacheDir, binaryId)
|
||||||
return BinaryFile(fileInCache, compression, protection)
|
BinaryFile(fileInCache, compression, protection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ import kotlin.collections.ArrayList
|
|||||||
|
|
||||||
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||||
|
|
||||||
private var backupGroupId: Int = BACKUP_FOLDER_UNDEFINED_ID
|
|
||||||
|
|
||||||
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
||||||
|
|
||||||
override val version: String
|
override val version: String
|
||||||
@@ -55,13 +53,14 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
return getGroupById(NodeIdInt(groupId))
|
return getGroupById(NodeIdInt(groupId))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve backup group in index
|
|
||||||
val backupGroup: GroupKDB?
|
val backupGroup: GroupKDB?
|
||||||
get() {
|
get() {
|
||||||
return if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
|
return retrieveBackup()
|
||||||
null
|
}
|
||||||
else
|
|
||||||
getGroupById(backupGroupId)
|
val groupNamesNotAllowed: List<String>
|
||||||
|
get() {
|
||||||
|
return listOf(BACKUP_FOLDER_TITLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val kdfEngine: KdfEngine
|
override val kdfEngine: KdfEngine
|
||||||
@@ -80,12 +79,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
|
|
||||||
val rootGroups: List<GroupKDB>
|
val rootGroups: List<GroupKDB>
|
||||||
get() {
|
get() {
|
||||||
val kids = ArrayList<GroupKDB>()
|
return rootGroup?.getChildGroups() ?: ArrayList()
|
||||||
doForEachGroupInIndex { group ->
|
|
||||||
if (group.level == 0)
|
|
||||||
kids.add(group)
|
|
||||||
}
|
|
||||||
return kids
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val passwordEncoding: String
|
override val passwordEncoding: String
|
||||||
@@ -169,21 +163,14 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
|
|
||||||
override fun isInRecycleBin(group: GroupKDB): Boolean {
|
override fun isInRecycleBin(group: GroupKDB): Boolean {
|
||||||
var currentGroup: GroupKDB? = group
|
var currentGroup: GroupKDB? = group
|
||||||
|
val currentBackupGroup = backupGroup ?: return false
|
||||||
|
|
||||||
// Init backup group variable
|
if (currentGroup == currentBackupGroup)
|
||||||
if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
|
|
||||||
findBackupGroupId()
|
|
||||||
|
|
||||||
if (backupGroup == null)
|
|
||||||
return false
|
|
||||||
|
|
||||||
if (currentGroup == backupGroup)
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
val backupGroupId = currentBackupGroup.id
|
||||||
while (currentGroup != null) {
|
while (currentGroup != null) {
|
||||||
if (currentGroup.level == 0
|
if (backupGroupId == currentGroup.id) {
|
||||||
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
|
|
||||||
backupGroupId = currentGroup.id
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
currentGroup = currentGroup.parent
|
currentGroup = currentGroup.parent
|
||||||
@@ -191,12 +178,12 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findBackupGroupId() {
|
/**
|
||||||
rootGroups.forEach { currentGroup ->
|
* Retrieve backup group with his name
|
||||||
if (currentGroup.level == 0
|
*/
|
||||||
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
|
private fun retrieveBackup(): GroupKDB? {
|
||||||
backupGroupId = currentGroup.id
|
return rootGroup?.searchChildGroup {
|
||||||
}
|
it.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,8 +192,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
* if it doesn't exist
|
* if it doesn't exist
|
||||||
*/
|
*/
|
||||||
fun ensureBackupExists() {
|
fun ensureBackupExists() {
|
||||||
findBackupGroupId()
|
|
||||||
|
|
||||||
if (backupGroup == null) {
|
if (backupGroup == null) {
|
||||||
// Create recycle bin
|
// Create recycle bin
|
||||||
val recycleBinGroup = createGroup().apply {
|
val recycleBinGroup = createGroup().apply {
|
||||||
@@ -214,7 +199,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
|
icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
|
||||||
}
|
}
|
||||||
addGroupTo(recycleBinGroup, rootGroup)
|
addGroupTo(recycleBinGroup, rootGroup)
|
||||||
backupGroupId = recycleBinGroup.id
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +252,5 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
val TYPE = DatabaseKDB::class.java
|
val TYPE = DatabaseKDB::class.java
|
||||||
|
|
||||||
const val BACKUP_FOLDER_TITLE = "Backup"
|
const val BACKUP_FOLDER_TITLE = "Backup"
|
||||||
private const val BACKUP_FOLDER_UNDEFINED_ID = -1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ import android.content.res.Resources
|
|||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.encrypt.HashManager
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
|
||||||
import com.kunzisoft.keepass.utils.longTo8Bytes
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
import com.kunzisoft.keepass.database.crypto.AesEngine
|
import com.kunzisoft.keepass.database.crypto.AesEngine
|
||||||
@@ -39,6 +37,7 @@ import com.kunzisoft.keepass.database.element.DeletedObject
|
|||||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
|
import com.kunzisoft.keepass.database.element.entry.FieldReferencesEngine
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
@@ -50,6 +49,8 @@ import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VER
|
|||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
|
||||||
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
|
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
|
||||||
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
import com.kunzisoft.keepass.utils.longTo8Bytes
|
||||||
import org.apache.commons.codec.binary.Hex
|
import org.apache.commons.codec.binary.Hex
|
||||||
import org.w3c.dom.Node
|
import org.w3c.dom.Node
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -75,6 +76,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
private var kdfList: MutableList<KdfEngine> = ArrayList()
|
private var kdfList: MutableList<KdfEngine> = ArrayList()
|
||||||
private var numKeyEncRounds: Long = 0
|
private var numKeyEncRounds: Long = 0
|
||||||
var publicCustomData = VariantDictionary()
|
var publicCustomData = VariantDictionary()
|
||||||
|
private val mFieldReferenceEngine = FieldReferencesEngine(this)
|
||||||
|
|
||||||
var kdbxVersion = UnsignedInt(0)
|
var kdbxVersion = UnsignedInt(0)
|
||||||
var name = ""
|
var name = ""
|
||||||
@@ -132,7 +134,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
icon.standard = getStandardIcon(IconImageStandard.FOLDER_ID)
|
icon.standard = getStandardIcon(IconImageStandard.FOLDER_ID)
|
||||||
}
|
}
|
||||||
rootGroup = group
|
rootGroup = group
|
||||||
addGroupIndex(group)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val version: String
|
override val version: String
|
||||||
@@ -334,6 +335,19 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
return customData.isNotEmpty()
|
return customData.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getEntryByCustomData(customDataValue: String): EntryKDBX? {
|
||||||
|
return entryIndexes.values.find { entry ->
|
||||||
|
entry.customData.containsValue(customDataValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the value of a field reference
|
||||||
|
*/
|
||||||
|
fun getFieldReferenceValue(textReference: String, recursionLevel: Int): String {
|
||||||
|
return mFieldReferenceEngine.compile(textReference, recursionLevel)
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
public override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
|
public override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
|
||||||
|
|
||||||
@@ -615,6 +629,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
return false
|
return false
|
||||||
if (recycleBin == null)
|
if (recycleBin == null)
|
||||||
return false
|
return false
|
||||||
|
if (node is GroupKDBX
|
||||||
|
&& recycleBin!!.isContainedIn(node))
|
||||||
|
return false
|
||||||
if (!node.isContainedIn(recycleBin!!))
|
if (!node.isContainedIn(recycleBin!!))
|
||||||
return true
|
return true
|
||||||
return false
|
return false
|
||||||
@@ -652,9 +669,20 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
this.deletedObjects.add(deletedObject)
|
this.deletedObjects.add(deletedObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun addEntryTo(newEntry: EntryKDBX, parent: GroupKDBX?) {
|
||||||
|
super.addEntryTo(newEntry, parent)
|
||||||
|
mFieldReferenceEngine.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateEntry(entry: EntryKDBX) {
|
||||||
|
super.updateEntry(entry)
|
||||||
|
mFieldReferenceEngine.clear()
|
||||||
|
}
|
||||||
|
|
||||||
override fun removeEntryFrom(entryToRemove: EntryKDBX, parent: GroupKDBX?) {
|
override fun removeEntryFrom(entryToRemove: EntryKDBX, parent: GroupKDBX?) {
|
||||||
super.removeEntryFrom(entryToRemove, parent)
|
super.removeEntryFrom(entryToRemove, parent)
|
||||||
deletedObjects.add(DeletedObject(entryToRemove.id))
|
deletedObjects.add(DeletedObject(entryToRemove.id))
|
||||||
|
mFieldReferenceEngine.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun undoDeleteEntryFrom(entry: EntryKDBX, origParent: GroupKDBX?) {
|
override fun undoDeleteEntryFrom(entry: EntryKDBX, origParent: GroupKDBX?) {
|
||||||
@@ -725,6 +753,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
override fun clearCache() {
|
override fun clearCache() {
|
||||||
try {
|
try {
|
||||||
super.clearCache()
|
super.clearCache()
|
||||||
|
mFieldReferenceEngine.clear()
|
||||||
attachmentPool.clear()
|
attachmentPool.clear()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to clear cache", e)
|
Log.e(TAG, "Unable to clear cache", e)
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import java.io.ByteArrayInputStream
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.UnsupportedEncodingException
|
import java.io.UnsupportedEncodingException
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
abstract class DatabaseVersioned<
|
abstract class DatabaseVersioned<
|
||||||
@@ -68,7 +67,7 @@ abstract class DatabaseVersioned<
|
|||||||
var changeDuplicateId = false
|
var changeDuplicateId = false
|
||||||
|
|
||||||
private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>()
|
private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>()
|
||||||
private var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
|
protected var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
|
||||||
|
|
||||||
abstract val version: String
|
abstract val version: String
|
||||||
|
|
||||||
@@ -87,6 +86,12 @@ abstract class DatabaseVersioned<
|
|||||||
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
|
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
|
||||||
|
|
||||||
var rootGroup: Group? = null
|
var rootGroup: Group? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
value?.let {
|
||||||
|
addGroupIndex(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
|
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
|
||||||
@@ -266,6 +271,26 @@ abstract class DatabaseVersioned<
|
|||||||
return this.entryIndexes[id]
|
return this.entryIndexes[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getEntryByTitle(title: String): Entry? {
|
||||||
|
return this.entryIndexes.values.find { entry -> entry.title.equals(title, true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryByUsername(username: String): Entry? {
|
||||||
|
return this.entryIndexes.values.find { entry -> entry.username.equals(username, true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryByURL(url: String): Entry? {
|
||||||
|
return this.entryIndexes.values.find { entry -> entry.url.equals(url, true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryByPassword(password: String): Entry? {
|
||||||
|
return this.entryIndexes.values.find { entry -> entry.password.equals(password, true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryByNotes(notes: String): Entry? {
|
||||||
|
return this.entryIndexes.values.find { entry -> entry.notes.equals(notes, true) }
|
||||||
|
}
|
||||||
|
|
||||||
fun addEntryIndex(entry: Entry) {
|
fun addEntryIndex(entry: Entry) {
|
||||||
val entryId = entry.nodeId
|
val entryId = entry.nodeId
|
||||||
if (entryIndexes.containsKey(entryId)) {
|
if (entryIndexes.containsKey(entryId)) {
|
||||||
@@ -331,14 +356,14 @@ abstract class DatabaseVersioned<
|
|||||||
removeGroupIndex(groupToRemove)
|
removeGroupIndex(groupToRemove)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addEntryTo(newEntry: Entry, parent: Group?) {
|
open fun addEntryTo(newEntry: Entry, parent: Group?) {
|
||||||
// Add entry to parent
|
// Add entry to parent
|
||||||
parent?.addChildEntry(newEntry)
|
parent?.addChildEntry(newEntry)
|
||||||
newEntry.parent = parent
|
newEntry.parent = parent
|
||||||
addEntryIndex(newEntry)
|
addEntryIndex(newEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateEntry(entry: Entry) {
|
open fun updateEntry(entry: Entry) {
|
||||||
updateEntryIndex(entry)
|
updateEntryIndex(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
url = parcel.readString() ?: url
|
url = parcel.readString() ?: url
|
||||||
notes = parcel.readString() ?: notes
|
notes = parcel.readString() ?: notes
|
||||||
binaryDescription = parcel.readString() ?: binaryDescription
|
binaryDescription = parcel.readString() ?: binaryDescription
|
||||||
binaryDataId = parcel.readInt()
|
val rawBinaryDataId = parcel.readInt()
|
||||||
|
binaryDataId = if (rawBinaryDataId == -1) null else rawBinaryDataId
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun readParentParcelable(parcel: Parcel): GroupKDB? {
|
override fun readParentParcelable(parcel: Parcel): GroupKDB? {
|
||||||
@@ -109,9 +110,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
dest.writeString(url)
|
dest.writeString(url)
|
||||||
dest.writeString(notes)
|
dest.writeString(notes)
|
||||||
dest.writeString(binaryDescription)
|
dest.writeString(binaryDescription)
|
||||||
binaryDataId?.let {
|
dest.writeInt(binaryDataId ?: -1)
|
||||||
dest.writeInt(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateWith(source: EntryKDB) {
|
fun updateWith(source: EntryKDB) {
|
||||||
|
|||||||
@@ -56,32 +56,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
var additional = ""
|
var additional = ""
|
||||||
var tags = ""
|
var tags = ""
|
||||||
|
|
||||||
fun getSize(attachmentPool: AttachmentPool): Long {
|
|
||||||
var size = FIXED_LENGTH_SIZE
|
|
||||||
|
|
||||||
for (entry in fields.entries) {
|
|
||||||
size += entry.key.length.toLong()
|
|
||||||
size += entry.value.length().toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
size += getAttachmentsSize(attachmentPool)
|
|
||||||
|
|
||||||
size += autoType.defaultSequence.length.toLong()
|
|
||||||
for ((key, value) in autoType.entrySet()) {
|
|
||||||
size += key.length.toLong()
|
|
||||||
size += value.length.toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
for (entry in history) {
|
|
||||||
size += entry.getSize(attachmentPool)
|
|
||||||
}
|
|
||||||
|
|
||||||
size += overrideURL.length.toLong()
|
|
||||||
size += tags.length.toLong()
|
|
||||||
|
|
||||||
return size
|
|
||||||
}
|
|
||||||
|
|
||||||
override var expires: Boolean = false
|
override var expires: Boolean = false
|
||||||
|
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
@@ -102,6 +76,14 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
tags = parcel.readString() ?: tags
|
tags = parcel.readString() ?: tags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun readParentParcelable(parcel: Parcel): GroupKDBX? {
|
||||||
|
return parcel.readParcelable(GroupKDBX::class.java.classLoader)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeParentParcelable(parent: GroupKDBX?, parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeParcelable(parent, flags)
|
||||||
|
}
|
||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
super.writeToParcel(dest, flags)
|
super.writeToParcel(dest, flags)
|
||||||
dest.writeLong(usageCount.toKotlinLong())
|
dest.writeLong(usageCount.toKotlinLong())
|
||||||
@@ -164,13 +146,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
return NodeIdUUID(nodeId.id)
|
return NodeIdUUID(nodeId.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun readParentParcelable(parcel: Parcel): GroupKDBX? {
|
override val type: Type
|
||||||
return parcel.readParcelable(GroupKDBX::class.java.classLoader)
|
get() = Type.ENTRY
|
||||||
}
|
|
||||||
|
|
||||||
override fun writeParentParcelable(parent: GroupKDBX?, parcel: Parcel, flags: Int) {
|
|
||||||
parcel.writeParcelable(parent, flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode a reference key with the FieldReferencesEngine
|
* Decode a reference key with the FieldReferencesEngine
|
||||||
@@ -178,47 +155,64 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
* @param key
|
* @param key
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private fun decodeRefKey(decodeRef: Boolean, key: String): String {
|
private fun decodeRefKey(decodeRef: Boolean, key: String, recursionLevel: Int): String {
|
||||||
return fields[key]?.toString()?.let { text ->
|
return fields[key]?.toString()?.let { text ->
|
||||||
return if (decodeRef) {
|
return if (decodeRef) {
|
||||||
if (mDatabase == null) text else FieldReferencesEngine().compile(text, this, mDatabase!!)
|
mDatabase?.getFieldReferenceValue(text, recursionLevel) ?: text
|
||||||
} else text
|
} else text
|
||||||
} ?: ""
|
} ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun decodeTitleKey(recursionLevel: Int): String {
|
||||||
|
return decodeRefKey(mDecodeRef, STR_TITLE, recursionLevel)
|
||||||
|
}
|
||||||
|
|
||||||
override var title: String
|
override var title: String
|
||||||
get() = decodeRefKey(mDecodeRef, STR_TITLE)
|
get() = decodeTitleKey(0)
|
||||||
set(value) {
|
set(value) {
|
||||||
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectTitle
|
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectTitle
|
||||||
fields[STR_TITLE] = ProtectedString(protect, value)
|
fields[STR_TITLE] = ProtectedString(protect, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val type: Type
|
fun decodeUsernameKey(recursionLevel: Int): String {
|
||||||
get() = Type.ENTRY
|
return decodeRefKey(mDecodeRef, STR_USERNAME, recursionLevel)
|
||||||
|
}
|
||||||
|
|
||||||
override var username: String
|
override var username: String
|
||||||
get() = decodeRefKey(mDecodeRef, STR_USERNAME)
|
get() = decodeUsernameKey(0)
|
||||||
set(value) {
|
set(value) {
|
||||||
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUserName
|
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUserName
|
||||||
fields[STR_USERNAME] = ProtectedString(protect, value)
|
fields[STR_USERNAME] = ProtectedString(protect, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun decodePasswordKey(recursionLevel: Int): String {
|
||||||
|
return decodeRefKey(mDecodeRef, STR_PASSWORD, recursionLevel)
|
||||||
|
}
|
||||||
|
|
||||||
override var password: String
|
override var password: String
|
||||||
get() = decodeRefKey(mDecodeRef, STR_PASSWORD)
|
get() = decodePasswordKey(0)
|
||||||
set(value) {
|
set(value) {
|
||||||
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectPassword
|
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectPassword
|
||||||
fields[STR_PASSWORD] = ProtectedString(protect, value)
|
fields[STR_PASSWORD] = ProtectedString(protect, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun decodeUrlKey(recursionLevel: Int): String {
|
||||||
|
return decodeRefKey(mDecodeRef, STR_URL, recursionLevel)
|
||||||
|
}
|
||||||
|
|
||||||
override var url
|
override var url
|
||||||
get() = decodeRefKey(mDecodeRef, STR_URL)
|
get() = decodeUrlKey(0)
|
||||||
set(value) {
|
set(value) {
|
||||||
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUrl
|
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUrl
|
||||||
fields[STR_URL] = ProtectedString(protect, value)
|
fields[STR_URL] = ProtectedString(protect, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun decodeNotesKey(recursionLevel: Int): String {
|
||||||
|
return decodeRefKey(mDecodeRef, STR_NOTES, recursionLevel)
|
||||||
|
}
|
||||||
|
|
||||||
override var notes: String
|
override var notes: String
|
||||||
get() = decodeRefKey(mDecodeRef, STR_NOTES)
|
get() = decodeNotesKey(0)
|
||||||
set(value) {
|
set(value) {
|
||||||
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectNotes
|
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectNotes
|
||||||
fields[STR_NOTES] = ProtectedString(protect, value)
|
fields[STR_NOTES] = ProtectedString(protect, value)
|
||||||
@@ -228,6 +222,32 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
|
|
||||||
override var locationChanged = DateInstant()
|
override var locationChanged = DateInstant()
|
||||||
|
|
||||||
|
fun getSize(attachmentPool: AttachmentPool): Long {
|
||||||
|
var size = FIXED_LENGTH_SIZE
|
||||||
|
|
||||||
|
for (entry in fields.entries) {
|
||||||
|
size += entry.key.length.toLong()
|
||||||
|
size += entry.value.length().toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
size += getAttachmentsSize(attachmentPool)
|
||||||
|
|
||||||
|
size += autoType.defaultSequence.length.toLong()
|
||||||
|
for ((key, value) in autoType.entrySet()) {
|
||||||
|
size += key.length.toLong()
|
||||||
|
size += value.length.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (entry in history) {
|
||||||
|
size += entry.getSize(attachmentPool)
|
||||||
|
}
|
||||||
|
|
||||||
|
size += overrideURL.length.toLong()
|
||||||
|
size += tags.length.toLong()
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
fun afterChangeParent() {
|
fun afterChangeParent() {
|
||||||
locationChanged = DateInstant()
|
locationChanged = DateInstant()
|
||||||
}
|
}
|
||||||
@@ -245,7 +265,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
field.clear()
|
field.clear()
|
||||||
for ((key, value) in fields) {
|
for ((key, value) in fields) {
|
||||||
if (!isStandardField(key)) {
|
if (!isStandardField(key)) {
|
||||||
field[key] = ProtectedString(value.isProtected, decodeRefKey(mDecodeRef, key))
|
field[key] = ProtectedString(value.isProtected, decodeRefKey(mDecodeRef, key, 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return field
|
return field
|
||||||
@@ -349,6 +369,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
const val STR_URL = "URL"
|
const val STR_URL = "URL"
|
||||||
const val STR_NOTES = "Notes"
|
const val STR_NOTES = "Notes"
|
||||||
|
|
||||||
|
private const val FIXED_LENGTH_SIZE: Long = 128 // Approximate fixed length size
|
||||||
|
|
||||||
fun newCustomNameAllowed(name: String): Boolean {
|
fun newCustomNameAllowed(name: String): Boolean {
|
||||||
return !(name.equals(STR_TITLE, true)
|
return !(name.equals(STR_TITLE, true)
|
||||||
|| name.equals(STR_USERNAME, true)
|
|| name.equals(STR_USERNAME, true)
|
||||||
@@ -367,7 +389,5 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
return arrayOfNulls(size)
|
return arrayOfNulls(size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val FIXED_LENGTH_SIZE: Long = 128 // Approximate fixed length size
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ abstract class EntryVersioned
|
|||||||
|
|
||||||
constructor(parcel: Parcel) : super(parcel)
|
constructor(parcel: Parcel) : super(parcel)
|
||||||
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
super.writeToParcel(dest, flags)
|
||||||
|
}
|
||||||
|
|
||||||
override fun nodeIndexInParentForNaturalOrder(): Int {
|
override fun nodeIndexInParentForNaturalOrder(): Int {
|
||||||
if (nodeIndexInParentForNaturalOrder == -1) {
|
if (nodeIndexInParentForNaturalOrder == -1) {
|
||||||
val numberOfGroups = parent?.getChildGroups()?.size
|
val numberOfGroups = parent?.getChildGroups()?.size
|
||||||
|
|||||||
@@ -20,267 +20,123 @@
|
|||||||
package com.kunzisoft.keepass.database.element.entry
|
package com.kunzisoft.keepass.database.element.entry
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.search.EntryKDBXSearchHandler
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class FieldReferencesEngine {
|
class FieldReferencesEngine(private val mDatabase: DatabaseKDBX) {
|
||||||
|
|
||||||
inner class TargetResult(var entry: EntryKDBX?, var wanted: Char)
|
// Key : <WantedField>@<SearchIn>:<Text>
|
||||||
|
// Value : content
|
||||||
|
private var refsCache: MutableMap<String, String?> = HashMap()
|
||||||
|
|
||||||
private inner class SprContextV4 {
|
fun clear() {
|
||||||
|
refsCache.clear()
|
||||||
var databaseV4: DatabaseKDBX? = null
|
|
||||||
var entry: EntryKDBX
|
|
||||||
var refsCache: MutableMap<String, String> = HashMap()
|
|
||||||
|
|
||||||
internal constructor(db: DatabaseKDBX, entry: EntryKDBX) {
|
|
||||||
this.databaseV4 = db
|
|
||||||
this.entry = entry
|
|
||||||
}
|
|
||||||
|
|
||||||
internal constructor(source: SprContextV4) {
|
|
||||||
this.databaseV4 = source.databaseV4
|
|
||||||
this.entry = source.entry
|
|
||||||
this.refsCache = source.refsCache
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun compile(text: String, entry: EntryKDBX, database: DatabaseKDBX): String {
|
fun compile(textReference: String, recursionLevel: Int): String {
|
||||||
return compileInternal(text, SprContextV4(database, entry), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun compileInternal(text: String?, sprContextV4: SprContextV4?, recursionLevel: Int): String {
|
|
||||||
if (text == null) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if (sprContextV4 == null) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return if (recursionLevel >= MAX_RECURSION_DEPTH) {
|
return if (recursionLevel >= MAX_RECURSION_DEPTH) {
|
||||||
""
|
""
|
||||||
} else fillRefPlaceholders(text, sprContextV4, recursionLevel)
|
} else
|
||||||
|
fillReferencesPlaceholders(textReference, recursionLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fillRefPlaceholders(textReference: String, contextV4: SprContextV4, recursionLevel: Int): String {
|
/**
|
||||||
var text = textReference
|
* Manage placeholders with {REF:<WantedField>@<SearchIn>:<Text>}
|
||||||
|
*/
|
||||||
if (contextV4.databaseV4 == null) {
|
private fun fillReferencesPlaceholders(textReference: String, recursionLevel: Int): String {
|
||||||
return text
|
var textValue = textReference
|
||||||
}
|
|
||||||
|
|
||||||
var offset = 0
|
var offset = 0
|
||||||
for (i in 0..19) {
|
var numberInlineRef = 0
|
||||||
text = fillRefsUsingCache(text, contextV4)
|
while (textValue.contains(STR_REF_START)
|
||||||
|
&& numberInlineRef <= MAX_INLINE_REF) {
|
||||||
|
numberInlineRef++
|
||||||
|
|
||||||
val start = text.indexOf(STR_REF_START, offset, true)
|
textValue = fillReferencesUsingCache(textValue)
|
||||||
|
|
||||||
|
val start = textValue.indexOf(STR_REF_START, offset, true)
|
||||||
if (start < 0) {
|
if (start < 0) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
val end = text.indexOf(STR_REF_END, start + 1, true)
|
val end = textValue.indexOf(STR_REF_END, offset, true)
|
||||||
if (end <= start) {
|
if (end <= start) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
val fullRef = text.substring(start, end + 1)
|
val reference = textValue.substring(start + STR_REF_START.length, end)
|
||||||
val result = findRefTarget(fullRef, contextV4)
|
val fullReference = "$STR_REF_START$reference$STR_REF_END"
|
||||||
|
|
||||||
if (result != null) {
|
if (!refsCache.containsKey(fullReference)) {
|
||||||
val found = result.entry
|
val result = findReferenceTarget(reference)
|
||||||
found?.stopToManageFieldReferences()
|
val entryFound = result.entry
|
||||||
val wanted = result.wanted
|
val newRecursionLevel = recursionLevel + 1
|
||||||
|
val data: String? = when (result.wanted) {
|
||||||
var data: String? = null
|
'T' -> entryFound?.decodeTitleKey(newRecursionLevel)
|
||||||
when (wanted) {
|
'U' -> entryFound?.decodeUsernameKey(newRecursionLevel)
|
||||||
'T' -> data = found?.title
|
'A' -> entryFound?.decodeUrlKey(newRecursionLevel)
|
||||||
'U' -> data = found?.username
|
'P' -> entryFound?.decodePasswordKey(newRecursionLevel)
|
||||||
'A' -> data = found?.url
|
'N' -> entryFound?.decodeNotesKey(newRecursionLevel)
|
||||||
'P' -> data = found?.password
|
'I' -> UuidUtil.toHexString(entryFound?.nodeId?.id)
|
||||||
'N' -> data = found?.notes
|
else -> null
|
||||||
'I' -> data = found?.nodeId.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data != null && found != null) {
|
|
||||||
val subCtx = SprContextV4(contextV4)
|
|
||||||
subCtx.entry = found
|
|
||||||
|
|
||||||
val innerContent = compileInternal(data, subCtx, recursionLevel + 1)
|
|
||||||
addRefsToCache(fullRef, innerContent, contextV4)
|
|
||||||
text = fillRefsUsingCache(text, contextV4)
|
|
||||||
} else {
|
|
||||||
offset = start + 1
|
|
||||||
}
|
}
|
||||||
|
refsCache[fullReference] = data
|
||||||
|
textValue = fillReferencesUsingCache(textValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
offset = end
|
||||||
}
|
}
|
||||||
|
return textValue
|
||||||
return text
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findRefTarget(fullReference: String?, contextV4: SprContextV4): TargetResult? {
|
private fun fillReferencesUsingCache(text: String): String {
|
||||||
var fullRef: String? = fullReference ?: return null
|
|
||||||
|
|
||||||
fullRef = fullRef!!.toUpperCase(Locale.ENGLISH)
|
|
||||||
if (!fullRef.startsWith(STR_REF_START) || !fullRef.endsWith(STR_REF_END)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val ref = fullRef.substring(STR_REF_START.length, fullRef.length - STR_REF_END.length)
|
|
||||||
if (ref.length <= 4) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (ref[1] != '@') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (ref[3] != ':') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val scan = Character.toUpperCase(ref[2])
|
|
||||||
val wanted = Character.toUpperCase(ref[0])
|
|
||||||
|
|
||||||
val searchParameters = SearchParameters()
|
|
||||||
searchParameters.setupNone()
|
|
||||||
|
|
||||||
searchParameters.searchString = ref.substring(4)
|
|
||||||
when (scan) {
|
|
||||||
'T' -> searchParameters.searchInTitles = true
|
|
||||||
'U' -> searchParameters.searchInUserNames = true
|
|
||||||
'A' -> searchParameters.searchInUrls = true
|
|
||||||
'P' -> searchParameters.searchInPasswords = true
|
|
||||||
'N' -> searchParameters.searchInNotes = true
|
|
||||||
'I' -> searchParameters.searchInUUIDs = true
|
|
||||||
'O' -> searchParameters.searchInOther = true
|
|
||||||
else -> return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val list = ArrayList<EntryKDBX>()
|
|
||||||
searchEntries(contextV4.databaseV4?.rootGroup, searchParameters, list)
|
|
||||||
|
|
||||||
return if (list.size > 0) {
|
|
||||||
TargetResult(list[0], wanted)
|
|
||||||
} else null
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addRefsToCache(ref: String?, value: String?, ctx: SprContextV4?) {
|
|
||||||
if (ref == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (value == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (ctx == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctx.refsCache.containsKey(ref)) {
|
|
||||||
ctx.refsCache[ref] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fillRefsUsingCache(text: String, sprContextV4: SprContextV4): String {
|
|
||||||
var newText = text
|
var newText = text
|
||||||
for ((key, value) in sprContextV4.refsCache) {
|
for ((key, value) in refsCache) {
|
||||||
newText = text.replace(key, value, true)
|
// Replace by key if value not found
|
||||||
|
newText = newText.replace(key, value ?: key, true)
|
||||||
}
|
}
|
||||||
return newText
|
return newText
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchEntries(root: GroupKDBX?, searchParameters: SearchParameters?, listStorage: MutableList<EntryKDBX>?) {
|
private fun findReferenceTarget(reference: String): TargetResult {
|
||||||
if (searchParameters == null) {
|
|
||||||
return
|
val targetResult = TargetResult(null, 'J')
|
||||||
|
|
||||||
|
if (reference.length <= 4) {
|
||||||
|
return targetResult
|
||||||
}
|
}
|
||||||
if (listStorage == null) {
|
if (reference[1] != '@') {
|
||||||
return
|
return targetResult
|
||||||
|
}
|
||||||
|
if (reference[3] != ':') {
|
||||||
|
return targetResult
|
||||||
}
|
}
|
||||||
|
|
||||||
val terms = splitStringTerms(searchParameters.searchString)
|
targetResult.wanted = Character.toUpperCase(reference[0])
|
||||||
if (terms.size <= 1 || searchParameters.regularExpression) {
|
val searchIn = Character.toUpperCase(reference[2])
|
||||||
root!!.doForEachChild(EntryKDBXSearchHandler(searchParameters, listStorage), null)
|
val searchQuery = reference.substring(4)
|
||||||
return
|
targetResult.entry = when (searchIn) {
|
||||||
}
|
'T' -> mDatabase.getEntryByTitle(searchQuery)
|
||||||
|
'U' -> mDatabase.getEntryByUsername(searchQuery)
|
||||||
// Search longest term first
|
'A' -> mDatabase.getEntryByURL(searchQuery)
|
||||||
val stringLengthComparator = Comparator<String> { lhs, rhs -> lhs.length - rhs.length }
|
'P' -> mDatabase.getEntryByPassword(searchQuery)
|
||||||
Collections.sort(terms, stringLengthComparator)
|
'N' -> mDatabase.getEntryByNotes(searchQuery)
|
||||||
|
'I' -> {
|
||||||
val fullSearch = searchParameters.searchString
|
UuidUtil.fromHexString(searchQuery)?.let { uuid ->
|
||||||
var childEntries: List<EntryKDBX>? = root!!.getChildEntries()
|
mDatabase.getEntryById(NodeIdUUID(uuid))
|
||||||
for (i in terms.indices) {
|
|
||||||
val pgNew = ArrayList<EntryKDBX>()
|
|
||||||
|
|
||||||
searchParameters.searchString = terms[i]
|
|
||||||
|
|
||||||
var negate = false
|
|
||||||
if (searchParameters.searchString.startsWith("-")) {
|
|
||||||
searchParameters.searchString = searchParameters.searchString.substring(1)
|
|
||||||
negate = searchParameters.searchString.isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!root.doForEachChild(EntryKDBXSearchHandler(searchParameters, pgNew), null)) {
|
|
||||||
childEntries = null
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
childEntries = if (negate) {
|
|
||||||
val complement = ArrayList<EntryKDBX>()
|
|
||||||
for (entry in childEntries!!) {
|
|
||||||
if (!pgNew.contains(entry)) {
|
|
||||||
complement.add(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
complement
|
|
||||||
} else {
|
|
||||||
pgNew
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (childEntries != null) {
|
|
||||||
listStorage.addAll(childEntries)
|
|
||||||
}
|
|
||||||
searchParameters.searchString = fullSearch
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a list of String by split text when ' ', '\t', '\r' or '\n' is found
|
|
||||||
*/
|
|
||||||
private fun splitStringTerms(text: String?): List<String> {
|
|
||||||
val list = ArrayList<String>()
|
|
||||||
if (text == null) {
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
val stringBuilder = StringBuilder()
|
|
||||||
var quoted = false
|
|
||||||
|
|
||||||
for (element in text) {
|
|
||||||
|
|
||||||
if ((element == ' ' || element == '\t' || element == '\r' || element == '\n') && !quoted) {
|
|
||||||
|
|
||||||
val len = stringBuilder.length
|
|
||||||
when {
|
|
||||||
len > 0 -> {
|
|
||||||
list.add(stringBuilder.toString())
|
|
||||||
stringBuilder.delete(0, len)
|
|
||||||
}
|
|
||||||
element == '\"' -> quoted = !quoted
|
|
||||||
else -> stringBuilder.append(element)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
'O' -> mDatabase.getEntryByCustomData(searchQuery)
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
|
return targetResult
|
||||||
if (stringBuilder.isNotEmpty()) {
|
|
||||||
list.add(stringBuilder.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
return list
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class TargetResult(var entry: EntryKDBX?, var wanted: Char)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val MAX_RECURSION_DEPTH = 12
|
private const val MAX_RECURSION_DEPTH = 10
|
||||||
|
private const val MAX_INLINE_REF = 10
|
||||||
private const val STR_REF_START = "{REF:"
|
private const val STR_REF_START = "{REF:"
|
||||||
private const val STR_REF_END = "}"
|
private const val STR_REF_END = "}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,14 +31,12 @@ import java.util.*
|
|||||||
|
|
||||||
class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface {
|
class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface {
|
||||||
|
|
||||||
var level = 0 // short
|
|
||||||
// Used by KeePass internally, don't use
|
// Used by KeePass internally, don't use
|
||||||
var groupFlags = 0
|
var groupFlags = 0
|
||||||
|
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
|
|
||||||
constructor(parcel: Parcel) : super(parcel) {
|
constructor(parcel: Parcel) : super(parcel) {
|
||||||
level = parcel.readInt()
|
|
||||||
groupFlags = parcel.readInt()
|
groupFlags = parcel.readInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,13 +50,11 @@ class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
super.writeToParcel(dest, flags)
|
super.writeToParcel(dest, flags)
|
||||||
dest.writeInt(level)
|
|
||||||
dest.writeInt(groupFlags)
|
dest.writeInt(groupFlags)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateWith(source: GroupKDB) {
|
fun updateWith(source: GroupKDB) {
|
||||||
super.updateWith(source)
|
super.updateWith(source)
|
||||||
level = source.level
|
|
||||||
groupFlags = source.groupFlags
|
groupFlags = source.groupFlags
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,15 +69,12 @@ class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
return NodeIdInt(nodeId.id)
|
return NodeIdInt(nodeId.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun afterAssignNewParent() {
|
|
||||||
if (parent != null)
|
|
||||||
level = parent!!.level + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setGroupId(groupId: Int) {
|
fun setGroupId(groupId: Int) {
|
||||||
this.nodeId = NodeIdInt(groupId)
|
this.nodeId = NodeIdInt(groupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun afterAssignNewParent() {}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
|
|||||||
@@ -63,6 +63,17 @@ abstract class GroupVersioned
|
|||||||
get() = titleGroup
|
get() = titleGroup
|
||||||
set(value) { titleGroup = value }
|
set(value) { titleGroup = value }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To determine the level from the root group (root group level is -1)
|
||||||
|
*/
|
||||||
|
fun getLevel(): Int {
|
||||||
|
var level = -1
|
||||||
|
parent?.let { parent ->
|
||||||
|
level = parent.getLevel() + 1
|
||||||
|
}
|
||||||
|
return level
|
||||||
|
}
|
||||||
|
|
||||||
override fun getChildGroups(): List<Group> {
|
override fun getChildGroups(): List<Group> {
|
||||||
return childGroups
|
return childGroups
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,23 +45,64 @@ interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>,
|
|||||||
groupHandler.operate(this as Group)
|
groupHandler.operate(this as Group)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun doForEachChild(entryHandler: NodeHandler<Entry>,
|
fun doForEachChild(entryHandler: NodeHandler<Entry>?,
|
||||||
groupHandler: NodeHandler<Group>?,
|
groupHandler: NodeHandler<Group>?,
|
||||||
stopIterationWhenGroupHandlerFails: Boolean = true): Boolean {
|
stopIterationWhenGroupHandlerOperateFalse: Boolean = true): Boolean {
|
||||||
for (entry in this.getChildEntries()) {
|
if (entryHandler != null) {
|
||||||
if (!entryHandler.operate(entry))
|
for (entry in this.getChildEntries()) {
|
||||||
return false
|
if (!entryHandler.operate(entry))
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (group in this.getChildGroups()) {
|
for (group in this.getChildGroups()) {
|
||||||
var doActionForChild = true
|
var doActionForChild = true
|
||||||
if (groupHandler != null && !groupHandler.operate(group)) {
|
if (groupHandler != null && !groupHandler.operate(group)) {
|
||||||
doActionForChild = false
|
doActionForChild = false
|
||||||
if (stopIterationWhenGroupHandlerFails)
|
if (stopIterationWhenGroupHandlerOperateFalse)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (doActionForChild)
|
if (doActionForChild)
|
||||||
group.doForEachChild(entryHandler, groupHandler)
|
group.doForEachChild(entryHandler, groupHandler, stopIterationWhenGroupHandlerOperateFalse)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun searchChildEntry(criteria: (entry: Entry) -> Boolean): Entry? {
|
||||||
|
return searchChildEntry(this, criteria)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchChildEntry(rootGroup: GroupVersionedInterface<Group, Entry>,
|
||||||
|
criteria: (entry: Entry) -> Boolean): Entry? {
|
||||||
|
for (childEntry in rootGroup.getChildEntries()) {
|
||||||
|
if (criteria.invoke(childEntry)) {
|
||||||
|
return childEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (group in rootGroup.getChildGroups()) {
|
||||||
|
val searchChildEntry = searchChildEntry(group, criteria)
|
||||||
|
if (searchChildEntry != null) {
|
||||||
|
return searchChildEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun searchChildGroup(criteria: (group: Group) -> Boolean): Group? {
|
||||||
|
return searchChildGroup(this, criteria)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchChildGroup(rootGroup: GroupVersionedInterface<Group, Entry>,
|
||||||
|
criteria: (group: Group) -> Boolean): Group? {
|
||||||
|
for (childGroup in rootGroup.getChildGroups()) {
|
||||||
|
if (criteria.invoke(childGroup)) {
|
||||||
|
return childGroup
|
||||||
|
} else {
|
||||||
|
val subGroup = searchChildGroup(childGroup, criteria)
|
||||||
|
if (subGroup != null) {
|
||||||
|
return subGroup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.element.icon
|
|||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
|
||||||
class IconImage() : IconImageDraw(), Parcelable {
|
class IconImage() : IconImageDraw() {
|
||||||
|
|
||||||
var standard: IconImageStandard = IconImageStandard()
|
var standard: IconImageStandard = IconImageStandard()
|
||||||
var custom: IconImageCustom = IconImageCustom()
|
var custom: IconImageCustom = IconImageCustom()
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import android.os.Parcelable
|
|||||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class IconImageCustom : Parcelable, IconImageDraw {
|
class IconImageCustom : IconImageDraw {
|
||||||
|
|
||||||
var uuid: UUID
|
var uuid: UUID
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,9 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element.icon
|
package com.kunzisoft.keepass.database.element.icon
|
||||||
|
|
||||||
abstract class IconImageDraw {
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
abstract class IconImageDraw : Parcelable {
|
||||||
|
|
||||||
var selected = false
|
var selected = false
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import android.os.Parcel
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
|
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
|
||||||
|
|
||||||
class IconImageStandard : Parcelable, IconImageDraw {
|
class IconImageStandard : IconImageDraw {
|
||||||
|
|
||||||
val id: Int
|
val id: Int
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class NoMemoryDatabaseException: LoadDatabaseException {
|
|||||||
constructor(exception: Throwable) : super(exception)
|
constructor(exception: Throwable) : super(exception)
|
||||||
}
|
}
|
||||||
|
|
||||||
class EntryDatabaseException: LoadDatabaseException {
|
class MoveEntryDatabaseException: LoadDatabaseException {
|
||||||
@StringRes
|
@StringRes
|
||||||
override var errorId: Int = R.string.error_move_entry_here
|
override var errorId: Int = R.string.error_move_entry_here
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
@@ -123,7 +123,7 @@ class EntryDatabaseException: LoadDatabaseException {
|
|||||||
|
|
||||||
class MoveGroupDatabaseException: LoadDatabaseException {
|
class MoveGroupDatabaseException: LoadDatabaseException {
|
||||||
@StringRes
|
@StringRes
|
||||||
override var errorId: Int = R.string.error_move_folder_in_itself
|
override var errorId: Int = R.string.error_move_group_here
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
constructor(exception: Throwable) : super(exception)
|
constructor(exception: Throwable) : super(exception)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import java.security.MessageDigest
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.CipherInputStream
|
import javax.crypto.CipherInputStream
|
||||||
|
import kotlin.collections.HashMap
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -154,11 +155,10 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
|
|
||||||
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
|
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
|
||||||
val newRoot = mDatabase.createGroup()
|
val newRoot = mDatabase.createGroup()
|
||||||
newRoot.level = -1
|
|
||||||
mDatabase.rootGroup = newRoot
|
mDatabase.rootGroup = newRoot
|
||||||
mDatabase.addGroupIndex(newRoot)
|
|
||||||
|
|
||||||
// Import all nodes
|
// Import all nodes
|
||||||
|
val groupLevelList = HashMap<GroupKDB, Int>()
|
||||||
var newGroup: GroupKDB? = null
|
var newGroup: GroupKDB? = null
|
||||||
var newEntry: EntryKDB? = null
|
var newEntry: EntryKDB? = null
|
||||||
var currentGroupNumber = 0
|
var currentGroupNumber = 0
|
||||||
@@ -248,7 +248,7 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
}
|
}
|
||||||
0x0008 -> {
|
0x0008 -> {
|
||||||
newGroup?.let { group ->
|
newGroup?.let { group ->
|
||||||
group.level = cipherInputStream.readBytes2ToUShort()
|
groupLevelList.put(group, cipherInputStream.readBytes2ToUShort())
|
||||||
} ?:
|
} ?:
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
entry.notes = cipherInputStream.readBytesToString(fieldSize)
|
entry.notes = cipherInputStream.readBytesToString(fieldSize)
|
||||||
@@ -318,7 +318,7 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
if (!Arrays.equals(messageDigest.digest(), header.contentsHash)) {
|
if (!Arrays.equals(messageDigest.digest(), header.contentsHash)) {
|
||||||
throw InvalidCredentialsDatabaseException()
|
throw InvalidCredentialsDatabaseException()
|
||||||
}
|
}
|
||||||
constructTreeFromIndex()
|
constructTreeFromIndex(groupLevelList)
|
||||||
|
|
||||||
stopContentTimer()
|
stopContentTimer()
|
||||||
|
|
||||||
@@ -339,34 +339,40 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
return mDatabase
|
return mDatabase
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildTreeGroups(previousGroup: GroupKDB, currentGroup: GroupKDB, groupIterator: Iterator<GroupKDB>) {
|
private fun buildTreeGroups(groupLevelList: HashMap<GroupKDB, Int>,
|
||||||
|
previousGroup: GroupKDB,
|
||||||
|
currentGroup: GroupKDB,
|
||||||
|
groupIterator: Iterator<GroupKDB>) {
|
||||||
|
|
||||||
if (currentGroup.parent == null && (previousGroup.level + 1) == currentGroup.level) {
|
val previousGroupLevel = groupLevelList[previousGroup] ?: -1
|
||||||
|
val currentGroupLevel = groupLevelList[currentGroup] ?: -1
|
||||||
|
|
||||||
|
if (currentGroup.parent == null && (previousGroupLevel + 1) == currentGroupLevel) {
|
||||||
// Current group has an increment level compare to the previous, current group is a child
|
// Current group has an increment level compare to the previous, current group is a child
|
||||||
previousGroup.addChildGroup(currentGroup)
|
previousGroup.addChildGroup(currentGroup)
|
||||||
currentGroup.parent = previousGroup
|
currentGroup.parent = previousGroup
|
||||||
} else if (previousGroup.parent != null && previousGroup.level == currentGroup.level) {
|
} else if (previousGroup.parent != null && previousGroupLevel == currentGroupLevel) {
|
||||||
// In the same level, previous parent is the same as previous group
|
// In the same level, previous parent is the same as previous group
|
||||||
previousGroup.parent!!.addChildGroup(currentGroup)
|
previousGroup.parent!!.addChildGroup(currentGroup)
|
||||||
currentGroup.parent = previousGroup.parent
|
currentGroup.parent = previousGroup.parent
|
||||||
} else if (previousGroup.parent != null) {
|
} else if (previousGroup.parent != null) {
|
||||||
// Previous group has a higher level than the current group, check it's parent
|
// Previous group has a higher level than the current group, check it's parent
|
||||||
buildTreeGroups(previousGroup.parent!!, currentGroup, groupIterator)
|
buildTreeGroups(groupLevelList, previousGroup.parent!!, currentGroup, groupIterator)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next current group
|
// Next current group
|
||||||
if (groupIterator.hasNext()){
|
if (groupIterator.hasNext()){
|
||||||
buildTreeGroups(currentGroup, groupIterator.next(), groupIterator)
|
buildTreeGroups(groupLevelList, currentGroup, groupIterator.next(), groupIterator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun constructTreeFromIndex() {
|
private fun constructTreeFromIndex(groupLevelList: HashMap<GroupKDB, Int>) {
|
||||||
mDatabase.rootGroup?.let {
|
mDatabase.rootGroup?.let { root ->
|
||||||
|
|
||||||
// add each group
|
// add each group
|
||||||
val groupIterator = mDatabase.getGroupIndexes().iterator()
|
val groupIterator = mDatabase.getGroupIndexes().iterator()
|
||||||
if (groupIterator.hasNext())
|
if (groupIterator.hasNext())
|
||||||
buildTreeGroups(it, groupIterator.next(), groupIterator)
|
buildTreeGroups(groupLevelList, root, groupIterator.next(), groupIterator)
|
||||||
|
|
||||||
// add each child
|
// add each child
|
||||||
for (currentEntry in mDatabase.getEntryIndexes()) {
|
for (currentEntry in mDatabase.getEntryIndexes()) {
|
||||||
|
|||||||
@@ -151,9 +151,11 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
val cipher: Cipher
|
val cipher: Cipher
|
||||||
try {
|
try {
|
||||||
engine = EncryptionAlgorithm.getFrom(mDatabase.cipherUuid).cipherEngine
|
engine = EncryptionAlgorithm.getFrom(mDatabase.cipherUuid).cipherEngine
|
||||||
|
engine.forcePaddingCompatibility = true
|
||||||
mDatabase.setDataEngine(engine)
|
mDatabase.setDataEngine(engine)
|
||||||
mDatabase.encryptionAlgorithm = engine.getEncryptionAlgorithm()
|
mDatabase.encryptionAlgorithm = engine.getEncryptionAlgorithm()
|
||||||
cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV)
|
cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV)
|
||||||
|
engine.forcePaddingCompatibility = false
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw InvalidAlgorithmDatabaseException(e)
|
throw InvalidAlgorithmDatabaseException(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,15 +20,15 @@
|
|||||||
package com.kunzisoft.keepass.database.file.output
|
package com.kunzisoft.keepass.database.file.output
|
||||||
|
|
||||||
import com.kunzisoft.encrypt.HashManager
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
|
||||||
import com.kunzisoft.keepass.utils.write2BytesUShort
|
|
||||||
import com.kunzisoft.keepass.utils.write4BytesUInt
|
|
||||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
import com.kunzisoft.keepass.utils.write2BytesUShort
|
||||||
|
import com.kunzisoft.keepass.utils.write4BytesUInt
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -59,6 +59,8 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
override fun output() {
|
override fun output() {
|
||||||
// Before we output the header, we should sort our list of groups
|
// Before we output the header, we should sort our list of groups
|
||||||
// and remove any orphaned nodes that are no longer part of the tree hierarchy
|
// and remove any orphaned nodes that are no longer part of the tree hierarchy
|
||||||
|
// also remove the virtual root not present in kdb
|
||||||
|
val rootGroup = mDatabaseKDB.rootGroup
|
||||||
sortGroupsForOutput()
|
sortGroupsForOutput()
|
||||||
|
|
||||||
val header = outputHeader(mOutputStream)
|
val header = outputHeader(mOutputStream)
|
||||||
@@ -86,8 +88,10 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
throw DatabaseOutputException("Invalid algorithm parameter.", e)
|
throw DatabaseOutputException("Invalid algorithm parameter.", e)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
throw DatabaseOutputException("Failed to output final encrypted part.", e)
|
throw DatabaseOutputException("Failed to output final encrypted part.", e)
|
||||||
|
} finally {
|
||||||
|
// Add again the virtual root group for better management
|
||||||
|
mDatabaseKDB.rootGroup = rootGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(DatabaseOutputException::class)
|
@Throws(DatabaseOutputException::class)
|
||||||
@@ -201,7 +205,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
|
|
||||||
private fun sortGroupsForOutput() {
|
private fun sortGroupsForOutput() {
|
||||||
val groupList = ArrayList<GroupKDB>()
|
val groupList = ArrayList<GroupKDB>()
|
||||||
// Rebuild list according to coalation sorting order removing any orphaned groups
|
// Rebuild list according to sorting order removing any orphaned groups
|
||||||
for (rootGroup in mDatabaseKDB.rootGroups) {
|
for (rootGroup in mDatabaseKDB.rootGroups) {
|
||||||
sortGroup(rootGroup, groupList)
|
sortGroup(rootGroup, groupList)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,9 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.file.output
|
package com.kunzisoft.keepass.database.file.output
|
||||||
|
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
|
||||||
import com.kunzisoft.keepass.utils.dateTo5Bytes
|
|
||||||
import com.kunzisoft.keepass.utils.uIntTo4Bytes
|
|
||||||
import com.kunzisoft.keepass.utils.uShortTo2Bytes
|
|
||||||
import com.kunzisoft.keepass.utils.writeStringToStream
|
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||||
|
import com.kunzisoft.keepass.utils.*
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
@@ -77,7 +73,7 @@ class GroupOutputKDB(private val mGroup: GroupKDB,
|
|||||||
// Level
|
// Level
|
||||||
mOutputStream.write(LEVEL_FIELD_TYPE)
|
mOutputStream.write(LEVEL_FIELD_TYPE)
|
||||||
mOutputStream.write(LEVEL_FIELD_SIZE)
|
mOutputStream.write(LEVEL_FIELD_SIZE)
|
||||||
mOutputStream.write(uShortTo2Bytes(mGroup.level))
|
mOutputStream.write(uShortTo2Bytes(mGroup.getLevel()))
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
mOutputStream.write(FLAGS_FIELD_TYPE)
|
mOutputStream.write(FLAGS_FIELD_TYPE)
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 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.database.search
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
|
||||||
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDBX
|
|
||||||
|
|
||||||
class EntryKDBXSearchHandler(private val mSearchParametersKDBX: SearchParameters,
|
|
||||||
private val mListStorage: MutableList<EntryKDBX>)
|
|
||||||
: NodeHandler<EntryKDBX>() {
|
|
||||||
|
|
||||||
override fun operate(node: EntryKDBX): Boolean {
|
|
||||||
|
|
||||||
if (mSearchParametersKDBX.excludeExpired
|
|
||||||
&& node.isCurrentlyExpires) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchStrings(node)) {
|
|
||||||
mListStorage.add(node)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchInGroupNames(node)) {
|
|
||||||
mListStorage.add(node)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchInUUID(node)) {
|
|
||||||
mListStorage.add(node)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchInGroupNames(entry: EntryKDBX): Boolean {
|
|
||||||
if (mSearchParametersKDBX.searchInGroupNames) {
|
|
||||||
val parent = entry.parent
|
|
||||||
if (parent != null) {
|
|
||||||
return parent.title
|
|
||||||
.contains(mSearchParametersKDBX.searchString, mSearchParametersKDBX.ignoreCase)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchInUUID(entry: EntryKDBX): Boolean {
|
|
||||||
if (mSearchParametersKDBX.searchInUUIDs) {
|
|
||||||
return UuidUtil.toHexString(entry.id)
|
|
||||||
.contains(mSearchParametersKDBX.searchString, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchStrings(entry: EntryKDBX): Boolean {
|
|
||||||
val iterator = EntrySearchStringIteratorKDBX(entry, mSearchParametersKDBX)
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
val stringValue = iterator.next()
|
|
||||||
if (stringValue.isNotEmpty()) {
|
|
||||||
if (stringValue.contains(mSearchParametersKDBX.searchString, mSearchParametersKDBX.ignoreCase)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,15 +24,68 @@ import com.kunzisoft.keepass.database.action.node.NodeHandler
|
|||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDB
|
|
||||||
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDBX
|
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_FIELD
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
import com.kunzisoft.keepass.utils.StringUtil.flattenToAscii
|
||||||
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
|
|
||||||
class SearchHelper {
|
class SearchHelper {
|
||||||
|
|
||||||
|
private var incrementEntry = 0
|
||||||
|
|
||||||
|
fun createVirtualGroupWithSearchResult(database: Database,
|
||||||
|
searchParameters: SearchParameters,
|
||||||
|
omitBackup: Boolean,
|
||||||
|
max: Int): Group? {
|
||||||
|
|
||||||
|
val searchGroup = database.createGroup()
|
||||||
|
searchGroup?.isVirtual = true
|
||||||
|
searchGroup?.title = "\"" + searchParameters.searchQuery + "\""
|
||||||
|
|
||||||
|
// Search all entries
|
||||||
|
incrementEntry = 0
|
||||||
|
database.rootGroup?.doForEachChild(
|
||||||
|
object : NodeHandler<Entry>() {
|
||||||
|
override fun operate(node: Entry): Boolean {
|
||||||
|
if (incrementEntry >= max)
|
||||||
|
return false
|
||||||
|
if (entryContainsString(database, node, searchParameters)) {
|
||||||
|
searchGroup?.addChildEntry(node)
|
||||||
|
incrementEntry++
|
||||||
|
}
|
||||||
|
// Stop searching when we have max entries
|
||||||
|
return incrementEntry < max
|
||||||
|
}
|
||||||
|
},
|
||||||
|
object : NodeHandler<Group>() {
|
||||||
|
override fun operate(node: Group): Boolean {
|
||||||
|
return when {
|
||||||
|
incrementEntry >= max -> false
|
||||||
|
database.isGroupSearchable(node, omitBackup) -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false)
|
||||||
|
|
||||||
|
return searchGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun entryContainsString(database: Database,
|
||||||
|
entry: Entry,
|
||||||
|
searchParameters: SearchParameters): Boolean {
|
||||||
|
// To search in field references
|
||||||
|
database.startManageEntry(entry)
|
||||||
|
// Search all strings in the entry
|
||||||
|
val searchFound = searchInEntry(entry, searchParameters)
|
||||||
|
database.stopManageEntry(entry)
|
||||||
|
|
||||||
|
return searchFound
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MAX_SEARCH_ENTRY = 10
|
const val MAX_SEARCH_ENTRY = 10
|
||||||
|
|
||||||
@@ -70,75 +123,67 @@ class SearchHelper {
|
|||||||
onDatabaseClosed.invoke()
|
onDatabaseClosed.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private var incrementEntry = 0
|
/**
|
||||||
|
* Return true if the search query in search parameters is found in available parameters
|
||||||
|
*/
|
||||||
|
fun searchInEntry(entry: Entry,
|
||||||
|
searchParameters: SearchParameters): Boolean {
|
||||||
|
val searchQuery = searchParameters.searchQuery
|
||||||
|
// Entry don't contains string if the search string is empty
|
||||||
|
if (searchQuery.isEmpty())
|
||||||
|
return false
|
||||||
|
|
||||||
fun createVirtualGroupWithSearchResult(database: Database,
|
// Search all strings in the KDBX entry
|
||||||
searchQuery: String,
|
if (searchParameters.searchInTitles) {
|
||||||
searchParameters: SearchParameters,
|
if (checkSearchQuery(entry.title, searchParameters))
|
||||||
omitBackup: Boolean,
|
return true
|
||||||
max: Int): Group? {
|
}
|
||||||
|
if (searchParameters.searchInUserNames) {
|
||||||
val searchGroup = database.createGroup()
|
if (checkSearchQuery(entry.username, searchParameters))
|
||||||
searchGroup?.isVirtual = true
|
return true
|
||||||
searchGroup?.title = "\"" + searchQuery + "\""
|
}
|
||||||
|
if (searchParameters.searchInPasswords) {
|
||||||
// Search all entries
|
if (checkSearchQuery(entry.password, searchParameters))
|
||||||
incrementEntry = 0
|
return true
|
||||||
database.rootGroup?.doForEachChild(
|
}
|
||||||
object : NodeHandler<Entry>() {
|
if (searchParameters.searchInUrls) {
|
||||||
override fun operate(node: Entry): Boolean {
|
if (checkSearchQuery(entry.url, searchParameters))
|
||||||
if (incrementEntry >= max)
|
return true
|
||||||
return false
|
}
|
||||||
if (entryContainsString(node, searchQuery, searchParameters)) {
|
if (searchParameters.searchInNotes) {
|
||||||
searchGroup?.addChildEntry(node)
|
if (checkSearchQuery(entry.notes, searchParameters))
|
||||||
incrementEntry++
|
return true
|
||||||
}
|
}
|
||||||
// Stop searching when we have max entries
|
if (searchParameters.searchInUUIDs) {
|
||||||
return incrementEntry < max
|
val hexString = UuidUtil.toHexString(entry.nodeId.id)
|
||||||
|
if (hexString != null && hexString.contains(searchQuery, true))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (searchParameters.searchInOther) {
|
||||||
|
entry.getExtraFields().forEach { field ->
|
||||||
|
if (field.name != OTP_FIELD
|
||||||
|
|| (field.name == OTP_FIELD && searchParameters.searchInOTP)) {
|
||||||
|
if (checkSearchQuery(field.protectedValue.toString(), searchParameters))
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
},
|
|
||||||
object : NodeHandler<Group>() {
|
|
||||||
override fun operate(node: Group): Boolean {
|
|
||||||
return when {
|
|
||||||
incrementEntry >= max -> false
|
|
||||||
database.isGroupSearchable(node, omitBackup) -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
false)
|
|
||||||
|
|
||||||
return searchGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun entryContainsString(entry: Entry,
|
|
||||||
searchQuery: String,
|
|
||||||
searchParameters: SearchParameters): Boolean {
|
|
||||||
|
|
||||||
// Entry don't contains string if the search string is empty
|
|
||||||
if (searchQuery.isEmpty())
|
|
||||||
return false
|
|
||||||
|
|
||||||
// Search all strings in the entry
|
|
||||||
var iterator: Iterator<String>? = null
|
|
||||||
entry.entryKDB?.let {
|
|
||||||
iterator = EntrySearchStringIteratorKDB(it, searchParameters)
|
|
||||||
}
|
|
||||||
entry.entryKDBX?.let {
|
|
||||||
iterator = EntrySearchStringIteratorKDBX(it, searchParameters)
|
|
||||||
}
|
|
||||||
|
|
||||||
iterator?.let {
|
|
||||||
while (it.hasNext()) {
|
|
||||||
val currentString = it.next()
|
|
||||||
if (currentString.isNotEmpty()
|
|
||||||
&& currentString.contains(searchQuery, true)) {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkSearchQuery(stringToCheck: String, searchParameters: SearchParameters): Boolean {
|
||||||
|
/*
|
||||||
|
// TODO Search settings
|
||||||
|
var regularExpression = false
|
||||||
|
var ignoreCase = true
|
||||||
|
var flattenToASCII = true
|
||||||
|
var excludeExpired = false
|
||||||
|
var searchOnlyInCurrentGroup = false
|
||||||
|
*/
|
||||||
|
return stringToCheck.isNotEmpty()
|
||||||
|
&& stringToCheck.flattenToAscii().contains(
|
||||||
|
searchParameters.searchQuery.flattenToAscii(), true)
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,57 +23,15 @@ package com.kunzisoft.keepass.database.search
|
|||||||
* Parameters for searching strings in the database.
|
* Parameters for searching strings in the database.
|
||||||
*/
|
*/
|
||||||
class SearchParameters {
|
class SearchParameters {
|
||||||
|
var searchQuery: String = ""
|
||||||
|
|
||||||
var searchString: String = ""
|
|
||||||
|
|
||||||
var regularExpression = false
|
|
||||||
var searchInTitles = true
|
var searchInTitles = true
|
||||||
var searchInUserNames = true
|
var searchInUserNames = true
|
||||||
var searchInPasswords = false
|
var searchInPasswords = false
|
||||||
var searchInUrls = true
|
var searchInUrls = true
|
||||||
var searchInGroupNames = false
|
|
||||||
var searchInNotes = true
|
var searchInNotes = true
|
||||||
var searchInOTP = false
|
var searchInOTP = false
|
||||||
var searchInOther = true
|
var searchInOther = true
|
||||||
var searchInUUIDs = false
|
var searchInUUIDs = false
|
||||||
var searchInTags = true
|
var searchInTags = true
|
||||||
var ignoreCase = true
|
|
||||||
var ignoreExpired = false
|
|
||||||
var excludeExpired = false
|
|
||||||
|
|
||||||
constructor()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy search parameters
|
|
||||||
* @param source
|
|
||||||
*/
|
|
||||||
constructor(source: SearchParameters) {
|
|
||||||
this.regularExpression = source.regularExpression
|
|
||||||
this.searchInTitles = source.searchInTitles
|
|
||||||
this.searchInUserNames = source.searchInUserNames
|
|
||||||
this.searchInPasswords = source.searchInPasswords
|
|
||||||
this.searchInUrls = source.searchInUrls
|
|
||||||
this.searchInGroupNames = source.searchInGroupNames
|
|
||||||
this.searchInNotes = source.searchInNotes
|
|
||||||
this.searchInOTP = source.searchInOTP
|
|
||||||
this.searchInOther = source.searchInOther
|
|
||||||
this.searchInUUIDs = source.searchInUUIDs
|
|
||||||
this.searchInTags = source.searchInTags
|
|
||||||
this.ignoreCase = source.ignoreCase
|
|
||||||
this.ignoreExpired = source.ignoreExpired
|
|
||||||
this.excludeExpired = source.excludeExpired
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setupNone() {
|
|
||||||
searchInTitles = false
|
|
||||||
searchInUserNames = false
|
|
||||||
searchInPasswords = false
|
|
||||||
searchInUrls = false
|
|
||||||
searchInGroupNames = false
|
|
||||||
searchInNotes = false
|
|
||||||
searchInOTP = false
|
|
||||||
searchInOther = false
|
|
||||||
searchInUUIDs = false
|
|
||||||
searchInTags = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.database.search;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static com.kunzisoft.keepass.utils.StreamBytesUtilsKt.uuidTo16Bytes;
|
|
||||||
|
|
||||||
public class UuidUtil {
|
|
||||||
|
|
||||||
public static String toHexString(UUID uuid) {
|
|
||||||
if (uuid == null) { return null; }
|
|
||||||
|
|
||||||
byte[] buf = uuidTo16Bytes(uuid);
|
|
||||||
|
|
||||||
int len = buf.length;
|
|
||||||
if (len == 0) { return ""; }
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
|
|
||||||
short bt;
|
|
||||||
char high, low;
|
|
||||||
for (byte b : buf) {
|
|
||||||
bt = (short) (b & 0xFF);
|
|
||||||
high = (char) (bt >>> 4);
|
|
||||||
low = (char) (bt & 0x0F);
|
|
||||||
sb.append(byteToChar(high));
|
|
||||||
sb.append(byteToChar(low));
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use short to represent unsigned byte
|
|
||||||
private static char byteToChar(char bt) {
|
|
||||||
if (bt >= 10) {
|
|
||||||
return (char)('A' + bt - 10);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return (char)('0' + bt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 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.database.search.iterator
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
|
||||||
|
|
||||||
import java.util.NoSuchElementException
|
|
||||||
|
|
||||||
class EntrySearchStringIteratorKDB(
|
|
||||||
private val mEntry: EntryKDB,
|
|
||||||
private val mSearchParameters: SearchParameters)
|
|
||||||
: Iterator<String> {
|
|
||||||
|
|
||||||
private var current = 0
|
|
||||||
|
|
||||||
private val currentString: String
|
|
||||||
get() {
|
|
||||||
return when (current) {
|
|
||||||
title -> mEntry.title
|
|
||||||
url -> mEntry.url
|
|
||||||
username -> mEntry.username
|
|
||||||
notes -> mEntry.notes
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hasNext(): Boolean {
|
|
||||||
return current < maxEntries
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun next(): String {
|
|
||||||
// Past the end of the list
|
|
||||||
if (current == maxEntries) {
|
|
||||||
throw NoSuchElementException("Past final string")
|
|
||||||
}
|
|
||||||
|
|
||||||
useSearchParameters()
|
|
||||||
|
|
||||||
val str = currentString
|
|
||||||
current++
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun useSearchParameters() {
|
|
||||||
var found = false
|
|
||||||
while (!found) {
|
|
||||||
found = when (current) {
|
|
||||||
title -> mSearchParameters.searchInTitles
|
|
||||||
url -> mSearchParameters.searchInUrls
|
|
||||||
username -> mSearchParameters.searchInUserNames
|
|
||||||
notes -> mSearchParameters.searchInNotes
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
current++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val title = 0
|
|
||||||
private const val url = 1
|
|
||||||
private const val username = 2
|
|
||||||
private const val notes = 3
|
|
||||||
private const val maxEntries = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.database.search.iterator
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.collections.Map.Entry
|
|
||||||
|
|
||||||
class EntrySearchStringIteratorKDBX(
|
|
||||||
entry: EntryKDBX,
|
|
||||||
private val mSearchParameters: SearchParameters)
|
|
||||||
: Iterator<String> {
|
|
||||||
|
|
||||||
private var mCurrent: String? = null
|
|
||||||
private var mSetIterator: Iterator<Entry<String, ProtectedString>>? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
mSetIterator = entry.fields.entries.iterator()
|
|
||||||
advance()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hasNext(): Boolean {
|
|
||||||
return mCurrent != null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun next(): String {
|
|
||||||
if (mCurrent == null) {
|
|
||||||
throw NoSuchElementException("Past the end of the list.")
|
|
||||||
}
|
|
||||||
|
|
||||||
val next:String = mCurrent!!
|
|
||||||
advance()
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun advance() {
|
|
||||||
mSetIterator?.let {
|
|
||||||
while (it.hasNext()) {
|
|
||||||
val entry = it.next()
|
|
||||||
val key = entry.key
|
|
||||||
|
|
||||||
if (searchInField(key)) {
|
|
||||||
mCurrent = entry.value.toString()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mCurrent = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchInField(key: String): Boolean {
|
|
||||||
return when (key) {
|
|
||||||
EntryKDBX.STR_TITLE -> mSearchParameters.searchInTitles
|
|
||||||
EntryKDBX.STR_USERNAME -> mSearchParameters.searchInUserNames
|
|
||||||
EntryKDBX.STR_PASSWORD -> mSearchParameters.searchInPasswords
|
|
||||||
EntryKDBX.STR_URL -> mSearchParameters.searchInUrls
|
|
||||||
EntryKDBX.STR_NOTES -> mSearchParameters.searchInNotes
|
|
||||||
OtpEntryFields.OTP_FIELD -> mSearchParameters.searchInOTP
|
|
||||||
else -> mSearchParameters.searchInOther
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -93,7 +93,7 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
|
|||||||
value
|
value
|
||||||
} else {
|
} else {
|
||||||
TokenCalculator.HOTP_INITIAL_COUNTER
|
TokenCalculator.HOTP_INITIAL_COUNTER
|
||||||
throw IllegalArgumentException()
|
throw NumberFormatException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MIN_HOTP_COUNTER = 1
|
const val MIN_HOTP_COUNTER = 0
|
||||||
const val MAX_HOTP_COUNTER = Long.MAX_VALUE
|
const val MAX_HOTP_COUNTER = Long.MAX_VALUE
|
||||||
|
|
||||||
const val MIN_TOTP_PERIOD = 1
|
const val MIN_TOTP_PERIOD = 1
|
||||||
|
|||||||
@@ -295,22 +295,30 @@ object OtpEntryFields {
|
|||||||
secretHexField != null -> otpElement.setHexSecret(secretHexField)
|
secretHexField != null -> otpElement.setHexSecret(secretHexField)
|
||||||
secretBase32Field != null -> otpElement.setBase32Secret(secretBase32Field)
|
secretBase32Field != null -> otpElement.setBase32Secret(secretBase32Field)
|
||||||
secretBase64Field != null -> otpElement.setBase64Secret(secretBase64Field)
|
secretBase64Field != null -> otpElement.setBase64Secret(secretBase64Field)
|
||||||
lengthField != null -> otpElement.digits = lengthField.toIntOrNull() ?: OTP_DEFAULT_DIGITS
|
|
||||||
periodField != null -> otpElement.period = periodField.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
|
|
||||||
algorithmField != null -> otpElement.algorithm =
|
|
||||||
when (algorithmField.toUpperCase(Locale.ENGLISH)) {
|
|
||||||
TIMEOTP_ALGORITHM_SHA1_VALUE -> HashAlgorithm.SHA1
|
|
||||||
TIMEOTP_ALGORITHM_SHA256_VALUE -> HashAlgorithm.SHA256
|
|
||||||
TIMEOTP_ALGORITHM_SHA512_VALUE -> HashAlgorithm.SHA512
|
|
||||||
else -> HashAlgorithm.SHA1
|
|
||||||
}
|
|
||||||
else -> return false
|
else -> return false
|
||||||
}
|
}
|
||||||
|
otpElement.type = OtpType.TOTP
|
||||||
|
if (lengthField != null) {
|
||||||
|
otpElement.digits = lengthField.toIntOrNull() ?: OTP_DEFAULT_DIGITS
|
||||||
|
}
|
||||||
|
if (lengthField != null) {
|
||||||
|
otpElement.digits = lengthField.toIntOrNull() ?: OTP_DEFAULT_DIGITS
|
||||||
|
}
|
||||||
|
if (periodField != null) {
|
||||||
|
otpElement.period = periodField.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
|
||||||
|
}
|
||||||
|
if (algorithmField != null) {
|
||||||
|
otpElement.algorithm =
|
||||||
|
when (algorithmField.toUpperCase(Locale.ENGLISH)) {
|
||||||
|
TIMEOTP_ALGORITHM_SHA1_VALUE -> HashAlgorithm.SHA1
|
||||||
|
TIMEOTP_ALGORITHM_SHA256_VALUE -> HashAlgorithm.SHA256
|
||||||
|
TIMEOTP_ALGORITHM_SHA512_VALUE -> HashAlgorithm.SHA512
|
||||||
|
else -> HashAlgorithm.SHA1
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
otpElement.type = OtpType.TOTP
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,10 +329,10 @@ object OtpEntryFields {
|
|||||||
return try {
|
return try {
|
||||||
// KeeOtp string format
|
// KeeOtp string format
|
||||||
val query = breakDownKeyValuePairs(plainText)
|
val query = breakDownKeyValuePairs(plainText)
|
||||||
|
otpElement.type = OtpType.TOTP
|
||||||
otpElement.setBase32Secret(query[SEED_KEY] ?: "")
|
otpElement.setBase32Secret(query[SEED_KEY] ?: "")
|
||||||
otpElement.digits = query[DIGITS_KEY]?.toIntOrNull() ?: OTP_DEFAULT_DIGITS
|
otpElement.digits = query[DIGITS_KEY]?.toIntOrNull() ?: OTP_DEFAULT_DIGITS
|
||||||
otpElement.period = query[STEP_KEY]?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
|
otpElement.period = query[STEP_KEY]?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
|
||||||
otpElement.type = OtpType.TOTP
|
|
||||||
true
|
true
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
false
|
false
|
||||||
@@ -351,6 +359,7 @@ object OtpEntryFields {
|
|||||||
// malformed
|
// malformed
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
otpElement.type = OtpType.TOTP
|
||||||
otpElement.period = matcher.group(1)?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
|
otpElement.period = matcher.group(1)?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
|
||||||
matcher.group(2)?.let { secondMatcher ->
|
matcher.group(2)?.let { secondMatcher ->
|
||||||
try {
|
try {
|
||||||
@@ -365,7 +374,6 @@ object OtpEntryFields {
|
|||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
otpElement.type = OtpType.TOTP
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,6 +382,7 @@ object OtpEntryFields {
|
|||||||
val secretHexField = getField(HMACOTP_SECRET_HEX_FIELD)
|
val secretHexField = getField(HMACOTP_SECRET_HEX_FIELD)
|
||||||
val secretBase32Field = getField(HMACOTP_SECRET_BASE32_FIELD)
|
val secretBase32Field = getField(HMACOTP_SECRET_BASE32_FIELD)
|
||||||
val secretBase64Field = getField(HMACOTP_SECRET_BASE64_FIELD)
|
val secretBase64Field = getField(HMACOTP_SECRET_BASE64_FIELD)
|
||||||
|
val secretCounterField = getField(HMACOTP_SECRET_COUNTER_FIELD)
|
||||||
try {
|
try {
|
||||||
when {
|
when {
|
||||||
secretField != null -> otpElement.setUTF8Secret(secretField)
|
secretField != null -> otpElement.setUTF8Secret(secretField)
|
||||||
@@ -382,16 +391,13 @@ object OtpEntryFields {
|
|||||||
secretBase64Field != null -> otpElement.setBase64Secret(secretBase64Field)
|
secretBase64Field != null -> otpElement.setBase64Secret(secretBase64Field)
|
||||||
else -> return false
|
else -> return false
|
||||||
}
|
}
|
||||||
|
otpElement.type = OtpType.HOTP
|
||||||
val secretCounterField = getField(HMACOTP_SECRET_COUNTER_FIELD)
|
|
||||||
if (secretCounterField != null) {
|
if (secretCounterField != null) {
|
||||||
otpElement.counter = secretCounterField.toLongOrNull() ?: HOTP_INITIAL_COUNTER
|
otpElement.counter = secretCounterField.toLongOrNull() ?: HOTP_INITIAL_COUNTER
|
||||||
}
|
}
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
otpElement.type = OtpType.HOTP
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package com.kunzisoft.keepass.services
|
package com.kunzisoft.keepass.services
|
||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.*
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
@@ -10,7 +9,6 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import kotlinx.coroutines.*
|
|
||||||
|
|
||||||
class AdvancedUnlockNotificationService : NotificationService() {
|
class AdvancedUnlockNotificationService : NotificationService() {
|
||||||
|
|
||||||
@@ -18,9 +16,6 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
|||||||
|
|
||||||
private var mActionTaskBinder = AdvancedUnlockBinder()
|
private var mActionTaskBinder = AdvancedUnlockBinder()
|
||||||
|
|
||||||
private var notificationTimeoutMilliSecs: Long = 0
|
|
||||||
private var mTimerJob: Job? = null
|
|
||||||
|
|
||||||
inner class AdvancedUnlockBinder: Binder() {
|
inner class AdvancedUnlockBinder: Binder() {
|
||||||
fun getCipherDatabase(databaseUri: Uri): CipherDatabaseEntity? {
|
fun getCipherDatabase(databaseUri: Uri): CipherDatabaseEntity? {
|
||||||
return mTempCipherDao.firstOrNull { it.databaseUri == databaseUri.toString()}
|
return mTempCipherDao.firstOrNull { it.databaseUri == databaseUri.toString()}
|
||||||
@@ -50,94 +45,79 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
|||||||
return getString(R.string.advanced_unlock)
|
return getString(R.string.advanced_unlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onCreate() {
|
||||||
super.onBind(intent)
|
super.onCreate()
|
||||||
return mActionTaskBinder
|
mTempCipherDao = ArrayList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onBind(intent: Intent): IBinder {
|
||||||
super.onStartCommand(intent, flags, startId)
|
super.onBind(intent)
|
||||||
|
|
||||||
val deleteIntent = Intent(this, AdvancedUnlockNotificationService::class.java).apply {
|
val pendingDeleteIntent = PendingIntent.getBroadcast(this,
|
||||||
action = ACTION_REMOVE_KEYS
|
4577, Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION), 0)
|
||||||
}
|
|
||||||
val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
val biometricUnlockEnabled = PreferencesUtil.isBiometricUnlockEnable(this)
|
val biometricUnlockEnabled = PreferencesUtil.isBiometricUnlockEnable(this)
|
||||||
val notificationBuilder = buildNewNotification().apply {
|
val notificationBuilder = buildNewNotification().apply {
|
||||||
setSmallIcon(if (biometricUnlockEnabled) {
|
setSmallIcon(if (biometricUnlockEnabled) {
|
||||||
R.drawable.notification_ic_fingerprint_unlock_24dp
|
R.drawable.notification_ic_fingerprint_unlock_24dp
|
||||||
} else {
|
} else {
|
||||||
R.drawable.notification_ic_device_unlock_24dp
|
R.drawable.notification_ic_device_unlock_24dp
|
||||||
})
|
})
|
||||||
intent?.let {
|
setContentTitle(getString(R.string.advanced_unlock))
|
||||||
setContentTitle(getString(R.string.advanced_unlock))
|
|
||||||
}
|
|
||||||
setContentText(getString(R.string.advanced_unlock_tap_delete))
|
setContentText(getString(R.string.advanced_unlock_tap_delete))
|
||||||
setContentIntent(pendingDeleteIntent)
|
setContentIntent(pendingDeleteIntent)
|
||||||
// Unfortunately swipe is disabled in lollipop+
|
// Unfortunately swipe is disabled in lollipop+
|
||||||
setDeleteIntent(pendingDeleteIntent)
|
setDeleteIntent(pendingDeleteIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
when (intent?.action) {
|
val notificationTimeoutMilliSecs = PreferencesUtil.getAdvancedUnlockTimeout(this)
|
||||||
ACTION_TIMEOUT -> {
|
// Not necessarily a foreground service
|
||||||
notificationTimeoutMilliSecs = PreferencesUtil.getAdvancedUnlockTimeout(this)
|
if (mTimerJob == null && notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
|
||||||
// Not necessarily a foreground service
|
defineTimerJob(notificationBuilder, notificationTimeoutMilliSecs) {
|
||||||
if (mTimerJob == null && notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
|
sendBroadcast(Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION))
|
||||||
mTimerJob = CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
val maxPos = 100
|
|
||||||
val posDurationMills = notificationTimeoutMilliSecs / maxPos
|
|
||||||
for (pos in maxPos downTo 0) {
|
|
||||||
notificationBuilder.setProgress(maxPos, pos, false)
|
|
||||||
startForeground(notificationId, notificationBuilder.build())
|
|
||||||
delay(posDurationMills)
|
|
||||||
if (pos <= 0) {
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notificationManager?.cancel(notificationId)
|
|
||||||
mTimerJob = null
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
startForeground(notificationId, notificationBuilder.build())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ACTION_REMOVE_KEYS -> {
|
} else {
|
||||||
stopSelf()
|
startForeground(notificationId, notificationBuilder.build())
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return START_STICKY
|
return mActionTaskBinder
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onUnbind(intent: Intent?): Boolean {
|
||||||
super.onCreate()
|
stopSelf()
|
||||||
mTempCipherDao = ArrayList()
|
return super.onUnbind(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
mTempCipherDao.clear()
|
mTempCipherDao.clear()
|
||||||
mTimerJob?.cancel()
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AdvancedUnlockReceiver(var removeKeyAction: () -> Unit): BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
intent.action?.let {
|
||||||
|
when (it) {
|
||||||
|
REMOVE_ADVANCED_UNLOCK_KEY_ACTION -> {
|
||||||
|
removeKeyAction.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val CHANNEL_ADVANCED_UNLOCK_ID = "com.kunzisoft.keepass.notification.channel.unlock"
|
private const val CHANNEL_ADVANCED_UNLOCK_ID = "com.kunzisoft.keepass.notification.channel.unlock"
|
||||||
|
const val REMOVE_ADVANCED_UNLOCK_KEY_ACTION = "com.kunzisoft.keepass.REMOVE_ADVANCED_UNLOCK_KEY"
|
||||||
|
|
||||||
private const val ACTION_TIMEOUT = "ACTION_TIMEOUT"
|
// Only one service connection
|
||||||
private const val ACTION_REMOVE_KEYS = "ACTION_REMOVE_KEYS"
|
fun bindService(context: Context, serviceConnection: ServiceConnection, flags: Int) {
|
||||||
|
context.bindService(Intent(context,
|
||||||
fun startServiceForTimeout(context: Context) {
|
AdvancedUnlockNotificationService::class.java),
|
||||||
if (PreferencesUtil.isTempAdvancedUnlockEnable(context)) {
|
serviceConnection,
|
||||||
context.startService(Intent(context, AdvancedUnlockNotificationService::class.java).apply {
|
flags)
|
||||||
action = ACTION_TIMEOUT
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopService(context: Context) {
|
fun unbindService(context: Context, serviceConnection: ServiceConnection) {
|
||||||
context.stopService(Intent(context, AdvancedUnlockNotificationService::class.java))
|
context.unbindService(serviceConnection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,6 @@ import android.net.Uri
|
|||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
@@ -34,6 +33,7 @@ import com.kunzisoft.keepass.model.AttachmentState
|
|||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
@@ -173,7 +173,8 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
putExtra(FILE_URI_KEY, attachmentNotification.uri)
|
putExtra(FILE_URI_KEY, attachmentNotification.uri)
|
||||||
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
||||||
|
|
||||||
val fileName = DocumentFile.fromSingleUri(this, attachmentNotification.uri)?.name ?: ""
|
val fileName = UriUtil.getFileData(this, attachmentNotification.uri)?.name
|
||||||
|
?: attachmentNotification.uri.path
|
||||||
|
|
||||||
val builder = buildNewNotification().apply {
|
val builder = buildNewNotification().apply {
|
||||||
when (attachmentNotification.entryAttachmentState.streamDirection) {
|
when (attachmentNotification.entryAttachmentState.streamDirection) {
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
override val notificationId = 485
|
override val notificationId = 485
|
||||||
private var mEntryInfo: EntryInfo? = null
|
private var mEntryInfo: EntryInfo? = null
|
||||||
private var clipboardHelper: ClipboardHelper? = null
|
private var clipboardHelper: ClipboardHelper? = null
|
||||||
private var notificationTimeoutMilliSecs: Long = 0
|
private var mNotificationTimeoutMilliSecs: Long = 0
|
||||||
private var cleanCopyNotificationTimerTask: Thread? = null
|
|
||||||
|
|
||||||
override fun retrieveChannelId(): String {
|
override fun retrieveChannelId(): String {
|
||||||
return CHANNEL_CLIPBOARD_ID
|
return CHANNEL_CLIPBOARD_ID
|
||||||
@@ -70,7 +69,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
mEntryInfo = intent?.getParcelableExtra(EXTRA_ENTRY_INFO)
|
mEntryInfo = intent?.getParcelableExtra(EXTRA_ENTRY_INFO)
|
||||||
|
|
||||||
//Get settings
|
//Get settings
|
||||||
notificationTimeoutMilliSecs = PreferencesUtil.getClipboardTimeout(this)
|
mNotificationTimeoutMilliSecs = PreferencesUtil.getClipboardTimeout(this)
|
||||||
|
|
||||||
when {
|
when {
|
||||||
intent == null -> Log.w(TAG, "null intent")
|
intent == null -> Log.w(TAG, "null intent")
|
||||||
@@ -78,7 +77,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
newNotification(mEntryInfo?.title, constructListOfField(intent))
|
newNotification(mEntryInfo?.title, constructListOfField(intent))
|
||||||
}
|
}
|
||||||
ACTION_CLEAN_CLIPBOARD == intent.action -> {
|
ACTION_CLEAN_CLIPBOARD == intent.action -> {
|
||||||
stopTask(cleanCopyNotificationTimerTask)
|
mTimerJob?.cancel()
|
||||||
cleanClipboard()
|
cleanClipboard()
|
||||||
stopNotificationAndSendLockIfNeeded()
|
stopNotificationAndSendLockIfNeeded()
|
||||||
}
|
}
|
||||||
@@ -121,7 +120,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun newNotification(title: String?, fieldsToAdd: ArrayList<ClipboardEntryNotificationField>) {
|
private fun newNotification(title: String?, fieldsToAdd: ArrayList<ClipboardEntryNotificationField>) {
|
||||||
stopTask(cleanCopyNotificationTimerTask)
|
mTimerJob?.cancel()
|
||||||
|
|
||||||
val builder = buildNewNotification()
|
val builder = buildNewNotification()
|
||||||
.setSmallIcon(R.drawable.notification_ic_clipboard_key_24dp)
|
.setSmallIcon(R.drawable.notification_ic_clipboard_key_24dp)
|
||||||
@@ -147,7 +146,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun copyField(fieldToCopy: ClipboardEntryNotificationField, nextFields: ArrayList<ClipboardEntryNotificationField>) {
|
private fun copyField(fieldToCopy: ClipboardEntryNotificationField, nextFields: ArrayList<ClipboardEntryNotificationField>) {
|
||||||
stopTask(cleanCopyNotificationTimerTask)
|
mTimerJob?.cancel()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var generatedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
|
var generatedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
|
||||||
@@ -170,40 +169,23 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
this, 0, cleanIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
this, 0, cleanIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
builder.setDeleteIntent(cleanPendingIntent)
|
builder.setDeleteIntent(cleanPendingIntent)
|
||||||
|
|
||||||
val myNotificationId = notificationId
|
if (mNotificationTimeoutMilliSecs != NEVER) {
|
||||||
|
defineTimerJob(builder, mNotificationTimeoutMilliSecs, {
|
||||||
if (notificationTimeoutMilliSecs != NEVER) {
|
val newGeneratedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
|
||||||
cleanCopyNotificationTimerTask = Thread {
|
// New auto generated value
|
||||||
val maxPos = 100
|
if (generatedValue != newGeneratedValue) {
|
||||||
val posDurationMills = notificationTimeoutMilliSecs / maxPos
|
generatedValue = newGeneratedValue
|
||||||
for (pos in maxPos downTo 0) {
|
clipboardHelper?.copyToClipboard(fieldToCopy.label, generatedValue)
|
||||||
val newGeneratedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
|
|
||||||
// New auto generated value
|
|
||||||
if (generatedValue != newGeneratedValue) {
|
|
||||||
generatedValue = newGeneratedValue
|
|
||||||
clipboardHelper?.copyToClipboard(fieldToCopy.label, generatedValue)
|
|
||||||
}
|
|
||||||
builder.setProgress(maxPos, pos, false)
|
|
||||||
notificationManager?.notify(myNotificationId, builder.build())
|
|
||||||
try {
|
|
||||||
Thread.sleep(posDurationMills)
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (pos <= 0) {
|
|
||||||
stopNotificationAndSendLockIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
stopTask(cleanCopyNotificationTimerTask)
|
}) {
|
||||||
notificationManager?.cancel(myNotificationId)
|
stopNotificationAndSendLockIfNeeded()
|
||||||
// Clean password only if no next field
|
// Clean password only if no next field
|
||||||
if (nextFields.size <= 0)
|
if (nextFields.size <= 0)
|
||||||
cleanClipboard()
|
cleanClipboard()
|
||||||
}
|
}
|
||||||
cleanCopyNotificationTimerTask?.start()
|
|
||||||
} else {
|
} else {
|
||||||
// No timer
|
// No timer
|
||||||
notificationManager?.notify(myNotificationId, builder.build())
|
notificationManager?.notify(notificationId, builder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -228,10 +210,6 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
cleanClipboard()
|
cleanClipboard()
|
||||||
|
|
||||||
stopTask(cleanCopyNotificationTimerTask)
|
|
||||||
cleanCopyNotificationTimerTask = null
|
|
||||||
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -200,10 +200,6 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
if (intentAction == null && !mDatabase.loaded) {
|
if (intentAction == null && !mDatabase.loaded) {
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
if (intentAction == ACTION_DATABASE_CLOSE) {
|
|
||||||
// Send lock action
|
|
||||||
sendBroadcast(Intent(LOCK_ACTION))
|
|
||||||
}
|
|
||||||
|
|
||||||
val actionRunnable: ActionRunnable? = when (intentAction) {
|
val actionRunnable: ActionRunnable? = when (intentAction) {
|
||||||
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent)
|
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent)
|
||||||
@@ -378,10 +374,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
ReadOnlyHelper.putReadOnlyInIntent(this, mDatabase.isReadOnly)
|
ReadOnlyHelper.putReadOnlyInIntent(this, mDatabase.isReadOnly)
|
||||||
},
|
},
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
val deleteIntent = Intent(this, DatabaseTaskNotificationService::class.java).apply {
|
val pendingDeleteIntent = PendingIntent.getBroadcast(this,
|
||||||
action = ACTION_DATABASE_CLOSE
|
4576, Intent(LOCK_ACTION), 0)
|
||||||
}
|
|
||||||
val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
// Add actions in notifications
|
// Add actions in notifications
|
||||||
notificationBuilder.apply {
|
notificationBuilder.apply {
|
||||||
setContentText(mDatabase.name + " (" + mDatabase.version + ")")
|
setContentText(mDatabase.name + " (" + mDatabase.version + ")")
|
||||||
@@ -877,7 +871,6 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
const val ACTION_DATABASE_UPDATE_PARALLELISM_TASK = "ACTION_DATABASE_UPDATE_PARALLELISM_TASK"
|
const val ACTION_DATABASE_UPDATE_PARALLELISM_TASK = "ACTION_DATABASE_UPDATE_PARALLELISM_TASK"
|
||||||
const val ACTION_DATABASE_UPDATE_ITERATIONS_TASK = "ACTION_DATABASE_UPDATE_ITERATIONS_TASK"
|
const val ACTION_DATABASE_UPDATE_ITERATIONS_TASK = "ACTION_DATABASE_UPDATE_ITERATIONS_TASK"
|
||||||
const val ACTION_DATABASE_SAVE = "ACTION_DATABASE_SAVE"
|
const val ACTION_DATABASE_SAVE = "ACTION_DATABASE_SAVE"
|
||||||
const val ACTION_DATABASE_CLOSE = "ACTION_DATABASE_CLOSE"
|
|
||||||
|
|
||||||
const val DATABASE_TASK_TITLE_KEY = "DATABASE_TASK_TITLE_KEY"
|
const val DATABASE_TASK_TITLE_KEY = "DATABASE_TASK_TITLE_KEY"
|
||||||
const val DATABASE_TASK_MESSAGE_KEY = "DATABASE_TASK_MESSAGE_KEY"
|
const val DATABASE_TASK_MESSAGE_KEY = "DATABASE_TASK_MESSAGE_KEY"
|
||||||
|
|||||||
@@ -35,8 +35,7 @@ import com.kunzisoft.keepass.utils.LOCK_ACTION
|
|||||||
class KeyboardEntryNotificationService : LockNotificationService() {
|
class KeyboardEntryNotificationService : LockNotificationService() {
|
||||||
|
|
||||||
override val notificationId = 486
|
override val notificationId = 486
|
||||||
private var cleanNotificationTimerTask: Thread? = null
|
private var mNotificationTimeoutMilliSecs: Long = 0
|
||||||
private var notificationTimeoutMilliSecs: Long = 0
|
|
||||||
|
|
||||||
private var pendingDeleteIntent: PendingIntent? = null
|
private var pendingDeleteIntent: PendingIntent? = null
|
||||||
|
|
||||||
@@ -61,7 +60,7 @@ class KeyboardEntryNotificationService : LockNotificationService() {
|
|||||||
super.onStartCommand(intent, flags, startId)
|
super.onStartCommand(intent, flags, startId)
|
||||||
|
|
||||||
//Get settings
|
//Get settings
|
||||||
notificationTimeoutMilliSecs = PreferenceManager.getDefaultSharedPreferences(this)
|
mNotificationTimeoutMilliSecs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
.getString(getString(R.string.keyboard_entry_timeout_key),
|
.getString(getString(R.string.keyboard_entry_timeout_key),
|
||||||
getString(R.string.timeout_default))?.toLong() ?: TimeoutHelper.DEFAULT_TIMEOUT
|
getString(R.string.timeout_default))?.toLong() ?: TimeoutHelper.DEFAULT_TIMEOUT
|
||||||
|
|
||||||
@@ -107,27 +106,12 @@ class KeyboardEntryNotificationService : LockNotificationService() {
|
|||||||
notificationManager?.cancel(notificationId)
|
notificationManager?.cancel(notificationId)
|
||||||
notificationManager?.notify(notificationId, builder.build())
|
notificationManager?.notify(notificationId, builder.build())
|
||||||
|
|
||||||
stopTask(cleanNotificationTimerTask)
|
|
||||||
// Timeout only if notification clear is available
|
// Timeout only if notification clear is available
|
||||||
if (PreferencesUtil.isClearKeyboardNotificationEnable(this)) {
|
if (PreferencesUtil.isClearKeyboardNotificationEnable(this)) {
|
||||||
if (notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
|
if (mNotificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
|
||||||
cleanNotificationTimerTask = Thread {
|
defineTimerJob(builder, mNotificationTimeoutMilliSecs) {
|
||||||
val maxPos = 100
|
stopNotificationAndSendLockIfNeeded()
|
||||||
val posDurationMills = notificationTimeoutMilliSecs / maxPos
|
|
||||||
for (pos in maxPos downTo 0) {
|
|
||||||
builder.setProgress(maxPos, pos, false)
|
|
||||||
notificationManager?.notify(notificationId, builder.build())
|
|
||||||
try {
|
|
||||||
Thread.sleep(posDurationMills)
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (pos <= 0) {
|
|
||||||
stopNotificationAndSendLockIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
cleanNotificationTimerTask?.start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,8 +126,6 @@ class KeyboardEntryNotificationService : LockNotificationService() {
|
|||||||
// Remove the entry from the keyboard
|
// Remove the entry from the keyboard
|
||||||
MagikIME.removeEntry(this)
|
MagikIME.removeEntry(this)
|
||||||
|
|
||||||
stopTask(cleanNotificationTimerTask)
|
|
||||||
cleanNotificationTimerTask = null
|
|
||||||
pendingDeleteIntent?.cancel()
|
pendingDeleteIntent?.cancel()
|
||||||
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|||||||
@@ -50,11 +50,6 @@ abstract class LockNotificationService : NotificationService() {
|
|||||||
return super.onStartCommand(intent, flags, startId)
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun stopTask(task: Thread?) {
|
|
||||||
if (task != null && task.isAlive)
|
|
||||||
task.interrupt()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
notificationManager?.cancel(notificationId)
|
notificationManager?.cancel(notificationId)
|
||||||
|
|
||||||
@@ -62,8 +57,8 @@ abstract class LockNotificationService : NotificationService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
|
||||||
unregisterLockReceiver(mLockReceiver)
|
unregisterLockReceiver(mLockReceiver)
|
||||||
|
mLockReceiver = null
|
||||||
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.core.app.NotificationCompat
|
|||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
|
||||||
abstract class NotificationService : Service() {
|
abstract class NotificationService : Service() {
|
||||||
@@ -18,6 +19,8 @@ abstract class NotificationService : Service() {
|
|||||||
protected var notificationManager: NotificationManagerCompat? = null
|
protected var notificationManager: NotificationManagerCompat? = null
|
||||||
private var colorNotificationAccent: Int = 0
|
private var colorNotificationAccent: Int = 0
|
||||||
|
|
||||||
|
protected var mTimerJob: Job? = null
|
||||||
|
|
||||||
protected abstract val notificationId: Int
|
protected abstract val notificationId: Int
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
@@ -71,7 +74,33 @@ abstract class NotificationService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun defineTimerJob(builder: NotificationCompat.Builder,
|
||||||
|
timeoutMilliseconds: Long,
|
||||||
|
actionAfterASecond: (() -> Unit)? = null,
|
||||||
|
actionEnd: () -> Unit) {
|
||||||
|
mTimerJob?.cancel()
|
||||||
|
mTimerJob = CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
val timeoutInSeconds = timeoutMilliseconds / 1000L
|
||||||
|
for (currentTime in timeoutInSeconds downTo 0) {
|
||||||
|
actionAfterASecond?.invoke()
|
||||||
|
builder.setProgress(100,
|
||||||
|
(currentTime * 100 / timeoutInSeconds).toInt(),
|
||||||
|
false)
|
||||||
|
startForeground(notificationId, builder.build())
|
||||||
|
delay(1000)
|
||||||
|
if (currentTime <= 0) {
|
||||||
|
actionEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notificationManager?.cancel(notificationId)
|
||||||
|
mTimerJob = null
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
mTimerJob?.cancel()
|
||||||
|
mTimerJob = null
|
||||||
notificationManager?.cancel(notificationId)
|
notificationManager?.cancel(notificationId)
|
||||||
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|||||||
@@ -20,9 +20,12 @@
|
|||||||
package com.kunzisoft.keepass.settings
|
package com.kunzisoft.keepass.settings
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DurationDialogFragmentCompat
|
||||||
|
|
||||||
class MagikeyboardSettingsFragment : PreferenceFragmentCompat() {
|
class MagikeyboardSettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
@@ -30,4 +33,31 @@ class MagikeyboardSettingsFragment : PreferenceFragmentCompat() {
|
|||||||
// Load the preferences from an XML resource
|
// Load the preferences from an XML resource
|
||||||
setPreferencesFromResource(R.xml.preferences_keyboard, rootKey)
|
setPreferencesFromResource(R.xml.preferences_keyboard, rootKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDisplayPreferenceDialog(preference: Preference?) {
|
||||||
|
|
||||||
|
var otherDialogFragment = false
|
||||||
|
|
||||||
|
var dialogFragment: DialogFragment? = null
|
||||||
|
// Main Preferences
|
||||||
|
when (preference?.key) {
|
||||||
|
getString(R.string.keyboard_entry_timeout_key) -> {
|
||||||
|
dialogFragment = DurationDialogFragmentCompat.newInstance(preference.key)
|
||||||
|
}
|
||||||
|
else -> otherDialogFragment = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialogFragment != null) {
|
||||||
|
dialogFragment.setTargetFragment(this, 0)
|
||||||
|
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
|
||||||
|
}
|
||||||
|
// Could not be handled here. Try with the super method.
|
||||||
|
else if (otherDialogFragment) {
|
||||||
|
super.onDisplayPreferenceDialog(preference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import android.view.autofill.AutofillManager
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
@@ -44,8 +45,8 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
|||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||||
import com.kunzisoft.keepass.education.Education
|
import com.kunzisoft.keepass.education.Education
|
||||||
import com.kunzisoft.keepass.icons.IconPackChooser
|
import com.kunzisoft.keepass.icons.IconPackChooser
|
||||||
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
|
||||||
import com.kunzisoft.keepass.settings.preference.IconPackListPreference
|
import com.kunzisoft.keepass.settings.preference.IconPackListPreference
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DurationDialogFragmentCompat
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
|
|
||||||
@@ -90,6 +91,20 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findPreference<Preference>(getString(R.string.import_app_properties_key))?.setOnPreferenceClickListener { _ ->
|
||||||
|
(activity as? SettingsActivity?)?.apply {
|
||||||
|
importAppProperties()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
findPreference<Preference>(getString(R.string.export_app_properties_key))?.setOnPreferenceClickListener { _ ->
|
||||||
|
(activity as? SettingsActivity?)?.apply {
|
||||||
|
exportAppProperties()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,7 +373,6 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
AdvancedUnlockNotificationService.stopService(activity.applicationContext)
|
|
||||||
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
|
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
|
||||||
}
|
}
|
||||||
.setNegativeButton(resources.getString(android.R.string.cancel)
|
.setNegativeButton(resources.getString(android.R.string.cancel)
|
||||||
@@ -388,10 +402,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
Stylish.assignStyle(activity, styleIdString)
|
Stylish.assignStyle(activity, styleIdString)
|
||||||
// Relaunch the current activity to redraw theme
|
// Relaunch the current activity to redraw theme
|
||||||
(activity as? SettingsActivity?)?.apply {
|
(activity as? SettingsActivity?)?.apply {
|
||||||
keepCurrentScreen()
|
relaunchCurrentScreen()
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
styleEnabled
|
styleEnabled
|
||||||
@@ -399,10 +410,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
|
|
||||||
findPreference<ListPreference>(getString(R.string.setting_style_brightness_key))?.setOnPreferenceChangeListener { _, _ ->
|
findPreference<ListPreference>(getString(R.string.setting_style_brightness_key))?.setOnPreferenceChangeListener { _, _ ->
|
||||||
(activity as? SettingsActivity?)?.apply {
|
(activity as? SettingsActivity?)?.apply {
|
||||||
keepCurrentScreen()
|
relaunchCurrentScreen()
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -440,6 +448,31 @@ 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_advanced_unlock_timeout_key) -> {
|
||||||
|
dialogFragment = DurationDialogFragmentCompat.newInstance(preference.key)
|
||||||
|
}
|
||||||
|
else -> otherDialogFragment = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialogFragment != null) {
|
||||||
|
dialogFragment.setTargetFragment(this, 0)
|
||||||
|
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
|
||||||
|
}
|
||||||
|
// Could not be handled here. Try with the super method.
|
||||||
|
else if (otherDialogFragment) {
|
||||||
|
super.onDisplayPreferenceDialog(preference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
@@ -470,7 +503,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val REQUEST_CODE_AUTOFILL = 5201
|
private const val REQUEST_CODE_AUTOFILL = 5201
|
||||||
|
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -576,7 +576,6 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
|
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,14 +21,17 @@ package com.kunzisoft.keepass.settings
|
|||||||
|
|
||||||
import android.app.backup.BackupManager
|
import android.app.backup.BackupManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
|
import com.kunzisoft.keepass.education.Education
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -134,28 +137,48 @@ object PreferencesUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getStyle(context: Context): String {
|
fun getStyle(context: Context): String {
|
||||||
val stylishPrefKey = context.getString(R.string.setting_style_key)
|
val defaultStyleString = Stylish.defaultStyle(context)
|
||||||
val defaultStyleString = context.getString(R.string.list_style_name_light)
|
|
||||||
val styleString = PreferenceManager.getDefaultSharedPreferences(context)
|
val styleString = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
.getString(stylishPrefKey, defaultStyleString)
|
.getString(context.getString(R.string.setting_style_key), defaultStyleString)
|
||||||
?: defaultStyleString
|
?: defaultStyleString
|
||||||
return Stylish.retrieveEquivalentLightStyle(context, styleString)
|
// Return the system style
|
||||||
|
return Stylish.retrieveEquivalentSystemStyle(context, styleString)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setStyle(context: Context, styleString: String) {
|
||||||
|
var tempThemeString = styleString
|
||||||
|
if (BuildConfig.CLOSED_STORE || !Education.isEducationScreenReclickedPerformed(context)) {
|
||||||
|
if (tempThemeString in BuildConfig.STYLES_DISABLED) {
|
||||||
|
tempThemeString = Stylish.defaultStyle(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Store light style to show selection in array list
|
||||||
|
tempThemeString = Stylish.retrieveEquivalentLightStyle(context, tempThemeString)
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.edit()
|
||||||
|
.putString(context.getString(R.string.setting_style_key), tempThemeString)
|
||||||
|
.apply()
|
||||||
|
Stylish.load(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getStyleBrightness(context: Context): String? {
|
fun getStyleBrightness(context: Context): String? {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getString(context.getString(R.string.setting_style_brightness_key),
|
return prefs.getString(context.getString(R.string.setting_style_brightness_key),
|
||||||
context.resources.getString(R.string.list_style_brightness_follow_system))
|
context.getString(R.string.list_style_brightness_follow_system))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the text size in % (1 for 100%)
|
* Retrieve the text size in % (1 for 100%)
|
||||||
*/
|
*/
|
||||||
fun getListTextSize(context: Context): Float {
|
fun getListTextSize(context: Context): Float {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val index = try {
|
||||||
val listSizeString = prefs.getString(context.getString(R.string.list_size_key),
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
context.getString(R.string.list_size_string_medium))
|
val listSizeString = prefs.getString(context.getString(R.string.list_size_key),
|
||||||
val index = context.resources.getStringArray(R.array.list_size_string_values).indexOf(listSizeString)
|
context.getString(R.string.list_size_string_medium))
|
||||||
|
context.resources.getStringArray(R.array.list_size_string_values).indexOf(listSizeString)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
1
|
||||||
|
}
|
||||||
val typedArray = context.resources.obtainTypedArray(R.array.list_size_values)
|
val typedArray = context.resources.obtainTypedArray(R.array.list_size_values)
|
||||||
val listSize = typedArray.getFloat(index, 1.0F)
|
val listSize = typedArray.getFloat(index, 1.0F)
|
||||||
typedArray.recycle()
|
typedArray.recycle()
|
||||||
@@ -289,11 +312,13 @@ object PreferencesUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getListSort(context: Context): SortNodeEnum {
|
fun getListSort(context: Context): SortNodeEnum {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
try {
|
||||||
prefs.getString(context.getString(R.string.sort_node_key),
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
SortNodeEnum.DB.name)?.let {
|
prefs.getString(context.getString(R.string.sort_node_key),
|
||||||
return SortNodeEnum.valueOf(it)
|
SortNodeEnum.DB.name)?.let {
|
||||||
}
|
return SortNodeEnum.valueOf(it)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {}
|
||||||
return SortNodeEnum.DB
|
return SortNodeEnum.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,4 +542,133 @@ object PreferencesUtil {
|
|||||||
.putStringSet(context.getString(R.string.autofill_web_domain_blocklist_key), setItems)
|
.putStringSet(context.getString(R.string.autofill_web_domain_blocklist_key), setItems)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAppProperties(context: Context): Properties {
|
||||||
|
val properties = Properties()
|
||||||
|
for ((name, value) in PreferenceManager.getDefaultSharedPreferences(context).all) {
|
||||||
|
properties[name] = value.toString()
|
||||||
|
}
|
||||||
|
for ((name, value) in Education.getEducationSharedPreferences(context).all) {
|
||||||
|
properties[name] = value.toString()
|
||||||
|
}
|
||||||
|
return properties
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getStringSetFromProperties(value: String): Set<String> {
|
||||||
|
return value.removePrefix("[")
|
||||||
|
.removeSuffix("]")
|
||||||
|
.split(", ")
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun putPropertiesInPreferences(properties: Properties,
|
||||||
|
preferences: SharedPreferences,
|
||||||
|
putProperty: (editor: SharedPreferences.Editor,
|
||||||
|
name: String,
|
||||||
|
value: String) -> Unit) {
|
||||||
|
preferences.edit().apply {
|
||||||
|
for ((name, value) in properties) {
|
||||||
|
try {
|
||||||
|
putProperty(this, name as String, value as String)
|
||||||
|
} catch (e:Exception) {
|
||||||
|
Log.e("PreferencesUtil", "Error when trying to parse app property $name=$value", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAppProperties(context: Context, properties: Properties) {
|
||||||
|
putPropertiesInPreferences(properties,
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context)) { editor, name, value ->
|
||||||
|
when (name) {
|
||||||
|
context.getString(R.string.allow_no_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.delete_entered_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.enable_read_only_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.enable_auto_save_database_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.omit_backup_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.auto_focus_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.subdomain_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.app_timeout_key) -> editor.putString(name, value.toLong().toString())
|
||||||
|
context.getString(R.string.lock_database_screen_off_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.lock_database_back_root_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.lock_database_show_button_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.password_length_key) -> editor.putInt(name, value.toInt())
|
||||||
|
context.getString(R.string.list_password_generator_options_key) -> editor.putStringSet(name, getStringSetFromProperties(value))
|
||||||
|
context.getString(R.string.hide_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.allow_copy_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.remember_database_locations_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.show_recent_files_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.hide_broken_locations_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.remember_keyfile_locations_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.biometric_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.device_credential_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.biometric_auto_open_prompt_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.temp_advanced_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.temp_advanced_unlock_timeout_key) -> editor.putString(name, value.toLong().toString())
|
||||||
|
|
||||||
|
context.getString(R.string.magic_keyboard_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.clipboard_notifications_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.clear_clipboard_notification_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.clipboard_timeout_key) -> editor.putString(name, value.toLong().toString())
|
||||||
|
context.getString(R.string.settings_autofill_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_notification_entry_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_notification_entry_clear_close_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_entry_timeout_key) -> editor.putString(name, value.toLong().toString())
|
||||||
|
context.getString(R.string.keyboard_selection_entry_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_search_share_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_save_search_info_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_auto_go_action_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_key_vibrate_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_key_sound_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_previous_database_credentials_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_previous_fill_in_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_previous_lock_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.autofill_auto_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.autofill_save_search_info_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.autofill_ask_to_save_data_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.autofill_application_id_blocklist_key) -> editor.putStringSet(name, getStringSetFromProperties(value))
|
||||||
|
context.getString(R.string.autofill_web_domain_blocklist_key) -> editor.putStringSet(name, getStringSetFromProperties(value))
|
||||||
|
|
||||||
|
context.getString(R.string.setting_style_key) -> setStyle(context, value)
|
||||||
|
context.getString(R.string.setting_style_brightness_key) -> editor.putString(name, value)
|
||||||
|
context.getString(R.string.setting_icon_pack_choose_key) -> editor.putString(name, value)
|
||||||
|
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())
|
||||||
|
context.getString(R.string.list_size_key) -> editor.putString(name, value)
|
||||||
|
context.getString(R.string.monospace_font_fields_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.hide_expired_entries_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.show_uuid_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.enable_education_screens_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
|
||||||
|
context.getString(R.string.sort_node_key) -> editor.putString(name, value)
|
||||||
|
context.getString(R.string.sort_group_before_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.sort_ascending_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.sort_recycle_bin_bottom_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.allow_copy_password_first_time_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
putPropertiesInPreferences(properties,
|
||||||
|
Education.getEducationSharedPreferences(context)) { editor, name, value ->
|
||||||
|
when (name) {
|
||||||
|
context.getString(R.string.education_create_db_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_select_db_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_unlock_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_read_only_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_biometric_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_new_node_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_sort_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_lock_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_copy_username_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_entry_edit_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_password_generator_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_entry_new_field_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_add_attachment_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_setup_OTP_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,22 +24,27 @@ import android.app.backup.BackupManager
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
open class SettingsActivity
|
open class SettingsActivity
|
||||||
: LockingActivity(),
|
: LockingActivity(),
|
||||||
@@ -48,6 +53,8 @@ open class SettingsActivity
|
|||||||
PasswordEncodingDialogFragment.Listener {
|
PasswordEncodingDialogFragment.Listener {
|
||||||
|
|
||||||
private var backupManager: BackupManager? = null
|
private var backupManager: BackupManager? = null
|
||||||
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
private var appPropertiesFileCreationRequestCode: Int? = null
|
||||||
|
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
@@ -70,6 +77,8 @@ open class SettingsActivity
|
|||||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||||
toolbar = findViewById(R.id.toolbar)
|
toolbar = findViewById(R.id.toolbar)
|
||||||
|
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
|
||||||
if (savedInstanceState?.getString(TITLE_KEY).isNullOrEmpty())
|
if (savedInstanceState?.getString(TITLE_KEY).isNullOrEmpty())
|
||||||
toolbar?.setTitle(R.string.settings)
|
toolbar?.setTitle(R.string.settings)
|
||||||
else
|
else
|
||||||
@@ -216,6 +225,13 @@ open class SettingsActivity
|
|||||||
hideOrShowLockButton(key)
|
hideOrShowLockButton(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun relaunchCurrentScreen() {
|
||||||
|
keepCurrentScreen()
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To keep the current screen when activity is reloaded
|
* To keep the current screen when activity is reloaded
|
||||||
*/
|
*/
|
||||||
@@ -235,6 +251,58 @@ open class SettingsActivity
|
|||||||
replaceFragment(key, reload)
|
replaceFragment(key, reload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun importAppProperties() {
|
||||||
|
mExternalFileHelper?.openDocument()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exportAppProperties() {
|
||||||
|
appPropertiesFileCreationRequestCode = mExternalFileHelper?.createDocument(getString(R.string.app_properties_file_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
// Import app properties result
|
||||||
|
try {
|
||||||
|
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { selectedfileUri ->
|
||||||
|
selectedfileUri?.let { uri ->
|
||||||
|
val appProperties = Properties()
|
||||||
|
contentResolver?.openInputStream(uri)?.use { inputStream ->
|
||||||
|
appProperties.load(inputStream)
|
||||||
|
}
|
||||||
|
PreferencesUtil.setAppProperties(this, appProperties)
|
||||||
|
|
||||||
|
// Restart the current activity
|
||||||
|
relaunchCurrentScreen()
|
||||||
|
Toast.makeText(this, R.string.success_import_app_properties, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(this, R.string.error_import_app_properties, Toast.LENGTH_LONG).show()
|
||||||
|
Log.e(TAG, "Unable to import app properties", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export app properties result
|
||||||
|
try {
|
||||||
|
if (requestCode == appPropertiesFileCreationRequestCode) {
|
||||||
|
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
||||||
|
createdFileUri?.let { uri ->
|
||||||
|
contentResolver?.openOutputStream(uri)?.use { outputStream ->
|
||||||
|
PreferencesUtil
|
||||||
|
.getAppProperties(this)
|
||||||
|
.store(outputStream, getString(R.string.description_app_properties))
|
||||||
|
}
|
||||||
|
Toast.makeText(this, R.string.success_export_app_properties, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appPropertiesFileCreationRequestCode = null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(this, R.string.error_export_app_properties, Toast.LENGTH_LONG).show()
|
||||||
|
Log.e(LockingActivity.TAG, "Unable to export app properties", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
|
|
||||||
@@ -244,6 +312,8 @@ open class SettingsActivity
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
private val TAG = SettingsActivity::class.java.name
|
||||||
|
|
||||||
private const val SHOW_LOCK = "SHOW_LOCK"
|
private const val SHOW_LOCK = "SHOW_LOCK"
|
||||||
private const val TITLE_KEY = "TITLE_KEY"
|
private const val TITLE_KEY = "TITLE_KEY"
|
||||||
private const val TAG_NESTED = "TAG_NESTED"
|
private const val TAG_NESTED = "TAG_NESTED"
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 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.settings.preference
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.TypedArray
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import androidx.preference.DialogPreference
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
|
class DurationDialogPreference @JvmOverloads constructor(context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
|
||||||
|
defStyleRes: Int = defStyleAttr)
|
||||||
|
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) {
|
||||||
|
|
||||||
|
private var mDuration: Long = 0L
|
||||||
|
|
||||||
|
override fun getDialogLayoutResource(): Int {
|
||||||
|
return R.layout.pref_dialog_duration
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current duration of preference
|
||||||
|
*/
|
||||||
|
fun getDuration(): Long {
|
||||||
|
return if (mDuration >= 0) mDuration else -1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign [duration] of preference
|
||||||
|
*/
|
||||||
|
fun setDuration(duration: Long) {
|
||||||
|
persistString(duration.toString())
|
||||||
|
notifyChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any?) {
|
||||||
|
if (restorePersistedValue) {
|
||||||
|
mDuration = getPersistedString(mDuration.toString()).toLongOrNull() ?: mDuration
|
||||||
|
} else {
|
||||||
|
mDuration = defaultValue?.toString()?.toLongOrNull() ?: mDuration
|
||||||
|
persistString(mDuration.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGetDefaultValue(a: TypedArray?, index: Int): Any {
|
||||||
|
return try {
|
||||||
|
a?.getString(index)?.toLongOrNull() ?: mDuration
|
||||||
|
} catch (e: Exception) {
|
||||||
|
mDuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Was previously a string
|
||||||
|
override fun persistString(value: String?): Boolean {
|
||||||
|
mDuration = value?.toLongOrNull() ?: mDuration
|
||||||
|
return super.persistString(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 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.settings.preferencedialogfragment
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.NumberPicker
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.settings.preference.DurationDialogPreference
|
||||||
|
|
||||||
|
|
||||||
|
class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
|
||||||
|
|
||||||
|
private var mEnabled = true
|
||||||
|
private var mDays = 0
|
||||||
|
private var mHours = 0
|
||||||
|
private var mMinutes = 0
|
||||||
|
private var mSeconds = 0
|
||||||
|
|
||||||
|
private var daysNumberPicker: NumberPicker? = null
|
||||||
|
private var hoursNumberPicker: NumberPicker? = null
|
||||||
|
private var minutesNumberPicker: NumberPicker? = null
|
||||||
|
private var secondsNumberPicker: NumberPicker? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// To get items from saved instance state
|
||||||
|
if (savedInstanceState != null
|
||||||
|
&& savedInstanceState.containsKey(ENABLE_KEY)
|
||||||
|
&& savedInstanceState.containsKey(DAYS_KEY)
|
||||||
|
&& savedInstanceState.containsKey(HOURS_KEY)
|
||||||
|
&& savedInstanceState.containsKey(MINUTES_KEY)
|
||||||
|
&& savedInstanceState.containsKey(SECONDS_KEY)) {
|
||||||
|
mEnabled = savedInstanceState.getBoolean(ENABLE_KEY)
|
||||||
|
mDays = savedInstanceState.getInt(DAYS_KEY)
|
||||||
|
mHours = savedInstanceState.getInt(HOURS_KEY)
|
||||||
|
mMinutes = savedInstanceState.getInt(MINUTES_KEY)
|
||||||
|
mSeconds = savedInstanceState.getInt(SECONDS_KEY)
|
||||||
|
} else {
|
||||||
|
val currentPreference = preference
|
||||||
|
if (currentPreference is DurationDialogPreference) {
|
||||||
|
durationToDaysHoursMinutesSeconds(currentPreference.getDuration())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun durationToDaysHoursMinutesSeconds(duration: Long) {
|
||||||
|
if (duration < 0) {
|
||||||
|
mDays = 0
|
||||||
|
mHours = 0
|
||||||
|
mMinutes = 0
|
||||||
|
mSeconds = 0
|
||||||
|
} else {
|
||||||
|
mDays = (duration / (24L * 60L * 60L * 1000L)).toInt()
|
||||||
|
val daysMilliseconds = mDays * 24L * 60L * 60L * 1000L
|
||||||
|
mHours = ((duration - daysMilliseconds) / (60L * 60L * 1000L)).toInt()
|
||||||
|
val hoursMilliseconds = mHours * 60L * 60L * 1000L
|
||||||
|
mMinutes = ((duration - daysMilliseconds - hoursMilliseconds) / (60L * 1000L)).toInt()
|
||||||
|
val minutesMilliseconds = mMinutes * 60L * 1000L
|
||||||
|
mSeconds = ((duration - daysMilliseconds - hoursMilliseconds - minutesMilliseconds) / (1000L)).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignValuesInViews() {
|
||||||
|
daysNumberPicker?.value = mDays
|
||||||
|
hoursNumberPicker?.value = mHours
|
||||||
|
minutesNumberPicker?.value = mMinutes
|
||||||
|
secondsNumberPicker?.value = mSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindDialogView(view: View) {
|
||||||
|
super.onBindDialogView(view)
|
||||||
|
|
||||||
|
daysNumberPicker = view.findViewById<NumberPicker>(R.id.days_picker).apply {
|
||||||
|
minValue = 0
|
||||||
|
maxValue = 364
|
||||||
|
setOnValueChangedListener { _, _, newVal ->
|
||||||
|
mDays = newVal
|
||||||
|
activateSwitch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hoursNumberPicker = view.findViewById<NumberPicker>(R.id.hours_picker).apply {
|
||||||
|
minValue = 0
|
||||||
|
maxValue = 23
|
||||||
|
setOnValueChangedListener { _, _, newVal ->
|
||||||
|
mHours = newVal
|
||||||
|
activateSwitch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
minutesNumberPicker = view.findViewById<NumberPicker>(R.id.minutes_picker).apply {
|
||||||
|
minValue = 0
|
||||||
|
maxValue = 59
|
||||||
|
setOnValueChangedListener { _, _, newVal ->
|
||||||
|
mMinutes = newVal
|
||||||
|
activateSwitch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
secondsNumberPicker = view.findViewById<NumberPicker>(R.id.seconds_picker).apply {
|
||||||
|
minValue = 0
|
||||||
|
maxValue = 59
|
||||||
|
setOnValueChangedListener { _, _, newVal ->
|
||||||
|
mSeconds = newVal
|
||||||
|
activateSwitch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSwitchAction({ isChecked ->
|
||||||
|
mEnabled = isChecked
|
||||||
|
}, mDays + mHours + mMinutes + mSeconds > 0)
|
||||||
|
|
||||||
|
assignValuesInViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildDuration(): Long {
|
||||||
|
return if (mEnabled) {
|
||||||
|
mDays * 24L * 60L * 60L * 1000L +
|
||||||
|
mHours * 60L * 60L * 1000L +
|
||||||
|
mMinutes * 60L * 1000L +
|
||||||
|
mSeconds * 1000L
|
||||||
|
} else {
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putBoolean(ENABLE_KEY, mEnabled)
|
||||||
|
outState.putInt(DAYS_KEY, mDays)
|
||||||
|
outState.putInt(HOURS_KEY, mHours)
|
||||||
|
outState.putInt(MINUTES_KEY, mMinutes)
|
||||||
|
outState.putInt(SECONDS_KEY, mSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDialogClosed(positiveResult: Boolean) {
|
||||||
|
if (positiveResult) {
|
||||||
|
val currentPreference = preference
|
||||||
|
if (currentPreference is DurationDialogPreference) {
|
||||||
|
currentPreference.setDuration(buildDuration())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ENABLE_KEY = "ENABLE_KEY"
|
||||||
|
private const val DAYS_KEY = "DAYS_KEY"
|
||||||
|
private const val HOURS_KEY = "HOURS_KEY"
|
||||||
|
private const val MINUTES_KEY = "MINUTES_KEY"
|
||||||
|
private const val SECONDS_KEY = "SECONDS_KEY"
|
||||||
|
|
||||||
|
fun newInstance(key: String): DurationDialogFragmentCompat {
|
||||||
|
val fragment = DurationDialogFragmentCompat()
|
||||||
|
val bundle = Bundle(1)
|
||||||
|
bundle.putString(ARG_KEY, key)
|
||||||
|
fragment.arguments = bundle
|
||||||
|
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -154,4 +154,14 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
|
|||||||
onCheckedChange?.invoke(isChecked)
|
onCheckedChange?.invoke(isChecked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun activateSwitch() {
|
||||||
|
if (switchElementView?.isChecked != true)
|
||||||
|
switchElementView?.isChecked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deactivateSwitch() {
|
||||||
|
if (switchElementView?.isChecked == true)
|
||||||
|
switchElementView?.isChecked = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.utils
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
|
||||||
|
|
||||||
|
|
||||||
private var CREATE_FILE_REQUEST_CODE_DEFAULT = 3853
|
|
||||||
private var fileRequestCodes = ArrayList<Int>()
|
|
||||||
|
|
||||||
fun getUnusedCreateFileRequestCode(): Int {
|
|
||||||
val newCreateFileRequestCode = CREATE_FILE_REQUEST_CODE_DEFAULT++
|
|
||||||
fileRequestCodes.add(newCreateFileRequestCode)
|
|
||||||
return newCreateFileRequestCode
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager): Boolean {
|
|
||||||
return when {
|
|
||||||
// To check if a custom file manager can manage the ACTION_CREATE_DOCUMENT
|
|
||||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT -> {
|
|
||||||
packageManager.queryIntentActivities(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "application/x-keepass"
|
|
||||||
}, PackageManager.MATCH_DEFAULT_ONLY).isNotEmpty()
|
|
||||||
}
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createDocument(activity: FragmentActivity,
|
|
||||||
titleString: String,
|
|
||||||
typeString: String = "application/octet-stream"): Int? {
|
|
||||||
|
|
||||||
val idCode = getUnusedCreateFileRequestCode()
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
|
||||||
try {
|
|
||||||
activity.startActivityForResult(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = typeString
|
|
||||||
putExtra(Intent.EXTRA_TITLE, titleString)
|
|
||||||
}, idCode)
|
|
||||||
return idCode
|
|
||||||
} catch (e: Exception) {
|
|
||||||
FileManagerDialogFragment().show(activity.supportFragmentManager, "browserDialog")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
FileManagerDialogFragment().show(activity.supportFragmentManager, "browserDialog")
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onCreateDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
|
|
||||||
action: (fileCreated: Uri?)->Unit) {
|
|
||||||
// Retrieve the created URI from the file manager
|
|
||||||
if (fileRequestCodes.contains(requestCode) && resultCode == Activity.RESULT_OK) {
|
|
||||||
action.invoke(data?.data)
|
|
||||||
fileRequestCodes.remove(requestCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.kunzisoft.keepass.utils
|
package com.kunzisoft.keepass.utils
|
||||||
|
|
||||||
|
import java.text.Normalizer
|
||||||
|
|
||||||
object StringUtil {
|
object StringUtil {
|
||||||
|
|
||||||
fun String.removeLineChars(): String {
|
fun String.removeLineChars(): String {
|
||||||
@@ -10,5 +12,20 @@ object StringUtil {
|
|||||||
return this.replace("[\\r|\\n|\\t|\\s|\\u00A0]+".toRegex(), "")
|
return this.replace("[\\r|\\n|\\t|\\s|\\u00A0]+".toRegex(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String.flattenToAscii(): String {
|
||||||
|
var string = this
|
||||||
|
val out = CharArray(string.length)
|
||||||
|
string = Normalizer.normalize(string, Normalizer.Form.NFD)
|
||||||
|
var j = 0
|
||||||
|
var i = 0
|
||||||
|
val n = string.length
|
||||||
|
while (i < n) {
|
||||||
|
val c = string[i]
|
||||||
|
if (c <= '\u007F') out[j++] = c
|
||||||
|
++i
|
||||||
|
}
|
||||||
|
return String(out)
|
||||||
|
}
|
||||||
|
|
||||||
fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) }
|
fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) }
|
||||||
}
|
}
|
||||||
@@ -38,16 +38,26 @@ object UriUtil {
|
|||||||
fun getFileData(context: Context, fileUri: Uri?): DocumentFile? {
|
fun getFileData(context: Context, fileUri: Uri?): DocumentFile? {
|
||||||
if (fileUri == null)
|
if (fileUri == null)
|
||||||
return null
|
return null
|
||||||
return when {
|
return try {
|
||||||
isFileScheme(fileUri) -> {
|
when {
|
||||||
fileUri.path?.let {
|
isFileScheme(fileUri) -> {
|
||||||
File(it).let { file ->
|
fileUri.path?.let {
|
||||||
return DocumentFile.fromFile(file)
|
File(it).let { file ->
|
||||||
|
return DocumentFile.fromFile(file)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
isContentScheme(fileUri) -> {
|
||||||
|
DocumentFile.fromSingleUri(context, fileUri)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.e("FileData", "Content scheme not known")
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
isContentScheme(fileUri) -> DocumentFile.fromSingleUri(context, fileUri)
|
} catch (e: Exception) {
|
||||||
else -> null
|
Log.e("FileData", "Unable to get document file", e)
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
101
app/src/main/java/com/kunzisoft/keepass/utils/UuidUtil.java
Normal file
101
app/src/main/java/com/kunzisoft/keepass/utils/UuidUtil.java
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 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.utils;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static com.kunzisoft.keepass.utils.StreamBytesUtilsKt.uuidTo16Bytes;
|
||||||
|
|
||||||
|
public class UuidUtil {
|
||||||
|
|
||||||
|
public static @Nullable String toHexString(@Nullable UUID uuid) {
|
||||||
|
if (uuid == null) { return null; }
|
||||||
|
try {
|
||||||
|
byte[] buf = uuidTo16Bytes(uuid);
|
||||||
|
|
||||||
|
int len = buf.length;
|
||||||
|
if (len == 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
short bt;
|
||||||
|
char high, low;
|
||||||
|
for (byte b : buf) {
|
||||||
|
bt = (short) (b & 0xFF);
|
||||||
|
high = (char) (bt >>> 4);
|
||||||
|
low = (char) (bt & 0x0F);
|
||||||
|
sb.append(byteToChar(high));
|
||||||
|
sb.append(byteToChar(low));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @Nullable UUID fromHexString(@Nullable String hexString) {
|
||||||
|
if (hexString == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (hexString.length() != 32)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
char[] charArray = hexString.toLowerCase().toCharArray();
|
||||||
|
char[] leastSignificantChars = new char[16];
|
||||||
|
char[] mostSignificantChars = new char[16];
|
||||||
|
|
||||||
|
for (int i = 31; i >= 0; i = i-2) {
|
||||||
|
if (i >= 16) {
|
||||||
|
mostSignificantChars[32-i] = charArray[i];
|
||||||
|
mostSignificantChars[31-i] = charArray[i-1];
|
||||||
|
} else {
|
||||||
|
leastSignificantChars[16-i] = charArray[i];
|
||||||
|
leastSignificantChars[15-i] = charArray[i-1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StringBuilder standardUUIDString = new StringBuilder();
|
||||||
|
standardUUIDString.append(leastSignificantChars);
|
||||||
|
standardUUIDString.append(mostSignificantChars);
|
||||||
|
standardUUIDString.insert(8, '-');
|
||||||
|
standardUUIDString.insert(13, '-');
|
||||||
|
standardUUIDString.insert(18, '-');
|
||||||
|
standardUUIDString.insert(23, '-');
|
||||||
|
try {
|
||||||
|
return UUID.fromString(standardUUIDString.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use short to represent unsigned byte
|
||||||
|
private static char byteToChar(char bt) {
|
||||||
|
if (bt >= 10) {
|
||||||
|
return (char)('A' + bt - 10);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return (char)('0' + bt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,7 +39,7 @@ import com.kunzisoft.keepass.database.element.Database
|
|||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.database.search.UuidUtil
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import android.util.AttributeSet
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class KeyFileSelectionView @JvmOverloads constructor(context: Context,
|
class KeyFileSelectionView @JvmOverloads constructor(context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
@@ -51,7 +51,7 @@ class KeyFileSelectionView @JvmOverloads constructor(context: Context,
|
|||||||
set(value) {
|
set(value) {
|
||||||
mUri = value
|
mUri = value
|
||||||
keyFileNameView.text = value?.let {
|
keyFileNameView.text = value?.let {
|
||||||
DocumentFile.fromSingleUri(context, value)?.name ?: value.path
|
UriUtil.getFileData(context, value)?.name ?: value.path
|
||||||
} ?: ""
|
} ?: ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,14 +50,18 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getDatabaseFilesLoadedValue(): DatabaseFileData {
|
||||||
|
var newValue = databaseFilesLoaded.value
|
||||||
|
if (newValue == null) {
|
||||||
|
newValue = DatabaseFileData()
|
||||||
|
}
|
||||||
|
return newValue
|
||||||
|
}
|
||||||
|
|
||||||
fun loadListOfDatabases() {
|
fun loadListOfDatabases() {
|
||||||
checkDefaultDatabase()
|
checkDefaultDatabase()
|
||||||
mFileDatabaseHistoryAction?.getDatabaseFileList { databaseFileListRetrieved ->
|
mFileDatabaseHistoryAction?.getDatabaseFileList { databaseFileListRetrieved ->
|
||||||
var newValue = databaseFilesLoaded.value
|
databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply {
|
||||||
if (newValue == null) {
|
|
||||||
newValue = DatabaseFileData()
|
|
||||||
}
|
|
||||||
newValue.apply {
|
|
||||||
databaseFileAction = DatabaseFileAction.NONE
|
databaseFileAction = DatabaseFileAction.NONE
|
||||||
databaseFileToActivate = null
|
databaseFileToActivate = null
|
||||||
databaseFileList.apply {
|
databaseFileList.apply {
|
||||||
@@ -65,14 +69,13 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
addAll(databaseFileListRetrieved)
|
addAll(databaseFileListRetrieved)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
databaseFilesLoaded.value = newValue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addDatabaseFile(databaseUri: Uri, keyFileUri: Uri?) {
|
fun addDatabaseFile(databaseUri: Uri, keyFileUri: Uri?) {
|
||||||
mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(databaseUri, keyFileUri) { databaseFileAdded ->
|
mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(databaseUri, keyFileUri) { databaseFileAdded ->
|
||||||
databaseFileAdded?.let { _ ->
|
databaseFileAdded?.let { _ ->
|
||||||
databaseFilesLoaded.value = databaseFilesLoaded.value?.apply {
|
databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply {
|
||||||
this.databaseFileAction = DatabaseFileAction.ADD
|
this.databaseFileAction = DatabaseFileAction.ADD
|
||||||
this.databaseFileList.add(databaseFileAdded)
|
this.databaseFileList.add(databaseFileAdded)
|
||||||
this.databaseFileToActivate = databaseFileAdded
|
this.databaseFileToActivate = databaseFileAdded
|
||||||
@@ -84,7 +87,7 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
fun updateDatabaseFile(databaseFileToUpdate: DatabaseFile) {
|
fun updateDatabaseFile(databaseFileToUpdate: DatabaseFile) {
|
||||||
mFileDatabaseHistoryAction?.addOrUpdateDatabaseFile(databaseFileToUpdate) { databaseFileUpdated ->
|
mFileDatabaseHistoryAction?.addOrUpdateDatabaseFile(databaseFileToUpdate) { databaseFileUpdated ->
|
||||||
databaseFileUpdated?.let { _ ->
|
databaseFileUpdated?.let { _ ->
|
||||||
databaseFilesLoaded.value = databaseFilesLoaded.value?.apply {
|
databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply {
|
||||||
this.databaseFileAction = DatabaseFileAction.UPDATE
|
this.databaseFileAction = DatabaseFileAction.UPDATE
|
||||||
this.databaseFileList
|
this.databaseFileList
|
||||||
.find { it.databaseUri == databaseFileUpdated.databaseUri }
|
.find { it.databaseUri == databaseFileUpdated.databaseUri }
|
||||||
@@ -104,7 +107,7 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
fun deleteDatabaseFile(databaseFileToDelete: DatabaseFile) {
|
fun deleteDatabaseFile(databaseFileToDelete: DatabaseFile) {
|
||||||
mFileDatabaseHistoryAction?.deleteDatabaseFile(databaseFileToDelete) { databaseFileDeleted ->
|
mFileDatabaseHistoryAction?.deleteDatabaseFile(databaseFileToDelete) { databaseFileDeleted ->
|
||||||
databaseFileDeleted?.let { _ ->
|
databaseFileDeleted?.let { _ ->
|
||||||
databaseFilesLoaded.value = databaseFilesLoaded.value?.apply {
|
databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply {
|
||||||
databaseFileAction = DatabaseFileAction.DELETE
|
databaseFileAction = DatabaseFileAction.DELETE
|
||||||
databaseFileToActivate = databaseFileDeleted
|
databaseFileToActivate = databaseFileDeleted
|
||||||
databaseFileList.remove(databaseFileDeleted)
|
databaseFileList.remove(databaseFileDeleted)
|
||||||
|
|||||||
5
app/src/main/res/drawable/ic_day_white_24dp.xml
Normal file
5
app/src/main/res/drawable/ic_day_white_24dp.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#FFFFFF" android:pathData="M19,3h-1L18,1h-2v2L8,3L8,1L6,1v2L5,3c-1.11,0 -1.99,0.9 -1.99,2L3,19c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM19,19L5,19L5,8h14v11zM7,10h5v5L7,15z"/>
|
||||||
|
</vector>
|
||||||
149
app/src/main/res/layout/pref_dialog_duration.xml
Normal file
149
app/src/main/res/layout/pref_dialog_duration.xml
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2019 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/>.
|
||||||
|
-->
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/edit"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:importantForAutofill="noExcludeDescendants"
|
||||||
|
tools:targetApi="o">
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/explanation_text"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
style="@style/KeepassDXStyle.TextAppearance.SmallTitle"/>
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SwitchCompat
|
||||||
|
android:id="@+id/switch_element"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/enable"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:layout_marginLeft="20dp"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
|
android:layout_marginRight="20dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/explanation_text" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/switch_element">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/duration_days_picker"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/duration_hours_picker"
|
||||||
|
app:layout_constraintRight_toLeftOf="@+id/duration_hours_picker">
|
||||||
|
<NumberPicker
|
||||||
|
android:id="@+id/days_picker"
|
||||||
|
android:scrollbarFadeDuration="0"
|
||||||
|
android:scrollbarDefaultDelayBeforeFade="0"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@drawable/ic_day_white_24dp"
|
||||||
|
app:tint="?android:attr/textColor"
|
||||||
|
android:contentDescription="@string/digits" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/duration_hours_picker"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
tools:ignore="HardcodedText"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/duration_days_picker"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/duration_days_picker"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/duration_time_picker"
|
||||||
|
app:layout_constraintRight_toLeftOf="@+id/duration_time_picker">
|
||||||
|
<NumberPicker
|
||||||
|
android:id="@+id/hours_picker"
|
||||||
|
android:scrollbarFadeDuration="0"
|
||||||
|
android:scrollbarDefaultDelayBeforeFade="0"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:text=":" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/duration_time_picker"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
tools:ignore="HardcodedText"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/duration_hours_picker"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/duration_hours_picker"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent">
|
||||||
|
<NumberPicker
|
||||||
|
android:id="@+id/minutes_picker"
|
||||||
|
android:scrollbarFadeDuration="0"
|
||||||
|
android:scrollbarDefaultDelayBeforeFade="0"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textSize="28sp"
|
||||||
|
android:text="'"/>
|
||||||
|
<NumberPicker
|
||||||
|
android:id="@+id/seconds_picker"
|
||||||
|
android:scrollbarFadeDuration="0"
|
||||||
|
android:scrollbarDefaultDelayBeforeFade="0"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textSize="28sp"
|
||||||
|
android:text="''"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -137,7 +137,6 @@
|
|||||||
<string name="error_string_key">يجب أن يكون لكل سلسلة اسم حقل.</string>
|
<string name="error_string_key">يجب أن يكون لكل سلسلة اسم حقل.</string>
|
||||||
<string name="error_wrong_length">أدخل عددًا صحيحًا موجبًا في حقل «الطول».</string>
|
<string name="error_wrong_length">أدخل عددًا صحيحًا موجبًا في حقل «الطول».</string>
|
||||||
<string name="error_autofill_enable_service">تعذر تمكين خدمة الملء التلقائي.</string>
|
<string name="error_autofill_enable_service">تعذر تمكين خدمة الملء التلقائي.</string>
|
||||||
<string name="error_move_folder_in_itself">لا يمكن نقل مجموعة إلى نفسها.</string>
|
|
||||||
<string name="file_not_found_content">تعذر إيجاد الملف. جرِّب فتحه من متصفح ملفات.</string>
|
<string name="file_not_found_content">تعذر إيجاد الملف. جرِّب فتحه من متصفح ملفات.</string>
|
||||||
<string name="file_browser">مدير الملفات</string>
|
<string name="file_browser">مدير الملفات</string>
|
||||||
<string name="invalid_credentials">تعذر قراءة الإعتمادات.</string>
|
<string name="invalid_credentials">تعذر قراءة الإعتمادات.</string>
|
||||||
|
|||||||
@@ -85,7 +85,6 @@
|
|||||||
<string name="error_copy_group_here">Ovde ne možete kopirati grupu.</string>
|
<string name="error_copy_group_here">Ovde ne možete kopirati grupu.</string>
|
||||||
<string name="error_copy_entry_here">Ovde ne možete kopirati unos.</string>
|
<string name="error_copy_entry_here">Ovde ne možete kopirati unos.</string>
|
||||||
<string name="error_move_entry_here">Ovde ne možete premestiti unos.</string>
|
<string name="error_move_entry_here">Ovde ne možete premestiti unos.</string>
|
||||||
<string name="error_move_folder_in_itself">Ne možete premestiti grupu u samu sebe.</string>
|
|
||||||
<string name="error_autofill_enable_service">Nije moguće omogućiti uslugu automatskog popunjavanja.</string>
|
<string name="error_autofill_enable_service">Nije moguće omogućiti uslugu automatskog popunjavanja.</string>
|
||||||
<string name="error_wrong_length">Unesite pozitivan ceo broj u polje \"Dužina\".</string>
|
<string name="error_wrong_length">Unesite pozitivan ceo broj u polje \"Dužina\".</string>
|
||||||
<string name="error_label_exists">Ova oznaka već postoji.</string>
|
<string name="error_label_exists">Ova oznaka već postoji.</string>
|
||||||
|
|||||||
@@ -122,17 +122,6 @@
|
|||||||
<string name="uppercase">Majúscules</string>
|
<string name="uppercase">Majúscules</string>
|
||||||
<string name="version_label">Versió %1$s</string>
|
<string name="version_label">Versió %1$s</string>
|
||||||
<string name="education_unlock_summary">Introdueix una contrasenya i/o un arxiu clau per desbloquejar la base de dades.</string>
|
<string name="education_unlock_summary">Introdueix una contrasenya i/o un arxiu clau per desbloquejar la base de dades.</string>
|
||||||
<string-array name="timeout_options">
|
|
||||||
<item>5 segons</item>
|
|
||||||
<item>10 segons</item>
|
|
||||||
<item>20 segons</item>
|
|
||||||
<item>30 segons</item>
|
|
||||||
<item>1 minut</item>
|
|
||||||
<item>5 minuts</item>
|
|
||||||
<item>15 minuts</item>
|
|
||||||
<item>30 minuts</item>
|
|
||||||
<item>Mai</item>
|
|
||||||
</string-array>
|
|
||||||
<string-array name="list_size_options">
|
<string-array name="list_size_options">
|
||||||
<item>Petita</item>
|
<item>Petita</item>
|
||||||
<item>Mitjana</item>
|
<item>Mitjana</item>
|
||||||
@@ -299,6 +288,5 @@
|
|||||||
<string name="error_string_type">Aquest text no coincideix amb l\'element sol·licitat.</string>
|
<string name="error_string_type">Aquest text no coincideix amb l\'element sol·licitat.</string>
|
||||||
<string name="error_otp_type">L\'OTP existent no està reconegut per aquest formulari, la seva validació ja no pot generar correctament el token.</string>
|
<string name="error_otp_type">L\'OTP existent no està reconegut per aquest formulari, la seva validació ja no pot generar correctament el token.</string>
|
||||||
<string name="error_create_database_file">No s\'ha pogut crear una base de dades amb aquesta contrasenya i arxiu de clau.</string>
|
<string name="error_create_database_file">No s\'ha pogut crear una base de dades amb aquesta contrasenya i arxiu de clau.</string>
|
||||||
<string name="error_move_folder_in_itself">No pots moure un grup dintre d\'ell mateix.</string>
|
|
||||||
<string name="error_autofill_enable_service">No s\'ha pogut habilitar el servei d\'autocompletat.</string>
|
<string name="error_autofill_enable_service">No s\'ha pogut habilitar el servei d\'autocompletat.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -133,17 +133,6 @@
|
|||||||
<string name="education_unlock_summary">Databázi odemknete zadáním hesla a/nebo souboru s klíčem.
|
<string name="education_unlock_summary">Databázi odemknete zadáním hesla a/nebo souboru s klíčem.
|
||||||
\n
|
\n
|
||||||
\nNezapomeňte po každé úpravě zálohovat kopii svého .kdbx souboru na bezpečné místo.</string>
|
\nNezapomeňte po každé úpravě zálohovat kopii svého .kdbx souboru na bezpečné místo.</string>
|
||||||
<string-array name="timeout_options">
|
|
||||||
<item>5 sekund</item>
|
|
||||||
<item>10 sekund</item>
|
|
||||||
<item>20 sekund</item>
|
|
||||||
<item>30 sekund</item>
|
|
||||||
<item>1 minuta</item>
|
|
||||||
<item>5 minut</item>
|
|
||||||
<item>15 minut</item>
|
|
||||||
<item>30 minut</item>
|
|
||||||
<item>Nikdy</item>
|
|
||||||
</string-array>
|
|
||||||
<string-array name="list_size_options">
|
<string-array name="list_size_options">
|
||||||
<item>Malý</item>
|
<item>Malý</item>
|
||||||
<item>Střední</item>
|
<item>Střední</item>
|
||||||
@@ -157,7 +146,6 @@
|
|||||||
<string name="error_load_database">Databázi se nepodařilo načíst.</string>
|
<string name="error_load_database">Databázi se nepodařilo načíst.</string>
|
||||||
<string name="error_load_database_KDF_memory">Klíč se nepodařilo načíst, zkuste snížit \"využití paměti\" pro KDF.</string>
|
<string name="error_load_database_KDF_memory">Klíč se nepodařilo načíst, zkuste snížit \"využití paměti\" pro KDF.</string>
|
||||||
<string name="error_autofill_enable_service">Službu automatického vyplňování se nepodařilo zapnout.</string>
|
<string name="error_autofill_enable_service">Službu automatického vyplňování se nepodařilo zapnout.</string>
|
||||||
<string name="error_move_folder_in_itself">Není možné přesunout skupinu do ní samotné.</string>
|
|
||||||
<string name="file_not_found_content">Soubor nenalezen. Zkuste jej otevřít ze správce souborů.</string>
|
<string name="file_not_found_content">Soubor nenalezen. Zkuste jej otevřít ze správce souborů.</string>
|
||||||
<string name="list_entries_show_username_title">Zobrazit uživatelská jména</string>
|
<string name="list_entries_show_username_title">Zobrazit uživatelská jména</string>
|
||||||
<string name="list_entries_show_username_summary">V seznamech záznamů zobrazit uživatelská jména</string>
|
<string name="list_entries_show_username_summary">V seznamech záznamů zobrazit uživatelská jména</string>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<string name="add_entry">Tilføj post</string>
|
<string name="add_entry">Tilføj post</string>
|
||||||
<string name="add_group">Tilføj gruppe</string>
|
<string name="add_group">Tilføj gruppe</string>
|
||||||
<string name="encryption_algorithm">Krypteringsalgoritme</string>
|
<string name="encryption_algorithm">Krypteringsalgoritme</string>
|
||||||
<string name="app_timeout">Timeout</string>
|
<string name="app_timeout">Tid udløbet</string>
|
||||||
<string name="app_timeout_summary">Inaktiv tid, før databasen låses</string>
|
<string name="app_timeout_summary">Inaktiv tid, før databasen låses</string>
|
||||||
<string name="application">Program</string>
|
<string name="application">Program</string>
|
||||||
<string name="menu_app_settings">Indstillinger</string>
|
<string name="menu_app_settings">Indstillinger</string>
|
||||||
@@ -132,17 +132,6 @@
|
|||||||
<string name="education_unlock_summary">Angiv en adgangskode og/eller en nøglefil til at låse databasen op.
|
<string name="education_unlock_summary">Angiv en adgangskode og/eller en nøglefil til at låse databasen op.
|
||||||
\n
|
\n
|
||||||
\nHusk at gemme en kopi af .kdbx filen i et sikkert sted efter hver ændring.</string>
|
\nHusk at gemme en kopi af .kdbx filen i et sikkert sted efter hver ændring.</string>
|
||||||
<string-array name="timeout_options">
|
|
||||||
<item>5 sekunder</item>
|
|
||||||
<item>10 sekunder</item>
|
|
||||||
<item>20 sekunder</item>
|
|
||||||
<item>30 sekunder</item>
|
|
||||||
<item>1 minut</item>
|
|
||||||
<item>5 minutter</item>
|
|
||||||
<item>15 minutter</item>
|
|
||||||
<item>30 minutter</item>
|
|
||||||
<item>Aldrig</item>
|
|
||||||
</string-array>
|
|
||||||
<string-array name="list_size_options">
|
<string-array name="list_size_options">
|
||||||
<item>Lille</item>
|
<item>Lille</item>
|
||||||
<item>Mellem</item>
|
<item>Mellem</item>
|
||||||
@@ -156,7 +145,6 @@
|
|||||||
<string name="error_load_database">Databasen kunne ikke indlæses.</string>
|
<string name="error_load_database">Databasen kunne ikke indlæses.</string>
|
||||||
<string name="error_load_database_KDF_memory">Kunne ikke indlæse nøglen. Prøv at reducere KDF \"hukommelsesforbrug\".</string>
|
<string name="error_load_database_KDF_memory">Kunne ikke indlæse nøglen. Prøv at reducere KDF \"hukommelsesforbrug\".</string>
|
||||||
<string name="error_autofill_enable_service">Kunne ikke aktivere autofyld tjenesten.</string>
|
<string name="error_autofill_enable_service">Kunne ikke aktivere autofyld tjenesten.</string>
|
||||||
<string name="error_move_folder_in_itself">Kan ikke flytte en gruppe til sig selv.</string>
|
|
||||||
<string name="file_not_found_content">Kunne ikke finde filen. Prøv at åbne den fra filhåndtering.</string>
|
<string name="file_not_found_content">Kunne ikke finde filen. Prøv at åbne den fra filhåndtering.</string>
|
||||||
<string name="list_entries_show_username_title">Vis brugernavne</string>
|
<string name="list_entries_show_username_title">Vis brugernavne</string>
|
||||||
<string name="list_entries_show_username_summary">Vis brugernavne i postlister</string>
|
<string name="list_entries_show_username_summary">Vis brugernavne i postlister</string>
|
||||||
@@ -232,7 +220,7 @@
|
|||||||
<string name="database_description_title">Database beskrivelse</string>
|
<string name="database_description_title">Database beskrivelse</string>
|
||||||
<string name="database_version_title">Databaseversion</string>
|
<string name="database_version_title">Databaseversion</string>
|
||||||
<string name="text_appearance">Tekst</string>
|
<string name="text_appearance">Tekst</string>
|
||||||
<string name="application_appearance">Program</string>
|
<string name="application_appearance">Brugerflade</string>
|
||||||
<string name="other">Øvrige</string>
|
<string name="other">Øvrige</string>
|
||||||
<string name="keyboard">Tastatur</string>
|
<string name="keyboard">Tastatur</string>
|
||||||
<string name="magic_keyboard_title">Magikeyboard</string>
|
<string name="magic_keyboard_title">Magikeyboard</string>
|
||||||
@@ -281,14 +269,14 @@
|
|||||||
<string name="html_text_ad_free">I modsætning til andre programmer til adgangskodeadministration er denne <strong>annoncefri</strong>, <strong>copyleft fri software</strong>, og indsamler ikke personlige data, uanset hvilken version der bruges.</string>
|
<string name="html_text_ad_free">I modsætning til andre programmer til adgangskodeadministration er denne <strong>annoncefri</strong>, <strong>copyleft fri software</strong>, og indsamler ikke personlige data, uanset hvilken version der bruges.</string>
|
||||||
<string name="html_text_buy_pro">Ved at købe pro-versionen, er der adgang til <strong>visuel stil</strong>, og det vil især hjælpe <strong>gennemførelsen af lokale projekter.</strong></string>
|
<string name="html_text_buy_pro">Ved at købe pro-versionen, er der adgang til <strong>visuel stil</strong>, og det vil især hjælpe <strong>gennemførelsen af lokale projekter.</strong></string>
|
||||||
<string name="html_text_feature_generosity">Denne <strong>visuelle stil</strong> er tilgængelige takket være bidrag.</string>
|
<string name="html_text_feature_generosity">Denne <strong>visuelle stil</strong> er tilgængelige takket være bidrag.</string>
|
||||||
<string name="html_text_donation">For at bevare uafhængighed og altid at være aktiv, regner vi med <strong>bidrag.</strong></string>
|
<string name="html_text_donation">For at bevare uafhængighed og altid at være aktiv, håber vi på dit <strong>bidrag.</strong></string>
|
||||||
<string name="html_text_dev_feature">Funktionen er <strong>under udvikling</strong>, og det kræver <strong>bidrag</strong>, for snart at være tilgængelig.</string>
|
<string name="html_text_dev_feature">Funktionen er <strong>under udvikling</strong>, og det kræver <strong>bidrag</strong>, for snart at være tilgængelig.</string>
|
||||||
<string name="html_text_dev_feature_buy_pro">Ved at købe <strong>pro</strong> versionen,</string>
|
<string name="html_text_dev_feature_buy_pro">Ved at købe <strong>pro</strong> versionen,</string>
|
||||||
<string name="html_text_dev_feature_contibute">Ved at <strong>bidrage</strong>,</string>
|
<string name="html_text_dev_feature_contibute">Ved at <strong>bidrage</strong>,</string>
|
||||||
<string name="html_text_dev_feature_encourage">tilskyndes udviklerne til at lave <strong>nye funktioner</strong> og <strong>rette fejl</strong> i henhold bemærkninger.</string>
|
<string name="html_text_dev_feature_encourage">tilskyndes udviklerne til at lave <strong>nye funktioner</strong> og <strong>rette fejl</strong> i henhold bemærkninger.</string>
|
||||||
<string name="html_text_dev_feature_thanks">Tak for bidrag.</string>
|
<string name="html_text_dev_feature_thanks">Tak for bidrag.</string>
|
||||||
<string name="html_text_dev_feature_work_hard">Vi arbejder hårdt på hurtigt at frigive denne funktion.</string>
|
<string name="html_text_dev_feature_work_hard">Vi arbejder hårdt på hurtigt at frigive denne funktion.</string>
|
||||||
<string name="html_text_dev_feature_upgrade">Glem ikke at holde appen opdateret ved at installere nye versioner.</string>
|
<string name="html_text_dev_feature_upgrade">Husk at holde din app opdateret ved at installere den nyeste version.</string>
|
||||||
<string name="download">Hent</string>
|
<string name="download">Hent</string>
|
||||||
<string name="contribute">Bidrag</string>
|
<string name="contribute">Bidrag</string>
|
||||||
<string name="style_choose_title">Tema</string>
|
<string name="style_choose_title">Tema</string>
|
||||||
@@ -352,7 +340,7 @@
|
|||||||
<string name="menu_advanced_unlock_settings">Avanceret oplåsning</string>
|
<string name="menu_advanced_unlock_settings">Avanceret oplåsning</string>
|
||||||
<string name="biometric">Biometrisk</string>
|
<string name="biometric">Biometrisk</string>
|
||||||
<string name="biometric_auto_open_prompt_title">Åbn automatisk biometrisk prompt</string>
|
<string name="biometric_auto_open_prompt_title">Åbn automatisk biometrisk prompt</string>
|
||||||
<string name="biometric_auto_open_prompt_summary">Spørg automatisk efter biometri, hvis databasen er konfigureret til at bruge den</string>
|
<string name="biometric_auto_open_prompt_summary">Spørg automatisk efter biometrisk oplåsning, hvis databasen er konfigureret til at bruge det</string>
|
||||||
<string name="enable">Aktiver</string>
|
<string name="enable">Aktiver</string>
|
||||||
<string name="disable">Deaktiver</string>
|
<string name="disable">Deaktiver</string>
|
||||||
<string name="master_key">Hovednøgle</string>
|
<string name="master_key">Hovednøgle</string>
|
||||||
@@ -438,7 +426,7 @@
|
|||||||
<string name="hide_broken_locations_title">Skjule brudte databaselinks</string>
|
<string name="hide_broken_locations_title">Skjule brudte databaselinks</string>
|
||||||
<string name="hide_broken_locations_summary">Skjul brudte links på listen over seneste databaser</string>
|
<string name="hide_broken_locations_summary">Skjul brudte links på listen over seneste databaser</string>
|
||||||
<string name="warning_database_read_only">Giv fil skriveadgang for at gemme databasændringer</string>
|
<string name="warning_database_read_only">Giv fil skriveadgang for at gemme databasændringer</string>
|
||||||
<string name="education_setup_OTP_summary">Opsætning af engangsadgangskodestyring (HOTP / TOTP) for at generere et token anmodet om tofaktorautentisering (2FA).</string>
|
<string name="education_setup_OTP_summary">Opsætning af engangs-adgangskode-styring (HOTP / TOTP) for at generere et token anmodet af tofaktor-autentisering (2FA).</string>
|
||||||
<string name="education_setup_OTP_title">Opsætning af OTP</string>
|
<string name="education_setup_OTP_title">Opsætning af OTP</string>
|
||||||
<string name="error_create_database">Databasefilen kunne ikke oprettes.</string>
|
<string name="error_create_database">Databasefilen kunne ikke oprettes.</string>
|
||||||
<string name="entry_add_attachment">Tilføj vedhæng</string>
|
<string name="entry_add_attachment">Tilføj vedhæng</string>
|
||||||
@@ -542,4 +530,35 @@
|
|||||||
<string name="error_rebuild_list">Listen kan ikke genopbygges korrekt.</string>
|
<string name="error_rebuild_list">Listen kan ikke genopbygges korrekt.</string>
|
||||||
<string name="error_database_uri_null">Database-URI kan ikke hentes.</string>
|
<string name="error_database_uri_null">Database-URI kan ikke hentes.</string>
|
||||||
<string name="content_description_otp_information">Oplysninger om engangsadgangskode</string>
|
<string name="content_description_otp_information">Oplysninger om engangsadgangskode</string>
|
||||||
|
<string name="advanced_unlock_prompt_extract_credential_message">Uddrag databasens legitimationsoplysninger med biometriske data</string>
|
||||||
|
<string name="advanced_unlock_prompt_store_credential_message">Advarsel: hovedadgangskoden skal stadig huskes, hvis der bruges biometrisk genkendelse.</string>
|
||||||
|
<string name="open_advanced_unlock_prompt_unlock_database">Åbn biometriske forespørgsel for at låse databasen op</string>
|
||||||
|
<string name="warning_database_revoked">Adgang til filen tilbagekaldt af filhåndteringsprogrammet, luk databasen og genåbn den fra dens placering.</string>
|
||||||
|
<string name="error_start_database_action">Der opstod en fejl under udførelsen af en handling på databasen.</string>
|
||||||
|
<string name="error_remove_file">Der opstod en fejl med at fjerne fildata.</string>
|
||||||
|
<string name="error_otp_type">Den existerende OTP type kunne ikke genkendes, den kan være tiden er udløbet for at lave dette token.</string>
|
||||||
|
<string name="education_advanced_unlock_summary">Sammenkæd din adgangskode, med din scannede biometriske oplysninger eller enheds legitimationsoplysninger for, hurtigt at låse din database op.</string>
|
||||||
|
<string name="enter">Enter</string>
|
||||||
|
<string name="temp_advanced_unlock_timeout_summary">Varigheden af avanceret oplåsning, før indholdet slettes</string>
|
||||||
|
<string name="device_credential_unlock_enable_summary">Giver dig mulighed for at bruge dine enhedsoplysninger for at åbne databasen</string>
|
||||||
|
<string name="device_credential_unlock_enable_title">Oplåsning via enhedsoplysninger</string>
|
||||||
|
<string name="autofill_inline_suggestions_summary">Forsøg på at vise forslag til automatisk udfyldning direkte fra et kompatibelt tastatur</string>
|
||||||
|
<string name="autofill_inline_suggestions_title">Indbyggede forslag</string>
|
||||||
|
<string name="backspace">Tilbagetast</string>
|
||||||
|
<string name="advanced_unlock_timeout">Avanceret oplåsningstimeout</string>
|
||||||
|
<string name="temp_advanced_unlock_timeout_title">Avanceret oplåsning udløbsdato</string>
|
||||||
|
<string name="temp_advanced_unlock_enable_summary">Gem ikke krypteret indhold for at bruge avanceret oplåsning</string>
|
||||||
|
<string name="temp_advanced_unlock_enable_title">Midlertidig biometrisk oplåsning</string>
|
||||||
|
<string name="device_credential">Enhedsoplysninger</string>
|
||||||
|
<string name="properties">Egenskaber</string>
|
||||||
|
<string name="open_advanced_unlock_prompt_store_credential">Åbn den biometrisk genkendelse for at gemme legitimationsoplysninger</string>
|
||||||
|
<string name="error_export_app_properties">Fejl under eksport af app-egenskaber</string>
|
||||||
|
<string name="success_export_app_properties">App-egenskaber eksporteret</string>
|
||||||
|
<string name="error_import_app_properties">Fejl under importering af app-egenskaber</string>
|
||||||
|
<string name="success_import_app_properties">App-egenskaber importeret</string>
|
||||||
|
<string name="description_app_properties">KeePassDX-egenskaber til at administrere app-indstillinger</string>
|
||||||
|
<string name="export_app_properties_summary">Opret en fil til at eksportere app-egenskaber</string>
|
||||||
|
<string name="export_app_properties_title">Eksporter app-egenskaber</string>
|
||||||
|
<string name="import_app_properties_summary">Vælg en fil for at importere app-egenskaber</string>
|
||||||
|
<string name="import_app_properties_title">Importer appegenskaber</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<string name="add_entry">Eintrag hinzufügen</string>
|
<string name="add_entry">Eintrag hinzufügen</string>
|
||||||
<string name="add_group">Gruppe hinzufügen</string>
|
<string name="add_group">Gruppe hinzufügen</string>
|
||||||
<string name="encryption_algorithm">Verschlüsselungsalgorithmus</string>
|
<string name="encryption_algorithm">Verschlüsselungsalgorithmus</string>
|
||||||
<string name="app_timeout">App-Timeout</string>
|
<string name="app_timeout">Zeitüberschreitung</string>
|
||||||
<string name="app_timeout_summary">Inaktivitätszeit vor dem Sperren der Datenbank</string>
|
<string name="app_timeout_summary">Inaktivitätszeit vor dem Sperren der Datenbank</string>
|
||||||
<string name="application">App</string>
|
<string name="application">App</string>
|
||||||
<string name="menu_app_settings">App-Einstellungen</string>
|
<string name="menu_app_settings">App-Einstellungen</string>
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
<string name="entry_user_name">Benutzername</string>
|
<string name="entry_user_name">Benutzername</string>
|
||||||
<string name="error_arc4">Die RC4/Arcfour-Stromverschlüsselung wird nicht unterstützt.</string>
|
<string name="error_arc4">Die RC4/Arcfour-Stromverschlüsselung wird nicht unterstützt.</string>
|
||||||
<string name="error_can_not_handle_uri">KeePassDX kann diese URI-Adresse nicht verarbeiten.</string>
|
<string name="error_can_not_handle_uri">KeePassDX kann diese URI-Adresse nicht verarbeiten.</string>
|
||||||
<string name="error_file_not_create">Konnte Datei nicht erstellen</string>
|
<string name="error_file_not_create">Datei konnte nicht erstellt werden</string>
|
||||||
<string name="error_invalid_db">Datenbank nicht lesbar.</string>
|
<string name="error_invalid_db">Datenbank nicht lesbar.</string>
|
||||||
<string name="error_invalid_path">Sicherstellen, dass der Pfad korrekt ist.</string>
|
<string name="error_invalid_path">Sicherstellen, dass der Pfad korrekt ist.</string>
|
||||||
<string name="error_no_name">Namen eingeben.</string>
|
<string name="error_no_name">Namen eingeben.</string>
|
||||||
@@ -118,8 +118,8 @@
|
|||||||
<string name="never">Nie</string>
|
<string name="never">Nie</string>
|
||||||
<string name="no_results">Keine Suchergebnisse</string>
|
<string name="no_results">Keine Suchergebnisse</string>
|
||||||
<string name="no_url_handler">Bitte einen Webbrowser installieren, um diese URL zu öffnen.</string>
|
<string name="no_url_handler">Bitte einen Webbrowser installieren, um diese URL zu öffnen.</string>
|
||||||
<string name="omit_backup_search_title">Recycle bin und Backup nicht durchsuchen</string>
|
<string name="omit_backup_search_title">Papierkorb und Backup nicht durchsuchen</string>
|
||||||
<string name="omit_backup_search_summary">Die Gruppen „Backup“ und „Recycle bin“ werden bei der Suche nicht berücksichtigt</string>
|
<string name="omit_backup_search_summary">Die Gruppen „Backup“ und „Papierkorb“ werden bei der Suche nicht berücksichtigt</string>
|
||||||
<string name="auto_focus_search_title">Schnellsuche</string>
|
<string name="auto_focus_search_title">Schnellsuche</string>
|
||||||
<string name="auto_focus_search_summary">Beim Öffnen einer Datenbank eine Suche veranlassen</string>
|
<string name="auto_focus_search_summary">Beim Öffnen einer Datenbank eine Suche veranlassen</string>
|
||||||
<string name="progress_create">Neue Datenbank anlegen …</string>
|
<string name="progress_create">Neue Datenbank anlegen …</string>
|
||||||
@@ -147,17 +147,6 @@
|
|||||||
<string name="education_unlock_summary">Geben Sie das Passwort und/oder die Schlüsseldatei ein, um Ihre Datenbank zu entsperren.
|
<string name="education_unlock_summary">Geben Sie das Passwort und/oder die Schlüsseldatei ein, um Ihre Datenbank zu entsperren.
|
||||||
\n
|
\n
|
||||||
\nSichern Sie Ihre Datenbankdatei nach jeder Änderung an einem sicheren Ort.</string>
|
\nSichern Sie Ihre Datenbankdatei nach jeder Änderung an einem sicheren Ort.</string>
|
||||||
<string-array name="timeout_options">
|
|
||||||
<item>5 Sekunden</item>
|
|
||||||
<item>10 Sekunden</item>
|
|
||||||
<item>20 Sekunden</item>
|
|
||||||
<item>30 Sekunden</item>
|
|
||||||
<item>1 Minute</item>
|
|
||||||
<item>5 Minuten</item>
|
|
||||||
<item>15 Minuten</item>
|
|
||||||
<item>30 Minuten</item>
|
|
||||||
<item>Nie</item>
|
|
||||||
</string-array>
|
|
||||||
<string-array name="list_size_options">
|
<string-array name="list_size_options">
|
||||||
<item>Klein</item>
|
<item>Klein</item>
|
||||||
<item>Mittel</item>
|
<item>Mittel</item>
|
||||||
@@ -178,7 +167,7 @@
|
|||||||
<string name="file_name">Dateiname</string>
|
<string name="file_name">Dateiname</string>
|
||||||
<string name="unavailable_feature_text">Dieses Feature konnte nicht gestartet werden.</string>
|
<string name="unavailable_feature_text">Dieses Feature konnte nicht gestartet werden.</string>
|
||||||
<string name="biometric_unlock_enable_summary">Ermöglicht Ihre Biometrie zu scannen, um die Datenbank zu öffnen.</string>
|
<string name="biometric_unlock_enable_summary">Ermöglicht Ihre Biometrie zu scannen, um die Datenbank zu öffnen.</string>
|
||||||
<string name="advanced_unlock">Erweiterte Entsperrung</string>
|
<string name="advanced_unlock">Moderne Entsperrung</string>
|
||||||
<string name="biometric_unlock_enable_title">Biometrische Entsperrung</string>
|
<string name="biometric_unlock_enable_title">Biometrische Entsperrung</string>
|
||||||
<string name="lock">Sperren</string>
|
<string name="lock">Sperren</string>
|
||||||
<string name="list_password_generator_options_summary">Erlaubte Zeichen für Passwortgenerator festlegen</string>
|
<string name="list_password_generator_options_summary">Erlaubte Zeichen für Passwortgenerator festlegen</string>
|
||||||
@@ -197,7 +186,7 @@
|
|||||||
<string name="encryption_explanation">Verschlüsselungsalgorithmus der Datenbank wird für sämtliche Daten verwendet.</string>
|
<string name="encryption_explanation">Verschlüsselungsalgorithmus der Datenbank wird für sämtliche Daten verwendet.</string>
|
||||||
<string name="kdf_explanation">Um den Schlüssel für den Verschlüsselungsalgorithmus zu generieren, wird der Hauptschlüssel umgewandelt, wobei ein zufälliger Salt in der Schlüsselberechnung verwendet wird.</string>
|
<string name="kdf_explanation">Um den Schlüssel für den Verschlüsselungsalgorithmus zu generieren, wird der Hauptschlüssel umgewandelt, wobei ein zufälliger Salt in der Schlüsselberechnung verwendet wird.</string>
|
||||||
<string name="memory_usage">Speichernutzung</string>
|
<string name="memory_usage">Speichernutzung</string>
|
||||||
<string name="memory_usage_explanation">Größe des Speichers der für die Schlüsselableitung genutzt wird.</string>
|
<string name="memory_usage_explanation">Größe des Speichers, der für die Schlüsselableitung genutzt wird.</string>
|
||||||
<string name="parallelism">Parallelismus</string>
|
<string name="parallelism">Parallelismus</string>
|
||||||
<string name="parallelism_explanation">Grad des Parallelismus (d. h. Anzahl der Threads), der für die Schlüsselableitung genutzt wird.</string>
|
<string name="parallelism_explanation">Grad des Parallelismus (d. h. Anzahl der Threads), der für die Schlüsselableitung genutzt wird.</string>
|
||||||
<string name="sort_menu">Sortieren</string>
|
<string name="sort_menu">Sortieren</string>
|
||||||
@@ -279,7 +268,6 @@
|
|||||||
<string name="contribute">Unterstützen</string>
|
<string name="contribute">Unterstützen</string>
|
||||||
<string name="icon_pack_choose_title">Symbolpaket</string>
|
<string name="icon_pack_choose_title">Symbolpaket</string>
|
||||||
<string name="icon_pack_choose_summary">In der App verwendetes Symbolpaket</string>
|
<string name="icon_pack_choose_summary">In der App verwendetes Symbolpaket</string>
|
||||||
<string name="error_move_folder_in_itself">Eine Gruppe kann nicht in sich selbst verschoben werden.</string>
|
|
||||||
<string name="menu_copy">Kopieren</string>
|
<string name="menu_copy">Kopieren</string>
|
||||||
<string name="menu_move">Verschieben</string>
|
<string name="menu_move">Verschieben</string>
|
||||||
<string name="menu_paste">Einfügen</string>
|
<string name="menu_paste">Einfügen</string>
|
||||||
@@ -365,12 +353,12 @@
|
|||||||
<string name="content_description_update_from_list">Aktualisieren</string>
|
<string name="content_description_update_from_list">Aktualisieren</string>
|
||||||
<string name="content_description_keyboard_close_fields">Felder schließen</string>
|
<string name="content_description_keyboard_close_fields">Felder schließen</string>
|
||||||
<string name="error_create_database_file">Es ist nicht möglich, eine Datenbank mit diesem Passwort und dieser Schlüsseldatei zu erstellen.</string>
|
<string name="error_create_database_file">Es ist nicht möglich, eine Datenbank mit diesem Passwort und dieser Schlüsseldatei zu erstellen.</string>
|
||||||
<string name="menu_advanced_unlock_settings">Erweitertes Entsperren</string>
|
<string name="menu_advanced_unlock_settings">Modernes Entsperren</string>
|
||||||
<string name="biometric">Biometrisch</string>
|
<string name="biometric">Biometrisch</string>
|
||||||
<string name="enable">Aktivieren</string>
|
<string name="enable">Aktivieren</string>
|
||||||
<string name="disable">Deaktivieren</string>
|
<string name="disable">Deaktivieren</string>
|
||||||
<string name="biometric_auto_open_prompt_title">Abfrage automatisch öffnen</string>
|
<string name="biometric_auto_open_prompt_title">Abfrage automatisch öffnen</string>
|
||||||
<string name="biometric_auto_open_prompt_summary">Automatisch nach der erweiterten Entsperrung fragen, wenn die Datenbank dafür eingerichtet ist</string>
|
<string name="biometric_auto_open_prompt_summary">Automatisch moderne Entsperrung abfragen, wenn die Datenbank dafür eingerichtet ist</string>
|
||||||
<string name="master_key">Hauptschlüssel</string>
|
<string name="master_key">Hauptschlüssel</string>
|
||||||
<string name="security">Sicherheit</string>
|
<string name="security">Sicherheit</string>
|
||||||
<string name="entry_history">Verlauf</string>
|
<string name="entry_history">Verlauf</string>
|
||||||
@@ -384,7 +372,7 @@
|
|||||||
<string name="entry_otp">OTP</string>
|
<string name="entry_otp">OTP</string>
|
||||||
<string name="error_invalid_OTP">Ungültiges OTP-Geheimnis.</string>
|
<string name="error_invalid_OTP">Ungültiges OTP-Geheimnis.</string>
|
||||||
<string name="error_disallow_no_credentials">Mindestens eine Anmeldeinformation muss festgelegt sein.</string>
|
<string name="error_disallow_no_credentials">Mindestens eine Anmeldeinformation muss festgelegt sein.</string>
|
||||||
<string name="error_copy_group_here">Eine Gruppe kann nicht hierher kopiert werden.</string>
|
<string name="error_copy_group_here">Hierher kann keine Gruppe kopiert werden.</string>
|
||||||
<string name="error_otp_secret_key">Geheimschlüssel muss im Base32-Format vorliegen.</string>
|
<string name="error_otp_secret_key">Geheimschlüssel muss im Base32-Format vorliegen.</string>
|
||||||
<string name="error_otp_counter">Zähler muss zwischen %1$d und %2$d eingestellt sein.</string>
|
<string name="error_otp_counter">Zähler muss zwischen %1$d und %2$d eingestellt sein.</string>
|
||||||
<string name="error_otp_period">Zeitraum muss zwischen %1$d und %2$d Sekunden liegen.</string>
|
<string name="error_otp_period">Zeitraum muss zwischen %1$d und %2$d Sekunden liegen.</string>
|
||||||
@@ -397,7 +385,7 @@
|
|||||||
<string name="contains_duplicate_uuid_procedure">Problem lösen, indem neue UUIDs für Duplikate generiert werden und danach fortfahren\?</string>
|
<string name="contains_duplicate_uuid_procedure">Problem lösen, indem neue UUIDs für Duplikate generiert werden und danach fortfahren\?</string>
|
||||||
<string name="database_opened">Datenbank geöffnet</string>
|
<string name="database_opened">Datenbank geöffnet</string>
|
||||||
<string name="clipboard_explanation_summary">Eintragsfelder mithilfe der Zwischenablage des Geräts kopieren</string>
|
<string name="clipboard_explanation_summary">Eintragsfelder mithilfe der Zwischenablage des Geräts kopieren</string>
|
||||||
<string name="advanced_unlock_explanation_summary">Erweitertes Entsperren verwenden, um eine Datenbank einfacher zu öffnen.</string>
|
<string name="advanced_unlock_explanation_summary">Modernes Entsperren verwenden, um eine Datenbank einfacher zu öffnen.</string>
|
||||||
<string name="database_data_compression_title">Datenkompression</string>
|
<string name="database_data_compression_title">Datenkompression</string>
|
||||||
<string name="database_data_compression_summary">Datenkompression reduziert die Datenbankgröße</string>
|
<string name="database_data_compression_summary">Datenkompression reduziert die Datenbankgröße</string>
|
||||||
<string name="max_history_items_title">Maximale Anzahl</string>
|
<string name="max_history_items_title">Maximale Anzahl</string>
|
||||||
@@ -470,7 +458,7 @@
|
|||||||
<string name="autofill_web_domain_blocklist_title">Liste gesperrter Web-Domains</string>
|
<string name="autofill_web_domain_blocklist_title">Liste gesperrter Web-Domains</string>
|
||||||
<string name="autofill_application_id_blocklist_summary">Liste der Apps, in denen ein automatisches Ausfüllen verhindert wird</string>
|
<string name="autofill_application_id_blocklist_summary">Liste der Apps, in denen ein automatisches Ausfüllen verhindert wird</string>
|
||||||
<string name="autofill_application_id_blocklist_title">Liste gesperrter Anwendungen</string>
|
<string name="autofill_application_id_blocklist_title">Liste gesperrter Anwendungen</string>
|
||||||
<string name="subdomain_search_summary">Suche Web-Domains mit Subdomain-Beschränkungen</string>
|
<string name="subdomain_search_summary">Web-Domains mit Subdomain-Beschränkungen durchsuchen</string>
|
||||||
<string name="subdomain_search_title">Subdomain-Suche</string>
|
<string name="subdomain_search_title">Subdomain-Suche</string>
|
||||||
<string name="error_string_type">Dieser Text stimmt nicht mit dem angeforderten Element überein.</string>
|
<string name="error_string_type">Dieser Text stimmt nicht mit dem angeforderten Element überein.</string>
|
||||||
<string name="content_description_add_item">Element hinzufügen</string>
|
<string name="content_description_add_item">Element hinzufügen</string>
|
||||||
@@ -487,7 +475,7 @@
|
|||||||
<string name="warning_empty_keyfile">Es wird nicht empfohlen, eine leere Schlüsseldatei hinzuzufügen.</string>
|
<string name="warning_empty_keyfile">Es wird nicht empfohlen, eine leere Schlüsseldatei hinzuzufügen.</string>
|
||||||
<string name="warning_empty_keyfile_explanation">Der Inhalt der Key-Datei sollte nie geändert werden und im besten Fall zufällig generierte Daten enthalten.</string>
|
<string name="warning_empty_keyfile_explanation">Der Inhalt der Key-Datei sollte nie geändert werden und im besten Fall zufällig generierte Daten enthalten.</string>
|
||||||
<string name="warning_sure_remove_data">Sollen diese Daten trotzdem entfernt werden\?</string>
|
<string name="warning_sure_remove_data">Sollen diese Daten trotzdem entfernt werden\?</string>
|
||||||
<string name="warning_remove_unlinked_attachment">Das Entfernen ungekoppelter Daten könnte die Größe Ihrer Datenbank reduzieren, jedoch auch Daten, die von KeePass-Plugins genutzt werden, entfernen.</string>
|
<string name="warning_remove_unlinked_attachment">Das Entfernen nicht verknüpfter Daten kann die Größe Ihrer Datenbank reduzieren, allerdings auch Daten löschen, die von KeePass-Plugins genutzt werden.</string>
|
||||||
<string name="warning_replace_file">Das Hochladen dieser Datei wird die bereits Existierende ersetzen.</string>
|
<string name="warning_replace_file">Das Hochladen dieser Datei wird die bereits Existierende ersetzen.</string>
|
||||||
<string name="warning_file_too_big">Eine KeePass-Datenbank sollte nur kleine Dienstprogrammdateien beinhalten (zum Beispiel PGP-Schlüsseldateien).
|
<string name="warning_file_too_big">Eine KeePass-Datenbank sollte nur kleine Dienstprogrammdateien beinhalten (zum Beispiel PGP-Schlüsseldateien).
|
||||||
\n
|
\n
|
||||||
@@ -518,22 +506,22 @@
|
|||||||
<string name="keyboard_save_search_info_title">Gemeinsam genutzte Informationen speichern</string>
|
<string name="keyboard_save_search_info_title">Gemeinsam genutzte Informationen speichern</string>
|
||||||
<string name="warning_empty_recycle_bin">Sollen alle ausgewählten Knoten wirklich aus dem Papierkorb gelöscht werden\?</string>
|
<string name="warning_empty_recycle_bin">Sollen alle ausgewählten Knoten wirklich aus dem Papierkorb gelöscht werden\?</string>
|
||||||
<string name="error_field_name_already_exists">Der Feldname existiert bereits.</string>
|
<string name="error_field_name_already_exists">Der Feldname existiert bereits.</string>
|
||||||
<string name="advanced_unlock_prompt_store_credential_message">Warnung: Sie müssen sich immer noch an ihr Masterpasswort erinnern, wenn sie die erweiterte Entsperrerkennung verwenden.</string>
|
<string name="advanced_unlock_prompt_store_credential_message">Warnung: Sie müssen sich immer noch an Ihr Masterpasswort erinnern, wenn Sie die moderne Entsperrerkennung verwenden.</string>
|
||||||
<string name="open_advanced_unlock_prompt_store_credential">Öffne den erweiterten Entsperrdialog zum Speichern von Anmeldeinformationen</string>
|
<string name="open_advanced_unlock_prompt_store_credential">Zum Speichern der Anmeldeinformationen Dialog zum modernen Entsperren öffnen</string>
|
||||||
<string name="open_advanced_unlock_prompt_unlock_database">Öffnen des erweiterten Entsperrdialogs zum Entsperren der Datenbank</string>
|
<string name="open_advanced_unlock_prompt_unlock_database">Dialog zum modernen Entsperren der Datenbank öffnen</string>
|
||||||
<string name="menu_keystore_remove_key">Löschen des Schlüssels zum erweiterten Entsperren</string>
|
<string name="menu_keystore_remove_key">Schlüssel für modernes Entsperren löschen</string>
|
||||||
<string name="advanced_unlock_prompt_store_credential_title">Erweiterte Entsperrerkennung</string>
|
<string name="advanced_unlock_prompt_store_credential_title">Moderne Entsperrerkennung</string>
|
||||||
<string name="education_advanced_unlock_summary">Ihr Passwort verbinden mit Ihrem gescannten biometrischen oder berechtigen Gerät um schnell Ihre Datenbank zu entsperren.</string>
|
<string name="education_advanced_unlock_summary">Verknüpfen Sie Ihr Passwort mit Ihren gescannten Biometriedaten oder Daten zur Geräteanmeldung, um schnell Ihre Datenbank zu entsperren.</string>
|
||||||
<string name="education_advanced_unlock_title">Erweiterte Entsperrung der Datenbank</string>
|
<string name="education_advanced_unlock_title">Erweiterte Entsperrung der Datenbank</string>
|
||||||
<string name="advanced_unlock_timeout">Verfallzeit der erweiterten Entsperrung</string>
|
<string name="advanced_unlock_timeout">Verfallzeit der erweiterten Entsperrung</string>
|
||||||
<string name="temp_advanced_unlock_timeout_summary">Dauer der erweiterten Entsperrung bevor sein Inhalt gelöscht wird</string>
|
<string name="temp_advanced_unlock_timeout_summary">Laufzeit der modernen Entsperrung bevor ihr Inhalt gelöscht wird</string>
|
||||||
<string name="temp_advanced_unlock_timeout_title">Verfall der erweiterten Entsperrung</string>
|
<string name="temp_advanced_unlock_timeout_title">Verfall der modernen Entsperrung</string>
|
||||||
<string name="temp_advanced_unlock_enable_summary">Keinen verschlüsselten Inhalt speichern, um erweiterte Entsperrung zu benutzen</string>
|
<string name="temp_advanced_unlock_enable_summary">Keinen verschlüsselten Inhalt speichern, um moderne Entsperrung zu benutzen</string>
|
||||||
<string name="temp_advanced_unlock_enable_title">Temporäre erweiterte Entsperrung</string>
|
<string name="temp_advanced_unlock_enable_title">Temporär moderne Entsperrung</string>
|
||||||
<string name="device_credential_unlock_enable_summary">Erlaubt Ihn die Geräteanmeldedaten zum Öffnen der Datenbank zu verwenden</string>
|
<string name="device_credential_unlock_enable_summary">Erlaubt Ihn die Geräteanmeldedaten zum Öffnen der Datenbank zu verwenden</string>
|
||||||
<string name="advanced_unlock_tap_delete">Drücken um erweiterte Entsperrschlüssel zu löschen</string>
|
<string name="advanced_unlock_tap_delete">Drücken, um Schlüssel für modernes Entsperren zu löschen</string>
|
||||||
<string name="content">Inhalt</string>
|
<string name="content">Inhalt</string>
|
||||||
<string name="advanced_unlock_prompt_extract_credential_title">Öffne Datenbank mit erweiterter Entsperrerkennung</string>
|
<string name="advanced_unlock_prompt_extract_credential_title">Datenbank mit moderner Entsperrerkennung öffnen</string>
|
||||||
<string name="enter">Eingabetaste</string>
|
<string name="enter">Eingabetaste</string>
|
||||||
<string name="backspace">Rücktaste</string>
|
<string name="backspace">Rücktaste</string>
|
||||||
<string name="select_entry">Wähle Eintrag</string>
|
<string name="select_entry">Wähle Eintrag</string>
|
||||||
@@ -542,13 +530,32 @@
|
|||||||
<string name="advanced_unlock_delete_all_key_warning">Alle Verschlüsselungsschlüssel, die mit der erweiterten Entsperrerkennung zusammenhängen, löschen\?</string>
|
<string name="advanced_unlock_delete_all_key_warning">Alle Verschlüsselungsschlüssel, die mit der erweiterten Entsperrerkennung zusammenhängen, löschen\?</string>
|
||||||
<string name="device_credential_unlock_enable_title">Geräteanmeldedaten entsperren</string>
|
<string name="device_credential_unlock_enable_title">Geräteanmeldedaten entsperren</string>
|
||||||
<string name="device_credential">Geräteanmeldedaten</string>
|
<string name="device_credential">Geräteanmeldedaten</string>
|
||||||
<string name="credential_before_click_advanced_unlock_button">Geben sie das Passwort ein, und dann klicken sie den \"Erweitertes Entsperren\" Knopf.</string>
|
<string name="credential_before_click_advanced_unlock_button">Geben Sie das Passwort ein und klicken Sie dann auf diesen Knopf.</string>
|
||||||
<string name="advanced_unlock_prompt_not_initialized">Initialisieren des erweitertes Entsperren Dialogs fehlgeschlagen.</string>
|
<string name="advanced_unlock_prompt_not_initialized">Dialog für modernes Entsperren konnte nicht gestartet werden.</string>
|
||||||
<string name="advanced_unlock_scanning_error">Erweitertes Entsperren Fehler: %1$s</string>
|
<string name="advanced_unlock_scanning_error">Fehler beim modernen Entsperren: %1$s</string>
|
||||||
<string name="advanced_unlock_not_recognized">Konnte den Abdruck des erweiterten Entsperrens nicht erkennen</string>
|
<string name="advanced_unlock_not_recognized">Abdruck zum modernen Entsperren nicht erkannt</string>
|
||||||
<string name="advanced_unlock_invalid_key">Kann den Schlüssel zum erweiterten Entsperren nicht lesen. Bitte löschen sie ihn und wiederholen sie Prozedur zum Erkennen des Entsperrens.</string>
|
<string name="advanced_unlock_invalid_key">Schlüssel zum modernen Entsperren nicht lesbar. Bitte löschen Sie ihn und wiederholen Sie die Prozedur zur Entsperrerkennung.</string>
|
||||||
<string name="advanced_unlock_prompt_extract_credential_message">Extrahiere Datenbankanmeldedaten mit Daten aus erweitertem Entsperren</string>
|
<string name="advanced_unlock_prompt_extract_credential_message">Datenbankanmeldedaten mit Daten aus moderner Entsperrung extrahieren</string>
|
||||||
<string name="error_rebuild_list">Die Liste kann nicht ordnungsgemäß neu erstellt werden.</string>
|
<string name="error_rebuild_list">Die Liste kann nicht ordnungsgemäß neu erstellt werden.</string>
|
||||||
<string name="error_database_uri_null">Datenbank-URI kann nicht abgerufen werden.</string>
|
<string name="error_database_uri_null">Datenbank-URI kann nicht abgerufen werden.</string>
|
||||||
<string name="menu_reload_database">Datenbank neu laden</string>
|
<string name="menu_reload_database">Datenbank neu laden</string>
|
||||||
|
<string name="warning_database_info_changed_options">Überschreiben Sie die externen Änderungen, indem Sie die Datenbank speichern, oder laden Sie sie mit den neuesten Änderungen neu.</string>
|
||||||
|
<string name="error_file_to_big">Die Datei, die hochgeladen werden soll, ist zu groß.</string>
|
||||||
|
<string name="warning_database_info_changed">Die in Ihrer Datenbank enthaltenen Informationen wurden außerhalb der App geändert.</string>
|
||||||
|
<string name="error_remove_file">Beim Löschen der Datei trat ein Fehler auf.</string>
|
||||||
|
<string name="error_duplicate_file">Die Datei gibt es bereits.</string>
|
||||||
|
<string name="error_upload_file">Beim Hochladen der Datei trat ein Fehler auf.</string>
|
||||||
|
<string name="import_app_properties_title">Importieren von Anwendungeneigenschaften</string>
|
||||||
|
<string name="error_start_database_action">Beim Ausführen einer Aktion in der Datenbank ist ein Fehler aufgetreten.</string>
|
||||||
|
<string name="error_otp_type">Der vorhandene Einmalpassworttyp wird von diesem Formular nicht erkannt, seine Validierung erzeugt das Token möglicherweise nicht mehr korrekt.</string>
|
||||||
|
<string name="content_description_otp_information">Informationen zu Einmalpasswörtern</string>
|
||||||
|
<string name="warning_database_revoked">Auf die Datei kann nicht zugegriffen werden. Schließen Sie die Datenbank und öffnen Sie die Datei erneut.</string>
|
||||||
|
<string name="error_export_app_properties">Fehler beim Exportieren der App-Eigenschaften</string>
|
||||||
|
<string name="success_export_app_properties">App-Eigenschaften exportiert</string>
|
||||||
|
<string name="error_import_app_properties">Fehler beim Importieren der App-Eigenschaften</string>
|
||||||
|
<string name="success_import_app_properties">App-Eigenschaften importiert</string>
|
||||||
|
<string name="export_app_properties_summary">Erstellen einer Datei zum Exportieren von App-Eigenschaften</string>
|
||||||
|
<string name="export_app_properties_title">App-Eigenschaften exportieren</string>
|
||||||
|
<string name="import_app_properties_summary">Wählen Sie eine Datei zum Importieren von App-Eigenschaften</string>
|
||||||
|
<string name="error_move_group_here">Sie können hier keine Gruppe verschieben.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<string name="add_entry">Προσθήκη καταχώρησης</string>
|
<string name="add_entry">Προσθήκη καταχώρησης</string>
|
||||||
<string name="add_group">Προσθήκη ομάδας</string>
|
<string name="add_group">Προσθήκη ομάδας</string>
|
||||||
<string name="encryption_algorithm">Αλγόριθμος κρυπτογράφησης</string>
|
<string name="encryption_algorithm">Αλγόριθμος κρυπτογράφησης</string>
|
||||||
<string name="app_timeout">Χρονικό όριο εφαρμογής</string>
|
<string name="app_timeout">Χρονικό όριο</string>
|
||||||
<string name="app_timeout_summary">Χρόνος αδράνειας πριν από το κλείδωμα της βάσης δεδομένων</string>
|
<string name="app_timeout_summary">Χρόνος αδράνειας πριν από το κλείδωμα της βάσης δεδομένων</string>
|
||||||
<string name="application">Εφαρμογή</string>
|
<string name="application">Εφαρμογή</string>
|
||||||
<string name="menu_app_settings">Ρυθμίσεις εφαρμογής</string>
|
<string name="menu_app_settings">Ρυθμίσεις εφαρμογής</string>
|
||||||
@@ -135,17 +135,6 @@
|
|||||||
<string name="education_unlock_summary">Καταχωρίστε τον κωδικό πρόσβασης και /ή το αρχείο-κλειδί για να ξεκλειδώσετε τη βάση δεδομένων σας.
|
<string name="education_unlock_summary">Καταχωρίστε τον κωδικό πρόσβασης και /ή το αρχείο-κλειδί για να ξεκλειδώσετε τη βάση δεδομένων σας.
|
||||||
\n
|
\n
|
||||||
\nΔημιουργήστε αντίγραφα ασφαλείας του αρχείου βάσης δεδομένων σας, σε ασφαλές μέρος μετά από κάθε αλλαγή.</string>
|
\nΔημιουργήστε αντίγραφα ασφαλείας του αρχείου βάσης δεδομένων σας, σε ασφαλές μέρος μετά από κάθε αλλαγή.</string>
|
||||||
<string-array name="timeout_options">
|
|
||||||
<item>5 δευτερόλεπτα</item>
|
|
||||||
<item>10 δευτερόλεπτα</item>
|
|
||||||
<item>20 δευτερόλεπτα</item>
|
|
||||||
<item>30 δευτερόλεπτα</item>
|
|
||||||
<item>1 λεπτό</item>
|
|
||||||
<item>5 λεπτά</item>
|
|
||||||
<item>15 λεπτά</item>
|
|
||||||
<item>30 λεπτά</item>
|
|
||||||
<item>Ποτέ</item>
|
|
||||||
</string-array>
|
|
||||||
<string-array name="list_size_options">
|
<string-array name="list_size_options">
|
||||||
<item>Μικρά</item>
|
<item>Μικρά</item>
|
||||||
<item>Μεσαία</item>
|
<item>Μεσαία</item>
|
||||||
@@ -262,7 +251,6 @@
|
|||||||
<string name="style_choose_summary">Θέμα που χρησιμοποιείται στην εφαρμογή</string>
|
<string name="style_choose_summary">Θέμα που χρησιμοποιείται στην εφαρμογή</string>
|
||||||
<string name="icon_pack_choose_title">Πακέτο Εικονιδίων</string>
|
<string name="icon_pack_choose_title">Πακέτο Εικονιδίων</string>
|
||||||
<string name="icon_pack_choose_summary">Πακέτο εικονιδίων που χρησιμοποιείται στην εφαρμογή</string>
|
<string name="icon_pack_choose_summary">Πακέτο εικονιδίων που χρησιμοποιείται στην εφαρμογή</string>
|
||||||
<string name="error_move_folder_in_itself">Δεν μπορείτε να μετακινήσετε μια ομάδα μέσα στον εαυτό της.</string>
|
|
||||||
<string name="menu_copy">Αντιγραφή</string>
|
<string name="menu_copy">Αντιγραφή</string>
|
||||||
<string name="menu_move">Μετακίνηση</string>
|
<string name="menu_move">Μετακίνηση</string>
|
||||||
<string name="menu_paste">Επικόλληση</string>
|
<string name="menu_paste">Επικόλληση</string>
|
||||||
@@ -561,4 +549,17 @@
|
|||||||
<string name="content_description_otp_information">Πληροφορίες One-time κωδικού πρόσβασης</string>
|
<string name="content_description_otp_information">Πληροφορίες One-time κωδικού πρόσβασης</string>
|
||||||
<string name="error_remove_file">Παρουσιάστηκε σφάλμα κατά την κατάργηση των δεδομένων αρχείου.</string>
|
<string name="error_remove_file">Παρουσιάστηκε σφάλμα κατά την κατάργηση των δεδομένων αρχείου.</string>
|
||||||
<string name="error_duplicate_file">Τα δεδομένα αρχείου υπάρχουν ήδη.</string>
|
<string name="error_duplicate_file">Τα δεδομένα αρχείου υπάρχουν ήδη.</string>
|
||||||
|
<string name="properties">Ιδιότητες</string>
|
||||||
|
<string name="error_export_app_properties">Σφάλμα κατά την εξαγωγή ιδιοτήτων εφαρμογής</string>
|
||||||
|
<string name="success_export_app_properties">Έγινε εξαγωγή ιδιοτήτων εφαρμογής</string>
|
||||||
|
<string name="error_import_app_properties">Σφάλμα κατά την εισαγωγή ιδιοτήτων εφαρμογής</string>
|
||||||
|
<string name="success_import_app_properties">Έγινε εισαγωγή ιδιοτήτων εφαρμογής</string>
|
||||||
|
<string name="description_app_properties">Ιδιότητες KeePassDX για διαχείριση ρυθμίσεων εφαρμογής</string>
|
||||||
|
<string name="export_app_properties_summary">Δημιουργήστε ένα αρχείο για εξαγωγή ιδιοτήτων εφαρμογής</string>
|
||||||
|
<string name="export_app_properties_title">Εξαγωγή ιδιοτήτων εφαρμογής</string>
|
||||||
|
<string name="import_app_properties_summary">Επιλέξτε ένα αρχείο για εισαγωγή ιδιοτήτων εφαρμογής</string>
|
||||||
|
<string name="import_app_properties_title">Εισαγωγή ιδιοτήτων εφαρμογής</string>
|
||||||
|
<string name="error_start_database_action">Παρουσιάστηκε σφάλμα κατά την εκτέλεση μιας ενέργειας στη βάση δεδομένων.</string>
|
||||||
|
<string name="error_move_group_here">Δεν μπορείτε να μετακινήσετε μια ομάδα εδώ.</string>
|
||||||
|
<string name="error_word_reserved">Αυτή η λέξη είναι δεσμευμένη και δεν μπορεί να χρησιμοποιηθεί.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -125,17 +125,6 @@
|
|||||||
<string name="education_unlock_summary">Introduzca la contraseña y/o el archivo clave para desbloquear su base de datos.
|
<string name="education_unlock_summary">Introduzca la contraseña y/o el archivo clave para desbloquear su base de datos.
|
||||||
\n
|
\n
|
||||||
\nHaga una copia de seguridad de su archivo de base de datos en un lugar seguro después de cada cambio.</string>
|
\nHaga una copia de seguridad de su archivo de base de datos en un lugar seguro después de cada cambio.</string>
|
||||||
<string-array name="timeout_options">
|
|
||||||
<item>5 segundos</item>
|
|
||||||
<item>10 segundos</item>
|
|
||||||
<item>20 segundos</item>
|
|
||||||
<item>30 segundos</item>
|
|
||||||
<item>1 minuto</item>
|
|
||||||
<item>5 minutos</item>
|
|
||||||
<item>15 minutos</item>
|
|
||||||
<item>30 minutos</item>
|
|
||||||
<item>Nunca</item>
|
|
||||||
</string-array>
|
|
||||||
<string-array name="list_size_options">
|
<string-array name="list_size_options">
|
||||||
<item>Pequeño</item>
|
<item>Pequeño</item>
|
||||||
<item>Mediano</item>
|
<item>Mediano</item>
|
||||||
@@ -278,7 +267,6 @@
|
|||||||
<string name="edit_entry">Editar entrada</string>
|
<string name="edit_entry">Editar entrada</string>
|
||||||
<string name="error_load_database">No se pudo cargar la base de datos.</string>
|
<string name="error_load_database">No se pudo cargar la base de datos.</string>
|
||||||
<string name="error_load_database_KDF_memory">No se pudo cargar la clave. Intente disminuir el uso de memoria de KDF.</string>
|
<string name="error_load_database_KDF_memory">No se pudo cargar la clave. Intente disminuir el uso de memoria de KDF.</string>
|
||||||
<string name="error_move_folder_in_itself">No puede mover un grupo dentro de sí mismo.</string>
|
|
||||||
<string name="list_entries_show_username_title">Enseña nombres de usuario</string>
|
<string name="list_entries_show_username_title">Enseña nombres de usuario</string>
|
||||||
<string name="list_entries_show_username_summary">Enseña nombres de usuador en las listras de entradas</string>
|
<string name="list_entries_show_username_summary">Enseña nombres de usuador en las listras de entradas</string>
|
||||||
<string name="menu_copy">Copiar</string>
|
<string name="menu_copy">Copiar</string>
|
||||||
|
|||||||
@@ -132,17 +132,6 @@
|
|||||||
<string name="uppercase">Maiuskulak</string>
|
<string name="uppercase">Maiuskulak</string>
|
||||||
<string name="version_label">Bertsioa %1$s</string>
|
<string name="version_label">Bertsioa %1$s</string>
|
||||||
<string name="education_unlock_summary">Sartu pasahitz eta / edo gako fitxategi bat zure datubasea desblokeatzeko.</string>
|
<string name="education_unlock_summary">Sartu pasahitz eta / edo gako fitxategi bat zure datubasea desblokeatzeko.</string>
|
||||||
<string-array name="timeout_options">
|
|
||||||
<item>5 segundu</item>
|
|
||||||
<item>10 segundu</item>
|
|
||||||
<item>20 segundu</item>
|
|
||||||
<item>30 segundu</item>
|
|
||||||
<item>minutu 1</item>
|
|
||||||
<item>5 minutu</item>
|
|
||||||
<item>15 minutu</item>
|
|
||||||
<item>30 minutu</item>
|
|
||||||
<item>Inoiz ez</item>
|
|
||||||
</string-array>
|
|
||||||
<string-array name="list_size_options">
|
<string-array name="list_size_options">
|
||||||
<item>Txikia</item>
|
<item>Txikia</item>
|
||||||
<item>Ertaina</item>
|
<item>Ertaina</item>
|
||||||
|
|||||||
@@ -141,7 +141,6 @@
|
|||||||
<string name="error_copy_group_here">شما نمی توانید یک گروه را در اینجا کپی کنید.</string>
|
<string name="error_copy_group_here">شما نمی توانید یک گروه را در اینجا کپی کنید.</string>
|
||||||
<string name="error_copy_entry_here">شما نمی توانید یک ورودی را در اینجا کپی کنید.</string>
|
<string name="error_copy_entry_here">شما نمی توانید یک ورودی را در اینجا کپی کنید.</string>
|
||||||
<string name="error_move_entry_here">شما نمی توانید یک ورودی را به اینجا منتقل کنید.</string>
|
<string name="error_move_entry_here">شما نمی توانید یک ورودی را به اینجا منتقل کنید.</string>
|
||||||
<string name="error_move_folder_in_itself">شما نمی توانید یک گروه را به خود منتقل کنید.</string>
|
|
||||||
<string name="error_autofill_enable_service">قادر به فعال کردن سرویس پر کردن خودکار نبود.</string>
|
<string name="error_autofill_enable_service">قادر به فعال کردن سرویس پر کردن خودکار نبود.</string>
|
||||||
<string name="error_wrong_length">یک عدد کامل مثبت را در زمینه \"طول\" وارد کنید.</string>
|
<string name="error_wrong_length">یک عدد کامل مثبت را در زمینه \"طول\" وارد کنید.</string>
|
||||||
<string name="error_label_exists">این برچسب در حال حاضر وجود دارد.</string>
|
<string name="error_label_exists">این برچسب در حال حاضر وجود دارد.</string>
|
||||||
|
|||||||
@@ -132,17 +132,6 @@
|
|||||||
<string name="uppercase">Isot kirjaimet</string>
|
<string name="uppercase">Isot kirjaimet</string>
|
||||||
<string name="version_label">Versio %1$s</string>
|
<string name="version_label">Versio %1$s</string>
|
||||||
<string name="education_unlock_summary">Syötä salasana ja/tai avaintiedosto avataksesi tietokantasi.</string>
|
<string name="education_unlock_summary">Syötä salasana ja/tai avaintiedosto avataksesi tietokantasi.</string>
|
||||||
<string-array name="timeout_options">
|
|
||||||
<item>5 sekuntia</item>
|
|
||||||
<item>10 sekuntia</item>
|
|
||||||
<item>20 sekuntia</item>
|
|
||||||
<item>30 sekuntia</item>
|
|
||||||
<item>1 minuutti</item>
|
|
||||||
<item>5 minuttia</item>
|
|
||||||
<item>15 minuttia</item>
|
|
||||||
<item>30 minuttia</item>
|
|
||||||
<item>Ei koskaan</item>
|
|
||||||
</string-array>
|
|
||||||
<string-array name="list_size_options">
|
<string-array name="list_size_options">
|
||||||
<item>Pieni</item>
|
<item>Pieni</item>
|
||||||
<item>Keskikokoinen</item>
|
<item>Keskikokoinen</item>
|
||||||
@@ -319,7 +308,6 @@
|
|||||||
<string name="error_copy_group_here">Et voi kopioida ryhmää tänne.</string>
|
<string name="error_copy_group_here">Et voi kopioida ryhmää tänne.</string>
|
||||||
<string name="error_copy_entry_here">Et voi kopioida tietuetta tänne.</string>
|
<string name="error_copy_entry_here">Et voi kopioida tietuetta tänne.</string>
|
||||||
<string name="error_move_entry_here">Et voi siirtää tietuetta tänne.</string>
|
<string name="error_move_entry_here">Et voi siirtää tietuetta tänne.</string>
|
||||||
<string name="error_move_folder_in_itself">Et voi siirtää ryhmää itsensä sisälle.</string>
|
|
||||||
<string name="error_autofill_enable_service">Automaattista täyttöä ei voitu ottaa käyttöön.</string>
|
<string name="error_autofill_enable_service">Automaattista täyttöä ei voitu ottaa käyttöön.</string>
|
||||||
<string name="content_description_node_children">Solmun lapset</string>
|
<string name="content_description_node_children">Solmun lapset</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<string name="encryption">Chiffrement</string>
|
<string name="encryption">Chiffrement</string>
|
||||||
<string name="encryption_algorithm">Algorithme de chiffrement</string>
|
<string name="encryption_algorithm">Algorithme de chiffrement</string>
|
||||||
<string name="key_derivation_function">Fonction de dérivation de clé</string>
|
<string name="key_derivation_function">Fonction de dérivation de clé</string>
|
||||||
<string name="app_timeout">Délai d’expiration de l’application</string>
|
<string name="app_timeout">Délai d’expiration</string>
|
||||||
<string name="app_timeout_summary">Durée d’inactivité avant le verrouillage de la base de données</string>
|
<string name="app_timeout_summary">Durée d’inactivité avant le verrouillage de la base de données</string>
|
||||||
<string name="application">Application</string>
|
<string name="application">Application</string>
|
||||||
<string name="menu_app_settings">Paramètres de l’application</string>
|
<string name="menu_app_settings">Paramètres de l’application</string>
|
||||||
@@ -258,17 +258,6 @@
|
|||||||
<string name="html_text_dev_feature_upgrade">N’oubliez pas de garder votre application à jour en installant les nouvelles versions.</string>
|
<string name="html_text_dev_feature_upgrade">N’oubliez pas de garder votre application à jour en installant les nouvelles versions.</string>
|
||||||
<string name="download">Télécharger</string>
|
<string name="download">Télécharger</string>
|
||||||
<string name="contribute">Contribuer</string>
|
<string name="contribute">Contribuer</string>
|
||||||
<string-array name="timeout_options">
|
|
||||||
<item>5 secondes</item>
|
|
||||||
<item>10 secondes</item>
|
|
||||||
<item>20 secondes</item>
|
|
||||||
<item>30 secondes</item>
|
|
||||||
<item>1 minute</item>
|
|
||||||
<item>5 minutes</item>
|
|
||||||
<item>15 minutes</item>
|
|
||||||
<item>30 minutes</item>
|
|
||||||
<item>Jamais</item>
|
|
||||||
</string-array>
|
|
||||||
<string-array name="list_size_options">
|
<string-array name="list_size_options">
|
||||||
<item>Petit</item>
|
<item>Petit</item>
|
||||||
<item>Moyen</item>
|
<item>Moyen</item>
|
||||||
@@ -286,7 +275,6 @@
|
|||||||
</string-array>
|
</string-array>
|
||||||
<string name="icon_pack_choose_title">Collection d’icônes</string>
|
<string name="icon_pack_choose_title">Collection d’icônes</string>
|
||||||
<string name="icon_pack_choose_summary">Collection d’icônes utilisées dans l’application</string>
|
<string name="icon_pack_choose_summary">Collection d’icônes utilisées dans l’application</string>
|
||||||
<string name="error_move_folder_in_itself">Vous ne pouvez pas déplacer un groupe dans lui-même.</string>
|
|
||||||
<string name="menu_copy">Copier</string>
|
<string name="menu_copy">Copier</string>
|
||||||
<string name="menu_move">Déplacer</string>
|
<string name="menu_move">Déplacer</string>
|
||||||
<string name="menu_paste">Coller</string>
|
<string name="menu_paste">Coller</string>
|
||||||
@@ -566,7 +554,20 @@
|
|||||||
<string name="style_brightness_title">Luminosité de thème</string>
|
<string name="style_brightness_title">Luminosité de thème</string>
|
||||||
<string name="error_remove_file">Une erreur s\'est produite lors de la suppression des données du fichier.</string>
|
<string name="error_remove_file">Une erreur s\'est produite lors de la suppression des données du fichier.</string>
|
||||||
<string name="error_duplicate_file">Les données du fichier existent déjà.</string>
|
<string name="error_duplicate_file">Les données du fichier existent déjà.</string>
|
||||||
<string name="error_upload_file">Une erreur s\'est produite lors du téléchargement des données du fichier.</string>
|
<string name="error_upload_file">Une erreur est survenue lors du téléversement des données du fichier.</string>
|
||||||
<string name="error_file_to_big">Le fichier que vous essayez de téléverser est trop gros.</string>
|
<string name="error_file_to_big">Le fichier que vous essayez de téléverser est trop volumineux.</string>
|
||||||
<string name="content_description_otp_information">Information sur le mot de passe à usage unique</string>
|
<string name="content_description_otp_information">Information sur le mot de passe à usage unique</string>
|
||||||
|
<string name="properties">Propriétés</string>
|
||||||
|
<string name="error_export_app_properties">Erreur lors de l\'exportation des propriétés de l\'application</string>
|
||||||
|
<string name="success_export_app_properties">Propriétés de l\'application exportées</string>
|
||||||
|
<string name="error_import_app_properties">Erreur lors de l\'importation des propriétés de l\'application</string>
|
||||||
|
<string name="success_import_app_properties">Propriétés de l\'application importées</string>
|
||||||
|
<string name="description_app_properties">Propriétés KeePassDX pour gérer les paramètres de l\'application</string>
|
||||||
|
<string name="export_app_properties_summary">Créer un fichier pour exporter les propriétés de l\'application</string>
|
||||||
|
<string name="export_app_properties_title">Export des propriétés de l\'app</string>
|
||||||
|
<string name="import_app_properties_summary">Sélectionner un fichier pour importer les propriétés de l\'application</string>
|
||||||
|
<string name="import_app_properties_title">Importation des propriétés de l\'appli</string>
|
||||||
|
<string name="error_start_database_action">Une erreur s\'est produite lors de l\'exécution d\'une action sur la base de données.</string>
|
||||||
|
<string name="error_move_group_here">Vous ne pouvez pas déplacer un groupe ici.</string>
|
||||||
|
<string name="error_word_reserved">Ce mot est réservé et ne peut pas être utilisé.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -242,7 +242,6 @@
|
|||||||
<string name="error_disallow_no_credentials">Barem jedan skup podataka za prijavu mora biti postavljen.</string>
|
<string name="error_disallow_no_credentials">Barem jedan skup podataka za prijavu mora biti postavljen.</string>
|
||||||
<string name="error_string_key">Svaki niz mora imati ime polja.</string>
|
<string name="error_string_key">Svaki niz mora imati ime polja.</string>
|
||||||
<string name="error_autofill_enable_service">Nije moguće aktivirati uslugu automatskog ispunjavanja.</string>
|
<string name="error_autofill_enable_service">Nije moguće aktivirati uslugu automatskog ispunjavanja.</string>
|
||||||
<string name="error_move_folder_in_itself">Nije moguće premjestiti grupu u samu sebe.</string>
|
|
||||||
<string name="error_move_entry_here">Unos se ne može ovdje premijestiti.</string>
|
<string name="error_move_entry_here">Unos se ne može ovdje premijestiti.</string>
|
||||||
<string name="error_copy_entry_here">Unos se ne može ovdje kopirati.</string>
|
<string name="error_copy_entry_here">Unos se ne može ovdje kopirati.</string>
|
||||||
<string name="error_copy_group_here">Grupa se ne može ovjde kopirati.</string>
|
<string name="error_copy_group_here">Grupa se ne može ovjde kopirati.</string>
|
||||||
@@ -545,4 +544,15 @@
|
|||||||
<string name="content_description_otp_information">Podaci jednokratne lozinke</string>
|
<string name="content_description_otp_information">Podaci jednokratne lozinke</string>
|
||||||
<string name="error_remove_file">Tijekom uklanjanja podataka datoteke došlo je do greške.</string>
|
<string name="error_remove_file">Tijekom uklanjanja podataka datoteke došlo je do greške.</string>
|
||||||
<string name="error_duplicate_file">Podaci datoteke već postoje.</string>
|
<string name="error_duplicate_file">Podaci datoteke već postoje.</string>
|
||||||
|
<string name="properties">Svojstva</string>
|
||||||
|
<string name="error_export_app_properties">Greška tijekom izvoza svojstava aplikacije</string>
|
||||||
|
<string name="success_export_app_properties">Svojstva aplikacije su izvezena</string>
|
||||||
|
<string name="error_import_app_properties">Greška tijekom uvoza svojstava aplikacije</string>
|
||||||
|
<string name="success_import_app_properties">Svojstva aplikacije su uvezena</string>
|
||||||
|
<string name="description_app_properties">KeePassDX svojstva za upravljanje postavkama aplikacije</string>
|
||||||
|
<string name="export_app_properties_summary">Stvori datoteku za izvoz svojstva aplikacije</string>
|
||||||
|
<string name="export_app_properties_title">Izvezi svojstva aplikacije</string>
|
||||||
|
<string name="import_app_properties_summary">Odaberi datoteku za uvoz svojstva aplikacije</string>
|
||||||
|
<string name="import_app_properties_title">Uvezi svojstva aplikacije</string>
|
||||||
|
<string name="error_start_database_action">Došlo je do greške tijekom izvođenja radnje u bazi podataka.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -140,17 +140,6 @@
|
|||||||
<string name="education_unlock_summary">Adja meg a jelszót és/vagy a kulcsfájlt, hogy kinyithassa az adatbázist.
|
<string name="education_unlock_summary">Adja meg a jelszót és/vagy a kulcsfájlt, hogy kinyithassa az adatbázist.
|
||||||
\n
|
\n
|
||||||
\nKészítsen biztonsági mentést az adatbázisról minden egyes módosítás után.</string>
|
\nKészítsen biztonsági mentést az adatbázisról minden egyes módosítás után.</string>
|
||||||
<string-array name="timeout_options">
|
|
||||||
<item>5 másodperc</item>
|
|
||||||
<item>10 másodperc</item>
|
|
||||||
<item>20 másodperc</item>
|
|
||||||
<item>30 másodperc</item>
|
|
||||||
<item>1 perc</item>
|
|
||||||
<item>5 perc</item>
|
|
||||||
<item>15 perc</item>
|
|
||||||
<item>30 perc</item>
|
|
||||||
<item>Soha</item>
|
|
||||||
</string-array>
|
|
||||||
<string-array name="list_size_options">
|
<string-array name="list_size_options">
|
||||||
<item>Kicsi</item>
|
<item>Kicsi</item>
|
||||||
<item>Közepes</item>
|
<item>Közepes</item>
|
||||||
@@ -165,7 +154,6 @@
|
|||||||
<string name="error_load_database">Az adatbázis betöltése meghiúsult.</string>
|
<string name="error_load_database">Az adatbázis betöltése meghiúsult.</string>
|
||||||
<string name="error_load_database_KDF_memory">A kulcs nem tölthető be. Próbálja meg csökkenteni a KDF „Memóriahasználatot”.</string>
|
<string name="error_load_database_KDF_memory">A kulcs nem tölthető be. Próbálja meg csökkenteni a KDF „Memóriahasználatot”.</string>
|
||||||
<string name="error_autofill_enable_service">Az automatikus kitöltési szolgáltatás nem engedélyezhető.</string>
|
<string name="error_autofill_enable_service">Az automatikus kitöltési szolgáltatás nem engedélyezhető.</string>
|
||||||
<string name="error_move_folder_in_itself">Nem helyezheti át a csoportot saját magába.</string>
|
|
||||||
<string name="list_entries_show_username_title">Felhasználónevek megjelenítése</string>
|
<string name="list_entries_show_username_title">Felhasználónevek megjelenítése</string>
|
||||||
<string name="list_entries_show_username_summary">Felhasználónevek megjelenítése a bejegyzéslistákban</string>
|
<string name="list_entries_show_username_summary">Felhasználónevek megjelenítése a bejegyzéslistákban</string>
|
||||||
<string name="copy_field">%1$s másolata</string>
|
<string name="copy_field">%1$s másolata</string>
|
||||||
|
|||||||
@@ -72,7 +72,6 @@
|
|||||||
<string name="error_copy_group_here">Anda tidak bisa menyalin grup di sini.</string>
|
<string name="error_copy_group_here">Anda tidak bisa menyalin grup di sini.</string>
|
||||||
<string name="error_copy_entry_here">Anda tidak bisa menyalin entri di sini.</string>
|
<string name="error_copy_entry_here">Anda tidak bisa menyalin entri di sini.</string>
|
||||||
<string name="error_move_entry_here">Anda tidak bisa memindahkan entri ke sini.</string>
|
<string name="error_move_entry_here">Anda tidak bisa memindahkan entri ke sini.</string>
|
||||||
<string name="error_move_folder_in_itself">Anda tidak bisa memindahkan grup ke grup itu sendiri.</string>
|
|
||||||
<string name="error_autofill_enable_service">Tidak bisa mengaktifkan layanan IsiOtomatis.</string>
|
<string name="error_autofill_enable_service">Tidak bisa mengaktifkan layanan IsiOtomatis.</string>
|
||||||
<string name="error_wrong_length">Masukkan bilangan bulat di bidang \"Panjang\".</string>
|
<string name="error_wrong_length">Masukkan bilangan bulat di bidang \"Panjang\".</string>
|
||||||
<string name="error_label_exists">Label ini sudah ada.</string>
|
<string name="error_label_exists">Label ini sudah ada.</string>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<string name="add_entry">Aggiungi elemento</string>
|
<string name="add_entry">Aggiungi elemento</string>
|
||||||
<string name="add_group">Aggiungi gruppo</string>
|
<string name="add_group">Aggiungi gruppo</string>
|
||||||
<string name="encryption_algorithm">Algoritmo di cifratura</string>
|
<string name="encryption_algorithm">Algoritmo di cifratura</string>
|
||||||
<string name="app_timeout">Scadenza app</string>
|
<string name="app_timeout">Timeout</string>
|
||||||
<string name="app_timeout_summary">Tempo di inattività prima del blocco del database</string>
|
<string name="app_timeout_summary">Tempo di inattività prima del blocco del database</string>
|
||||||
<string name="application">App</string>
|
<string name="application">App</string>
|
||||||
<string name="menu_app_settings">Impostazioni app</string>
|
<string name="menu_app_settings">Impostazioni app</string>
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
<string name="error_invalid_path">Assicurati che il percorso sia corretto.</string>
|
<string name="error_invalid_path">Assicurati che il percorso sia corretto.</string>
|
||||||
<string name="error_no_name">Inserisci un nome.</string>
|
<string name="error_no_name">Inserisci un nome.</string>
|
||||||
<string name="error_out_of_memory">Memoria insufficiente per caricare l\'intero database.</string>
|
<string name="error_out_of_memory">Memoria insufficiente per caricare l\'intero database.</string>
|
||||||
<string name="error_pass_gen_type">Deve essere selezionato almeno un tipo di generazione password.</string>
|
<string name="error_pass_gen_type">Selezionare almeno un tipo di generazione della password.</string>
|
||||||
<string name="error_pass_match">Le password non corrispondono.</string>
|
<string name="error_pass_match">Le password non corrispondono.</string>
|
||||||
<string name="error_rounds_too_large">«Livello» troppo alto. Impostato a 2147483648.</string>
|
<string name="error_rounds_too_large">«Livello» troppo alto. Impostato a 2147483648.</string>
|
||||||
<string name="error_string_key">Ogni stringa deve avere un nome.</string>
|
<string name="error_string_key">Ogni stringa deve avere un nome.</string>
|
||||||
@@ -142,17 +142,6 @@
|
|||||||
<string name="education_unlock_summary">Inserisci la password o il file chiave per sbloccare la base di dati.
|
<string name="education_unlock_summary">Inserisci la password o il file chiave per sbloccare la base di dati.
|
||||||
\n
|
\n
|
||||||
\nEseguire il backup del file del database in un luogo sicuro dopo ogni modifica.</string>
|
\nEseguire il backup del file del database in un luogo sicuro dopo ogni modifica.</string>
|
||||||
<string-array name="timeout_options">
|
|
||||||
<item>5 secondi</item>
|
|
||||||
<item>10 secondi</item>
|
|
||||||
<item>20 secondi</item>
|
|
||||||
<item>30 secondi</item>
|
|
||||||
<item>1 minuto</item>
|
|
||||||
<item>5 minuti</item>
|
|
||||||
<item>15 minuti</item>
|
|
||||||
<item>30 minuti</item>
|
|
||||||
<item>Mai</item>
|
|
||||||
</string-array>
|
|
||||||
<string-array name="list_size_options">
|
<string-array name="list_size_options">
|
||||||
<item>Piccolo</item>
|
<item>Piccolo</item>
|
||||||
<item>Medio</item>
|
<item>Medio</item>
|
||||||
@@ -165,7 +154,6 @@
|
|||||||
<string name="extended_ASCII">ASCII esteso</string>
|
<string name="extended_ASCII">ASCII esteso</string>
|
||||||
<string name="error_nokeyfile">Seleziona un file chiave.</string>
|
<string name="error_nokeyfile">Seleziona un file chiave.</string>
|
||||||
<string name="error_autofill_enable_service">Attivazione del servizio di auto-completamento fallita.</string>
|
<string name="error_autofill_enable_service">Attivazione del servizio di auto-completamento fallita.</string>
|
||||||
<string name="error_move_folder_in_itself">Non puoi spostare un gruppo in se stesso.</string>
|
|
||||||
<string name="menu_form_filling_settings">Riempimento campi</string>
|
<string name="menu_form_filling_settings">Riempimento campi</string>
|
||||||
<string name="menu_copy">Copia</string>
|
<string name="menu_copy">Copia</string>
|
||||||
<string name="menu_move">Sposta</string>
|
<string name="menu_move">Sposta</string>
|
||||||
@@ -546,7 +534,7 @@
|
|||||||
<string name="autofill_inline_suggestions_summary">Mostra i suggerimenti di riempimento campi in una tastiera compatibile</string>
|
<string name="autofill_inline_suggestions_summary">Mostra i suggerimenti di riempimento campi in una tastiera compatibile</string>
|
||||||
<string name="autofill_inline_suggestions_title">Suggerimenti in linea</string>
|
<string name="autofill_inline_suggestions_title">Suggerimenti in linea</string>
|
||||||
<string name="warning_database_revoked">L\'accesso al file è stato revocato dal file manager, chiudi il database e riaprilo dalla sua posizione originale.</string>
|
<string name="warning_database_revoked">L\'accesso al file è stato revocato dal file manager, chiudi il database e riaprilo dalla sua posizione originale.</string>
|
||||||
<string name="warning_database_info_changed_options">Sovrascrivi le modifiche esterne salvano il database o ricaricalo con gli ultimi cambiamenti.</string>
|
<string name="warning_database_info_changed_options">Sovrascrivi le modifiche esterne salvando il database o ricaricalo con gli ultimi cambiamenti.</string>
|
||||||
<string name="warning_database_info_changed">I dati nel tuo database sono stati modificati al di fuori di questa app.</string>
|
<string name="warning_database_info_changed">I dati nel tuo database sono stati modificati al di fuori di questa app.</string>
|
||||||
<string name="menu_reload_database">Ricarica database</string>
|
<string name="menu_reload_database">Ricarica database</string>
|
||||||
<string name="error_otp_type">Il tipo di OTP esistente non è riconosciuto da questo modulo, la sua convalida potrebbe non generare più correttamente il token.</string>
|
<string name="error_otp_type">Il tipo di OTP esistente non è riconosciuto da questo modulo, la sua convalida potrebbe non generare più correttamente il token.</string>
|
||||||
@@ -564,4 +552,16 @@
|
|||||||
<string name="error_duplicate_file">Il file esiste già.</string>
|
<string name="error_duplicate_file">Il file esiste già.</string>
|
||||||
<string name="error_upload_file">Si è verificato un errore durante il caricamento del file.</string>
|
<string name="error_upload_file">Si è verificato un errore durante il caricamento del file.</string>
|
||||||
<string name="error_file_to_big">Il file che stai cercando di caricare è troppo grande.</string>
|
<string name="error_file_to_big">Il file che stai cercando di caricare è troppo grande.</string>
|
||||||
|
<string name="error_start_database_action">Si è verificato un errore durante l\'esecuzione di una azione sul database.</string>
|
||||||
|
<string name="properties">Proprietà</string>
|
||||||
|
<string name="error_export_app_properties">Errore durante l\'esportazione delle proprietà dell\'app</string>
|
||||||
|
<string name="success_export_app_properties">Proprietà dell\'app esportate</string>
|
||||||
|
<string name="error_import_app_properties">Errore durante l\'importazione delle proprietà dell\'app</string>
|
||||||
|
<string name="success_import_app_properties">Proprietà dell\'app importate</string>
|
||||||
|
<string name="description_app_properties">Proprietà di KeePassDX per gestire le impostazioni dell\'app</string>
|
||||||
|
<string name="export_app_properties_summary">Crea un file in cui esportare le proprietà dell\'app</string>
|
||||||
|
<string name="export_app_properties_title">Esporta le proprietà dell\'app</string>
|
||||||
|
<string name="import_app_properties_summary">Seleziona un file da cui importare le proprietà dell\'app</string>
|
||||||
|
<string name="import_app_properties_title">Importa le proprietà dell\'app</string>
|
||||||
|
<string name="error_word_reserved">Questa parola è riservata e non può essere usata.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -129,17 +129,6 @@
|
|||||||
<string name="uppercase">רישית</string>
|
<string name="uppercase">רישית</string>
|
||||||
<string name="version_label">גרסה %1$s</string>
|
<string name="version_label">גרסה %1$s</string>
|
||||||
<string name="education_unlock_summary">הזן סיסמה ו/או קובץ מפתח כדי לפתוח את מסד הנתונים.</string>
|
<string name="education_unlock_summary">הזן סיסמה ו/או קובץ מפתח כדי לפתוח את מסד הנתונים.</string>
|
||||||
<string-array name="timeout_options">
|
|
||||||
<item>5 שניות</item>
|
|
||||||
<item>10 שניות</item>
|
|
||||||
<item>20 שניות</item>
|
|
||||||
<item>30 שניות</item>
|
|
||||||
<item>דקה אחת</item>
|
|
||||||
<item>5 דקות</item>
|
|
||||||
<item>15 דקות</item>
|
|
||||||
<item>30 דקות</item>
|
|
||||||
<item>אף פעם</item>
|
|
||||||
</string-array>
|
|
||||||
<string-array name="list_size_options">
|
<string-array name="list_size_options">
|
||||||
<item>קטן</item>
|
<item>קטן</item>
|
||||||
<item>בינוני</item>
|
<item>בינוני</item>
|
||||||
|
|||||||
@@ -120,7 +120,6 @@
|
|||||||
<string name="error_label_exists">このラベルはすでに存在します。</string>
|
<string name="error_label_exists">このラベルはすでに存在します。</string>
|
||||||
<string name="error_wrong_length">[長さ] フィールドには正の整数を入力してください。</string>
|
<string name="error_wrong_length">[長さ] フィールドには正の整数を入力してください。</string>
|
||||||
<string name="error_autofill_enable_service">自動入力サービスを有効にできませんでした。</string>
|
<string name="error_autofill_enable_service">自動入力サービスを有効にできませんでした。</string>
|
||||||
<string name="error_move_folder_in_itself">グループを自分自身の中に移動することはできません。</string>
|
|
||||||
<string name="error_move_entry_here">ここではエントリーを移動することはできません。</string>
|
<string name="error_move_entry_here">ここではエントリーを移動することはできません。</string>
|
||||||
<string name="error_copy_entry_here">ここではエントリーをコピーすることはできません。</string>
|
<string name="error_copy_entry_here">ここではエントリーをコピーすることはできません。</string>
|
||||||
<string name="error_copy_group_here">ここではグループをコピーすることはできません。</string>
|
<string name="error_copy_group_here">ここではグループをコピーすることはできません。</string>
|
||||||
@@ -243,7 +242,7 @@
|
|||||||
<string name="sort_title">タイトル</string>
|
<string name="sort_title">タイトル</string>
|
||||||
<string name="sort_username">ユーザー名</string>
|
<string name="sort_username">ユーザー名</string>
|
||||||
<string name="sort_creation_time">作成日</string>
|
<string name="sort_creation_time">作成日</string>
|
||||||
<string name="sort_last_modify_time">変更日</string>
|
<string name="sort_last_modify_time">変更日時</string>
|
||||||
<string name="sort_last_access_time">最終アクセス</string>
|
<string name="sort_last_access_time">最終アクセス</string>
|
||||||
<string name="special">特殊文字</string>
|
<string name="special">特殊文字</string>
|
||||||
<string name="search">検索</string>
|
<string name="search">検索</string>
|
||||||
@@ -452,7 +451,7 @@
|
|||||||
<string name="education_donation_title">参加</string>
|
<string name="education_donation_title">参加</string>
|
||||||
<string name="education_donation_summary">安定性やセキュリティを向上させ、機能を追加することを支援します。</string>
|
<string name="education_donation_summary">安定性やセキュリティを向上させ、機能を追加することを支援します。</string>
|
||||||
<string name="html_text_ad_free">多くのパスワード管理アプリとは異なり、このアプリは<strong>広告なし</strong>かつ<strong>コピーレフトの自由ソフトウェア</strong>です。どのバージョンを使っても、サーバー上で個人情報が収集されることはありません。</string>
|
<string name="html_text_ad_free">多くのパスワード管理アプリとは異なり、このアプリは<strong>広告なし</strong>かつ<strong>コピーレフトの自由ソフトウェア</strong>です。どのバージョンを使っても、サーバー上で個人情報が収集されることはありません。</string>
|
||||||
<string name="html_text_buy_pro">pro バージョンを購入すると、この<strong>ビジュアル スタイル</strong>にアクセスできるようになり、また<strong>コミュニティ プロジェクトの実現</strong>を特に支援できます。</string>
|
<string name="html_text_buy_pro">Pro バージョンを購入すると、この<strong>ビジュアル スタイル</strong>にアクセスできるようになり、また<strong>コミュニティ プロジェクトの実現</strong>を特に支援できます。</string>
|
||||||
<string name="html_text_feature_generosity">この<strong>ビジュアル スタイル</strong>はあなたの厚意により利用可能となります。</string>
|
<string name="html_text_feature_generosity">この<strong>ビジュアル スタイル</strong>はあなたの厚意により利用可能となります。</string>
|
||||||
<string name="html_text_donation">自由を維持し活発に開発し続けるために、私たちはあなたの<strong>貢献</strong>に期待しています。</string>
|
<string name="html_text_donation">自由を維持し活発に開発し続けるために、私たちはあなたの<strong>貢献</strong>に期待しています。</string>
|
||||||
<string name="html_text_dev_feature">この機能は<strong>開発中</strong>であり、早期に提供するにはあなたの<strong>貢献</strong>が必要です。</string>
|
<string name="html_text_dev_feature">この機能は<strong>開発中</strong>であり、早期に提供するにはあなたの<strong>貢献</strong>が必要です。</string>
|
||||||
@@ -470,17 +469,6 @@
|
|||||||
<string name="download_progression">進行中:%1$d%%</string>
|
<string name="download_progression">進行中:%1$d%%</string>
|
||||||
<string name="download_finalization">終了しています…</string>
|
<string name="download_finalization">終了しています…</string>
|
||||||
<string name="download_complete">完了しました!</string>
|
<string name="download_complete">完了しました!</string>
|
||||||
<string-array name="timeout_options">
|
|
||||||
<item>5秒</item>
|
|
||||||
<item>10秒</item>
|
|
||||||
<item>20秒</item>
|
|
||||||
<item>30秒</item>
|
|
||||||
<item>1分</item>
|
|
||||||
<item>5分</item>
|
|
||||||
<item>15分</item>
|
|
||||||
<item>30分</item>
|
|
||||||
<item>なし</item>
|
|
||||||
</string-array>
|
|
||||||
<string-array name="list_size_options">
|
<string-array name="list_size_options">
|
||||||
<item>小</item>
|
<item>小</item>
|
||||||
<item>中</item>
|
<item>中</item>
|
||||||
@@ -553,11 +541,22 @@
|
|||||||
<string name="error_otp_type">既存の OTP の種類がこのフォームでは認識されていないため、フォームの検証によってトークンが正しく生成されなくなる可能性があります。</string>
|
<string name="error_otp_type">既存の OTP の種類がこのフォームでは認識されていないため、フォームの検証によってトークンが正しく生成されなくなる可能性があります。</string>
|
||||||
<string name="icon_section_custom">カスタム</string>
|
<string name="icon_section_custom">カスタム</string>
|
||||||
<string name="icon_section_standard">標準</string>
|
<string name="icon_section_standard">標準</string>
|
||||||
<string name="style_brightness_summary">ライトテーマとダークテーマのどちらかを選択します</string>
|
<string name="style_brightness_summary">ライト / ダークテーマを選択します</string>
|
||||||
<string name="style_brightness_title">テーマの明るさ</string>
|
<string name="style_brightness_title">テーマの明るさ</string>
|
||||||
<string name="error_remove_file">ファイルデータの削除中にエラーが発生しました。</string>
|
<string name="error_remove_file">ファイルデータの削除中にエラーが発生しました。</string>
|
||||||
<string name="error_duplicate_file">ファイルデータはすでに存在します。</string>
|
<string name="error_duplicate_file">ファイルデータはすでに存在します。</string>
|
||||||
<string name="error_upload_file">ファイルデータのアップロード中にエラーが発生しました。</string>
|
<string name="error_upload_file">ファイルデータのアップロード中にエラーが発生しました。</string>
|
||||||
<string name="error_file_to_big">アップロードしようとしているファイルが大きすぎます。</string>
|
<string name="error_file_to_big">アップロードしようとしているファイルが大きすぎます。</string>
|
||||||
<string name="content_description_otp_information">ワンタイムパスワードについて</string>
|
<string name="content_description_otp_information">ワンタイムパスワードについて</string>
|
||||||
|
<string name="import_app_properties_summary">アプリのプロパティをインポートするファイルを選択します</string>
|
||||||
|
<string name="import_app_properties_title">アプリのプロパティをインポートする</string>
|
||||||
|
<string name="properties">プロパティ</string>
|
||||||
|
<string name="error_export_app_properties">アプリのプロパティのエクスポート時にエラーが発生しました</string>
|
||||||
|
<string name="success_export_app_properties">アプリのプロパティをエクスポートしました</string>
|
||||||
|
<string name="error_import_app_properties">アプリのプロパティのインポート時にエラーが発生しました</string>
|
||||||
|
<string name="success_import_app_properties">アプリのプロパティをインポートしました</string>
|
||||||
|
<string name="description_app_properties">アプリの設定を管理する KeePassDX のプロパティ</string>
|
||||||
|
<string name="export_app_properties_summary">アプリのプロパティをエクスポートするファイルを作成します</string>
|
||||||
|
<string name="export_app_properties_title">アプリのプロパティをエクスポートする</string>
|
||||||
|
<string name="error_start_database_action">データベースに対するアクションの実行中にエラーが発生しました。</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -78,7 +78,6 @@
|
|||||||
<string name="error_string_key">각 항목은 필드 이름을 가져야 합니다.</string>
|
<string name="error_string_key">각 항목은 필드 이름을 가져야 합니다.</string>
|
||||||
<string name="error_wrong_length">\"길이\" 필드에는 양수를 입력하십시오.</string>
|
<string name="error_wrong_length">\"길이\" 필드에는 양수를 입력하십시오.</string>
|
||||||
<string name="error_autofill_enable_service">자동 채우기 서비스를 활성화할 수 없습니다.</string>
|
<string name="error_autofill_enable_service">자동 채우기 서비스를 활성화할 수 없습니다.</string>
|
||||||
<string name="error_move_folder_in_itself">그룹을 자신에게 옮길 수 없습니다.</string>
|
|
||||||
<string name="field_name">필드 이름</string>
|
<string name="field_name">필드 이름</string>
|
||||||
<string name="field_value">필드 값</string>
|
<string name="field_value">필드 값</string>
|
||||||
<string name="file_not_found_content">파일을 찾을 수 없습니다. 파일 탐색기에서 열리는지 확인해 주세요.</string>
|
<string name="file_not_found_content">파일을 찾을 수 없습니다. 파일 탐색기에서 열리는지 확인해 주세요.</string>
|
||||||
|
|||||||
@@ -129,17 +129,6 @@
|
|||||||
<string name="uppercase">Lielie burti</string>
|
<string name="uppercase">Lielie burti</string>
|
||||||
<string name="version_label">Versija %1$s</string>
|
<string name="version_label">Versija %1$s</string>
|
||||||
<string name="education_unlock_summary">Ievadiet paroli/atslēgas failu, lai atbloķētu savu datu bāzi.</string>
|
<string name="education_unlock_summary">Ievadiet paroli/atslēgas failu, lai atbloķētu savu datu bāzi.</string>
|
||||||
<string-array name="timeout_options">
|
|
||||||
<item>5 sekundes</item>
|
|
||||||
<item>10 sekundes</item>
|
|
||||||
<item>20 sekundes</item>
|
|
||||||
<item>30 sekundes</item>
|
|
||||||
<item>1 minūte</item>
|
|
||||||
<item>5 minūtes</item>
|
|
||||||
<item>15 minūtes</item>
|
|
||||||
<item>30 minūtes</item>
|
|
||||||
<item>Nekad</item>
|
|
||||||
</string-array>
|
|
||||||
<string-array name="list_size_options">
|
<string-array name="list_size_options">
|
||||||
<item>Mazs</item>
|
<item>Mazs</item>
|
||||||
<item>Vidējs</item>
|
<item>Vidējs</item>
|
||||||
|
|||||||
@@ -128,7 +128,6 @@
|
|||||||
<string name="error_create_database_file">ഈ പാസ്വേഡും കീഫയലും ഉപയോഗിച്ച് ഡാറ്റാബേസ് സൃഷ്ടിക്കാൻ കഴിയില്ല.</string>
|
<string name="error_create_database_file">ഈ പാസ്വേഡും കീഫയലും ഉപയോഗിച്ച് ഡാറ്റാബേസ് സൃഷ്ടിക്കാൻ കഴിയില്ല.</string>
|
||||||
<string name="error_copy_group_here">നിങ്ങൾക്ക് ഇവിടെ ഒരു ഗ്രൂപ്പ് പകർത്താൻ കഴിയില്ല.</string>
|
<string name="error_copy_group_here">നിങ്ങൾക്ക് ഇവിടെ ഒരു ഗ്രൂപ്പ് പകർത്താൻ കഴിയില്ല.</string>
|
||||||
<string name="error_copy_entry_here">നിങ്ങൾക്ക് ഇവിടെ ഒരു എൻട്രി പകർത്താൻ കഴിയില്ല.</string>
|
<string name="error_copy_entry_here">നിങ്ങൾക്ക് ഇവിടെ ഒരു എൻട്രി പകർത്താൻ കഴിയില്ല.</string>
|
||||||
<string name="error_move_folder_in_itself">നിങ്ങൾക്ക് ഒരു ഗ്രൂപ്പിനെ അതിലേക്ക് നീക്കാൻ കഴിയില്ല.</string>
|
|
||||||
<string name="error_label_exists">ഈ ലേബൽ ഇതിനകം നിലവിലുണ്ട്.</string>
|
<string name="error_label_exists">ഈ ലേബൽ ഇതിനകം നിലവിലുണ്ട്.</string>
|
||||||
<string name="error_string_key">ഓരോ സ്ട്രിംഗിനും ഒരു ഫീൽഡ് നാമം ഉണ്ടായിരിക്കണം.</string>
|
<string name="error_string_key">ഓരോ സ്ട്രിംഗിനും ഒരു ഫീൽഡ് നാമം ഉണ്ടായിരിക്കണം.</string>
|
||||||
<string name="error_rounds_too_large">\"Transformation rounds\" too high. Setting to 2147483648.</string>
|
<string name="error_rounds_too_large">\"Transformation rounds\" too high. Setting to 2147483648.</string>
|
||||||
|
|||||||
@@ -78,7 +78,6 @@
|
|||||||
<string name="error_string_key">Hver streng må ha et feltnavn.</string>
|
<string name="error_string_key">Hver streng må ha et feltnavn.</string>
|
||||||
<string name="error_wrong_length">Skriv inn et positivt heltall i \"Lengde\" feltet.</string>
|
<string name="error_wrong_length">Skriv inn et positivt heltall i \"Lengde\" feltet.</string>
|
||||||
<string name="error_autofill_enable_service">Autofyll-tjenesten kan ikke skrus på.</string>
|
<string name="error_autofill_enable_service">Autofyll-tjenesten kan ikke skrus på.</string>
|
||||||
<string name="error_move_folder_in_itself">Kan ikke flytte gruppe inn i seg selv.</string>
|
|
||||||
<string name="field_name">Feltnavn</string>
|
<string name="field_name">Feltnavn</string>
|
||||||
<string name="field_value">Feltverdi</string>
|
<string name="field_value">Feltverdi</string>
|
||||||
<string name="file_not_found_content">Fant ikke filen. Prøv å åpne den fra din innholdsleverandør.</string>
|
<string name="file_not_found_content">Fant ikke filen. Prøv å åpne den fra din innholdsleverandør.</string>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user