mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
232 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6b33d60c3 | ||
|
|
3fd06890d7 | ||
|
|
4af4ad7663 | ||
|
|
6ca8501e28 | ||
|
|
432b385f60 | ||
|
|
6cebdefa4a | ||
|
|
bc665eb83d | ||
|
|
cb187300fe | ||
|
|
f34e007ecd | ||
|
|
3b6ad080b4 | ||
|
|
9919e90ba5 | ||
|
|
f4af44925b | ||
|
|
4bb366b568 | ||
|
|
7e7ab4ce19 | ||
|
|
4d833d25ce | ||
|
|
a9c508ecd9 | ||
|
|
ef4dbb8fdb | ||
|
|
3fd13f3e3b | ||
|
|
319c9cad4b | ||
|
|
c12297c98d | ||
|
|
7c38361844 | ||
|
|
559554a975 | ||
|
|
7e2ffa2124 | ||
|
|
66dbac4bb2 | ||
|
|
8b6a843a85 | ||
|
|
976cff2751 | ||
|
|
f7c30fa8eb | ||
|
|
7757c8218b | ||
|
|
2928b7daa3 | ||
|
|
3a55dea276 | ||
|
|
2a25213d66 | ||
|
|
035ffd8135 | ||
|
|
b040487f1f | ||
|
|
6fc821aecf | ||
|
|
cdceb1fb6f | ||
|
|
07d185913d | ||
|
|
f2a245a9c8 | ||
|
|
33338f4759 | ||
|
|
f7a4370b29 | ||
|
|
77b7afedda | ||
|
|
caa13039e5 | ||
|
|
02845d93ed | ||
|
|
9ef4695cc7 | ||
|
|
d619e089c0 | ||
|
|
3c50348a79 | ||
|
|
167ea3b82b | ||
|
|
9eda3e62f7 | ||
|
|
99c4319b51 | ||
|
|
790b25db65 | ||
|
|
97d4972f9a | ||
|
|
8e6853756f | ||
|
|
6d3aae187b | ||
|
|
b8c7acf7ce | ||
|
|
17a356ae76 | ||
|
|
bd847e632d | ||
|
|
2bfb9b048d | ||
|
|
dc40b50b65 | ||
|
|
3e2271e596 | ||
|
|
4b4fd2a11d | ||
|
|
23468290df | ||
|
|
a276f6aa06 | ||
|
|
f2a58361a1 | ||
|
|
271023b528 | ||
|
|
e1771ca249 | ||
|
|
ca4f4bd151 | ||
|
|
d81454d618 | ||
|
|
fb43c1c624 | ||
|
|
9e060f878d | ||
|
|
bd9c21ee8a | ||
|
|
3e6d40e8da | ||
|
|
79683cb3fc | ||
|
|
52a2090a31 | ||
|
|
3dfe4ace7b | ||
|
|
bd0d17b134 | ||
|
|
6b0ccc1780 | ||
|
|
d75ac4b825 | ||
|
|
b60d610d02 | ||
|
|
f7a5c5d0ea | ||
|
|
28f79aec11 | ||
|
|
778d963fbf | ||
|
|
a765bc84e7 | ||
|
|
804ecc1baa | ||
|
|
d331c3dc03 | ||
|
|
7010d2f86a | ||
|
|
b1d6117eb2 | ||
|
|
f3b814388d | ||
|
|
b62996a57c | ||
|
|
a49e056f02 | ||
|
|
a6dece16bf | ||
|
|
8e3ddd64d2 | ||
|
|
45a847fa3e | ||
|
|
6b6f03b143 | ||
|
|
5446efca4a | ||
|
|
8d04a7f90b | ||
|
|
626495c19e | ||
|
|
e5a198f524 | ||
|
|
161524843f | ||
|
|
5550e7dea3 | ||
|
|
64f66c290c | ||
|
|
e8925b3c0b | ||
|
|
cf67ce04a8 | ||
|
|
84ee4ca2c7 | ||
|
|
27eb095720 | ||
|
|
d273f21819 | ||
|
|
455fd0cd6d | ||
|
|
c5a8650c81 | ||
|
|
b5f9bbed5e | ||
|
|
e789741090 | ||
|
|
5c6d93bc57 | ||
|
|
697b672038 | ||
|
|
2d9e9c24a8 | ||
|
|
5fb281c800 | ||
|
|
96896c1c42 | ||
|
|
d7052bd9e6 | ||
|
|
8b23932788 | ||
|
|
50912c6966 | ||
|
|
53b51934b9 | ||
|
|
a8a3685965 | ||
|
|
149b67e28b | ||
|
|
a83032bffa | ||
|
|
a5d3a153bf | ||
|
|
4210c155eb | ||
|
|
4bf110a9b1 | ||
|
|
50f2684500 | ||
|
|
e95424b8f9 | ||
|
|
8462882707 | ||
|
|
a5c8d25f64 | ||
|
|
689ce2f9b3 | ||
|
|
54246533ac | ||
|
|
66e4b0fe47 | ||
|
|
3e8ae3e2e3 | ||
|
|
d856ef3772 | ||
|
|
5727880ac7 | ||
|
|
ec4302a780 | ||
|
|
d4203598a1 | ||
|
|
a278c8c718 | ||
|
|
faf5f4b51a | ||
|
|
b2f503b326 | ||
|
|
beb5484bf6 | ||
|
|
ec63d75349 | ||
|
|
4c0e79b245 | ||
|
|
50a77684c1 | ||
|
|
8bb84b486d | ||
|
|
4b05f2536f | ||
|
|
d6f968fe7e | ||
|
|
ed758edd44 | ||
|
|
94b7fce2e5 | ||
|
|
dbd9c6cbb7 | ||
|
|
0f6376fb80 | ||
|
|
9522328238 | ||
|
|
e6ad716119 | ||
|
|
440006bb08 | ||
|
|
ea289ef7cf | ||
|
|
352b171484 | ||
|
|
969ab56bf8 | ||
|
|
062a9852e5 | ||
|
|
dd77d7a5e6 | ||
|
|
f60e32522a | ||
|
|
070a91f19c | ||
|
|
0790e80670 | ||
|
|
20841e3d7b | ||
|
|
0caae233c3 | ||
|
|
5497d8fafb | ||
|
|
f69b43249c | ||
|
|
b606fd98f6 | ||
|
|
ba3b7b0f1f | ||
|
|
058d82dc36 | ||
|
|
6f0b0ac4fa | ||
|
|
35f87b0f94 | ||
|
|
0ead9ce9b4 | ||
|
|
80479a6a7c | ||
|
|
a7cea8201e | ||
|
|
081a7fa798 | ||
|
|
85782c4f93 | ||
|
|
d7b7df26d7 | ||
|
|
b6b1c8e31d | ||
|
|
17156f7ca2 | ||
|
|
0761d356b8 | ||
|
|
6da747ce6f | ||
|
|
87b1a1f527 | ||
|
|
72a8a55faf | ||
|
|
9a6a709746 | ||
|
|
428b53cc56 | ||
|
|
e688859e32 | ||
|
|
98336da116 | ||
|
|
c037e443b0 | ||
|
|
d339a50e0a | ||
|
|
7d836f2633 | ||
|
|
45d8470b4c | ||
|
|
1ca3bfe472 | ||
|
|
066da83d70 | ||
|
|
44ab881751 | ||
|
|
5ab3cf985a | ||
|
|
f271f2b181 | ||
|
|
91d75be0ea | ||
|
|
774dddca54 | ||
|
|
e18b3436c9 | ||
|
|
fcb1b5ae6b | ||
|
|
de980d030a | ||
|
|
0e859646fe | ||
|
|
059c7b7713 | ||
|
|
5fb7bf71c8 | ||
|
|
8b0133ff7f | ||
|
|
8d834946b8 | ||
|
|
2f646395d4 | ||
|
|
f6e79ba37b | ||
|
|
e633c7a861 | ||
|
|
dc02a8d78c | ||
|
|
baa9b88512 | ||
|
|
c522e87da8 | ||
|
|
ef5ebf2c15 | ||
|
|
4b147e770c | ||
|
|
157a5c0b05 | ||
|
|
f2288b0c64 | ||
|
|
d8506450aa | ||
|
|
f9b085e73f | ||
|
|
388cf6a91b | ||
|
|
9e6e77b363 | ||
|
|
ec33ca8173 | ||
|
|
6be0457947 | ||
|
|
f3b84aa845 | ||
|
|
bd0b5b0954 | ||
|
|
7dc93604ad | ||
|
|
0ab22698a6 | ||
|
|
c885ce7aaf | ||
|
|
92d1a7b901 | ||
|
|
6119054b45 | ||
|
|
e7aed72398 | ||
|
|
cee7fa50f5 | ||
|
|
39a38bb223 | ||
|
|
7159a993db | ||
|
|
23933e80e3 |
42
CHANGELOG
42
CHANGELOG
@@ -1,3 +1,45 @@
|
|||||||
|
KeePassDX(2.9.12)
|
||||||
|
* Fix OTP token type #863
|
||||||
|
* Fix auto open biometric prompt #862
|
||||||
|
* Fix back appearance setting #865
|
||||||
|
* Fix orientation change in settings #872
|
||||||
|
* Change memory unit to MiB #851
|
||||||
|
* Small changes #642
|
||||||
|
|
||||||
|
KeePassDX(2.9.11)
|
||||||
|
* Add Keyfile XML version 2 (fix hex) #844
|
||||||
|
* Fix hex Keyfile #861
|
||||||
|
|
||||||
|
KeePassDX(2.9.10)
|
||||||
|
* Try to fix autofill #852
|
||||||
|
* Fix database change dialog displayed too often #853
|
||||||
|
|
||||||
|
KeePassDX(2.9.9)
|
||||||
|
* Detect file changes and reload database #794
|
||||||
|
* Inline suggestions autofill with compatible keyboard (Android R) #827
|
||||||
|
* Add Keyfile XML version 2 #844
|
||||||
|
* Fix binaries of 64 bytes #835
|
||||||
|
* Special search in title fields #830
|
||||||
|
* Priority to OTP button in notifications #845
|
||||||
|
* Fix OTP generation for long secret key #848
|
||||||
|
* Fix small bugs #849
|
||||||
|
|
||||||
|
KeePassDX(2.9.8)
|
||||||
|
* Fix specific attachments with kdbx3.1 databases #828
|
||||||
|
* Fix small bugs
|
||||||
|
|
||||||
|
KeePassDX(2.9.7)
|
||||||
|
* Remove write permission since Android 10 #823
|
||||||
|
* Fix small bugs
|
||||||
|
|
||||||
|
KeePassDX(2.9.6)
|
||||||
|
* Fix KeyFile bug #820
|
||||||
|
|
||||||
|
KeePassDX(2.9.5)
|
||||||
|
* Unlock database by device credentials (PIN/Password/Pattern) with Android M+ #102 #152 #811
|
||||||
|
* Prevent auto switch back to previous keyboard if otp field exists #814
|
||||||
|
* Fix timeout reset #817
|
||||||
|
|
||||||
KeePassDX(2.9.4)
|
KeePassDX(2.9.4)
|
||||||
* Fix small bugs #812
|
* Fix small bugs #812
|
||||||
* Argon2ID implementation #791
|
* Argon2ID implementation #791
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ apply plugin: 'kotlin-kapt'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 30
|
compileSdkVersion 30
|
||||||
buildToolsVersion '30.0.2'
|
buildToolsVersion '30.0.3'
|
||||||
ndkVersion '21.3.6528147'
|
ndkVersion '21.3.6528147'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 14
|
minSdkVersion 14
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode = 48
|
versionCode = 56
|
||||||
versionName = "2.9.4"
|
versionName = "2.9.12"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
@@ -92,7 +92,7 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def room_version = "2.2.5"
|
def room_version = "2.2.6"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
@@ -110,6 +110,8 @@ dependencies {
|
|||||||
// Database
|
// Database
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
kapt "androidx.room:room-compiler:$room_version"
|
kapt "androidx.room:room-compiler:$room_version"
|
||||||
|
// Autofill
|
||||||
|
implementation "androidx.autofill:autofill:1.1.0-rc01"
|
||||||
// Crypto
|
// Crypto
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01'
|
implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01'
|
||||||
// Time
|
// Time
|
||||||
@@ -121,7 +123,7 @@ dependencies {
|
|||||||
// Apache Commons Collections
|
// Apache Commons Collections
|
||||||
implementation 'commons-collections:commons-collections:3.2.2'
|
implementation 'commons-collections:commons-collections:3.2.2'
|
||||||
// Apache Commons Codec
|
// Apache Commons Codec
|
||||||
implementation 'commons-codec:commons-codec:1.14'
|
implementation 'commons-codec:commons-codec:1.15'
|
||||||
// Icon pack
|
// Icon pack
|
||||||
implementation project(path: ':icon-pack-classic')
|
implementation project(path: ':icon-pack-classic')
|
||||||
implementation project(path: ':icon-pack-material')
|
implementation project(path: ':icon-pack-material')
|
||||||
|
|||||||
@@ -14,10 +14,15 @@
|
|||||||
android:name="android.permission.USE_BIOMETRIC" />
|
android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.VIBRATE"/>
|
android:name="android.permission.VIBRATE"/>
|
||||||
|
<!-- Write permission until Android 10 -->
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="28"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
|
<!-- Open apps from links -->
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.QUERY_ALL_PACKAGES"/>
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import android.content.Intent
|
|||||||
import android.content.IntentSender
|
import android.content.IntentSender
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.inputmethod.InlineSuggestionsRequest
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
@@ -33,6 +34,7 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST
|
||||||
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
@@ -40,7 +42,6 @@ import com.kunzisoft.keepass.model.RegisterInfo
|
|||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class AutofillLauncherActivity : AppCompatActivity() {
|
class AutofillLauncherActivity : AppCompatActivity() {
|
||||||
@@ -84,9 +85,9 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun launchSelection(searchInfo: SearchInfo) {
|
private fun launchSelection(searchInfo: SearchInfo) {
|
||||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||||
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
|
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
|
||||||
|
|
||||||
if (assistStructure == null) {
|
if (autofillComponent == null) {
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(Activity.RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
} else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
} else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
||||||
@@ -105,21 +106,21 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
searchInfo,
|
searchInfo,
|
||||||
{ items ->
|
{ items ->
|
||||||
// Items found
|
// Items found
|
||||||
AutofillHelper.buildResponse(this, items)
|
AutofillHelper.buildResponseAndSetResult(this, items)
|
||||||
finish()
|
finish()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
GroupActivity.launchForAutofillResult(this,
|
GroupActivity.launchForAutofillResult(this,
|
||||||
readOnly,
|
readOnly,
|
||||||
assistStructure,
|
autofillComponent,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
false)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// If database not open
|
// If database not open
|
||||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||||
assistStructure,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -196,7 +197,8 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
||||||
|
|
||||||
fun getAuthIntentSenderForSelection(context: Context,
|
fun getAuthIntentSenderForSelection(context: Context,
|
||||||
searchInfo: SearchInfo? = null): IntentSender {
|
searchInfo: SearchInfo? = null,
|
||||||
|
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): IntentSender {
|
||||||
return PendingIntent.getActivity(context, 0,
|
return PendingIntent.getActivity(context, 0,
|
||||||
// Doesn't work with Parcelable (don't know why?)
|
// Doesn't work with Parcelable (don't know why?)
|
||||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
@@ -205,6 +207,11 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
||||||
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
|
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
|
||||||
}
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
inlineSuggestionsRequest?.let {
|
||||||
|
putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ 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.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.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
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
|
||||||
@@ -52,7 +54,9 @@ import com.kunzisoft.keepass.model.StreamDirection
|
|||||||
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||||
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
@@ -133,7 +137,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Focus view to reinitialize timeout
|
// Focus view to reinitialize timeout
|
||||||
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
|
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
||||||
|
|
||||||
// Init the clipboard helper
|
// Init the clipboard helper
|
||||||
clipboardHelper = ClipboardHelper(this)
|
clipboardHelper = ClipboardHelper(this)
|
||||||
@@ -150,6 +154,10 @@ class EntryActivity : LockingActivity() {
|
|||||||
if (result.isSuccess)
|
if (result.isSuccess)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
ACTION_DATABASE_RELOAD_TASK -> {
|
||||||
|
// Close the current activity
|
||||||
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
coordinatorLayout?.showActionError(result)
|
coordinatorLayout?.showActionError(result)
|
||||||
}
|
}
|
||||||
@@ -198,8 +206,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
// Refresh Menu
|
// Refresh Menu
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
val entryInfo = entry.getEntryInfo(Database.getInstance())
|
val entryInfo = entry.getEntryInfo(mDatabase)
|
||||||
|
|
||||||
// Manage entry copy to start notification if allowed
|
// Manage entry copy to start notification if allowed
|
||||||
if (mFirstLaunchOfActivity) {
|
if (mFirstLaunchOfActivity) {
|
||||||
// Manage entry to launch copying notification if allowed
|
// Manage entry to launch copying notification if allowed
|
||||||
@@ -231,23 +238,21 @@ class EntryActivity : LockingActivity() {
|
|||||||
|
|
||||||
private fun fillEntryDataInContentsView(entry: Entry) {
|
private fun fillEntryDataInContentsView(entry: Entry) {
|
||||||
|
|
||||||
val database = Database.getInstance()
|
val entryInfo = entry.getEntryInfo(mDatabase)
|
||||||
database.startManageEntry(entry)
|
|
||||||
// Assign title icon
|
// Assign title icon
|
||||||
titleIconView?.assignDatabaseIcon(database.drawFactory, entry.icon, iconColor)
|
titleIconView?.assignDatabaseIcon(mDatabase!!.drawFactory, entryInfo.icon, iconColor)
|
||||||
|
|
||||||
// Assign title text
|
// Assign title text
|
||||||
val entryTitle = entry.title
|
val entryTitle = entryInfo.title
|
||||||
collapsingToolbarLayout?.title = entryTitle
|
collapsingToolbarLayout?.title = entryTitle
|
||||||
toolbar?.title = entryTitle
|
toolbar?.title = entryTitle
|
||||||
|
|
||||||
// Assign basic fields
|
// Assign basic fields
|
||||||
entryContentsView?.assignUserName(entry.username) {
|
entryContentsView?.assignUserName(entryInfo.username) {
|
||||||
database.startManageEntry(entry)
|
clipboardHelper?.timeoutCopyToClipboard(entryInfo.username,
|
||||||
clipboardHelper?.timeoutCopyToClipboard(entry.username,
|
|
||||||
getString(R.string.copy_field,
|
getString(R.string.copy_field,
|
||||||
getString(R.string.entry_user_name)))
|
getString(R.string.entry_user_name)))
|
||||||
database.stopManageEntry(entry)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
||||||
@@ -277,11 +282,9 @@ class EntryActivity : LockingActivity() {
|
|||||||
|
|
||||||
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
|
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
|
||||||
View.OnClickListener {
|
View.OnClickListener {
|
||||||
database.startManageEntry(entry)
|
clipboardHelper?.timeoutCopyToClipboard(entryInfo.password,
|
||||||
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
|
||||||
getString(R.string.copy_field,
|
getString(R.string.copy_field,
|
||||||
getString(R.string.entry_password)))
|
getString(R.string.entry_password)))
|
||||||
database.stopManageEntry(entry)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If dialog not already shown
|
// If dialog not already shown
|
||||||
@@ -291,44 +294,46 @@ class EntryActivity : LockingActivity() {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entryContentsView?.assignPassword(entry.password,
|
entryContentsView?.assignPassword(entryInfo.password,
|
||||||
allowCopyPasswordAndProtectedFields,
|
allowCopyPasswordAndProtectedFields,
|
||||||
onPasswordCopyClickListener)
|
onPasswordCopyClickListener)
|
||||||
|
|
||||||
//Assign OTP field
|
//Assign OTP field
|
||||||
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
|
entry.getOtpElement()?.let { otpElement ->
|
||||||
View.OnClickListener {
|
entryContentsView?.assignOtp(otpElement, entryProgress) {
|
||||||
entry.getOtpElement()?.let { otpElement ->
|
clipboardHelper?.timeoutCopyToClipboard(
|
||||||
clipboardHelper?.timeoutCopyToClipboard(
|
otpElement.token,
|
||||||
otpElement.token,
|
getString(R.string.copy_field, getString(R.string.entry_otp))
|
||||||
getString(R.string.copy_field, getString(R.string.entry_otp))
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
entryContentsView?.assignURL(entry.url)
|
entryContentsView?.assignURL(entryInfo.url)
|
||||||
entryContentsView?.assignNotes(entry.notes)
|
entryContentsView?.assignNotes(entryInfo.notes)
|
||||||
|
|
||||||
// Assign custom fields
|
// Assign custom fields
|
||||||
if (mDatabase?.allowEntryCustomFields() == true) {
|
if (mDatabase?.allowEntryCustomFields() == true) {
|
||||||
entryContentsView?.clearExtraFields()
|
entryContentsView?.clearExtraFields()
|
||||||
entry.getExtraFields().forEach { field ->
|
entryInfo.customFields.forEach { field ->
|
||||||
val label = field.name
|
val label = field.name
|
||||||
val value = field.protectedValue
|
// OTP field is already managed in dedicated view
|
||||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
if (label != OtpEntryFields.OTP_TOKEN_FIELD) {
|
||||||
if (allowCopyProtectedField) {
|
val value = field.protectedValue
|
||||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
|
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
||||||
clipboardHelper?.timeoutCopyToClipboard(
|
if (allowCopyProtectedField) {
|
||||||
value.toString(),
|
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
|
||||||
getString(R.string.copy_field, label)
|
clipboardHelper?.timeoutCopyToClipboard(
|
||||||
)
|
value.toString(),
|
||||||
}
|
getString(R.string.copy_field, label)
|
||||||
} else {
|
)
|
||||||
// If dialog not already shown
|
}
|
||||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
|
||||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
|
|
||||||
} else {
|
} else {
|
||||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
|
// If dialog not already shown
|
||||||
|
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||||
|
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
|
||||||
|
} else {
|
||||||
|
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,24 +341,16 @@ class EntryActivity : LockingActivity() {
|
|||||||
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
||||||
|
|
||||||
// Manage attachments
|
// Manage attachments
|
||||||
mDatabase?.binaryPool?.let { binaryPool ->
|
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
||||||
entryContentsView?.assignAttachments(entry.getAttachments(binaryPool).toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
||||||
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign dates
|
// Assign dates
|
||||||
entryContentsView?.assignCreationDate(entry.creationTime)
|
entryContentsView?.assignCreationDate(entryInfo.creationTime)
|
||||||
entryContentsView?.assignModificationDate(entry.lastModificationTime)
|
entryContentsView?.assignModificationDate(entryInfo.modificationTime)
|
||||||
entryContentsView?.assignLastAccessDate(entry.lastAccessTime)
|
entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime)
|
||||||
entryContentsView?.setExpires(entry.isCurrentlyExpires)
|
|
||||||
if (entry.expires) {
|
|
||||||
entryContentsView?.assignExpiresDate(entry.expiryTime)
|
|
||||||
} else {
|
|
||||||
entryContentsView?.assignExpiresDate(getString(R.string.never))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manage history
|
// Manage history
|
||||||
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
|
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
|
||||||
@@ -368,8 +365,6 @@ class EntryActivity : LockingActivity() {
|
|||||||
|
|
||||||
// Assign special data
|
// Assign special data
|
||||||
entryContentsView?.assignUUID(entry.nodeId.id)
|
entryContentsView?.assignUUID(entry.nodeId.id)
|
||||||
|
|
||||||
database.stopManageEntry(entry)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
@@ -407,6 +402,9 @@ class EntryActivity : LockingActivity() {
|
|||||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
menu.findItem(R.id.menu_edit)?.isVisible = false
|
||||||
}
|
}
|
||||||
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
|
menu.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
||||||
gotoUrl?.apply {
|
gotoUrl?.apply {
|
||||||
@@ -500,6 +498,9 @@ class EntryActivity : LockingActivity() {
|
|||||||
R.id.menu_save_database -> {
|
R.id.menu_save_database -> {
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
||||||
}
|
}
|
||||||
|
R.id.menu_reload_database -> {
|
||||||
|
mProgressDatabaseTaskProvider?.startDatabaseReload(false)
|
||||||
|
}
|
||||||
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
|
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.activities
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.DatePickerDialog
|
import android.app.DatePickerDialog
|
||||||
import android.app.TimePickerDialog
|
import android.app.TimePickerDialog
|
||||||
import android.app.assist.AssistStructure
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -48,6 +47,8 @@ import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Compani
|
|||||||
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.SelectFileHelper
|
||||||
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.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.*
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
@@ -60,6 +61,7 @@ import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
|||||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
@@ -134,7 +136,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Focus view to reinitialize timeout
|
// Focus view to reinitialize timeout
|
||||||
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
|
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
||||||
|
|
||||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
||||||
@@ -334,6 +336,10 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
Log.e(TAG, "Unable to retrieve entry after database action", e)
|
Log.e(TAG, "Unable to retrieve entry after database action", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ACTION_DATABASE_RELOAD_TASK -> {
|
||||||
|
// Close the current activity
|
||||||
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
coordinatorLayout?.showActionError(result)
|
coordinatorLayout?.showActionError(result)
|
||||||
}
|
}
|
||||||
@@ -360,7 +366,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
// Build Autofill response with the entry selected
|
// Build Autofill response with the entry selected
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
AutofillHelper.buildResponse(this@EntryEditActivity,
|
AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity,
|
||||||
entry.getEntryInfo(database))
|
entry.getEntryInfo(database))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -478,8 +484,12 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onEditCustomFieldApproved(oldField: Field, newField: Field) {
|
override fun onEditCustomFieldApproved(oldField: Field, newField: Field) {
|
||||||
verifyNameField(newField) {
|
if (oldField.name.equals(newField.name, true)) {
|
||||||
entryEditFragment?.replaceExtraField(oldField, newField)
|
entryEditFragment?.replaceExtraField(oldField, newField)
|
||||||
|
} else {
|
||||||
|
verifyNameField(newField) {
|
||||||
|
entryEditFragment?.replaceExtraField(oldField, newField)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,13 +619,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
MenuUtil.contributionMenuInflater(menuInflater, menu)
|
||||||
val inflater = menuInflater
|
|
||||||
inflater.inflate(R.menu.database, menu)
|
|
||||||
// Save database not needed here
|
|
||||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
|
||||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,9 +676,6 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.menu_save_database -> {
|
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
|
||||||
}
|
|
||||||
R.id.menu_contribute -> {
|
R.id.menu_contribute -> {
|
||||||
MenuUtil.onContributionItemSelected(this)
|
MenuUtil.onContributionItemSelected(this)
|
||||||
return true
|
return true
|
||||||
@@ -908,7 +909,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
*/
|
*/
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: Activity,
|
fun launchForAutofillResult(activity: Activity,
|
||||||
assistStructure: AssistStructure,
|
autofillComponent: AutofillComponent,
|
||||||
group: Group,
|
group: Group,
|
||||||
searchInfo: SearchInfo? = null) {
|
searchInfo: SearchInfo? = null) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
@@ -916,7 +917,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
intent.putExtra(KEY_PARENT, group.nodeId)
|
intent.putExtra(KEY_PARENT, group.nodeId)
|
||||||
AutofillHelper.startActivityForAutofillResult(activity,
|
AutofillHelper.startActivityForAutofillResult(activity,
|
||||||
intent,
|
intent,
|
||||||
assistStructure,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import com.google.android.material.textfield.TextInputEditText
|
|||||||
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.dialogs.GeneratePasswordDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
@@ -148,6 +149,8 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
|
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
|
||||||
taIconColor?.recycle()
|
taIconColor?.recycle()
|
||||||
|
|
||||||
|
rootView?.resetAppTimeoutWhenViewFocusedOrChanged(requireContext())
|
||||||
|
|
||||||
// Retrieve the new entry after an orientation change
|
// Retrieve the new entry after an orientation change
|
||||||
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
|
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
|
||||||
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.assist.AssistStructure
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -48,6 +47,7 @@ import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|||||||
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
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
@@ -434,8 +434,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
|
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
|
||||||
}
|
}
|
||||||
|
MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||||
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -501,11 +501,11 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: Activity,
|
fun launchForAutofillResult(activity: Activity,
|
||||||
assistStructure: AssistStructure,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo? = null) {
|
searchInfo: SearchInfo? = null) {
|
||||||
AutofillHelper.startActivityForAutofillResult(activity,
|
AutofillHelper.startActivityForAutofillResult(activity,
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
assistStructure,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ package com.kunzisoft.keepass.activities
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.SearchManager
|
import android.app.SearchManager
|
||||||
import android.app.assist.AssistStructure
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -50,7 +49,9 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
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.adapters.SearchEntryCursorAdapter
|
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
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
|
||||||
@@ -69,6 +70,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
|
|||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
|
||||||
@@ -153,7 +155,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
taTextColor.recycle()
|
taTextColor.recycle()
|
||||||
|
|
||||||
// Focus view to reinitialize timeout
|
// Focus view to reinitialize timeout
|
||||||
resetAppTimeoutWhenViewFocusedOrChanged(rootContainerView)
|
rootContainerView?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
||||||
|
|
||||||
// Retrieve elements after an orientation change
|
// Retrieve elements after an orientation change
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
@@ -227,10 +229,10 @@ class GroupActivity : LockingActivity(),
|
|||||||
currentGroup, searchInfo)
|
currentGroup, searchInfo)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ searchInfo, assistStructure ->
|
{ searchInfo, autofillComponent ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
EntryEditActivity.launchForAutofillResult(this@GroupActivity,
|
EntryEditActivity.launchForAutofillResult(this@GroupActivity,
|
||||||
assistStructure,
|
autofillComponent,
|
||||||
currentGroup, searchInfo)
|
currentGroup, searchInfo)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
} else {
|
} else {
|
||||||
@@ -316,7 +318,12 @@ class GroupActivity : LockingActivity(),
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
|
|
||||||
// Rebuild all the list to avoid bug when delete node from sort
|
// Rebuild all the list to avoid bug when delete node from sort
|
||||||
mListNodesFragment?.rebuildList()
|
try {
|
||||||
|
mListNodesFragment?.rebuildList()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to rebuild the list after deletion")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
// Add trash in views list if it doesn't exists
|
// Add trash in views list if it doesn't exists
|
||||||
if (database.isRecycleBinEnabled) {
|
if (database.isRecycleBinEnabled) {
|
||||||
@@ -336,6 +343,12 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ACTION_DATABASE_RELOAD_TASK -> {
|
||||||
|
// Reload the current activity
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
coordinatorLayout?.showActionError(result)
|
coordinatorLayout?.showActionError(result)
|
||||||
@@ -659,7 +672,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
// Build response with the entry selected
|
// Build response with the entry selected
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
AutofillHelper.buildResponse(this,
|
AutofillHelper.buildResponseAndSetResult(this,
|
||||||
entry.getEntryInfo(database))
|
entry.getEntryInfo(database))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -872,6 +885,8 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
if (mSpecialMode == SpecialMode.DEFAULT) {
|
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
MenuUtil.defaultMenuInflater(inflater, menu)
|
||||||
|
} else {
|
||||||
|
menu.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menu for recycle bin
|
// Menu for recycle bin
|
||||||
@@ -997,6 +1012,10 @@ class GroupActivity : LockingActivity(),
|
|||||||
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
R.id.menu_reload_database -> {
|
||||||
|
mProgressDatabaseTaskProvider?.startDatabaseReload(false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
R.id.menu_empty_recycle_bin -> {
|
R.id.menu_empty_recycle_bin -> {
|
||||||
mCurrentGroup?.getChildren()?.let { listChildren ->
|
mCurrentGroup?.getChildren()?.let { listChildren ->
|
||||||
// Automatically delete all elements
|
// Automatically delete all elements
|
||||||
@@ -1124,7 +1143,16 @@ class GroupActivity : LockingActivity(),
|
|||||||
private fun rebuildListNodes() {
|
private fun rebuildListNodes() {
|
||||||
mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment?
|
mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment?
|
||||||
// to refresh fragment
|
// to refresh fragment
|
||||||
mListNodesFragment?.rebuildList()
|
try {
|
||||||
|
mListNodesFragment?.rebuildList()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
coordinatorLayout?.let { coordinatorLayout ->
|
||||||
|
Snackbar.make(coordinatorLayout,
|
||||||
|
R.string.error_rebuild_list,
|
||||||
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
}
|
||||||
|
}
|
||||||
mCurrentGroup = mListNodesFragment?.mainGroup
|
mCurrentGroup = mListNodesFragment?.mainGroup
|
||||||
// Remove search in intent
|
// Remove search in intent
|
||||||
deletePreviousSearchGroup()
|
deletePreviousSearchGroup()
|
||||||
@@ -1295,14 +1323,14 @@ class GroupActivity : LockingActivity(),
|
|||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: Activity,
|
fun launchForAutofillResult(activity: Activity,
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
assistStructure: AssistStructure,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo? = null,
|
searchInfo: SearchInfo? = null,
|
||||||
autoSearch: Boolean = false) {
|
autoSearch: Boolean = false) {
|
||||||
checkTimeAndBuildIntent(activity, null, readOnly) { intent ->
|
checkTimeAndBuildIntent(activity, null, readOnly) { intent ->
|
||||||
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||||
AutofillHelper.startActivityForAutofillResult(activity,
|
AutofillHelper.startActivityForAutofillResult(activity,
|
||||||
intent,
|
intent,
|
||||||
assistStructure,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1419,21 +1447,21 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ searchInfo, assistStructure ->
|
{ searchInfo, autofillComponent ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
SearchHelper.checkAutoSearchInfo(activity,
|
SearchHelper.checkAutoSearchInfo(activity,
|
||||||
Database.getInstance(),
|
Database.getInstance(),
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ items ->
|
{ items ->
|
||||||
// Response is build
|
// Response is build
|
||||||
AutofillHelper.buildResponse(activity, items)
|
AutofillHelper.buildResponseAndSetResult(activity, items)
|
||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Here no search info found, disable auto search
|
// Here no search info found, disable auto search
|
||||||
GroupActivity.launchForAutofillResult(activity,
|
GroupActivity.launchForAutofillResult(activity,
|
||||||
readOnly,
|
readOnly,
|
||||||
assistStructure,
|
autofillComponent,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
false)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
|
|||||||
@@ -22,30 +22,24 @@ package com.kunzisoft.keepass.activities
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.*
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.view.ActionMode
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
|
import com.kunzisoft.keepass.adapters.NodeAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
|
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.settings.PreferencesUtil
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
|
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
|
||||||
@@ -197,7 +191,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Refresh data
|
// Refresh data
|
||||||
rebuildList()
|
try {
|
||||||
|
rebuildList()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to rebuild the list during resume")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
|
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
|
||||||
// To show the " no search entry found "
|
// To show the " no search entry found "
|
||||||
@@ -209,10 +208,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
fun rebuildList() {
|
fun rebuildList() {
|
||||||
// Add elements to the list
|
// Add elements to the list
|
||||||
mainGroup?.let { mainGroup ->
|
mainGroup?.let { mainGroup ->
|
||||||
mAdapter?.apply {
|
mAdapter?.apply {
|
||||||
|
// Thrown an exception when sort cannot be performed
|
||||||
rebuildList(mainGroup)
|
rebuildList(mainGroup)
|
||||||
// To visually change the elements
|
// To visually change the elements
|
||||||
if (PreferencesUtil.APPEARANCE_CHANGED) {
|
if (PreferencesUtil.APPEARANCE_CHANGED) {
|
||||||
@@ -231,8 +232,13 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tell the adapter to refresh it's list
|
// Tell the adapter to refresh it's list
|
||||||
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
|
try {
|
||||||
rebuildList()
|
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
|
||||||
|
rebuildList()
|
||||||
|
} catch (e:Exception) {
|
||||||
|
Log.e(TAG, "Unable to rebuild the list with the sort")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.assist.AssistStructure
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -37,8 +36,8 @@ import android.widget.*
|
|||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.biometric.BiometricManager
|
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
|
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
|
||||||
@@ -49,9 +48,9 @@ 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
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
||||||
import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper
|
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
@@ -69,14 +68,13 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||||
import kotlinx.android.synthetic.main.activity_password.*
|
import kotlinx.android.synthetic.main.activity_password.*
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
open class PasswordActivity : SpecialModeActivity() {
|
open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
@@ -86,9 +84,8 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
private var confirmButtonView: Button? = null
|
private var confirmButtonView: Button? = null
|
||||||
private var checkboxPasswordView: CompoundButton? = null
|
private var checkboxPasswordView: CompoundButton? = null
|
||||||
private var checkboxKeyFileView: CompoundButton? = null
|
private var checkboxKeyFileView: CompoundButton? = null
|
||||||
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
|
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
||||||
private var infoContainerView: ViewGroup? = null
|
private var infoContainerView: ViewGroup? = null
|
||||||
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
|
||||||
|
|
||||||
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
|
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||||
|
|
||||||
@@ -114,7 +111,6 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
|
|
||||||
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||||
|
|
||||||
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
|
|
||||||
private var mAllowAutoOpenBiometricPrompt: Boolean = true
|
private var mAllowAutoOpenBiometricPrompt: Boolean = true
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -134,7 +130,6 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
||||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
||||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||||
advancedUnlockInfoView = findViewById(R.id.biometric_info)
|
|
||||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||||
|
|
||||||
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
||||||
@@ -161,10 +156,6 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
|
|
||||||
enableOrNotTheConfirmationButton()
|
|
||||||
}
|
|
||||||
|
|
||||||
// If is a view intent
|
// If is a view intent
|
||||||
getUriFromIntent(intent)
|
getUriFromIntent(intent)
|
||||||
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
||||||
@@ -174,6 +165,24 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
|
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Init Biometric elements
|
||||||
|
advancedUnlockFragment = supportFragmentManager
|
||||||
|
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
|
||||||
|
if (advancedUnlockFragment == null) {
|
||||||
|
advancedUnlockFragment = AdvancedUnlockFragment()
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
replace(R.id.fragment_advanced_unlock_container_view,
|
||||||
|
advancedUnlockFragment!!,
|
||||||
|
UNLOCK_FRAGMENT_TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen password checkbox to init advanced unlock and confirmation button
|
||||||
|
checkboxPasswordView?.setOnCheckedChangeListener { _, _ ->
|
||||||
|
advancedUnlockFragment?.checkUnlockAvailability()
|
||||||
|
enableOrNotTheConfirmationButton()
|
||||||
|
}
|
||||||
|
|
||||||
// Observe if default database
|
// Observe if default database
|
||||||
databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||||
mDefaultDatabase = isDefaultDatabase
|
mDefaultDatabase = isDefaultDatabase
|
||||||
@@ -207,12 +216,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
// Recheck advanced unlock if error
|
// Recheck advanced unlock if error
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
advancedUnlockFragment?.initAdvancedUnlockMode()
|
||||||
if (PreferencesUtil.isAdvancedUnlockEnable(this@PasswordActivity)) {
|
|
||||||
// Stay with the same mode and init it
|
|
||||||
advancedUnlockedManager?.initAdvancedUnlockMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
mDatabaseKeyFileUri = null
|
mDatabaseKeyFileUri = null
|
||||||
@@ -320,6 +324,33 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun retrieveCredentialForEncryption(): String {
|
||||||
|
return passwordView?.text?.toString() ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun conditionToStoreCredential(): Boolean {
|
||||||
|
return checkboxPasswordView?.isChecked == true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCredentialEncrypted(databaseUri: Uri,
|
||||||
|
encryptedCredential: String,
|
||||||
|
ivSpec: String) {
|
||||||
|
// Load the database if password is registered with biometric
|
||||||
|
verifyCheckboxesAndLoadDatabase(
|
||||||
|
CipherDatabaseEntity(
|
||||||
|
databaseUri.toString(),
|
||||||
|
encryptedCredential,
|
||||||
|
ivSpec)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCredentialDecrypted(databaseUri: Uri,
|
||||||
|
decryptedCredential: String) {
|
||||||
|
// Load the database if password is retrieve from biometric
|
||||||
|
// Retrieve from biometric
|
||||||
|
verifyKeyFileCheckboxAndLoadDatabase(decryptedCredential)
|
||||||
|
}
|
||||||
|
|
||||||
private val onEditorActionListener = object : TextView.OnEditorActionListener {
|
private val onEditorActionListener = object : TextView.OnEditorActionListener {
|
||||||
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
||||||
if (actionId == IME_ACTION_DONE) {
|
if (actionId == IME_ACTION_DONE) {
|
||||||
@@ -386,48 +417,9 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
||||||
} else {
|
} else {
|
||||||
// Init Biometric elements
|
// Init Biometric elements
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
advancedUnlockFragment?.loadDatabase(databaseFileUri,
|
||||||
if (PreferencesUtil.isAdvancedUnlockEnable(this)) {
|
mAllowAutoOpenBiometricPrompt
|
||||||
if (advancedUnlockedManager == null
|
&& mProgressDatabaseTaskProvider?.isBinded() != true)
|
||||||
&& databaseFileUri != null) {
|
|
||||||
advancedUnlockedManager = AdvancedUnlockedManager(this,
|
|
||||||
databaseFileUri,
|
|
||||||
advancedUnlockInfoView,
|
|
||||||
checkboxPasswordView,
|
|
||||||
enableButtonOnCheckedChangeListener,
|
|
||||||
passwordView,
|
|
||||||
{ passwordEncrypted, ivSpec ->
|
|
||||||
// Load the database if password is registered with biometric
|
|
||||||
if (passwordEncrypted != null && ivSpec != null) {
|
|
||||||
verifyCheckboxesAndLoadDatabase(
|
|
||||||
CipherDatabaseEntity(
|
|
||||||
databaseFileUri.toString(),
|
|
||||||
passwordEncrypted,
|
|
||||||
ivSpec)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ passwordDecrypted ->
|
|
||||||
// Load the database if password is retrieve from biometric
|
|
||||||
passwordDecrypted?.let {
|
|
||||||
// Retrieve from biometric
|
|
||||||
verifyKeyFileCheckboxAndLoadDatabase(it)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
advancedUnlockedManager?.isBiometricPromptAutoOpenEnable =
|
|
||||||
mAllowAutoOpenBiometricPrompt && mProgressDatabaseTaskProvider?.isBinded() != true
|
|
||||||
advancedUnlockedManager?.checkBiometricAvailability()
|
|
||||||
} else {
|
|
||||||
advancedUnlockInfoView?.visibility = View.GONE
|
|
||||||
advancedUnlockedManager?.destroy()
|
|
||||||
advancedUnlockedManager = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (advancedUnlockedManager == null) {
|
|
||||||
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
|
||||||
}
|
|
||||||
checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enableOrNotTheConfirmationButton()
|
enableOrNotTheConfirmationButton()
|
||||||
@@ -479,11 +471,6 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
advancedUnlockedManager?.destroy()
|
|
||||||
advancedUnlockedManager = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reinit locking activity UI variable
|
// Reinit locking activity UI variable
|
||||||
LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
||||||
mAllowAutoOpenBiometricPrompt = true
|
mAllowAutoOpenBiometricPrompt = true
|
||||||
@@ -592,11 +579,6 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
MenuUtil.defaultMenuInflater(inflater, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
// biometric menu
|
|
||||||
advancedUnlockedManager?.inflateOptionsMenu(inflater, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
launchEducation(menu)
|
launchEducation(menu)
|
||||||
@@ -606,13 +588,13 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
|
|
||||||
// Check permission
|
// Check permission
|
||||||
private fun checkPermission() {
|
private fun checkPermission() {
|
||||||
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
if (Build.VERSION.SDK_INT in 23..28
|
||||||
val permissions = arrayOf(writePermission)
|
|
||||||
if (Build.VERSION.SDK_INT >= 23
|
|
||||||
&& !readOnly
|
&& !readOnly
|
||||||
&& !mPermissionAsked) {
|
&& !mPermissionAsked) {
|
||||||
mPermissionAsked = true
|
mPermissionAsked = true
|
||||||
// Check self permission to show or not the dialog
|
// Check self permission to show or not the dialog
|
||||||
|
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
val permissions = arrayOf(writePermission)
|
||||||
if (toolbar != null
|
if (toolbar != null
|
||||||
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
|
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
|
||||||
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
|
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
|
||||||
@@ -672,21 +654,14 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
performedNextEducation(passwordActivityEducation, menu)
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
advancedUnlockFragment?.performEducation(passwordActivityEducation,
|
||||||
&& !readOnlyEducationPerformed) {
|
readOnlyEducationPerformed,
|
||||||
val biometricCanAuthenticate = BiometricUnlockDatabaseHelper.canAuthenticate(this)
|
{
|
||||||
PreferencesUtil.isAdvancedUnlockEnable(applicationContext)
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
},
|
||||||
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.visibility == View.VISIBLE
|
{
|
||||||
&& advancedUnlockInfoView?.unlockIconImageView != null
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
&& passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!,
|
})
|
||||||
{
|
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -708,10 +683,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
readOnly = !readOnly
|
readOnly = !readOnly
|
||||||
changeOpenFileReadIcon(item)
|
changeOpenFileReadIcon(item)
|
||||||
}
|
}
|
||||||
R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||||
advancedUnlockedManager?.deleteEncryptedDatabaseKey()
|
|
||||||
}
|
|
||||||
else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
@@ -725,6 +697,9 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
|
|
||||||
mAllowAutoOpenBiometricPrompt = false
|
mAllowAutoOpenBiometricPrompt = false
|
||||||
|
|
||||||
|
// To get device credential unlock result
|
||||||
|
advancedUnlockFragment?.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
// To get entry in result
|
// To get entry in result
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
@@ -745,7 +720,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
when (resultCode) {
|
when (resultCode) {
|
||||||
LockingActivity.RESULT_EXIT_LOCK -> {
|
LockingActivity.RESULT_EXIT_LOCK -> {
|
||||||
clearCredentialsViews()
|
clearCredentialsViews()
|
||||||
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
|
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
|
||||||
}
|
}
|
||||||
Activity.RESULT_CANCELED -> {
|
Activity.RESULT_CANCELED -> {
|
||||||
clearCredentialsViews()
|
clearCredentialsViews()
|
||||||
@@ -758,6 +733,8 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
|
|
||||||
private val TAG = PasswordActivity::class.java.name
|
private val TAG = PasswordActivity::class.java.name
|
||||||
|
|
||||||
|
private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG"
|
||||||
|
|
||||||
private const val KEY_FILENAME = "fileName"
|
private const val KEY_FILENAME = "fileName"
|
||||||
private const val KEY_KEYFILE = "keyFile"
|
private const val KEY_KEYFILE = "keyFile"
|
||||||
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
||||||
@@ -861,13 +838,13 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
fun launchForAutofillResult(activity: Activity,
|
fun launchForAutofillResult(activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
assistStructure: AssistStructure,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo?) {
|
searchInfo: SearchInfo?) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
AutofillHelper.startActivityForAutofillResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
assistStructure,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -925,11 +902,11 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
searchInfo)
|
searchInfo)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ searchInfo, assistStructure -> // Autofill Selection Action
|
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
PasswordActivity.launchForAutofillResult(activity,
|
PasswordActivity.launchForAutofillResult(activity,
|
||||||
databaseUri, keyFile,
|
databaseUri, keyFile,
|
||||||
assistStructure,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* 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.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseChangedDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
var actionDatabaseListener: ActionDatabaseChangedListener? = null
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
actionDatabaseListener = null
|
||||||
|
this.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
|
||||||
|
val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelable(OLD_FILE_DATABASE_INFO)
|
||||||
|
val newSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelable(NEW_FILE_DATABASE_INFO)
|
||||||
|
|
||||||
|
if (oldSnapFileDatabaseInfo != null && newSnapFileDatabaseInfo != null) {
|
||||||
|
// Use the Builder class for convenient dialog construction
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
|
val stringBuilder = SpannableStringBuilder()
|
||||||
|
if (newSnapFileDatabaseInfo.exists) {
|
||||||
|
stringBuilder.append(getString(R.string.warning_database_info_changed))
|
||||||
|
stringBuilder.append("\n\n" +oldSnapFileDatabaseInfo.toString(activity)
|
||||||
|
+ "\n→\n" +
|
||||||
|
newSnapFileDatabaseInfo.toString(activity) + "\n\n")
|
||||||
|
stringBuilder.append(getString(R.string.warning_database_info_changed_options))
|
||||||
|
} else {
|
||||||
|
stringBuilder.append(getString(R.string.warning_database_revoked))
|
||||||
|
}
|
||||||
|
builder.setMessage(stringBuilder)
|
||||||
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
actionDatabaseListener?.validateDatabaseChanged()
|
||||||
|
}
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionDatabaseChangedListener {
|
||||||
|
fun validateDatabaseChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val DATABASE_CHANGED_DIALOG_TAG = "databaseChangedDialogFragment"
|
||||||
|
private const val OLD_FILE_DATABASE_INFO = "OLD_FILE_DATABASE_INFO"
|
||||||
|
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
|
||||||
|
|
||||||
|
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
newSnapFileDatabaseInfo: SnapFileDatabaseInfo)
|
||||||
|
: DatabaseChangedDialogFragment {
|
||||||
|
val fragment = DatabaseChangedDialogFragment()
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putParcelable(OLD_FILE_DATABASE_INFO, oldSnapFileDatabaseInfo)
|
||||||
|
putParcelable(NEW_FILE_DATABASE_INFO, newSnapFileDatabaseInfo)
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,10 +29,7 @@ import android.view.MotionEvent
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.AdapterView
|
import android.widget.*
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.Spinner
|
|
||||||
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
|
||||||
@@ -57,6 +54,7 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
private var mOtpElement: OtpElement = OtpElement()
|
private var mOtpElement: OtpElement = OtpElement()
|
||||||
|
|
||||||
|
private var otpTypeMessage: TextView? = null
|
||||||
private var otpTypeSpinner: Spinner? = null
|
private var otpTypeSpinner: Spinner? = null
|
||||||
private var otpTokenTypeSpinner: Spinner? = null
|
private var otpTokenTypeSpinner: Spinner? = null
|
||||||
private var otpSecretContainer: TextInputLayout? = null
|
private var otpSecretContainer: TextInputLayout? = null
|
||||||
@@ -74,6 +72,8 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
private var totpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
private var totpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
||||||
private var hotpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
private var hotpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
||||||
private var otpAlgorithmAdapter: ArrayAdapter<TokenCalculator.HashAlgorithm>? = null
|
private var otpAlgorithmAdapter: ArrayAdapter<TokenCalculator.HashAlgorithm>? = null
|
||||||
|
private var mHotpTokenTypeArray: Array<OtpTokenType>? = null
|
||||||
|
private var mTotpTokenTypeArray: Array<OtpTokenType>? = null
|
||||||
|
|
||||||
private var mManualEvent = false
|
private var mManualEvent = false
|
||||||
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
|
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
|
||||||
@@ -134,6 +134,7 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup?
|
val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup?
|
||||||
|
otpTypeMessage = root?.findViewById(R.id.setup_otp_type_message)
|
||||||
otpTypeSpinner = root?.findViewById(R.id.setup_otp_type)
|
otpTypeSpinner = root?.findViewById(R.id.setup_otp_type)
|
||||||
otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type)
|
otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type)
|
||||||
otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label)
|
otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label)
|
||||||
@@ -183,23 +184,23 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
// HOTP / TOTP Type selection
|
// HOTP / TOTP Type selection
|
||||||
val otpTypeArray = OtpType.values()
|
val otpTypeArray = OtpType.values()
|
||||||
otpTypeAdapter = ArrayAdapter<OtpType>(activity,
|
otpTypeAdapter = ArrayAdapter(activity,
|
||||||
android.R.layout.simple_spinner_item, otpTypeArray).apply {
|
android.R.layout.simple_spinner_item, otpTypeArray).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
otpTypeSpinner?.adapter = otpTypeAdapter
|
otpTypeSpinner?.adapter = otpTypeAdapter
|
||||||
|
|
||||||
// Otp Token type selection
|
// Otp Token type selection
|
||||||
val hotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues()
|
mHotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues()
|
||||||
hotpTokenTypeAdapter = ArrayAdapter(activity,
|
hotpTokenTypeAdapter = ArrayAdapter(activity,
|
||||||
android.R.layout.simple_spinner_item, hotpTokenTypeArray).apply {
|
android.R.layout.simple_spinner_item, mHotpTokenTypeArray!!).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
// Proprietary only on closed and full version
|
// Proprietary only on closed and full version
|
||||||
val totpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
||||||
BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION)
|
BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION)
|
||||||
totpTokenTypeAdapter = ArrayAdapter(activity,
|
totpTokenTypeAdapter = ArrayAdapter(activity,
|
||||||
android.R.layout.simple_spinner_item, totpTokenTypeArray).apply {
|
android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
otpTokenTypeAdapter = hotpTokenTypeAdapter
|
otpTokenTypeAdapter = hotpTokenTypeAdapter
|
||||||
@@ -207,7 +208,7 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
// OTP Algorithm
|
// OTP Algorithm
|
||||||
val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values()
|
val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values()
|
||||||
otpAlgorithmAdapter = ArrayAdapter<TokenCalculator.HashAlgorithm>(activity,
|
otpAlgorithmAdapter = ArrayAdapter(activity,
|
||||||
android.R.layout.simple_spinner_item, otpAlgorithmArray).apply {
|
android.R.layout.simple_spinner_item, otpAlgorithmArray).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
@@ -372,24 +373,40 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun upgradeTokenType() {
|
private fun upgradeTokenType() {
|
||||||
|
val tokenType = mOtpElement.tokenType
|
||||||
when (mOtpElement.type) {
|
when (mOtpElement.type) {
|
||||||
OtpType.HOTP -> {
|
OtpType.HOTP -> {
|
||||||
otpPeriodContainer?.visibility = View.GONE
|
otpPeriodContainer?.visibility = View.GONE
|
||||||
otpCounterContainer?.visibility = View.VISIBLE
|
otpCounterContainer?.visibility = View.VISIBLE
|
||||||
otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter
|
otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter
|
||||||
otpTokenTypeSpinner?.setSelection(OtpTokenType
|
mHotpTokenTypeArray?.let { otpTokenTypeArray ->
|
||||||
.getHotpTokenTypeValues().indexOf(mOtpElement.tokenType))
|
defineOtpTokenTypeSpinner(otpTokenTypeArray, tokenType, OtpTokenType.RFC4226)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
OtpType.TOTP -> {
|
OtpType.TOTP -> {
|
||||||
otpPeriodContainer?.visibility = View.VISIBLE
|
otpPeriodContainer?.visibility = View.VISIBLE
|
||||||
otpCounterContainer?.visibility = View.GONE
|
otpCounterContainer?.visibility = View.GONE
|
||||||
otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter
|
otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter
|
||||||
otpTokenTypeSpinner?.setSelection(OtpTokenType
|
mTotpTokenTypeArray?.let { otpTokenTypeArray ->
|
||||||
.getTotpTokenTypeValues().indexOf(mOtpElement.tokenType))
|
defineOtpTokenTypeSpinner(otpTokenTypeArray, tokenType, OtpTokenType.RFC6238)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun defineOtpTokenTypeSpinner(otpTokenTypeArray: Array<OtpTokenType>,
|
||||||
|
tokenType: OtpTokenType,
|
||||||
|
defaultTokenType: OtpTokenType) {
|
||||||
|
val formTokenType = if (otpTokenTypeArray.contains(tokenType)) {
|
||||||
|
otpTypeMessage?.visibility = View.GONE
|
||||||
|
tokenType
|
||||||
|
} else {
|
||||||
|
otpTypeMessage?.visibility = View.VISIBLE
|
||||||
|
defaultTokenType
|
||||||
|
}
|
||||||
|
otpTokenTypeSpinner?.setSelection(otpTokenTypeArray.indexOf(formTokenType))
|
||||||
|
}
|
||||||
|
|
||||||
private fun upgradeParameters() {
|
private fun upgradeParameters() {
|
||||||
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
|
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
|
||||||
.indexOf(mOtpElement.algorithm))
|
.indexOf(mOtpElement.algorithm))
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.helpers
|
package com.kunzisoft.keepass.activities.helpers
|
||||||
|
|
||||||
import android.app.assist.AssistStructure
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
@@ -106,7 +106,7 @@ object EntrySelectionHelper {
|
|||||||
|
|
||||||
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
if (AutofillHelper.retrieveAssistStructure(intent) != null)
|
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
|
||||||
return SpecialMode.SELECTION
|
return SpecialMode.SELECTION
|
||||||
}
|
}
|
||||||
return intent.getSerializableExtra(KEY_SPECIAL_MODE) as SpecialMode?
|
return intent.getSerializableExtra(KEY_SPECIAL_MODE) as SpecialMode?
|
||||||
@@ -119,7 +119,7 @@ object EntrySelectionHelper {
|
|||||||
|
|
||||||
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
|
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
if (AutofillHelper.retrieveAssistStructure(intent) != null)
|
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
|
||||||
return TypeMode.AUTOFILL
|
return TypeMode.AUTOFILL
|
||||||
}
|
}
|
||||||
return intent.getSerializableExtra(KEY_TYPE_MODE) as TypeMode? ?: TypeMode.DEFAULT
|
return intent.getSerializableExtra(KEY_TYPE_MODE) as TypeMode? ?: TypeMode.DEFAULT
|
||||||
@@ -136,7 +136,7 @@ object EntrySelectionHelper {
|
|||||||
saveAction: (searchInfo: SearchInfo) -> Unit,
|
saveAction: (searchInfo: SearchInfo) -> Unit,
|
||||||
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
||||||
autofillSelectionAction: (searchInfo: SearchInfo?,
|
autofillSelectionAction: (searchInfo: SearchInfo?,
|
||||||
assistStructure: AssistStructure) -> Unit,
|
autofillComponent: AutofillComponent) -> Unit,
|
||||||
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
|
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
|
||||||
|
|
||||||
when (retrieveSpecialModeFromIntent(intent)) {
|
when (retrieveSpecialModeFromIntent(intent)) {
|
||||||
@@ -167,14 +167,14 @@ object EntrySelectionHelper {
|
|||||||
}
|
}
|
||||||
SpecialMode.SELECTION -> {
|
SpecialMode.SELECTION -> {
|
||||||
val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent)
|
val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent)
|
||||||
var assistStructureInit = false
|
var autofillComponentInit = false
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
AutofillHelper.retrieveAssistStructure(intent)?.let { assistStructure ->
|
AutofillHelper.retrieveAutofillComponent(intent)?.let { autofillComponent ->
|
||||||
autofillSelectionAction.invoke(searchInfo, assistStructure)
|
autofillSelectionAction.invoke(searchInfo, autofillComponent)
|
||||||
assistStructureInit = true
|
autofillComponentInit = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!assistStructureInit) {
|
if (!autofillComponentInit) {
|
||||||
if (intent.getSerializableExtra(KEY_SPECIAL_MODE) != null) {
|
if (intent.getSerializableExtra(KEY_SPECIAL_MODE) != null) {
|
||||||
when (retrieveTypeModeFromIntent(intent)) {
|
when (retrieveTypeModeFromIntent(intent)) {
|
||||||
TypeMode.DEFAULT -> {
|
TypeMode.DEFAULT -> {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
package com.kunzisoft.keepass.activities.lock
|
package com.kunzisoft.keepass.activities.lock
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
@@ -59,6 +60,9 @@ abstract class LockingActivity : SpecialModeActivity() {
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
|
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
if (savedInstanceState != null
|
if (savedInstanceState != null
|
||||||
@@ -83,8 +87,6 @@ abstract class LockingActivity : SpecialModeActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mExitLock = false
|
mExitLock = false
|
||||||
|
|
||||||
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
@@ -163,35 +165,6 @@ abstract class LockingActivity : SpecialModeActivity() {
|
|||||||
sendBroadcast(Intent(LOCK_ACTION))
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* To reset the app timeout when a view is focused or changed
|
|
||||||
*/
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View?) {
|
|
||||||
views.forEach {
|
|
||||||
it?.setOnTouchListener { _, event ->
|
|
||||||
when (event.action) {
|
|
||||||
MotionEvent.ACTION_DOWN -> {
|
|
||||||
// Log.d(TAG, "View touched, try to reset app timeout")
|
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
it?.setOnFocusChangeListener { _, hasFocus ->
|
|
||||||
if (hasFocus) {
|
|
||||||
// Log.d(TAG, "View focused, try to reset app timeout")
|
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (it is ViewGroup) {
|
|
||||||
for (i in 0..it.childCount) {
|
|
||||||
resetAppTimeoutWhenViewFocusedOrChanged(it.getChildAt(i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if (mTimeoutEnable) {
|
if (mTimeoutEnable) {
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
|
||||||
@@ -204,7 +177,7 @@ abstract class LockingActivity : SpecialModeActivity() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val TAG = "LockingActivity"
|
const val TAG = "LockingActivity"
|
||||||
|
|
||||||
const val RESULT_EXIT_LOCK = 1450
|
const val RESULT_EXIT_LOCK = 1450
|
||||||
|
|
||||||
@@ -215,3 +188,28 @@ abstract class LockingActivity : SpecialModeActivity() {
|
|||||||
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
|
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To reset the app timeout when a view is focused or changed
|
||||||
|
*/
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
fun View.resetAppTimeoutWhenViewFocusedOrChanged(context: Context) {
|
||||||
|
setOnTouchListener { _, event ->
|
||||||
|
when (event.action) {
|
||||||
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
//Log.d(LockingActivity.TAG, "View touched, try to reset app timeout")
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
setOnFocusChangeListener { _, _ ->
|
||||||
|
//Log.d(LockingActivity.TAG, "View focused, try to reset app timeout")
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
|
||||||
|
}
|
||||||
|
if (this is ViewGroup) {
|
||||||
|
for (i in 0..childCount) {
|
||||||
|
getChildAt(i)?.resetAppTimeoutWhenViewFocusedOrChanged(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import com.kunzisoft.keepass.view.SpecialModeView
|
|||||||
abstract class SpecialModeActivity : StylishActivity() {
|
abstract class SpecialModeActivity : StylishActivity() {
|
||||||
|
|
||||||
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
||||||
protected var mTypeMode: TypeMode = TypeMode.DEFAULT
|
private var mTypeMode: TypeMode = TypeMode.DEFAULT
|
||||||
|
|
||||||
private var mSpecialModeView: SpecialModeView? = null
|
private var mSpecialModeView: SpecialModeView? = null
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class App : MultiDexApplication() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onTerminate() {
|
override fun onTerminate() {
|
||||||
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
|
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
|
||||||
super.onTerminate()
|
super.onTerminate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
|||||||
UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri),
|
UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri),
|
||||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""),
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""),
|
||||||
fileDatabaseInfo.exists,
|
fileDatabaseInfo.exists,
|
||||||
fileDatabaseInfo.getModificationString(),
|
fileDatabaseInfo.getLastModificationString(),
|
||||||
fileDatabaseInfo.getSizeString()
|
fileDatabaseInfo.getSizeString()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -90,7 +90,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
|||||||
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
|
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
|
||||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
||||||
fileDatabaseInfo.exists,
|
fileDatabaseInfo.exists,
|
||||||
fileDatabaseInfo.getModificationString(),
|
fileDatabaseInfo.getLastModificationString(),
|
||||||
fileDatabaseInfo.getSizeString()
|
fileDatabaseInfo.getSizeString()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -152,7 +152,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
|||||||
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
||||||
fileDatabaseInfo.exists,
|
fileDatabaseInfo.exists,
|
||||||
fileDatabaseInfo.getModificationString(),
|
fileDatabaseInfo.getLastModificationString(),
|
||||||
fileDatabaseInfo.getSizeString()
|
fileDatabaseInfo.getSizeString()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,12 @@ class IOActionTask<T>(
|
|||||||
mainScope.launch {
|
mainScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val asyncResult: Deferred<T?> = async {
|
val asyncResult: Deferred<T?> = async {
|
||||||
action.invoke()
|
try {
|
||||||
|
action.invoke()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
afterActionDatabaseListener?.invoke(asyncResult.await())
|
afterActionDatabaseListener?.invoke(asyncResult.await())
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.kunzisoft.keepass.autofill
|
||||||
|
|
||||||
|
import android.app.assist.AssistStructure
|
||||||
|
import android.view.inputmethod.InlineSuggestionsRequest
|
||||||
|
|
||||||
|
data class AutofillComponent(val assistStructure: AssistStructure,
|
||||||
|
val inlineSuggestionsRequest: InlineSuggestionsRequest?)
|
||||||
@@ -19,18 +19,27 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.autofill
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.BlendMode
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.service.autofill.Dataset
|
import android.service.autofill.Dataset
|
||||||
import android.service.autofill.FillResponse
|
import android.service.autofill.FillResponse
|
||||||
|
import android.service.autofill.InlinePresentation
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.autofill.AutofillManager
|
import android.view.autofill.AutofillManager
|
||||||
import android.view.autofill.AutofillValue
|
import android.view.autofill.AutofillValue
|
||||||
|
import android.view.inputmethod.InlineSuggestionsRequest
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.autofill.inline.UiVersions
|
||||||
|
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
@@ -38,8 +47,11 @@ import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
|
import com.kunzisoft.keepass.icons.createIconFromDatabaseIcon
|
||||||
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.settings.AutofillSettingsActivity
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
@@ -47,11 +59,17 @@ object AutofillHelper {
|
|||||||
|
|
||||||
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
|
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
|
||||||
|
|
||||||
private const val ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
||||||
|
const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
||||||
|
|
||||||
fun retrieveAssistStructure(intent: Intent?): AssistStructure? {
|
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
|
||||||
intent?.let {
|
intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
||||||
return it.getParcelableExtra(ASSIST_STRUCTURE)
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
AutofillComponent(assistStructure,
|
||||||
|
intent.getParcelableExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST))
|
||||||
|
} else {
|
||||||
|
AutofillComponent(assistStructure, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -68,26 +86,10 @@ object AutofillHelper {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun addHeader(responseBuilder: FillResponse.Builder,
|
private fun buildDataset(context: Context,
|
||||||
packageName: String,
|
|
||||||
webDomain: String?,
|
|
||||||
applicationId: String?) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
if (webDomain != null) {
|
|
||||||
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_web_domain).apply {
|
|
||||||
setTextViewText(R.id.autofill_web_domain_text, webDomain)
|
|
||||||
})
|
|
||||||
} else if (applicationId != null) {
|
|
||||||
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_app_id).apply {
|
|
||||||
setTextViewText(R.id.autofill_app_id_text, applicationId)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun buildDataset(context: Context,
|
|
||||||
entryInfo: EntryInfo,
|
entryInfo: EntryInfo,
|
||||||
struct: StructureParser.Result): Dataset? {
|
struct: StructureParser.Result,
|
||||||
|
inlinePresentation: InlinePresentation?): Dataset? {
|
||||||
val title = makeEntryTitle(entryInfo)
|
val title = makeEntryTitle(entryInfo)
|
||||||
val views = newRemoteViews(context, title, entryInfo.icon)
|
val views = newRemoteViews(context, title, entryInfo.icon)
|
||||||
val builder = Dataset.Builder(views)
|
val builder = Dataset.Builder(views)
|
||||||
@@ -100,6 +102,12 @@ object AutofillHelper {
|
|||||||
builder.setValue(password, AutofillValue.forText(entryInfo.password))
|
builder.setValue(password, AutofillValue.forText(entryInfo.password))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
inlinePresentation?.let {
|
||||||
|
builder.setInlinePresentation(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
builder.build()
|
builder.build()
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
@@ -108,44 +116,120 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
private fun buildInlinePresentationForEntry(context: Context,
|
||||||
|
inlineSuggestionsRequest: InlineSuggestionsRequest,
|
||||||
|
positionItem: Int,
|
||||||
|
entryInfo: EntryInfo): InlinePresentation? {
|
||||||
|
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||||
|
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||||
|
|
||||||
|
if (positionItem <= maxSuggestion-1
|
||||||
|
&& inlinePresentationSpecs.size > positionItem) {
|
||||||
|
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
|
||||||
|
|
||||||
|
// Make sure that the IME spec claims support for v1 UI template.
|
||||||
|
val imeStyle = inlinePresentationSpec.style
|
||||||
|
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
|
||||||
|
return null
|
||||||
|
|
||||||
|
// Build the content for IME UI
|
||||||
|
val pendingIntent = PendingIntent.getActivity(context,
|
||||||
|
0,
|
||||||
|
Intent(context, AutofillSettingsActivity::class.java),
|
||||||
|
0)
|
||||||
|
return InlinePresentation(
|
||||||
|
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
|
||||||
|
setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
|
||||||
|
setTitle(entryInfo.title)
|
||||||
|
setSubtitle(entryInfo.username)
|
||||||
|
setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
||||||
|
setTintBlendMode(BlendMode.DST)
|
||||||
|
})
|
||||||
|
buildIconFromEntry(context, entryInfo)?.let { icon ->
|
||||||
|
setEndIcon(icon.apply {
|
||||||
|
setTintBlendMode(BlendMode.DST)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}.build().slice, inlinePresentationSpec, false)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildResponse(context: Context,
|
||||||
|
entriesInfo: List<EntryInfo>,
|
||||||
|
parseResult: StructureParser.Result,
|
||||||
|
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse {
|
||||||
|
val responseBuilder = FillResponse.Builder()
|
||||||
|
// Add Header
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
val packageName = context.packageName
|
||||||
|
parseResult.webDomain?.let { webDomain ->
|
||||||
|
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_web_domain).apply {
|
||||||
|
setTextViewText(R.id.autofill_web_domain_text, webDomain)
|
||||||
|
})
|
||||||
|
} ?: kotlin.run {
|
||||||
|
parseResult.applicationId?.let { applicationId ->
|
||||||
|
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_app_id).apply {
|
||||||
|
setTextViewText(R.id.autofill_app_id_text, applicationId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add inline suggestion for new IME and dataset
|
||||||
|
entriesInfo.forEachIndexed { index, entryInfo ->
|
||||||
|
val inlinePresentation = inlineSuggestionsRequest?.let {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
buildInlinePresentationForEntry(context, inlineSuggestionsRequest, index, entryInfo)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responseBuilder.addDataset(buildDataset(context, entryInfo, parseResult, inlinePresentation))
|
||||||
|
}
|
||||||
|
return responseBuilder.build()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the Autofill response for one entry
|
* Build the Autofill response for one entry
|
||||||
*/
|
*/
|
||||||
fun buildResponse(activity: Activity, entryInfo: EntryInfo) {
|
fun buildResponseAndSetResult(activity: Activity, entryInfo: EntryInfo) {
|
||||||
buildResponse(activity, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
buildResponseAndSetResult(activity, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the Autofill response for many entry
|
* Build the Autofill response for many entry
|
||||||
*/
|
*/
|
||||||
fun buildResponse(activity: Activity, entriesInfo: List<EntryInfo>) {
|
fun buildResponseAndSetResult(activity: Activity, entriesInfo: List<EntryInfo>) {
|
||||||
if (entriesInfo.isEmpty()) {
|
if (entriesInfo.isEmpty()) {
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
activity.setResult(Activity.RESULT_CANCELED)
|
||||||
} else {
|
} else {
|
||||||
var setResultOk = false
|
var setResultOk = false
|
||||||
activity.intent?.extras?.let { extras ->
|
activity.intent?.getParcelableExtra<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
|
||||||
if (extras.containsKey(ASSIST_STRUCTURE)) {
|
StructureParser(structure).parse()?.let { result ->
|
||||||
activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure ->
|
// New Response
|
||||||
StructureParser(structure).parse()?.let { result ->
|
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
// New Response
|
val inlineSuggestionsRequest = activity.intent?.getParcelableExtra<InlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
||||||
val responseBuilder = FillResponse.Builder()
|
if (inlineSuggestionsRequest != null) {
|
||||||
entriesInfo.forEach {
|
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
||||||
responseBuilder.addDataset(buildDataset(activity, it, result))
|
|
||||||
}
|
|
||||||
val mReplyIntent = Intent()
|
|
||||||
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
|
||||||
mReplyIntent.putExtra(
|
|
||||||
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
|
||||||
responseBuilder.build())
|
|
||||||
setResultOk = true
|
|
||||||
activity.setResult(Activity.RESULT_OK, mReplyIntent)
|
|
||||||
}
|
}
|
||||||
|
buildResponse(activity, entriesInfo, result, inlineSuggestionsRequest)
|
||||||
|
} else {
|
||||||
|
buildResponse(activity, entriesInfo, result, null)
|
||||||
}
|
}
|
||||||
|
val mReplyIntent = Intent()
|
||||||
|
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
||||||
|
mReplyIntent.putExtra(
|
||||||
|
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
||||||
|
response)
|
||||||
|
setResultOk = true
|
||||||
|
activity.setResult(Activity.RESULT_OK, mReplyIntent)
|
||||||
}
|
}
|
||||||
if (!setResultOk) {
|
}
|
||||||
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
if (!setResultOk) {
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
||||||
}
|
activity.setResult(Activity.RESULT_CANCELED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,10 +239,16 @@ object AutofillHelper {
|
|||||||
*/
|
*/
|
||||||
fun startActivityForAutofillResult(activity: Activity,
|
fun startActivityForAutofillResult(activity: Activity,
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
assistStructure: AssistStructure,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo?) {
|
searchInfo: SearchInfo?) {
|
||||||
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||||
intent.putExtra(ASSIST_STRUCTURE, assistStructure)
|
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
|
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
|
||||||
|
autofillComponent.inlineSuggestionsRequest?.let {
|
||||||
|
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
||||||
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
|
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
|
||||||
}
|
}
|
||||||
@@ -192,4 +282,11 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
return presentation
|
return presentation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? {
|
||||||
|
return createIconFromDatabaseIcon(context,
|
||||||
|
Database.getInstance().drawFactory,
|
||||||
|
entryInfo.icon,
|
||||||
|
ContextCompat.getColor(context, R.color.green))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,37 +19,50 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.autofill
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.BlendMode
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.CancellationSignal
|
import android.os.CancellationSignal
|
||||||
import android.service.autofill.*
|
import android.service.autofill.*
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.autofill.AutofillId
|
import android.view.autofill.AutofillId
|
||||||
|
import android.view.inputmethod.InlineSuggestionsRequest
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.autofill.inline.UiVersions
|
||||||
|
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class KeeAutofillService : AutofillService() {
|
class KeeAutofillService : AutofillService() {
|
||||||
|
|
||||||
var applicationIdBlocklist: Set<String>? = null
|
var applicationIdBlocklist: Set<String>? = null
|
||||||
var webDomainBlocklist: Set<String>? = null
|
var webDomainBlocklist: Set<String>? = null
|
||||||
var askToSaveData: Boolean = false
|
var askToSaveData: Boolean = false
|
||||||
|
var autofillInlineSuggestionsEnabled: Boolean = false
|
||||||
private var mLock = AtomicBoolean()
|
private var mLock = AtomicBoolean()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
getPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPreferences() {
|
||||||
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
|
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
|
||||||
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
|
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
|
||||||
askToSaveData = PreferencesUtil.askToSaveAutofillData(this) // TODO apply when changed
|
askToSaveData = PreferencesUtil.askToSaveAutofillData(this)
|
||||||
|
autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFillRequest(request: FillRequest,
|
override fun onFillRequest(request: FillRequest,
|
||||||
@@ -75,7 +88,16 @@ class KeeAutofillService : AutofillService() {
|
|||||||
}
|
}
|
||||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
|
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
|
||||||
searchInfo.webDomain = webDomainWithoutSubDomain
|
searchInfo.webDomain = webDomainWithoutSubDomain
|
||||||
launchSelection(searchInfo, parseResult, callback)
|
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
|
&& autofillInlineSuggestionsEnabled) {
|
||||||
|
request.inlineSuggestionsRequest
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
launchSelection(searchInfo,
|
||||||
|
parseResult,
|
||||||
|
inlineSuggestionsRequest,
|
||||||
|
callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,39 +106,40 @@ class KeeAutofillService : AutofillService() {
|
|||||||
|
|
||||||
private fun launchSelection(searchInfo: SearchInfo,
|
private fun launchSelection(searchInfo: SearchInfo,
|
||||||
parseResult: StructureParser.Result,
|
parseResult: StructureParser.Result,
|
||||||
|
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
||||||
callback: FillCallback) {
|
callback: FillCallback) {
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
Database.getInstance(),
|
Database.getInstance(),
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ items ->
|
{ items ->
|
||||||
val responseBuilder = FillResponse.Builder()
|
callback.onSuccess(
|
||||||
AutofillHelper.addHeader(responseBuilder, packageName,
|
AutofillHelper.buildResponse(this,
|
||||||
parseResult.webDomain, parseResult.applicationId)
|
items, parseResult, inlineSuggestionsRequest)
|
||||||
items.forEach {
|
)
|
||||||
responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult))
|
|
||||||
}
|
|
||||||
callback.onSuccess(responseBuilder.build())
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Show UI if no search result
|
// Show UI if no search result
|
||||||
showUIForEntrySelection(parseResult, searchInfo, callback)
|
showUIForEntrySelection(parseResult,
|
||||||
|
searchInfo, inlineSuggestionsRequest, callback)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Show UI if database not open
|
// Show UI if database not open
|
||||||
showUIForEntrySelection(parseResult, searchInfo, callback)
|
showUIForEntrySelection(parseResult,
|
||||||
|
searchInfo, inlineSuggestionsRequest, callback)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||||
searchInfo: SearchInfo,
|
searchInfo: SearchInfo,
|
||||||
|
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
||||||
callback: FillCallback) {
|
callback: FillCallback) {
|
||||||
parseResult.allAutofillIds().let { autofillIds ->
|
parseResult.allAutofillIds().let { autofillIds ->
|
||||||
if (autofillIds.isNotEmpty()) {
|
if (autofillIds.isNotEmpty()) {
|
||||||
// If the entire Autofill Response is authenticated, AuthActivity is used
|
// If the entire Autofill Response is authenticated, AuthActivity is used
|
||||||
// to generate Response.
|
// to generate Response.
|
||||||
val intentSender = AutofillLauncherActivity.getAuthIntentSenderForSelection(this,
|
val intentSender = AutofillLauncherActivity.getAuthIntentSenderForSelection(this,
|
||||||
searchInfo)
|
searchInfo, inlineSuggestionsRequest)
|
||||||
val responseBuilder = FillResponse.Builder()
|
val responseBuilder = FillResponse.Builder()
|
||||||
val remoteViewsUnlock: RemoteViews = if (!parseResult.webDomain.isNullOrEmpty()) {
|
val remoteViewsUnlock: RemoteViews = if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||||
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
|
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
|
||||||
@@ -149,7 +172,40 @@ class KeeAutofillService : AutofillService() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Build response
|
|
||||||
|
// Build inline presentation
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
|
&& autofillInlineSuggestionsEnabled) {
|
||||||
|
var inlinePresentation: InlinePresentation? = null
|
||||||
|
inlineSuggestionsRequest?.let {
|
||||||
|
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||||
|
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
||||||
|
&& inlinePresentationSpecs.size > 0) {
|
||||||
|
val inlinePresentationSpec = inlinePresentationSpecs[0]
|
||||||
|
|
||||||
|
// Make sure that the IME spec claims support for v1 UI template.
|
||||||
|
val imeStyle = inlinePresentationSpec.style
|
||||||
|
if (UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1)) {
|
||||||
|
// Build the content for IME UI
|
||||||
|
inlinePresentation = InlinePresentation(
|
||||||
|
InlineSuggestionUi.newContentBuilder(
|
||||||
|
PendingIntent.getActivity(this,
|
||||||
|
0,
|
||||||
|
Intent(this, AutofillSettingsActivity::class.java),
|
||||||
|
0)
|
||||||
|
).apply {
|
||||||
|
setContentDescription(getString(R.string.autofill_sign_in_prompt))
|
||||||
|
setTitle(getString(R.string.autofill_sign_in_prompt))
|
||||||
|
setStartIcon(Icon.createWithResource(this@KeeAutofillService, R.mipmap.ic_launcher_round).apply {
|
||||||
|
setTintBlendMode(BlendMode.DST)
|
||||||
|
})
|
||||||
|
}.build().slice, inlinePresentationSpec, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Build response
|
||||||
|
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
|
||||||
|
}
|
||||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
||||||
callback.onSuccess(responseBuilder.build())
|
callback.onSuccess(responseBuilder.build())
|
||||||
}
|
}
|
||||||
@@ -190,6 +246,7 @@ class KeeAutofillService : AutofillService() {
|
|||||||
|
|
||||||
override fun onConnected() {
|
override fun onConnected() {
|
||||||
Log.d(TAG, "onConnected")
|
Log.d(TAG, "onConnected")
|
||||||
|
getPreferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDisconnected() {
|
override fun onDisconnected() {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import java.util.*
|
|||||||
* Parse AssistStructure and guess username and password fields.
|
* Parse AssistStructure and guess username and password fields.
|
||||||
*/
|
*/
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
internal class StructureParser(private val structure: AssistStructure) {
|
class StructureParser(private val structure: AssistStructure) {
|
||||||
private var result: Result? = null
|
private var result: Result? = null
|
||||||
|
|
||||||
private var usernameNeeded = true
|
private var usernameNeeded = true
|
||||||
@@ -274,7 +274,7 @@ internal class StructureParser(private val structure: AssistStructure) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
internal class Result {
|
class Result {
|
||||||
var applicationId: String? = null
|
var applicationId: String? = null
|
||||||
|
|
||||||
var webDomain: String? = null
|
var webDomain: String? = null
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.kunzisoft.keepass.biometric
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
|
||||||
|
data class AdvancedUnlockCryptoPrompt(var cipher: Cipher,
|
||||||
|
@StringRes var promptTitleId: Int,
|
||||||
|
@StringRes var promptDescriptionId: Int? = null,
|
||||||
|
var isDeviceCredentialOperation: Boolean,
|
||||||
|
var isBiometricOperation: Boolean)
|
||||||
@@ -0,0 +1,628 @@
|
|||||||
|
/*
|
||||||
|
* 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.biometric
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.*
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.biometric.BiometricPrompt
|
||||||
|
import com.getkeepsafe.taptargetview.TapTargetView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
|
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||||
|
import com.kunzisoft.keepass.database.exception.IODatabaseException
|
||||||
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
|
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||||
|
|
||||||
|
class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedUnlockCallback {
|
||||||
|
|
||||||
|
private var mBuilderListener: BuilderListener? = null
|
||||||
|
|
||||||
|
private var mAdvancedUnlockEnabled = false
|
||||||
|
private var mAutoOpenPromptEnabled = false
|
||||||
|
|
||||||
|
private var advancedUnlockManager: AdvancedUnlockManager? = null
|
||||||
|
private var biometricMode: Mode = Mode.BIOMETRIC_UNAVAILABLE
|
||||||
|
private var mAdvancedUnlockInfoView: AdvancedUnlockInfoView? = null
|
||||||
|
|
||||||
|
var databaseFileUri: Uri? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage setting to auto open biometric prompt
|
||||||
|
*/
|
||||||
|
private var mAutoOpenPrompt: Boolean = false
|
||||||
|
get() {
|
||||||
|
return field && mAutoOpenPromptEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variable to check if the prompt can be open (if the right activity is currently shown)
|
||||||
|
// checkBiometricAvailability() allows open biometric prompt and onDestroy() removes the authorization
|
||||||
|
private var allowOpenBiometricPrompt = false
|
||||||
|
|
||||||
|
private lateinit var cipherDatabaseAction : CipherDatabaseAction
|
||||||
|
|
||||||
|
private var cipherDatabaseListener: CipherDatabaseAction.DatabaseListener? = null
|
||||||
|
|
||||||
|
// Only to fix multiple fingerprint menu #332
|
||||||
|
private var mAllowAdvancedUnlockMenu = false
|
||||||
|
private var mAddBiometricMenuInProgress = false
|
||||||
|
|
||||||
|
// Only keep connection when we request a device credential activity
|
||||||
|
private var keepConnection = false
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
|
||||||
|
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(context)
|
||||||
|
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(context)
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
mBuilderListener = context as BuilderListener
|
||||||
|
}
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + BuilderListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
retainInstance = true
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
|
||||||
|
cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
|
val rootView = inflater.cloneInContext(contextThemed)
|
||||||
|
.inflate(R.layout.fragment_advanced_unlock, container, false)
|
||||||
|
|
||||||
|
mAdvancedUnlockInfoView = rootView.findViewById(R.id.advanced_unlock_view)
|
||||||
|
|
||||||
|
return rootView
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ActivityResult(var requestCode: Int, var resultCode: Int, var data: Intent?)
|
||||||
|
private var activityResult: ActivityResult? = null
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
// To wait resume
|
||||||
|
if (keepConnection) {
|
||||||
|
activityResult = ActivityResult(requestCode, resultCode, data)
|
||||||
|
}
|
||||||
|
keepConnection = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(requireContext())
|
||||||
|
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext())
|
||||||
|
keepConnection = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
// biometric menu
|
||||||
|
if (mAllowAdvancedUnlockMenu)
|
||||||
|
inflater.inflate(R.menu.advanced_unlock, menu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
deleteEncryptedDatabaseKey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadDatabase(databaseUri: Uri?, autoOpenPrompt: Boolean) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
// To get device credential unlock result, only if same database uri
|
||||||
|
if (databaseUri != null
|
||||||
|
&& mAdvancedUnlockEnabled) {
|
||||||
|
activityResult?.let {
|
||||||
|
if (databaseUri == databaseFileUri) {
|
||||||
|
advancedUnlockManager?.onActivityResult(it.requestCode, it.resultCode)
|
||||||
|
} else {
|
||||||
|
disconnect()
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
this.mAutoOpenPrompt = autoOpenPrompt
|
||||||
|
connect(databaseUri)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disconnect()
|
||||||
|
}
|
||||||
|
activityResult = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check unlock availability and change the current mode depending of device's state
|
||||||
|
*/
|
||||||
|
fun checkUnlockAvailability() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
allowOpenBiometricPrompt = true
|
||||||
|
if (PreferencesUtil.isBiometricUnlockEnable(requireContext())) {
|
||||||
|
mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
|
||||||
|
|
||||||
|
// biometric not supported (by API level or hardware) so keep option hidden
|
||||||
|
// or manually disable
|
||||||
|
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext())
|
||||||
|
if (!PreferencesUtil.isAdvancedUnlockEnable(requireContext())
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
|
||||||
|
toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
|
||||||
|
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
|
||||||
|
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
|
||||||
|
} else {
|
||||||
|
// biometric is available but not configured, show icon but in disabled state with some information
|
||||||
|
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
||||||
|
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
||||||
|
} else {
|
||||||
|
selectMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(requireContext())) {
|
||||||
|
mAdvancedUnlockInfoView?.setIconResource(R.drawable.bolt)
|
||||||
|
if (AdvancedUnlockManager.isDeviceSecure(requireContext())) {
|
||||||
|
selectMode()
|
||||||
|
} else {
|
||||||
|
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun selectMode() {
|
||||||
|
// Check if fingerprint well init (be called the first time the fingerprint is configured
|
||||||
|
// and the activity still active)
|
||||||
|
if (advancedUnlockManager?.isKeyManagerInitialized != true) {
|
||||||
|
advancedUnlockManager = AdvancedUnlockManager { requireActivity() }
|
||||||
|
// callback for fingerprint findings
|
||||||
|
advancedUnlockManager?.advancedUnlockCallback = this
|
||||||
|
}
|
||||||
|
// Recheck to change the mode
|
||||||
|
if (advancedUnlockManager?.isKeyManagerInitialized != true) {
|
||||||
|
toggleMode(Mode.KEY_MANAGER_UNAVAILABLE)
|
||||||
|
} else {
|
||||||
|
if (mBuilderListener?.conditionToStoreCredential() == true) {
|
||||||
|
// listen for encryption
|
||||||
|
toggleMode(Mode.STORE_CREDENTIAL)
|
||||||
|
} else {
|
||||||
|
databaseFileUri?.let { databaseUri ->
|
||||||
|
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
|
||||||
|
// biometric available but no stored password found yet for this DB so show info don't listen
|
||||||
|
toggleMode(if (containsCipher) {
|
||||||
|
// listen for decryption
|
||||||
|
Mode.EXTRACT_CREDENTIAL
|
||||||
|
} else {
|
||||||
|
// wait for typing
|
||||||
|
Mode.WAIT_CREDENTIAL
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun toggleMode(newBiometricMode: Mode) {
|
||||||
|
if (newBiometricMode != biometricMode) {
|
||||||
|
biometricMode = newBiometricMode
|
||||||
|
initAdvancedUnlockMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun initNotAvailable() {
|
||||||
|
showViews(false)
|
||||||
|
|
||||||
|
mAdvancedUnlockInfoView?.setIconViewClickListener(false, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun openBiometricSetting() {
|
||||||
|
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||||
|
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
|
||||||
|
requireContext().startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun initSecurityUpdateRequired() {
|
||||||
|
showViews(true)
|
||||||
|
setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
|
||||||
|
|
||||||
|
openBiometricSetting()
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun initNotConfigured() {
|
||||||
|
showViews(true)
|
||||||
|
setAdvancedUnlockedTitleView(R.string.configure_biometric)
|
||||||
|
setAdvancedUnlockedMessageView("")
|
||||||
|
|
||||||
|
openBiometricSetting()
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun initKeyManagerNotAvailable() {
|
||||||
|
showViews(true)
|
||||||
|
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
|
||||||
|
|
||||||
|
openBiometricSetting()
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun initWaitData() {
|
||||||
|
showViews(true)
|
||||||
|
setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
|
||||||
|
setAdvancedUnlockedMessageView("")
|
||||||
|
|
||||||
|
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||||
|
onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
|
||||||
|
requireContext().getString(R.string.credential_before_click_advanced_unlock_button))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
if (allowOpenBiometricPrompt) {
|
||||||
|
if (cryptoPrompt.isDeviceCredentialOperation)
|
||||||
|
keepConnection = true
|
||||||
|
try {
|
||||||
|
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to open advanced unlock prompt", e)
|
||||||
|
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun initEncryptData() {
|
||||||
|
showViews(true)
|
||||||
|
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_store_credential)
|
||||||
|
setAdvancedUnlockedMessageView("")
|
||||||
|
|
||||||
|
advancedUnlockManager?.initEncryptData { cryptoPrompt ->
|
||||||
|
// Set listener to open the biometric dialog and save credential
|
||||||
|
mAdvancedUnlockInfoView?.setIconViewClickListener { _ ->
|
||||||
|
openAdvancedUnlockPrompt(cryptoPrompt)
|
||||||
|
}
|
||||||
|
} ?: throw Exception("AdvancedUnlockManager not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun initDecryptData() {
|
||||||
|
showViews(true)
|
||||||
|
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_unlock_database)
|
||||||
|
setAdvancedUnlockedMessageView("")
|
||||||
|
|
||||||
|
advancedUnlockManager?.let { unlockHelper ->
|
||||||
|
databaseFileUri?.let { databaseUri ->
|
||||||
|
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
|
||||||
|
cipherDatabase?.let {
|
||||||
|
unlockHelper.initDecryptData(it.specParameters) { cryptoPrompt ->
|
||||||
|
|
||||||
|
// Set listener to open the biometric dialog and check credential
|
||||||
|
mAdvancedUnlockInfoView?.setIconViewClickListener { _ ->
|
||||||
|
openAdvancedUnlockPrompt(cryptoPrompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto open the biometric prompt
|
||||||
|
if (mAutoOpenPrompt) {
|
||||||
|
mAutoOpenPrompt = false
|
||||||
|
openAdvancedUnlockPrompt(cryptoPrompt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: deleteEncryptedDatabaseKey()
|
||||||
|
}
|
||||||
|
} ?: throw IODatabaseException()
|
||||||
|
} ?: throw Exception("AdvancedUnlockManager not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun initAdvancedUnlockMode() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
mAllowAdvancedUnlockMenu = false
|
||||||
|
try {
|
||||||
|
when (biometricMode) {
|
||||||
|
Mode.BIOMETRIC_UNAVAILABLE -> initNotAvailable()
|
||||||
|
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired()
|
||||||
|
Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
|
||||||
|
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
|
||||||
|
Mode.WAIT_CREDENTIAL -> initWaitData()
|
||||||
|
Mode.STORE_CREDENTIAL -> initEncryptData()
|
||||||
|
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
onGenericException(e)
|
||||||
|
}
|
||||||
|
invalidateBiometricMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invalidateBiometricMenu() {
|
||||||
|
// Show fingerprint key deletion
|
||||||
|
if (!mAddBiometricMenuInProgress) {
|
||||||
|
mAddBiometricMenuInProgress = true
|
||||||
|
databaseFileUri?.let { databaseUri ->
|
||||||
|
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
|
||||||
|
mAllowAdvancedUnlockMenu = containsCipher
|
||||||
|
&& (biometricMode != Mode.BIOMETRIC_UNAVAILABLE
|
||||||
|
&& biometricMode != Mode.KEY_MANAGER_UNAVAILABLE)
|
||||||
|
mAddBiometricMenuInProgress = false
|
||||||
|
activity?.invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun connect(databaseUri: Uri) {
|
||||||
|
showViews(true)
|
||||||
|
this.databaseFileUri = databaseUri
|
||||||
|
cipherDatabaseListener = object: CipherDatabaseAction.DatabaseListener {
|
||||||
|
override fun onDatabaseCleared() {
|
||||||
|
deleteEncryptedDatabaseKey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cipherDatabaseAction.apply {
|
||||||
|
reloadPreferences()
|
||||||
|
cipherDatabaseListener?.let {
|
||||||
|
registerDatabaseListener(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkUnlockAvailability()
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun disconnect(hideViews: Boolean = true,
|
||||||
|
closePrompt: Boolean = true) {
|
||||||
|
this.databaseFileUri = null
|
||||||
|
// Close the biometric prompt
|
||||||
|
allowOpenBiometricPrompt = false
|
||||||
|
if (closePrompt)
|
||||||
|
advancedUnlockManager?.closeBiometricPrompt()
|
||||||
|
cipherDatabaseListener?.let {
|
||||||
|
cipherDatabaseAction.unregisterDatabaseListener(it)
|
||||||
|
}
|
||||||
|
biometricMode = Mode.BIOMETRIC_UNAVAILABLE
|
||||||
|
if (hideViews) {
|
||||||
|
showViews(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun deleteEncryptedDatabaseKey() {
|
||||||
|
allowOpenBiometricPrompt = false
|
||||||
|
mAdvancedUnlockInfoView?.setIconViewClickListener(false, null)
|
||||||
|
advancedUnlockManager?.closeBiometricPrompt()
|
||||||
|
databaseFileUri?.let { databaseUri ->
|
||||||
|
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
|
||||||
|
checkUnlockAvailability()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
|
||||||
|
setAdvancedUnlockedMessageView(errString.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
override fun onAuthenticationFailed() {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
|
||||||
|
setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
override fun onAuthenticationSucceeded() {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
when (biometricMode) {
|
||||||
|
Mode.BIOMETRIC_UNAVAILABLE -> {
|
||||||
|
}
|
||||||
|
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> {
|
||||||
|
}
|
||||||
|
Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> {
|
||||||
|
}
|
||||||
|
Mode.KEY_MANAGER_UNAVAILABLE -> {
|
||||||
|
}
|
||||||
|
Mode.WAIT_CREDENTIAL -> {
|
||||||
|
}
|
||||||
|
Mode.STORE_CREDENTIAL -> {
|
||||||
|
// newly store the entered password in encrypted way
|
||||||
|
mBuilderListener?.retrieveCredentialForEncryption()?.let { credential ->
|
||||||
|
advancedUnlockManager?.encryptData(credential)
|
||||||
|
}
|
||||||
|
AdvancedUnlockNotificationService.startServiceForTimeout(requireContext())
|
||||||
|
}
|
||||||
|
Mode.EXTRACT_CREDENTIAL -> {
|
||||||
|
// retrieve the encrypted value from preferences
|
||||||
|
databaseFileUri?.let { databaseUri ->
|
||||||
|
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
|
||||||
|
cipherDatabase?.encryptedValue?.let { value ->
|
||||||
|
advancedUnlockManager?.decryptData(value)
|
||||||
|
} ?: deleteEncryptedDatabaseKey()
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
onAuthenticationError(-1, getString(R.string.error_database_uri_null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
|
||||||
|
databaseFileUri?.let { databaseUri ->
|
||||||
|
mBuilderListener?.onCredentialEncrypted(databaseUri, encryptedValue, ivSpec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleDecryptedResult(decryptedValue: String) {
|
||||||
|
// Load database directly with password retrieve
|
||||||
|
databaseFileUri?.let {
|
||||||
|
mBuilderListener?.onCredentialDecrypted(it, decryptedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
override fun onInvalidKeyException(e: Exception) {
|
||||||
|
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGenericException(e: Exception) {
|
||||||
|
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
|
||||||
|
setAdvancedUnlockedMessageView(errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showViews(show: Boolean) {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
mAdvancedUnlockInfoView?.visibility = if (show)
|
||||||
|
View.VISIBLE
|
||||||
|
else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun setAdvancedUnlockedTitleView(textId: Int) {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
mAdvancedUnlockInfoView?.setTitle(textId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun setAdvancedUnlockedMessageView(textId: Int) {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
mAdvancedUnlockInfoView?.setMessage(textId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
mAdvancedUnlockInfoView?.message = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun performEducation(passwordActivityEducation: PasswordActivityEducation,
|
||||||
|
readOnlyEducationPerformed: Boolean,
|
||||||
|
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
||||||
|
onOuterViewClick: ((TapTargetView?) -> Unit)? = null) {
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||||
|
&& !readOnlyEducationPerformed) {
|
||||||
|
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext())
|
||||||
|
PreferencesUtil.isAdvancedUnlockEnable(requireContext())
|
||||||
|
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||||
|
&& mAdvancedUnlockInfoView != null && mAdvancedUnlockInfoView?.visibility == View.VISIBLE
|
||||||
|
&& mAdvancedUnlockInfoView?.unlockIconImageView != null
|
||||||
|
&& passwordActivityEducation.checkAndPerformedBiometricEducation(mAdvancedUnlockInfoView!!.unlockIconImageView!!,
|
||||||
|
onEducationViewClick,
|
||||||
|
onOuterViewClick)
|
||||||
|
}
|
||||||
|
} catch (ignored: Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Mode {
|
||||||
|
BIOMETRIC_UNAVAILABLE,
|
||||||
|
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
|
||||||
|
DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED,
|
||||||
|
KEY_MANAGER_UNAVAILABLE,
|
||||||
|
WAIT_CREDENTIAL,
|
||||||
|
STORE_CREDENTIAL,
|
||||||
|
EXTRACT_CREDENTIAL
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BuilderListener {
|
||||||
|
fun retrieveCredentialForEncryption(): String
|
||||||
|
fun conditionToStoreCredential(): Boolean
|
||||||
|
fun onCredentialEncrypted(databaseUri: Uri, encryptedCredential: String, ivSpec: String)
|
||||||
|
fun onCredentialDecrypted(databaseUri: Uri, decryptedCredential: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
if (!keepConnection) {
|
||||||
|
// If close prompt, bug "user not authenticated in Android R"
|
||||||
|
disconnect(false)
|
||||||
|
advancedUnlockManager = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
mAdvancedUnlockInfoView = null
|
||||||
|
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
disconnect()
|
||||||
|
advancedUnlockManager = null
|
||||||
|
mBuilderListener = null
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mBuilderListener = null
|
||||||
|
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val TAG = AdvancedUnlockFragment::class.java.name
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePassDX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.biometric
|
package com.kunzisoft.keepass.biometric
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.app.KeyguardManager
|
import android.app.KeyguardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -31,6 +32,7 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.biometric.BiometricManager.Authenticators.*
|
import androidx.biometric.BiometricManager.Authenticators.*
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
@@ -44,48 +46,74 @@ import javax.crypto.SecretKey
|
|||||||
import javax.crypto.spec.IvParameterSpec
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) {
|
||||||
|
|
||||||
private var biometricPrompt: BiometricPrompt? = null
|
|
||||||
|
|
||||||
private var keyStore: KeyStore? = null
|
private var keyStore: KeyStore? = null
|
||||||
private var keyGenerator: KeyGenerator? = null
|
private var keyGenerator: KeyGenerator? = null
|
||||||
private var cipher: Cipher? = null
|
private var cipher: Cipher? = null
|
||||||
private var keyguardManager: KeyguardManager? = null
|
|
||||||
private var cryptoObject: BiometricPrompt.CryptoObject? = null
|
private var biometricPrompt: BiometricPrompt? = null
|
||||||
|
private var authenticationCallback = object: BiometricPrompt.AuthenticationCallback() {
|
||||||
|
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||||
|
advancedUnlockCallback?.onAuthenticationSucceeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationFailed() {
|
||||||
|
advancedUnlockCallback?.onAuthenticationFailed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
|
advancedUnlockCallback?.onAuthenticationError(errorCode, errString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var advancedUnlockCallback: AdvancedUnlockCallback? = null
|
||||||
|
|
||||||
private var isKeyManagerInit = false
|
private var isKeyManagerInit = false
|
||||||
var authenticationCallback: BiometricPrompt.AuthenticationCallback? = null
|
|
||||||
var biometricUnlockCallback: BiometricUnlockCallback? = null
|
|
||||||
|
|
||||||
private val deviceCredentialUnlockEnable = PreferencesUtil.isDeviceCredentialUnlockEnable(context)
|
private val biometricUnlockEnable = PreferencesUtil.isBiometricUnlockEnable(retrieveContext())
|
||||||
|
private val deviceCredentialUnlockEnable = PreferencesUtil.isDeviceCredentialUnlockEnable(retrieveContext())
|
||||||
|
|
||||||
val isKeyManagerInitialized: Boolean
|
val isKeyManagerInitialized: Boolean
|
||||||
get() {
|
get() {
|
||||||
if (!isKeyManagerInit) {
|
if (!isKeyManagerInit) {
|
||||||
biometricUnlockCallback?.onBiometricException(Exception("Biometric not initialized"))
|
advancedUnlockCallback?.onGenericException(Exception("Biometric not initialized"))
|
||||||
}
|
}
|
||||||
return isKeyManagerInit
|
return isKeyManagerInit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isBiometricOperation(): Boolean {
|
||||||
|
return biometricUnlockEnable || isDeviceCredentialBiometricOperation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since Android 30, device credential is also a biometric operation
|
||||||
|
private fun isDeviceCredentialOperation(): Boolean {
|
||||||
|
return Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||||
|
&& deviceCredentialUnlockEnable
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isDeviceCredentialBiometricOperation(): Boolean {
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
|
&& deviceCredentialUnlockEnable
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (allowInitKeyStore(context)) {
|
if (isDeviceSecure(retrieveContext())
|
||||||
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
|
&& (biometricUnlockEnable || deviceCredentialUnlockEnable)) {
|
||||||
try {
|
try {
|
||||||
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)
|
this.keyStore = KeyStore.getInstance(ADVANCED_UNLOCK_KEYSTORE)
|
||||||
this.keyGenerator = KeyGenerator.getInstance(BIOMETRIC_KEY_ALGORITHM, BIOMETRIC_KEYSTORE)
|
this.keyGenerator = KeyGenerator.getInstance(ADVANCED_UNLOCK_KEY_ALGORITHM, ADVANCED_UNLOCK_KEYSTORE)
|
||||||
this.cipher = Cipher.getInstance(
|
this.cipher = Cipher.getInstance(
|
||||||
BIOMETRIC_KEY_ALGORITHM + "/"
|
ADVANCED_UNLOCK_KEY_ALGORITHM + "/"
|
||||||
+ BIOMETRIC_BLOCKS_MODES + "/"
|
+ ADVANCED_UNLOCK_BLOCKS_MODES + "/"
|
||||||
+ BIOMETRIC_ENCRYPTION_PADDING)
|
+ ADVANCED_UNLOCK_ENCRYPTION_PADDING)
|
||||||
this.cryptoObject = BiometricPrompt.CryptoObject(cipher!!)
|
|
||||||
isKeyManagerInit = (keyStore != null
|
isKeyManagerInit = (keyStore != null
|
||||||
&& keyGenerator != null
|
&& keyGenerator != null
|
||||||
&& cipher != null)
|
&& cipher != null)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to initialize the keystore", e)
|
Log.e(TAG, "Unable to initialize the keystore", e)
|
||||||
isKeyManagerInit = false
|
isKeyManagerInit = false
|
||||||
biometricUnlockCallback?.onBiometricException(e)
|
advancedUnlockCallback?.onGenericException(e)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// really not much to do when no fingerprint support found
|
// really not much to do when no fingerprint support found
|
||||||
@@ -103,22 +131,20 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
keyStore.load(null)
|
keyStore.load(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!keyStore.containsAlias(BIOMETRIC_KEYSTORE_KEY)) {
|
if (!keyStore.containsAlias(ADVANCED_UNLOCK_KEYSTORE_KEY)) {
|
||||||
// Set the alias of the entry in Android KeyStore where the key will appear
|
// Set the alias of the entry in Android KeyStore where the key will appear
|
||||||
// and the constrains (purposes) in the constructor of the Builder
|
// and the constrains (purposes) in the constructor of the Builder
|
||||||
keyGenerator?.init(
|
keyGenerator?.init(
|
||||||
KeyGenParameterSpec.Builder(
|
KeyGenParameterSpec.Builder(
|
||||||
BIOMETRIC_KEYSTORE_KEY,
|
ADVANCED_UNLOCK_KEYSTORE_KEY,
|
||||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||||
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
||||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
||||||
// Require the user to authenticate with a fingerprint to authorize every use
|
// Require the user to authenticate with a fingerprint to authorize every use
|
||||||
// of the key
|
// of the key, don't use it for device credential because it's the user authentication
|
||||||
.setUserAuthenticationRequired(true)
|
|
||||||
.apply {
|
.apply {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
if (biometricUnlockEnable) {
|
||||||
&& deviceCredentialUnlockEnable) {
|
setUserAuthenticationRequired(true)
|
||||||
setUserAuthenticationParameters(0, KeyProperties.AUTH_DEVICE_CREDENTIAL)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.build())
|
.build())
|
||||||
@@ -126,56 +152,46 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to create a key in keystore", e)
|
Log.e(TAG, "Unable to create a key in keystore", e)
|
||||||
biometricUnlockCallback?.onBiometricException(e)
|
advancedUnlockCallback?.onGenericException(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
return keyStore.getKey(BIOMETRIC_KEYSTORE_KEY, null) as SecretKey?
|
return keyStore.getKey(ADVANCED_UNLOCK_KEYSTORE_KEY, null) as SecretKey?
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to retrieve the key in keystore", e)
|
Log.e(TAG, "Unable to retrieve the key in keystore", e)
|
||||||
biometricUnlockCallback?.onBiometricException(e)
|
advancedUnlockCallback?.onGenericException(e)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initEncryptData(actionIfCypherInit
|
fun initEncryptData(actionIfCypherInit
|
||||||
: (biometricPrompt: BiometricPrompt?,
|
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
||||||
cryptoObject: BiometricPrompt.CryptoObject?,
|
|
||||||
promptInfo: BiometricPrompt.PromptInfo) -> Unit) {
|
|
||||||
if (!isKeyManagerInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// TODO if (keyguardManager?.isDeviceSecure == true) {
|
|
||||||
getSecretKey()?.let { secretKey ->
|
getSecretKey()?.let { secretKey ->
|
||||||
cipher?.init(Cipher.ENCRYPT_MODE, secretKey)
|
cipher?.let { cipher ->
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||||
|
|
||||||
initBiometricPrompt()
|
actionIfCypherInit.invoke(
|
||||||
|
AdvancedUnlockCryptoPrompt(
|
||||||
val promptInfoStoreCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
cipher,
|
||||||
setTitle(context.getString(R.string.advanced_unlock_prompt_store_credential_title))
|
R.string.advanced_unlock_prompt_store_credential_title,
|
||||||
setDescription(context.getString(R.string.advanced_unlock_prompt_store_credential_message))
|
R.string.advanced_unlock_prompt_store_credential_message,
|
||||||
setConfirmationRequired(true)
|
isDeviceCredentialOperation(), isBiometricOperation())
|
||||||
if (deviceCredentialUnlockEnable) {
|
)
|
||||||
setAllowedAuthenticators(DEVICE_CREDENTIAL)
|
}
|
||||||
} else {
|
|
||||||
setNegativeButtonText(context.getString(android.R.string.cancel))
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
actionIfCypherInit.invoke(biometricPrompt,
|
|
||||||
cryptoObject,
|
|
||||||
promptInfoStoreCredential)
|
|
||||||
}
|
}
|
||||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||||
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
|
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
|
||||||
biometricUnlockCallback?.onInvalidKeyException(unrecoverableKeyException)
|
advancedUnlockCallback?.onInvalidKeyException(unrecoverableKeyException)
|
||||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||||
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
|
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
|
||||||
biometricUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to initialize encrypt data", e)
|
Log.e(TAG, "Unable to initialize encrypt data", e)
|
||||||
biometricUnlockCallback?.onBiometricException(e)
|
advancedUnlockCallback?.onGenericException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,57 +206,46 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
// passes updated iv spec on to callback so this can be stored for decryption
|
// passes updated iv spec on to callback so this can be stored for decryption
|
||||||
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
|
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
|
||||||
val ivSpecValue = Base64.encodeToString(spec.iv, Base64.NO_WRAP)
|
val ivSpecValue = Base64.encodeToString(spec.iv, Base64.NO_WRAP)
|
||||||
biometricUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue)
|
advancedUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val exception = Exception(context.getString(R.string.keystore_not_accessible), e)
|
|
||||||
Log.e(TAG, "Unable to encrypt data", e)
|
Log.e(TAG, "Unable to encrypt data", e)
|
||||||
biometricUnlockCallback?.onBiometricException(exception)
|
advancedUnlockCallback?.onGenericException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
|
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
|
||||||
: (biometricPrompt: BiometricPrompt?,
|
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
||||||
cryptoObject: BiometricPrompt.CryptoObject?,
|
|
||||||
promptInfo: BiometricPrompt.PromptInfo) -> Unit) {
|
|
||||||
if (!isKeyManagerInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// TODO if (keyguardManager?.isDeviceSecure == true) {
|
|
||||||
// important to restore spec here that was used for decryption
|
// important to restore spec here that was used for decryption
|
||||||
val iv = Base64.decode(ivSpecValue, Base64.NO_WRAP)
|
val iv = Base64.decode(ivSpecValue, Base64.NO_WRAP)
|
||||||
val spec = IvParameterSpec(iv)
|
val spec = IvParameterSpec(iv)
|
||||||
|
|
||||||
getSecretKey()?.let { secretKey ->
|
getSecretKey()?.let { secretKey ->
|
||||||
cipher?.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
cipher?.let { cipher ->
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
||||||
|
|
||||||
initBiometricPrompt()
|
actionIfCypherInit.invoke(
|
||||||
|
AdvancedUnlockCryptoPrompt(
|
||||||
val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
cipher,
|
||||||
setTitle(context.getString(R.string.advanced_unlock_prompt_extract_credential_title))
|
R.string.advanced_unlock_prompt_extract_credential_title,
|
||||||
//setDescription(context.getString(R.string.biometric_prompt_extract_credential_message))
|
null,
|
||||||
setConfirmationRequired(false)
|
isDeviceCredentialOperation(), isBiometricOperation())
|
||||||
if (deviceCredentialUnlockEnable) {
|
)
|
||||||
setAllowedAuthenticators(DEVICE_CREDENTIAL)
|
}
|
||||||
} else {
|
|
||||||
setNegativeButtonText(context.getString(android.R.string.cancel))
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
actionIfCypherInit.invoke(biometricPrompt,
|
|
||||||
cryptoObject,
|
|
||||||
promptInfoExtractCredential)
|
|
||||||
}
|
}
|
||||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||||
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
|
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
|
||||||
deleteKeystoreKey()
|
deleteKeystoreKey()
|
||||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||||
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
|
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
|
||||||
biometricUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to initialize decrypt data", e)
|
Log.e(TAG, "Unable to initialize decrypt data", e)
|
||||||
biometricUnlockCallback?.onBiometricException(e)
|
advancedUnlockCallback?.onGenericException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,33 +257,73 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
// actual decryption here
|
// actual decryption here
|
||||||
val encrypted = Base64.decode(encryptedValue, Base64.NO_WRAP)
|
val encrypted = Base64.decode(encryptedValue, Base64.NO_WRAP)
|
||||||
cipher?.doFinal(encrypted)?.let { decrypted ->
|
cipher?.doFinal(encrypted)?.let { decrypted ->
|
||||||
biometricUnlockCallback?.handleDecryptedResult(String(decrypted))
|
advancedUnlockCallback?.handleDecryptedResult(String(decrypted))
|
||||||
}
|
}
|
||||||
} catch (badPaddingException: BadPaddingException) {
|
} catch (badPaddingException: BadPaddingException) {
|
||||||
Log.e(TAG, "Unable to decrypt data", badPaddingException)
|
Log.e(TAG, "Unable to decrypt data", badPaddingException)
|
||||||
biometricUnlockCallback?.onInvalidKeyException(badPaddingException)
|
advancedUnlockCallback?.onInvalidKeyException(badPaddingException)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val exception = Exception(context.getString(R.string.keystore_not_accessible), e)
|
Log.e(TAG, "Unable to decrypt data", e)
|
||||||
Log.e(TAG, "Unable to decrypt data", exception)
|
advancedUnlockCallback?.onGenericException(e)
|
||||||
biometricUnlockCallback?.onBiometricException(exception)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteKeystoreKey() {
|
fun deleteKeystoreKey() {
|
||||||
try {
|
try {
|
||||||
keyStore?.load(null)
|
keyStore?.load(null)
|
||||||
keyStore?.deleteEntry(BIOMETRIC_KEYSTORE_KEY)
|
keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to delete entry key in keystore", e)
|
Log.e(TAG, "Unable to delete entry key in keystore", e)
|
||||||
biometricUnlockCallback?.onBiometricException(e)
|
advancedUnlockCallback?.onGenericException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
@Synchronized
|
||||||
|
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
|
||||||
|
// Init advanced unlock prompt
|
||||||
|
if (biometricPrompt == null) {
|
||||||
|
biometricPrompt = BiometricPrompt(retrieveContext(),
|
||||||
|
Executors.newSingleThreadExecutor(),
|
||||||
|
authenticationCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
val promptTitle = retrieveContext().getString(cryptoPrompt.promptTitleId)
|
||||||
|
val promptDescription = cryptoPrompt.promptDescriptionId?.let { descriptionId ->
|
||||||
|
retrieveContext().getString(descriptionId)
|
||||||
|
} ?: ""
|
||||||
|
|
||||||
|
if (cryptoPrompt.isBiometricOperation) {
|
||||||
|
val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
||||||
|
setTitle(promptTitle)
|
||||||
|
if (promptDescription.isNotEmpty())
|
||||||
|
setDescription(promptDescription)
|
||||||
|
setConfirmationRequired(false)
|
||||||
|
if (isDeviceCredentialBiometricOperation()) {
|
||||||
|
setAllowedAuthenticators(DEVICE_CREDENTIAL)
|
||||||
|
} else {
|
||||||
|
setNegativeButtonText(retrieveContext().getString(android.R.string.cancel))
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
biometricPrompt?.authenticate(
|
||||||
|
promptInfoExtractCredential,
|
||||||
|
BiometricPrompt.CryptoObject(cryptoPrompt.cipher))
|
||||||
|
}
|
||||||
|
else if (cryptoPrompt.isDeviceCredentialOperation) {
|
||||||
|
val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java)
|
||||||
|
retrieveContext().startActivityForResult(
|
||||||
|
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription),
|
||||||
|
REQUEST_DEVICE_CREDENTIAL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun initBiometricPrompt() {
|
fun onActivityResult(requestCode: Int, resultCode: Int) {
|
||||||
if (biometricPrompt == null) {
|
if (requestCode == REQUEST_DEVICE_CREDENTIAL) {
|
||||||
authenticationCallback?.let {
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
biometricPrompt = BiometricPrompt(context, Executors.newSingleThreadExecutor(), it)
|
advancedUnlockCallback?.onAuthenticationSucceeded()
|
||||||
|
} else {
|
||||||
|
advancedUnlockCallback?.onAuthenticationFailed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,25 +332,30 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
biometricPrompt?.cancelAuthentication()
|
biometricPrompt?.cancelAuthentication()
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BiometricUnlockErrorCallback {
|
interface AdvancedUnlockErrorCallback {
|
||||||
fun onInvalidKeyException(e: Exception)
|
fun onInvalidKeyException(e: Exception)
|
||||||
fun onBiometricException(e: Exception)
|
fun onGenericException(e: Exception)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BiometricUnlockCallback : BiometricUnlockErrorCallback {
|
interface AdvancedUnlockCallback : AdvancedUnlockErrorCallback {
|
||||||
|
fun onAuthenticationSucceeded()
|
||||||
|
fun onAuthenticationFailed()
|
||||||
|
fun onAuthenticationError(errorCode: Int, errString: CharSequence)
|
||||||
fun handleEncryptedResult(encryptedValue: String, ivSpec: String)
|
fun handleEncryptedResult(encryptedValue: String, ivSpec: String)
|
||||||
fun handleDecryptedResult(decryptedValue: String)
|
fun handleDecryptedResult(decryptedValue: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val TAG = BiometricUnlockDatabaseHelper::class.java.name
|
private val TAG = AdvancedUnlockManager::class.java.name
|
||||||
|
|
||||||
private const val BIOMETRIC_KEYSTORE = "AndroidKeyStore"
|
private const val ADVANCED_UNLOCK_KEYSTORE = "AndroidKeyStore"
|
||||||
private const val BIOMETRIC_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
|
private const val ADVANCED_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
|
||||||
private const val BIOMETRIC_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
|
private const val ADVANCED_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
|
||||||
private const val BIOMETRIC_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
||||||
private const val BIOMETRIC_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||||
|
|
||||||
|
private const val REQUEST_DEVICE_CREDENTIAL = 556
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
fun canAuthenticate(context: Context): Int {
|
fun canAuthenticate(context: Context): Int {
|
||||||
@@ -337,11 +387,9 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
fun allowInitKeyStore(context: Context): Boolean {
|
fun isDeviceSecure(context: Context): Boolean {
|
||||||
val biometricCanAuthenticate = canAuthenticate(context)
|
val keyguardManager = ContextCompat.getSystemService(context, KeyguardManager::class.java)
|
||||||
return ( biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
return keyguardManager?.isDeviceSecure ?: false
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
@@ -365,36 +413,48 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.R)
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
fun deviceCredentialUnlockSupported(context: Context): Boolean {
|
fun deviceCredentialUnlockSupported(context: Context): Boolean {
|
||||||
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||||
)
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
|
||||||
|
)
|
||||||
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
ContextCompat.getSystemService(context, KeyguardManager::class.java)?.apply {
|
||||||
|
return isDeviceSecure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove entry key in keystore
|
* Remove entry key in keystore
|
||||||
*/
|
*/
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
fun deleteEntryKeyInKeystoreForBiometric(context: FragmentActivity,
|
fun deleteEntryKeyInKeystoreForBiometric(fragmentActivity: FragmentActivity,
|
||||||
biometricCallback: BiometricUnlockErrorCallback) {
|
advancedCallback: AdvancedUnlockErrorCallback) {
|
||||||
BiometricUnlockDatabaseHelper(context).apply {
|
AdvancedUnlockManager{ fragmentActivity }.apply {
|
||||||
biometricUnlockCallback = object : BiometricUnlockCallback {
|
advancedUnlockCallback = object : AdvancedUnlockCallback {
|
||||||
|
override fun onAuthenticationSucceeded() {}
|
||||||
|
|
||||||
|
override fun onAuthenticationFailed() {}
|
||||||
|
|
||||||
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {}
|
||||||
|
|
||||||
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {}
|
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {}
|
||||||
|
|
||||||
override fun handleDecryptedResult(decryptedValue: String) {}
|
override fun handleDecryptedResult(decryptedValue: String) {}
|
||||||
|
|
||||||
override fun onInvalidKeyException(e: Exception) {
|
override fun onInvalidKeyException(e: Exception) {
|
||||||
biometricCallback.onInvalidKeyException(e)
|
advancedCallback.onInvalidKeyException(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBiometricException(e: Exception) {
|
override fun onGenericException(e: Exception) {
|
||||||
biometricCallback.onBiometricException(e)
|
advancedCallback.onGenericException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deleteKeystoreKey()
|
deleteKeystoreKey()
|
||||||
@@ -1,422 +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.biometric
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.provider.Settings
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.CompoundButton
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.biometric.BiometricManager
|
|
||||||
import androidx.biometric.BiometricPrompt
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
|
||||||
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
class AdvancedUnlockedManager(var context: FragmentActivity,
|
|
||||||
var databaseFileUri: Uri,
|
|
||||||
private var advancedUnlockInfoView: AdvancedUnlockInfoView?,
|
|
||||||
private var checkboxPasswordView: CompoundButton?,
|
|
||||||
private var onCheckedPasswordChangeListener: CompoundButton.OnCheckedChangeListener? = null,
|
|
||||||
var passwordView: TextView?,
|
|
||||||
private var loadDatabaseAfterRegisterCredentials: (encryptedPassword: String?, ivSpec: String?) -> Unit,
|
|
||||||
private var loadDatabaseAfterRetrieveCredentials: (decryptedPassword: String?) -> Unit)
|
|
||||||
: BiometricUnlockDatabaseHelper.BiometricUnlockCallback {
|
|
||||||
|
|
||||||
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
|
|
||||||
private var biometricMode: Mode = Mode.BIOMETRIC_UNAVAILABLE
|
|
||||||
|
|
||||||
// Only to fix multiple fingerprint menu #332
|
|
||||||
private var mAllowAdvancedUnlockMenu = false
|
|
||||||
private var mAddBiometricMenuInProgress = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manage setting to auto open biometric prompt
|
|
||||||
*/
|
|
||||||
private var biometricPromptAutoOpenPreference = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(context)
|
|
||||||
var isBiometricPromptAutoOpenEnable: Boolean = false
|
|
||||||
get() {
|
|
||||||
return field && biometricPromptAutoOpenPreference
|
|
||||||
}
|
|
||||||
|
|
||||||
// Variable to check if the prompt can be open (if the right activity is currently shown)
|
|
||||||
// checkBiometricAvailability() allows open biometric prompt and onDestroy() removes the authorization
|
|
||||||
private var allowOpenBiometricPrompt = false
|
|
||||||
|
|
||||||
private var cipherDatabaseAction = CipherDatabaseAction.getInstance(context.applicationContext)
|
|
||||||
|
|
||||||
private val cipherDatabaseListener = object: CipherDatabaseAction.DatabaseListener {
|
|
||||||
override fun onDatabaseCleared() {
|
|
||||||
deleteEncryptedDatabaseKey()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Add a check listener to change fingerprint mode
|
|
||||||
checkboxPasswordView?.setOnCheckedChangeListener { compoundButton, checked ->
|
|
||||||
checkBiometricAvailability()
|
|
||||||
// Add old listener to enable the button, only be call here because of onCheckedChange bug
|
|
||||||
onCheckedPasswordChangeListener?.onCheckedChanged(compoundButton, checked)
|
|
||||||
}
|
|
||||||
cipherDatabaseAction.apply {
|
|
||||||
reloadPreferences()
|
|
||||||
registerDatabaseListener(cipherDatabaseListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check biometric availability and change the current mode depending of device's state
|
|
||||||
*/
|
|
||||||
fun checkBiometricAvailability() {
|
|
||||||
|
|
||||||
if (PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
|
||||||
advancedUnlockInfoView?.setIconResource(R.drawable.bolt)
|
|
||||||
} else if (PreferencesUtil.isBiometricUnlockEnable(context)) {
|
|
||||||
advancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// biometric not supported (by API level or hardware) so keep option hidden
|
|
||||||
// or manually disable
|
|
||||||
val biometricCanAuthenticate = BiometricUnlockDatabaseHelper.canAuthenticate(context)
|
|
||||||
allowOpenBiometricPrompt = true
|
|
||||||
|
|
||||||
if (!PreferencesUtil.isAdvancedUnlockEnable(context)
|
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
|
|
||||||
toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
|
|
||||||
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED){
|
|
||||||
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
|
|
||||||
} else {
|
|
||||||
// biometric is available but not configured, show icon but in disabled state with some information
|
|
||||||
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
|
||||||
toggleMode(Mode.BIOMETRIC_NOT_CONFIGURED)
|
|
||||||
} else {
|
|
||||||
// Check if fingerprint well init (be called the first time the fingerprint is configured
|
|
||||||
// and the activity still active)
|
|
||||||
if (biometricUnlockDatabaseHelper?.isKeyManagerInitialized != true) {
|
|
||||||
biometricUnlockDatabaseHelper = BiometricUnlockDatabaseHelper(context)
|
|
||||||
// callback for fingerprint findings
|
|
||||||
biometricUnlockDatabaseHelper?.biometricUnlockCallback = this
|
|
||||||
biometricUnlockDatabaseHelper?.authenticationCallback = biometricAuthenticationCallback
|
|
||||||
}
|
|
||||||
// Recheck to change the mode
|
|
||||||
if (biometricUnlockDatabaseHelper?.isKeyManagerInitialized != true) {
|
|
||||||
toggleMode(Mode.KEY_MANAGER_UNAVAILABLE)
|
|
||||||
} else {
|
|
||||||
if (checkboxPasswordView?.isChecked == true) {
|
|
||||||
// listen for encryption
|
|
||||||
toggleMode(Mode.STORE_CREDENTIAL)
|
|
||||||
} else {
|
|
||||||
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
|
|
||||||
// biometric available but no stored password found yet for this DB so show info don't listen
|
|
||||||
toggleMode(if (containsCipher) {
|
|
||||||
// listen for decryption
|
|
||||||
Mode.EXTRACT_CREDENTIAL
|
|
||||||
} else {
|
|
||||||
// wait for typing
|
|
||||||
Mode.WAIT_CREDENTIAL
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toggleMode(newBiometricMode: Mode) {
|
|
||||||
if (newBiometricMode != biometricMode) {
|
|
||||||
biometricMode = newBiometricMode
|
|
||||||
initAdvancedUnlockMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val biometricAuthenticationCallback = object : BiometricPrompt.AuthenticationCallback () {
|
|
||||||
|
|
||||||
override fun onAuthenticationError(
|
|
||||||
errorCode: Int,
|
|
||||||
errString: CharSequence) {
|
|
||||||
context.runOnUiThread {
|
|
||||||
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
|
|
||||||
setAdvancedUnlockedMessageView(errString.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAuthenticationFailed() {
|
|
||||||
context.runOnUiThread {
|
|
||||||
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
|
|
||||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
|
||||||
context.runOnUiThread {
|
|
||||||
when (biometricMode) {
|
|
||||||
Mode.BIOMETRIC_UNAVAILABLE -> {
|
|
||||||
}
|
|
||||||
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> {
|
|
||||||
}
|
|
||||||
Mode.BIOMETRIC_NOT_CONFIGURED -> {
|
|
||||||
}
|
|
||||||
Mode.KEY_MANAGER_UNAVAILABLE -> {
|
|
||||||
}
|
|
||||||
Mode.WAIT_CREDENTIAL -> {
|
|
||||||
}
|
|
||||||
Mode.STORE_CREDENTIAL -> {
|
|
||||||
// newly store the entered password in encrypted way
|
|
||||||
biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString())
|
|
||||||
AdvancedUnlockNotificationService.startServiceForTimeout(context)
|
|
||||||
}
|
|
||||||
Mode.EXTRACT_CREDENTIAL -> {
|
|
||||||
// retrieve the encrypted value from preferences
|
|
||||||
cipherDatabaseAction.getCipherDatabase(databaseFileUri) { cipherDatabase ->
|
|
||||||
cipherDatabase?.encryptedValue?.let { value ->
|
|
||||||
biometricUnlockDatabaseHelper?.decryptData(value)
|
|
||||||
} ?: deleteEncryptedDatabaseKey()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initNotAvailable() {
|
|
||||||
showFingerPrintViews(false)
|
|
||||||
|
|
||||||
advancedUnlockInfoView?.setIconViewClickListener(false, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun openBiometricSetting() {
|
|
||||||
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
|
||||||
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
|
|
||||||
context.startActivity(Intent(Settings.ACTION_SETTINGS))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initSecurityUpdateRequired() {
|
|
||||||
showFingerPrintViews(true)
|
|
||||||
setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
|
|
||||||
|
|
||||||
openBiometricSetting()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initNotConfigured() {
|
|
||||||
showFingerPrintViews(true)
|
|
||||||
setAdvancedUnlockedTitleView(R.string.configure_biometric)
|
|
||||||
setAdvancedUnlockedMessageView("")
|
|
||||||
|
|
||||||
openBiometricSetting()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initKeyManagerNotAvailable() {
|
|
||||||
showFingerPrintViews(true)
|
|
||||||
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
|
|
||||||
|
|
||||||
openBiometricSetting()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initWaitData() {
|
|
||||||
showFingerPrintViews(true)
|
|
||||||
setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
|
|
||||||
setAdvancedUnlockedMessageView("")
|
|
||||||
|
|
||||||
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
|
||||||
biometricAuthenticationCallback.onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
|
|
||||||
context.getString(R.string.credential_before_click_advanced_unlock_button))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openBiometricPrompt(biometricPrompt: BiometricPrompt?,
|
|
||||||
cryptoObject: BiometricPrompt.CryptoObject?,
|
|
||||||
promptInfo: BiometricPrompt.PromptInfo) {
|
|
||||||
context.runOnUiThread {
|
|
||||||
if (allowOpenBiometricPrompt) {
|
|
||||||
if (biometricPrompt != null) {
|
|
||||||
if (cryptoObject != null) {
|
|
||||||
biometricPrompt.authenticate(promptInfo, cryptoObject)
|
|
||||||
} else {
|
|
||||||
setAdvancedUnlockedTitleView(R.string.crypto_object_not_initialized)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initEncryptData() {
|
|
||||||
showFingerPrintViews(true)
|
|
||||||
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_store_credential)
|
|
||||||
setAdvancedUnlockedMessageView("")
|
|
||||||
|
|
||||||
biometricUnlockDatabaseHelper?.initEncryptData { biometricPrompt, cryptoObject, promptInfo ->
|
|
||||||
// Set listener to open the biometric dialog and save credential
|
|
||||||
advancedUnlockInfoView?.setIconViewClickListener { _ ->
|
|
||||||
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initDecryptData() {
|
|
||||||
showFingerPrintViews(true)
|
|
||||||
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_unlock_database)
|
|
||||||
setAdvancedUnlockedMessageView("")
|
|
||||||
|
|
||||||
if (biometricUnlockDatabaseHelper != null) {
|
|
||||||
cipherDatabaseAction.getCipherDatabase(databaseFileUri) { cipherDatabase ->
|
|
||||||
cipherDatabase?.let {
|
|
||||||
biometricUnlockDatabaseHelper?.initDecryptData(it.specParameters) { biometricPrompt, cryptoObject, promptInfo ->
|
|
||||||
|
|
||||||
// Set listener to open the biometric dialog and check credential
|
|
||||||
advancedUnlockInfoView?.setIconViewClickListener { _ ->
|
|
||||||
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto open the biometric prompt
|
|
||||||
if (isBiometricPromptAutoOpenEnable) {
|
|
||||||
isBiometricPromptAutoOpenEnable = false
|
|
||||||
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: deleteEncryptedDatabaseKey()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun initAdvancedUnlockMode() {
|
|
||||||
mAllowAdvancedUnlockMenu = false
|
|
||||||
when (biometricMode) {
|
|
||||||
Mode.BIOMETRIC_UNAVAILABLE -> initNotAvailable()
|
|
||||||
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired()
|
|
||||||
Mode.BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
|
|
||||||
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
|
|
||||||
Mode.WAIT_CREDENTIAL -> initWaitData()
|
|
||||||
Mode.STORE_CREDENTIAL -> initEncryptData()
|
|
||||||
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidateBiometricMenu()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun invalidateBiometricMenu() {
|
|
||||||
// Show fingerprint key deletion
|
|
||||||
if (!mAddBiometricMenuInProgress) {
|
|
||||||
mAddBiometricMenuInProgress = true
|
|
||||||
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
|
|
||||||
mAllowAdvancedUnlockMenu = containsCipher
|
|
||||||
&& (biometricMode != Mode.BIOMETRIC_UNAVAILABLE
|
|
||||||
&& biometricMode != Mode.KEY_MANAGER_UNAVAILABLE)
|
|
||||||
mAddBiometricMenuInProgress = false
|
|
||||||
context.invalidateOptionsMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun destroy() {
|
|
||||||
// Close the biometric prompt
|
|
||||||
allowOpenBiometricPrompt = false
|
|
||||||
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
|
|
||||||
// Restore the checked listener
|
|
||||||
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
|
|
||||||
cipherDatabaseAction.unregisterDatabaseListener(cipherDatabaseListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun inflateOptionsMenu(menuInflater: MenuInflater, menu: Menu) {
|
|
||||||
if (mAllowAdvancedUnlockMenu)
|
|
||||||
menuInflater.inflate(R.menu.advanced_unlock, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteEncryptedDatabaseKey() {
|
|
||||||
allowOpenBiometricPrompt = false
|
|
||||||
advancedUnlockInfoView?.setIconViewClickListener(false, null)
|
|
||||||
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
|
|
||||||
cipherDatabaseAction.deleteByDatabaseUri(databaseFileUri) {
|
|
||||||
checkBiometricAvailability()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
|
|
||||||
loadDatabaseAfterRegisterCredentials.invoke(encryptedValue, ivSpec)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleDecryptedResult(decryptedValue: String) {
|
|
||||||
// Load database directly with password retrieve
|
|
||||||
loadDatabaseAfterRetrieveCredentials.invoke(decryptedValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInvalidKeyException(e: Exception) {
|
|
||||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBiometricException(e: Exception) {
|
|
||||||
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
|
|
||||||
setAdvancedUnlockedMessageView(errorMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showFingerPrintViews(show: Boolean) {
|
|
||||||
context.runOnUiThread {
|
|
||||||
advancedUnlockInfoView?.visibility = if (show) View.VISIBLE else View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setAdvancedUnlockedTitleView(textId: Int) {
|
|
||||||
context.runOnUiThread {
|
|
||||||
advancedUnlockInfoView?.setTitle(textId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setAdvancedUnlockedMessageView(textId: Int) {
|
|
||||||
context.runOnUiThread {
|
|
||||||
advancedUnlockInfoView?.setMessage(textId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
|
|
||||||
context.runOnUiThread {
|
|
||||||
advancedUnlockInfoView?.message = text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Mode {
|
|
||||||
BIOMETRIC_UNAVAILABLE,
|
|
||||||
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
|
|
||||||
BIOMETRIC_NOT_CONFIGURED,
|
|
||||||
KEY_MANAGER_UNAVAILABLE,
|
|
||||||
WAIT_CREDENTIAL,
|
|
||||||
STORE_CREDENTIAL,
|
|
||||||
EXTRACT_CREDENTIAL
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val TAG = AdvancedUnlockedManager::class.java.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,11 +29,12 @@ object StreamCipherFactory {
|
|||||||
|
|
||||||
private val SALSA_IV = byteArrayOf(0xE8.toByte(), 0x30, 0x09, 0x4B, 0x97.toByte(), 0x20, 0x5D, 0x2A)
|
private val SALSA_IV = byteArrayOf(0xE8.toByte(), 0x30, 0x09, 0x4B, 0x97.toByte(), 0x20, 0x5D, 0x2A)
|
||||||
|
|
||||||
fun getInstance(alg: CrsAlgorithm?, key: ByteArray): StreamCipher? {
|
@Throws(Exception::class)
|
||||||
|
fun getInstance(alg: CrsAlgorithm?, key: ByteArray): StreamCipher {
|
||||||
return when {
|
return when {
|
||||||
alg === CrsAlgorithm.Salsa20 -> getSalsa20(key)
|
alg === CrsAlgorithm.Salsa20 -> getSalsa20(key)
|
||||||
alg === CrsAlgorithm.ChaCha20 -> getChaCha20(key)
|
alg === CrsAlgorithm.ChaCha20 -> getChaCha20(key)
|
||||||
else -> null
|
else -> throw Exception("Invalid random cipher")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
|||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.utils.closeDatabase
|
|
||||||
|
|
||||||
class CreateDatabaseRunnable(context: Context,
|
class CreateDatabaseRunnable(context: Context,
|
||||||
private val mDatabase: Database,
|
private val mDatabase: Database,
|
||||||
@@ -47,7 +46,7 @@ class CreateDatabaseRunnable(context: Context,
|
|||||||
createData(mDatabaseUri, databaseName, rootName)
|
createData(mDatabaseUri, databaseName, rootName)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
mDatabase.closeAndClear(UriUtil.getBinaryDir(context))
|
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.utils.closeDatabase
|
|
||||||
|
|
||||||
class LoadDatabaseRunnable(private val context: Context,
|
class LoadDatabaseRunnable(private val context: Context,
|
||||||
private val mDatabase: Database,
|
private val mDatabase: Database,
|
||||||
@@ -47,7 +46,7 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
// Clear before we load
|
// Clear before we load
|
||||||
mDatabase.closeAndClear(UriUtil.getBinaryDir(context))
|
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionRun() {
|
override fun onActionRun() {
|
||||||
@@ -59,9 +58,6 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
mFixDuplicateUUID,
|
mFixDuplicateUUID,
|
||||||
progressTaskUpdater)
|
progressTaskUpdater)
|
||||||
}
|
}
|
||||||
catch (e: DuplicateUuidDatabaseException) {
|
|
||||||
setError(e)
|
|
||||||
}
|
|
||||||
catch (e: LoadDatabaseException) {
|
catch (e: LoadDatabaseException) {
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
@@ -83,7 +79,7 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
// Register the current time to init the lock timer
|
// Register the current time to init the lock timer
|
||||||
PreferencesUtil.saveCurrentTime(context)
|
PreferencesUtil.saveCurrentTime(context)
|
||||||
} else {
|
} else {
|
||||||
mDatabase.closeAndClear(UriUtil.getBinaryDir(context))
|
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
@@ -35,6 +37,7 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||||
@@ -44,6 +47,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
|
|||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||||
@@ -84,6 +88,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
private var serviceConnection: ServiceConnection? = null
|
private var serviceConnection: ServiceConnection? = null
|
||||||
|
|
||||||
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
|
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
|
||||||
|
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
|
||||||
|
|
||||||
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
||||||
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||||
@@ -101,6 +106,28 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val mActionDatabaseListener = object: DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
|
||||||
|
override fun validateDatabaseChanged() {
|
||||||
|
mBinder?.getService()?.saveDatabaseInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var databaseInfoListener = object: DatabaseTaskNotificationService.DatabaseInfoListener {
|
||||||
|
override fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
newDatabaseInfo: SnapFileDatabaseInfo) {
|
||||||
|
if (databaseChangedDialogFragment == null) {
|
||||||
|
databaseChangedDialogFragment = activity.supportFragmentManager
|
||||||
|
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
|
||||||
|
databaseChangedDialogFragment?.actionDatabaseListener = mActionDatabaseListener
|
||||||
|
}
|
||||||
|
if (progressTaskDialogFragment == null) {
|
||||||
|
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(previousDatabaseInfo, newDatabaseInfo)
|
||||||
|
databaseChangedDialogFragment?.actionDatabaseListener = mActionDatabaseListener
|
||||||
|
databaseChangedDialogFragment?.show(activity.supportFragmentManager, DATABASE_CHANGED_DIALOG_TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun startDialog(titleId: Int? = null,
|
private fun startDialog(titleId: Int? = null,
|
||||||
messageId: Int? = null,
|
messageId: Int? = null,
|
||||||
warningId: Int? = null) {
|
warningId: Int? = null) {
|
||||||
@@ -140,11 +167,14 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||||
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
||||||
addActionTaskListener(actionTaskListener)
|
addActionTaskListener(actionTaskListener)
|
||||||
|
addDatabaseFileInfoListener(databaseInfoListener)
|
||||||
getService().checkAction()
|
getService().checkAction()
|
||||||
|
getService().checkDatabaseInfo()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||||
mBinder?.removeActionTaskListener(actionTaskListener)
|
mBinder?.removeActionTaskListener(actionTaskListener)
|
||||||
mBinder = null
|
mBinder = null
|
||||||
}
|
}
|
||||||
@@ -206,6 +236,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
fun unregisterProgressTask() {
|
fun unregisterProgressTask() {
|
||||||
stopDialog()
|
stopDialog()
|
||||||
|
|
||||||
|
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||||
mBinder?.removeActionTaskListener(actionTaskListener)
|
mBinder?.removeActionTaskListener(actionTaskListener)
|
||||||
mBinder = null
|
mBinder = null
|
||||||
|
|
||||||
@@ -264,6 +295,13 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
, ACTION_DATABASE_LOAD_TASK)
|
, ACTION_DATABASE_LOAD_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
|
||||||
|
start(Bundle().apply {
|
||||||
|
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||||
|
}
|
||||||
|
, ACTION_DATABASE_RELOAD_TASK)
|
||||||
|
}
|
||||||
|
|
||||||
fun startDatabaseAssignPassword(databaseUri: Uri,
|
fun startDatabaseAssignPassword(databaseUri: Uri,
|
||||||
masterPasswordChecked: Boolean,
|
masterPasswordChecked: Boolean,
|
||||||
masterPassword: String?,
|
masterPassword: String?,
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* 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.action
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
|
class ReloadDatabaseRunnable(private val context: Context,
|
||||||
|
private val mDatabase: Database,
|
||||||
|
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
|
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||||
|
: ActionRunnable() {
|
||||||
|
|
||||||
|
override fun onStartRun() {
|
||||||
|
// Clear before we load
|
||||||
|
mDatabase.clear(UriUtil.getBinaryDir(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActionRun() {
|
||||||
|
try {
|
||||||
|
mDatabase.reloadData(context.contentResolver,
|
||||||
|
UriUtil.getBinaryDir(context),
|
||||||
|
progressTaskUpdater)
|
||||||
|
}
|
||||||
|
catch (e: LoadDatabaseException) {
|
||||||
|
setError(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isSuccess) {
|
||||||
|
// Register the current time to init the lock timer
|
||||||
|
PreferencesUtil.saveCurrentTime(context)
|
||||||
|
} else {
|
||||||
|
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinishRun() {
|
||||||
|
mLoadDatabaseResult?.invoke(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,10 +31,7 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
import com.kunzisoft.keepass.database.exception.*
|
||||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
|
||||||
import com.kunzisoft.keepass.database.exception.SignatureDatabaseException
|
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||||
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
|
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
|
||||||
@@ -330,29 +327,11 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
fun loadData(uri: Uri, password: String?, keyfile: Uri?,
|
private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri,
|
||||||
readOnly: Boolean,
|
openDatabaseKDB: (InputStream) -> DatabaseKDB,
|
||||||
contentResolver: ContentResolver,
|
openDatabaseKDBX: (InputStream) -> DatabaseKDBX) {
|
||||||
cacheDirectory: File,
|
|
||||||
fixDuplicateUUID: Boolean,
|
|
||||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
|
||||||
|
|
||||||
this.fileUri = uri
|
|
||||||
isReadOnly = readOnly
|
|
||||||
if (uri.scheme == "file") {
|
|
||||||
val file = File(uri.path!!)
|
|
||||||
isReadOnly = !file.canWrite()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass KeyFile Uri as InputStreams
|
|
||||||
var databaseInputStream: InputStream? = null
|
var databaseInputStream: InputStream? = null
|
||||||
var keyFileInputStream: InputStream? = null
|
|
||||||
try {
|
try {
|
||||||
// Get keyFile inputStream
|
|
||||||
keyfile?.let {
|
|
||||||
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load Data, pass Uris as InputStreams
|
// Load Data, pass Uris as InputStreams
|
||||||
val databaseStream = UriUtil.getUriInputStream(contentResolver, uri)
|
val databaseStream = UriUtil.getUriInputStream(contentResolver, uri)
|
||||||
?: throw IOException("Database input stream cannot be retrieve")
|
?: throw IOException("Database input stream cannot be retrieve")
|
||||||
@@ -374,22 +353,10 @@ class Database {
|
|||||||
|
|
||||||
when {
|
when {
|
||||||
// Header of database KDB
|
// Header of database KDB
|
||||||
DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(DatabaseInputKDB(
|
DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(openDatabaseKDB(databaseInputStream))
|
||||||
cacheDirectory,
|
|
||||||
fixDuplicateUUID)
|
|
||||||
.openDatabase(databaseInputStream,
|
|
||||||
password,
|
|
||||||
keyFileInputStream,
|
|
||||||
progressTaskUpdater))
|
|
||||||
|
|
||||||
// Header of database KDBX
|
// Header of database KDBX
|
||||||
DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> setDatabaseKDBX(DatabaseInputKDBX(
|
DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> setDatabaseKDBX(openDatabaseKDBX(databaseInputStream))
|
||||||
cacheDirectory,
|
|
||||||
fixDuplicateUUID)
|
|
||||||
.openDatabase(databaseInputStream,
|
|
||||||
password,
|
|
||||||
keyFileInputStream,
|
|
||||||
progressTaskUpdater))
|
|
||||||
|
|
||||||
// Header not recognized
|
// Header not recognized
|
||||||
else -> throw SignatureDatabaseException()
|
else -> throw SignatureDatabaseException()
|
||||||
@@ -397,14 +364,90 @@ class Database {
|
|||||||
|
|
||||||
this.mSearchHelper = SearchHelper()
|
this.mSearchHelper = SearchHelper()
|
||||||
loaded = true
|
loaded = true
|
||||||
|
} catch (e: LoadDatabaseException) {
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
databaseInputStream?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(LoadDatabaseException::class)
|
||||||
|
fun loadData(uri: Uri, password: String?, keyfile: Uri?,
|
||||||
|
readOnly: Boolean,
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
cacheDirectory: File,
|
||||||
|
fixDuplicateUUID: Boolean,
|
||||||
|
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||||
|
|
||||||
|
// Save database URI
|
||||||
|
this.fileUri = uri
|
||||||
|
|
||||||
|
// Check if the file is writable
|
||||||
|
this.isReadOnly = readOnly
|
||||||
|
|
||||||
|
// Pass KeyFile Uri as InputStreams
|
||||||
|
var keyFileInputStream: InputStream? = null
|
||||||
|
try {
|
||||||
|
// Get keyFile inputStream
|
||||||
|
keyfile?.let {
|
||||||
|
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read database stream for the first time
|
||||||
|
readDatabaseStream(contentResolver, uri,
|
||||||
|
{ databaseInputStream ->
|
||||||
|
DatabaseInputKDB(cacheDirectory)
|
||||||
|
.openDatabase(databaseInputStream,
|
||||||
|
password,
|
||||||
|
keyFileInputStream,
|
||||||
|
progressTaskUpdater,
|
||||||
|
fixDuplicateUUID)
|
||||||
|
},
|
||||||
|
{ databaseInputStream ->
|
||||||
|
DatabaseInputKDBX(cacheDirectory)
|
||||||
|
.openDatabase(databaseInputStream,
|
||||||
|
password,
|
||||||
|
keyFileInputStream,
|
||||||
|
progressTaskUpdater,
|
||||||
|
fixDuplicateUUID)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
Log.e(TAG, "Unable to load keyfile", e)
|
||||||
|
throw FileNotFoundDatabaseException()
|
||||||
} catch (e: LoadDatabaseException) {
|
} catch (e: LoadDatabaseException) {
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw FileNotFoundDatabaseException()
|
throw LoadDatabaseException(e)
|
||||||
} finally {
|
} finally {
|
||||||
keyFileInputStream?.close()
|
keyFileInputStream?.close()
|
||||||
databaseInputStream?.close()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(LoadDatabaseException::class)
|
||||||
|
fun reloadData(contentResolver: ContentResolver,
|
||||||
|
cacheDirectory: File,
|
||||||
|
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||||
|
|
||||||
|
// Retrieve the stream from the old database URI
|
||||||
|
fileUri?.let { oldDatabaseUri ->
|
||||||
|
readDatabaseStream(contentResolver, oldDatabaseUri,
|
||||||
|
{ databaseInputStream ->
|
||||||
|
DatabaseInputKDB(cacheDirectory)
|
||||||
|
.openDatabase(databaseInputStream,
|
||||||
|
masterKey,
|
||||||
|
progressTaskUpdater)
|
||||||
|
},
|
||||||
|
{ databaseInputStream ->
|
||||||
|
DatabaseInputKDBX(cacheDirectory)
|
||||||
|
.openDatabase(databaseInputStream,
|
||||||
|
masterKey,
|
||||||
|
progressTaskUpdater)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} ?: run {
|
||||||
|
Log.e(TAG, "Database URI is null, database cannot be reloaded")
|
||||||
|
throw IODatabaseException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +469,7 @@ class Database {
|
|||||||
max: Int = Integer.MAX_VALUE): Group? {
|
max: Int = Integer.MAX_VALUE): Group? {
|
||||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
||||||
searchInfoString, SearchParameters().apply {
|
searchInfoString, SearchParameters().apply {
|
||||||
searchInTitles = false
|
searchInTitles = true
|
||||||
searchInUserNames = false
|
searchInUserNames = false
|
||||||
searchInPasswords = false
|
searchInPasswords = false
|
||||||
searchInUrls = true
|
searchInUrls = true
|
||||||
@@ -531,7 +574,7 @@ class Database {
|
|||||||
this.fileUri = uri
|
this.fileUri = uri
|
||||||
}
|
}
|
||||||
|
|
||||||
fun closeAndClear(filesDirectory: File? = null) {
|
fun clear(filesDirectory: File? = null) {
|
||||||
drawFactory.clearCache()
|
drawFactory.clearCache()
|
||||||
// Delete the cache of the database if present
|
// Delete the cache of the database if present
|
||||||
mDatabaseKDB?.clearCache()
|
mDatabaseKDB?.clearCache()
|
||||||
@@ -544,7 +587,10 @@ class Database {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to clear the directory cache.", e)
|
Log.e(TAG, "Unable to clear the directory cache.", e)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearAndClose(filesDirectory: File? = null) {
|
||||||
|
clear(filesDirectory)
|
||||||
this.mDatabaseKDB = null
|
this.mDatabaseKDB = null
|
||||||
this.mDatabaseKDBX = null
|
this.mDatabaseKDBX = null
|
||||||
this.fileUri = null
|
this.fileUri = null
|
||||||
|
|||||||
@@ -426,6 +426,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
entryInfo.icon = icon
|
entryInfo.icon = icon
|
||||||
entryInfo.username = username
|
entryInfo.username = username
|
||||||
entryInfo.password = password
|
entryInfo.password = password
|
||||||
|
entryInfo.creationTime = creationTime
|
||||||
|
entryInfo.modificationTime = lastModificationTime
|
||||||
entryInfo.expires = expires
|
entryInfo.expires = expires
|
||||||
entryInfo.expiryTime = expiryTime
|
entryInfo.expiryTime = expiryTime
|
||||||
entryInfo.url = url
|
entryInfo.url = url
|
||||||
@@ -456,6 +458,9 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
icon = newEntryInfo.icon
|
icon = newEntryInfo.icon
|
||||||
username = newEntryInfo.username
|
username = newEntryInfo.username
|
||||||
password = newEntryInfo.password
|
password = newEntryInfo.password
|
||||||
|
// Update date time, creation time stay as is
|
||||||
|
lastModificationTime = DateInstant()
|
||||||
|
lastAccessTime = DateInstant()
|
||||||
expires = newEntryInfo.expires
|
expires = newEntryInfo.expires
|
||||||
expiryTime = newEntryInfo.expiryTime
|
expiryTime = newEntryInfo.expiryTime
|
||||||
url = newEntryInfo.url
|
url = newEntryInfo.url
|
||||||
@@ -464,9 +469,6 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
database?.binaryPool?.let { binaryPool ->
|
database?.binaryPool?.let { binaryPool ->
|
||||||
addAttachments(binaryPool, newEntryInfo.attachments)
|
addAttachments(binaryPool, newEntryInfo.attachments)
|
||||||
}
|
}
|
||||||
// Update date time
|
|
||||||
lastAccessTime = DateInstant()
|
|
||||||
lastModificationTime = DateInstant()
|
|
||||||
|
|
||||||
database?.stopManageEntry(this)
|
database?.stopManageEntry(this)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,10 +163,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
finalKey = messageDigest.digest()
|
finalKey = messageDigest.digest()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createGroup(): GroupKDB {
|
override fun createGroup(): GroupKDB {
|
||||||
return GroupKDB()
|
return GroupKDB()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,10 +43,12 @@ import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
|||||||
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3
|
||||||
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.toHexString
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
import com.kunzisoft.keepass.utils.VariantDictionary
|
||||||
|
import org.apache.commons.codec.binary.Hex
|
||||||
import org.w3c.dom.Node
|
import org.w3c.dom.Node
|
||||||
import org.w3c.dom.Text
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -180,7 +182,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
when (oldCompression) {
|
when (oldCompression) {
|
||||||
CompressionAlgorithm.None -> {
|
CompressionAlgorithm.None -> {
|
||||||
when (newCompression) {
|
when (newCompression) {
|
||||||
CompressionAlgorithm.None -> {}
|
CompressionAlgorithm.None -> {
|
||||||
|
}
|
||||||
CompressionAlgorithm.GZip -> {
|
CompressionAlgorithm.GZip -> {
|
||||||
// Only in databaseV3.1, in databaseV4 the header is zipped during the save
|
// Only in databaseV3.1, in databaseV4 the header is zipped during the save
|
||||||
if (kdbxVersion.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) {
|
if (kdbxVersion.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) {
|
||||||
@@ -198,7 +201,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
CompressionAlgorithm.None -> {
|
CompressionAlgorithm.None -> {
|
||||||
decompressAllBinaries()
|
decompressAllBinaries()
|
||||||
}
|
}
|
||||||
CompressionAlgorithm.GZip -> {}
|
CompressionAlgorithm.GZip -> {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,36 +382,82 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
try {
|
try {
|
||||||
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
|
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
|
||||||
} catch (e : ParserConfigurationException) {
|
} catch (e : ParserConfigurationException) {
|
||||||
Log.e(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)", e)
|
Log.w(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)")
|
||||||
}
|
}
|
||||||
|
|
||||||
val documentBuilder = documentBuilderFactory.newDocumentBuilder()
|
val documentBuilder = documentBuilderFactory.newDocumentBuilder()
|
||||||
val doc = documentBuilder.parse(keyInputStream)
|
val doc = documentBuilder.parse(keyInputStream)
|
||||||
|
|
||||||
|
var xmlKeyFileVersion = 1F
|
||||||
|
|
||||||
val docElement = doc.documentElement
|
val docElement = doc.documentElement
|
||||||
if (docElement == null || !docElement.nodeName.equals(RootElementName, ignoreCase = true)) {
|
val keyFileChildNodes = docElement.childNodes
|
||||||
|
// <KeyFile> Root node
|
||||||
|
if (docElement == null
|
||||||
|
|| !docElement.nodeName.equals(XML_NODE_ROOT_NAME, ignoreCase = true)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
if (keyFileChildNodes.length < 2)
|
||||||
val children = docElement.childNodes
|
|
||||||
if (children.length < 2) {
|
|
||||||
return null
|
return null
|
||||||
}
|
for (keyFileChildPosition in 0 until keyFileChildNodes.length) {
|
||||||
|
val keyFileChildNode = keyFileChildNodes.item(keyFileChildPosition)
|
||||||
for (i in 0 until children.length) {
|
// <Meta>
|
||||||
val child = children.item(i)
|
if (keyFileChildNode.nodeName.equals(XML_NODE_META_NAME, ignoreCase = true)) {
|
||||||
|
val metaChildNodes = keyFileChildNode.childNodes
|
||||||
if (child.nodeName.equals(KeyElementName, ignoreCase = true)) {
|
for (metaChildPosition in 0 until metaChildNodes.length) {
|
||||||
val keyChildren = child.childNodes
|
val metaChildNode = metaChildNodes.item(metaChildPosition)
|
||||||
for (j in 0 until keyChildren.length) {
|
// <Version>
|
||||||
val keyChild = keyChildren.item(j)
|
if (metaChildNode.nodeName.equals(XML_NODE_VERSION_NAME, ignoreCase = true)) {
|
||||||
if (keyChild.nodeName.equals(KeyDataElementName, ignoreCase = true)) {
|
val versionChildNodes = metaChildNode.childNodes
|
||||||
val children2 = keyChild.childNodes
|
for (versionChildPosition in 0 until versionChildNodes.length) {
|
||||||
for (k in 0 until children2.length) {
|
val versionChildNode = versionChildNodes.item(versionChildPosition)
|
||||||
val text = children2.item(k)
|
if (versionChildNode.nodeType == Node.TEXT_NODE) {
|
||||||
if (text.nodeType == Node.TEXT_NODE) {
|
val versionText = versionChildNode.textContent.removeSpaceChars()
|
||||||
val txt = text as Text
|
try {
|
||||||
return Base64.decode(txt.nodeValue, BASE_64_FLAG)
|
xmlKeyFileVersion = versionText.toFloat()
|
||||||
|
Log.i(TAG, "Reading XML KeyFile version : $xmlKeyFileVersion")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "XML Keyfile version cannot be read : $versionText")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// <Key>
|
||||||
|
if (keyFileChildNode.nodeName.equals(XML_NODE_KEY_NAME, ignoreCase = true)) {
|
||||||
|
val keyChildNodes = keyFileChildNode.childNodes
|
||||||
|
for (keyChildPosition in 0 until keyChildNodes.length) {
|
||||||
|
val keyChildNode = keyChildNodes.item(keyChildPosition)
|
||||||
|
// <Data>
|
||||||
|
if (keyChildNode.nodeName.equals(XML_NODE_DATA_NAME, ignoreCase = true)) {
|
||||||
|
var hashString : String? = null
|
||||||
|
if (keyChildNode.hasAttributes()) {
|
||||||
|
val dataNodeAttributes = keyChildNode.attributes
|
||||||
|
hashString = dataNodeAttributes
|
||||||
|
.getNamedItem(XML_ATTRIBUTE_DATA_HASH).nodeValue
|
||||||
|
}
|
||||||
|
val dataChildNodes = keyChildNode.childNodes
|
||||||
|
for (dataChildPosition in 0 until dataChildNodes.length) {
|
||||||
|
val dataChildNode = dataChildNodes.item(dataChildPosition)
|
||||||
|
if (dataChildNode.nodeType == Node.TEXT_NODE) {
|
||||||
|
val dataString = dataChildNode.textContent.removeSpaceChars()
|
||||||
|
when (xmlKeyFileVersion) {
|
||||||
|
1F -> {
|
||||||
|
// No hash in KeyFile XML version 1
|
||||||
|
return Base64.decode(dataString, BASE_64_FLAG)
|
||||||
|
}
|
||||||
|
2F -> {
|
||||||
|
return if (hashString != null
|
||||||
|
&& checkKeyFileHash(dataString, hashString)) {
|
||||||
|
Log.i(TAG, "Successful key file hash check.")
|
||||||
|
Hex.decodeHex(dataString.toCharArray())
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Unable to check the hash of the key file.")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,10 +467,26 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun checkKeyFileHash(data: String, hash: String): Boolean {
|
||||||
|
val digest: MessageDigest?
|
||||||
|
var success = false
|
||||||
|
try {
|
||||||
|
digest = MessageDigest.getInstance("SHA-256")
|
||||||
|
digest?.reset()
|
||||||
|
// hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key.
|
||||||
|
val dataDigest = digest.digest(Hex.decodeHex(data.toCharArray()))
|
||||||
|
.copyOfRange(0, 4)
|
||||||
|
.toHexString()
|
||||||
|
success = dataDigest == hash
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
override fun newGroupId(): NodeIdUUID {
|
override fun newGroupId(): NodeIdUUID {
|
||||||
var newId: NodeIdUUID
|
var newId: NodeIdUUID
|
||||||
do {
|
do {
|
||||||
@@ -634,11 +700,12 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
private const val DEFAULT_HISTORY_MAX_ITEMS = 10 // -1 unlimited
|
private const val DEFAULT_HISTORY_MAX_ITEMS = 10 // -1 unlimited
|
||||||
private const val DEFAULT_HISTORY_MAX_SIZE = (6 * 1024 * 1024).toLong() // -1 unlimited
|
private const val DEFAULT_HISTORY_MAX_SIZE = (6 * 1024 * 1024).toLong() // -1 unlimited
|
||||||
|
|
||||||
private const val RootElementName = "KeyFile"
|
private const val XML_NODE_ROOT_NAME = "KeyFile"
|
||||||
//private const val MetaElementName = "Meta";
|
private const val XML_NODE_META_NAME = "Meta"
|
||||||
//private const val VersionElementName = "Version";
|
private const val XML_NODE_VERSION_NAME = "Version"
|
||||||
private const val KeyElementName = "Key"
|
private const val XML_NODE_KEY_NAME = "Key"
|
||||||
private const val KeyDataElementName = "Data"
|
private const val XML_NODE_DATA_NAME = "Data"
|
||||||
|
private const val XML_ATTRIBUTE_DATA_HASH = "Hash"
|
||||||
|
|
||||||
const val BASE_64_FLAG = Base64.NO_WRAP
|
const val BASE_64_FLAG = Base64.NO_WRAP
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
|||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
import java.io.*
|
import org.apache.commons.codec.binary.Hex
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.UnsupportedEncodingException
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -124,42 +128,35 @@ abstract class DatabaseVersioned<
|
|||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
|
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
|
||||||
|
|
||||||
val keyByteArrayOutputStream = ByteArrayOutputStream()
|
val keyData = keyInputStream.readBytes()
|
||||||
keyInputStream.copyTo(keyByteArrayOutputStream)
|
|
||||||
val keyData = keyByteArrayOutputStream.toByteArray()
|
|
||||||
|
|
||||||
val keyByteArrayInputStream = ByteArrayInputStream(keyData)
|
// Check XML key file
|
||||||
val key = loadXmlKeyFile(keyByteArrayInputStream)
|
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
|
||||||
if (key != null) {
|
if (xmlKeyByteArray != null) {
|
||||||
return key
|
return xmlKeyByteArray
|
||||||
}
|
}
|
||||||
|
|
||||||
when (keyData.size.toLong()) {
|
// Check 32 bytes key file
|
||||||
32L -> return keyData
|
when (keyData.size) {
|
||||||
64L -> try {
|
32 -> return keyData
|
||||||
return hexStringToByteArray(String(keyData))
|
64 -> try {
|
||||||
} catch (e: IndexOutOfBoundsException) {
|
return Hex.decodeHex(String(keyData).toCharArray())
|
||||||
|
} catch (ignoredException: Exception) {
|
||||||
// Key is not base 64, treat it as binary data
|
// Key is not base 64, treat it as binary data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val messageDigest: MessageDigest
|
// Hash file as binary data
|
||||||
try {
|
try {
|
||||||
messageDigest = MessageDigest.getInstance("SHA-256")
|
return MessageDigest.getInstance("SHA-256").digest(keyData)
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
throw IOException("SHA-256 not supported")
|
throw IOException("SHA-256 not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
messageDigest.update(keyData)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println(e.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
return messageDigest.digest()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray?
|
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
open fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
open fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
||||||
if (password == null && !containsKeyFile)
|
if (password == null && !containsKeyFile)
|
||||||
@@ -391,16 +388,5 @@ abstract class DatabaseVersioned<
|
|||||||
private const val TAG = "DatabaseVersioned"
|
private const val TAG = "DatabaseVersioned"
|
||||||
|
|
||||||
val UUID_ZERO = UUID(0, 0)
|
val UUID_ZERO = UUID(0, 0)
|
||||||
|
|
||||||
fun hexStringToByteArray(s: String): ByteArray {
|
|
||||||
val len = s.length
|
|
||||||
val data = ByteArray(len / 2)
|
|
||||||
var i = 0
|
|
||||||
while (i < len) {
|
|
||||||
data[i / 2] = ((Character.digit(s[i], 16) shl 4) + Character.digit(s[i + 1], 16)).toByte()
|
|
||||||
i += 2
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,20 +43,12 @@ abstract class DatabaseException : Exception {
|
|||||||
}
|
}
|
||||||
|
|
||||||
open class LoadDatabaseException : DatabaseException {
|
open class LoadDatabaseException : DatabaseException {
|
||||||
|
|
||||||
@StringRes
|
@StringRes
|
||||||
override var errorId: Int = R.string.error_load_database
|
override var errorId: Int = R.string.error_load_database
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
constructor(throwable: Throwable) : super(throwable)
|
constructor(throwable: Throwable) : super(throwable)
|
||||||
}
|
}
|
||||||
|
|
||||||
class ArcFourDatabaseException : LoadDatabaseException {
|
|
||||||
@StringRes
|
|
||||||
override var errorId: Int = R.string.error_arc4
|
|
||||||
constructor() : super()
|
|
||||||
constructor(exception: Throwable) : super(exception)
|
|
||||||
}
|
|
||||||
|
|
||||||
class FileNotFoundDatabaseException : LoadDatabaseException {
|
class FileNotFoundDatabaseException : LoadDatabaseException {
|
||||||
@StringRes
|
@StringRes
|
||||||
override var errorId: Int = R.string.file_not_found_content
|
override var errorId: Int = R.string.file_not_found_content
|
||||||
@@ -67,7 +59,6 @@ class FileNotFoundDatabaseException : LoadDatabaseException {
|
|||||||
class InvalidAlgorithmDatabaseException : LoadDatabaseException {
|
class InvalidAlgorithmDatabaseException : LoadDatabaseException {
|
||||||
@StringRes
|
@StringRes
|
||||||
override var errorId: Int = R.string.invalid_algorithm
|
override var errorId: Int = R.string.invalid_algorithm
|
||||||
|
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
constructor(exception: Throwable) : super(exception)
|
constructor(exception: Throwable) : super(exception)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ abstract class DatabaseInput<PwDb : DatabaseVersioned<*, *, *, *>>
|
|||||||
abstract fun openDatabase(databaseInputStream: InputStream,
|
abstract fun openDatabase(databaseInputStream: InputStream,
|
||||||
password: String?,
|
password: String?,
|
||||||
keyInputStream: InputStream?,
|
keyInputStream: InputStream?,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?): PwDb
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
|
fixDuplicateUUID: Boolean = false): PwDb
|
||||||
|
|
||||||
|
|
||||||
|
@Throws(LoadDatabaseException::class)
|
||||||
|
abstract fun openDatabase(databaseInputStream: InputStream,
|
||||||
|
masterKey: ByteArray,
|
||||||
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
|
fixDuplicateUUID: Boolean = false): PwDb
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,7 @@ import javax.crypto.spec.SecretKeySpec
|
|||||||
/**
|
/**
|
||||||
* Load a KDB database file.
|
* Load a KDB database file.
|
||||||
*/
|
*/
|
||||||
class DatabaseInputKDB(cacheDirectory: File,
|
class DatabaseInputKDB(cacheDirectory: File)
|
||||||
private val fixDuplicateUUID: Boolean = false)
|
|
||||||
: DatabaseInput<DatabaseKDB>(cacheDirectory) {
|
: DatabaseInput<DatabaseKDB>(cacheDirectory) {
|
||||||
|
|
||||||
private lateinit var mDatabaseToOpen: DatabaseKDB
|
private lateinit var mDatabaseToOpen: DatabaseKDB
|
||||||
@@ -55,7 +54,28 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
override fun openDatabase(databaseInputStream: InputStream,
|
override fun openDatabase(databaseInputStream: InputStream,
|
||||||
password: String?,
|
password: String?,
|
||||||
keyInputStream: InputStream?,
|
keyInputStream: InputStream?,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB {
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
|
fixDuplicateUUID: Boolean): DatabaseKDB {
|
||||||
|
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||||
|
mDatabaseToOpen.retrieveMasterKey(password, keyInputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(LoadDatabaseException::class)
|
||||||
|
override fun openDatabase(databaseInputStream: InputStream,
|
||||||
|
masterKey: ByteArray,
|
||||||
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
|
fixDuplicateUUID: Boolean): DatabaseKDB {
|
||||||
|
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||||
|
mDatabaseToOpen.masterKey = masterKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(LoadDatabaseException::class)
|
||||||
|
private fun openDatabase(databaseInputStream: InputStream,
|
||||||
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
|
fixDuplicateUUID: Boolean,
|
||||||
|
assignMasterKey: (() -> Unit)? = null): DatabaseKDB {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load entire file, most of it's encrypted.
|
// Load entire file, most of it's encrypted.
|
||||||
@@ -84,7 +104,7 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
mDatabaseToOpen = DatabaseKDB()
|
mDatabaseToOpen = DatabaseKDB()
|
||||||
|
|
||||||
mDatabaseToOpen.changeDuplicateId = fixDuplicateUUID
|
mDatabaseToOpen.changeDuplicateId = fixDuplicateUUID
|
||||||
mDatabaseToOpen.retrieveMasterKey(password, keyInputStream)
|
assignMasterKey?.invoke()
|
||||||
|
|
||||||
// Select algorithm
|
// Select algorithm
|
||||||
when {
|
when {
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||||
import com.kunzisoft.keepass.crypto.StreamCipherFactory
|
import com.kunzisoft.keepass.crypto.StreamCipherFactory
|
||||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
||||||
@@ -37,7 +38,6 @@ 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.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.database.exception.*
|
import com.kunzisoft.keepass.database.exception.*
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||||
@@ -63,8 +63,7 @@ import javax.crypto.Cipher
|
|||||||
import javax.crypto.CipherInputStream
|
import javax.crypto.CipherInputStream
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class DatabaseInputKDBX(cacheDirectory: File,
|
class DatabaseInputKDBX(cacheDirectory: File)
|
||||||
private val fixDuplicateUUID: Boolean = false)
|
|
||||||
: DatabaseInput<DatabaseKDBX>(cacheDirectory) {
|
: DatabaseInput<DatabaseKDBX>(cacheDirectory) {
|
||||||
|
|
||||||
private var randomStream: StreamCipher? = null
|
private var randomStream: StreamCipher? = null
|
||||||
@@ -98,12 +97,30 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
override fun openDatabase(databaseInputStream: InputStream,
|
override fun openDatabase(databaseInputStream: InputStream,
|
||||||
password: String?,
|
password: String?,
|
||||||
keyInputStream: InputStream?,
|
keyInputStream: InputStream?,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX {
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
|
fixDuplicateUUID: Boolean): DatabaseKDBX {
|
||||||
|
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||||
|
mDatabase.retrieveMasterKey(password, keyInputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(LoadDatabaseException::class)
|
||||||
|
override fun openDatabase(databaseInputStream: InputStream,
|
||||||
|
masterKey: ByteArray,
|
||||||
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
|
fixDuplicateUUID: Boolean): DatabaseKDBX {
|
||||||
|
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||||
|
mDatabase.masterKey = masterKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(LoadDatabaseException::class)
|
||||||
|
private fun openDatabase(databaseInputStream: InputStream,
|
||||||
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
|
fixDuplicateUUID: Boolean,
|
||||||
|
assignMasterKey: (() -> Unit)? = null): DatabaseKDBX {
|
||||||
try {
|
try {
|
||||||
// TODO performance
|
|
||||||
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
|
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
|
||||||
|
|
||||||
mDatabase = DatabaseKDBX()
|
mDatabase = DatabaseKDBX()
|
||||||
|
|
||||||
mDatabase.changeDuplicateId = fixDuplicateUUID
|
mDatabase.changeDuplicateId = fixDuplicateUUID
|
||||||
@@ -116,9 +133,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
hashOfHeader = headerAndHash.hash
|
hashOfHeader = headerAndHash.hash
|
||||||
val pbHeader = headerAndHash.header
|
val pbHeader = headerAndHash.header
|
||||||
|
|
||||||
mDatabase.retrieveMasterKey(password, keyInputStream)
|
assignMasterKey?.invoke()
|
||||||
mDatabase.makeFinalKey(header.masterSeed)
|
mDatabase.makeFinalKey(header.masterSeed)
|
||||||
// TODO performance
|
|
||||||
|
|
||||||
progressTaskUpdater?.updateMessage(R.string.decrypting_db)
|
progressTaskUpdater?.updateMessage(R.string.decrypting_db)
|
||||||
val engine: CipherEngine
|
val engine: CipherEngine
|
||||||
@@ -185,10 +201,10 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
loadInnerHeader(inputStreamXml, header)
|
loadInnerHeader(inputStreamXml, header)
|
||||||
}
|
}
|
||||||
|
|
||||||
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
|
try {
|
||||||
|
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
|
||||||
if (randomStream == null) {
|
} catch (e: Exception) {
|
||||||
throw ArcFourDatabaseException()
|
throw LoadDatabaseException(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
readDocumentStreamed(createPullParser(inputStreamXml))
|
readDocumentStreamed(createPullParser(inputStreamXml))
|
||||||
@@ -436,8 +452,6 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
val strData = readString(xpp)
|
val strData = readString(xpp)
|
||||||
if (strData.isNotEmpty()) {
|
if (strData.isNotEmpty()) {
|
||||||
customIconData = Base64.decode(strData, BASE_64_FLAG)
|
customIconData = Base64.decode(strData, BASE_64_FLAG)
|
||||||
} else {
|
|
||||||
assert(false)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
readUnknown(xpp)
|
readUnknown(xpp)
|
||||||
@@ -958,7 +972,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
// Create empty binary if not retrieved in pool
|
// Create empty binary if not retrieved in pool
|
||||||
if (binaryRetrieve == null) {
|
if (binaryRetrieve == null) {
|
||||||
binaryRetrieve = mDatabase.buildNewBinary(cacheDirectory,
|
binaryRetrieve = mDatabase.buildNewBinary(cacheDirectory,
|
||||||
compression = false, protection = true, binaryPoolId = id)
|
compression = false, protection = false, binaryPoolId = id)
|
||||||
}
|
}
|
||||||
return binaryRetrieve
|
return binaryRetrieve
|
||||||
}
|
}
|
||||||
@@ -1024,29 +1038,20 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
return xpp.safeNextText()
|
return xpp.safeNextText()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(XmlPullParserException::class, IOException::class)
|
|
||||||
private fun readBase64String(xpp: XmlPullParser): ByteArray {
|
|
||||||
|
|
||||||
//readNextNode = false;
|
|
||||||
Base64.decode(xpp.safeNextText(), BASE_64_FLAG)?.let { data ->
|
|
||||||
val plainText = ByteArray(data.size)
|
|
||||||
randomStream?.processBytes(data, 0, data.size, plainText, 0)
|
|
||||||
return plainText
|
|
||||||
}
|
|
||||||
return ByteArray(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(XmlPullParserException::class, IOException::class)
|
@Throws(XmlPullParserException::class, IOException::class)
|
||||||
private fun readProtectedBase64String(xpp: XmlPullParser): ByteArray? {
|
private fun readProtectedBase64String(xpp: XmlPullParser): ByteArray? {
|
||||||
//(xpp.getEventType() == XmlPullParser.START_TAG);
|
|
||||||
|
|
||||||
if (xpp.attributeCount > 0) {
|
if (xpp.attributeCount > 0) {
|
||||||
val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
|
val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
|
||||||
if (protect != null && protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)) {
|
if (protect != null && protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)) {
|
||||||
return readBase64String(xpp)
|
Base64.decode(xpp.safeNextText(), BASE_64_FLAG)?.let { data ->
|
||||||
|
val plainText = ByteArray(data.size)
|
||||||
|
randomStream?.processBytes(data, 0, data.size, plainText, 0)
|
||||||
|
return plainText
|
||||||
|
}
|
||||||
|
return ByteArray(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +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.file.output
|
|
||||||
|
|
||||||
open class DatabaseHeaderOutput {
|
|
||||||
var hashOfHeader: ByteArray? = null
|
|
||||||
protected set
|
|
||||||
}
|
|
||||||
@@ -40,13 +40,16 @@ import javax.crypto.spec.SecretKeySpec
|
|||||||
class DatabaseHeaderOutputKDBX @Throws(DatabaseOutputException::class)
|
class DatabaseHeaderOutputKDBX @Throws(DatabaseOutputException::class)
|
||||||
constructor(private val databaseKDBX: DatabaseKDBX,
|
constructor(private val databaseKDBX: DatabaseKDBX,
|
||||||
private val header: DatabaseHeaderKDBX,
|
private val header: DatabaseHeaderKDBX,
|
||||||
outputStream: OutputStream) : DatabaseHeaderOutput() {
|
outputStream: OutputStream) {
|
||||||
|
|
||||||
private val los: LittleEndianDataOutputStream
|
private val los: LittleEndianDataOutputStream
|
||||||
private val mos: MacOutputStream
|
private val mos: MacOutputStream
|
||||||
private val dos: DigestOutputStream
|
private val dos: DigestOutputStream
|
||||||
lateinit var headerHmac: ByteArray
|
lateinit var headerHmac: ByteArray
|
||||||
|
|
||||||
|
var hashOfHeader: ByteArray? = null
|
||||||
|
private set
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
val md: MessageDigest
|
val md: MessageDigest
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDroid 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.
|
|
||||||
*
|
|
||||||
* KeePassDroid 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 KeePassDroid. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.database.file.output
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BUFFER_SIZE_BYTES
|
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
|
||||||
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
|
|
||||||
import com.kunzisoft.keepass.stream.readBytes
|
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.OutputStream
|
|
||||||
import kotlin.experimental.or
|
|
||||||
|
|
||||||
class DatabaseInnerHeaderOutputKDBX(private val database: DatabaseKDBX,
|
|
||||||
private val header: DatabaseHeaderKDBX,
|
|
||||||
outputStream: OutputStream) {
|
|
||||||
|
|
||||||
private val dataOutputStream: LittleEndianDataOutputStream = LittleEndianDataOutputStream(outputStream)
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun output() {
|
|
||||||
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID)
|
|
||||||
dataOutputStream.writeInt(4)
|
|
||||||
if (header.innerRandomStream == null)
|
|
||||||
throw IOException("Can't write innerRandomStream")
|
|
||||||
dataOutputStream.writeUInt(header.innerRandomStream!!.id)
|
|
||||||
|
|
||||||
val streamKeySize = header.innerRandomStreamKey.size
|
|
||||||
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey)
|
|
||||||
dataOutputStream.writeInt(streamKeySize)
|
|
||||||
dataOutputStream.write(header.innerRandomStreamKey)
|
|
||||||
|
|
||||||
database.binaryPool.doForEachOrderedBinary { _, keyBinary ->
|
|
||||||
val protectedBinary = keyBinary.binary
|
|
||||||
// Force decompression to add binary in header
|
|
||||||
protectedBinary.decompress()
|
|
||||||
// Write type binary
|
|
||||||
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
|
|
||||||
// Write size
|
|
||||||
dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length() + 1))
|
|
||||||
// Write protected flag
|
|
||||||
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
|
|
||||||
if (protectedBinary.isProtected) {
|
|
||||||
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
|
||||||
}
|
|
||||||
dataOutputStream.writeByte(flag)
|
|
||||||
|
|
||||||
protectedBinary.getInputDataStream().use { inputStream ->
|
|
||||||
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
|
||||||
dataOutputStream.write(buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader)
|
|
||||||
dataOutputStream.writeInt(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,7 +26,7 @@ import java.io.OutputStream
|
|||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
|
||||||
abstract class DatabaseOutput<Header : DatabaseHeader> protected constructor(protected var mOS: OutputStream) {
|
abstract class DatabaseOutput<Header : DatabaseHeader> protected constructor(protected var mOutputStream: OutputStream) {
|
||||||
|
|
||||||
@Throws(DatabaseOutputException::class)
|
@Throws(DatabaseOutputException::class)
|
||||||
protected open fun setIVs(header: Header): SecureRandom {
|
protected open fun setIVs(header: Header): SecureRandom {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
// 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
|
||||||
sortGroupsForOutput()
|
sortGroupsForOutput()
|
||||||
|
|
||||||
val header = outputHeader(mOS)
|
val header = outputHeader(mOutputStream)
|
||||||
|
|
||||||
val finalKey = getFinalKey(header)
|
val finalKey = getFinalKey(header)
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
cipher.init(Cipher.ENCRYPT_MODE,
|
cipher.init(Cipher.ENCRYPT_MODE,
|
||||||
SecretKeySpec(finalKey, "AES"),
|
SecretKeySpec(finalKey, "AES"),
|
||||||
IvParameterSpec(header.encryptionIV))
|
IvParameterSpec(header.encryptionIV))
|
||||||
val cos = CipherOutputStream(mOS, cipher)
|
val cos = CipherOutputStream(mOutputStream, cipher)
|
||||||
val bos = BufferedOutputStream(cos)
|
val bos = BufferedOutputStream(cos)
|
||||||
outputPlanGroupAndEntries(bos)
|
outputPlanGroupAndEntries(bos)
|
||||||
bos.flush()
|
bos.flush()
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
|||||||
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.node.NodeKDBXInterface
|
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
|
||||||
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||||
@@ -47,6 +46,7 @@ import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
|||||||
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
|
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
|
||||||
import com.kunzisoft.keepass.database.file.DateKDBXUtil
|
import com.kunzisoft.keepass.database.file.DateKDBXUtil
|
||||||
import com.kunzisoft.keepass.stream.*
|
import com.kunzisoft.keepass.stream.*
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
import org.bouncycastle.crypto.StreamCipher
|
import org.bouncycastle.crypto.StreamCipher
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import org.xmlpull.v1.XmlSerializer
|
import org.xmlpull.v1.XmlSerializer
|
||||||
@@ -58,6 +58,7 @@ import java.util.*
|
|||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.CipherOutputStream
|
import javax.crypto.CipherOutputStream
|
||||||
|
import kotlin.experimental.or
|
||||||
|
|
||||||
|
|
||||||
class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||||
@@ -81,20 +82,19 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
throw DatabaseOutputException("No such cipher", e)
|
throw DatabaseOutputException("No such cipher", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
header = outputHeader(mOS)
|
header = outputHeader(mOutputStream)
|
||||||
|
|
||||||
val osPlain: OutputStream
|
val osPlain: OutputStream
|
||||||
osPlain = if (header!!.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
|
osPlain = if (header!!.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
|
||||||
val cos = attachStreamEncryptor(header!!, mOS)
|
val cos = attachStreamEncryptor(header!!, mOutputStream)
|
||||||
cos.write(header!!.streamStartBytes)
|
cos.write(header!!.streamStartBytes)
|
||||||
|
|
||||||
HashedBlockOutputStream(cos)
|
HashedBlockOutputStream(cos)
|
||||||
} else {
|
} else {
|
||||||
mOS.write(hashOfHeader!!)
|
mOutputStream.write(hashOfHeader!!)
|
||||||
mOS.write(headerHmac!!)
|
mOutputStream.write(headerHmac!!)
|
||||||
|
|
||||||
|
attachStreamEncryptor(header!!, HmacBlockOutputStream(mOutputStream, mDatabaseKDBX.hmacKey!!))
|
||||||
attachStreamEncryptor(header!!, HmacBlockOutputStream(mOS, mDatabaseKDBX.hmacKey!!))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val osXml: OutputStream
|
val osXml: OutputStream
|
||||||
@@ -105,8 +105,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (header!!.version.toKotlinLong() >= DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
|
if (header!!.version.toKotlinLong() >= DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
|
||||||
val ihOut = DatabaseInnerHeaderOutputKDBX(mDatabaseKDBX, header!!, osXml)
|
outputInnerHeader(mDatabaseKDBX, header!!, osXml)
|
||||||
ihOut.output()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
outputDatabase(osXml)
|
outputDatabase(osXml)
|
||||||
@@ -122,6 +121,49 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun outputInnerHeader(database: DatabaseKDBX,
|
||||||
|
header: DatabaseHeaderKDBX,
|
||||||
|
outputStream: OutputStream) {
|
||||||
|
val dataOutputStream = LittleEndianDataOutputStream(outputStream)
|
||||||
|
|
||||||
|
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID)
|
||||||
|
dataOutputStream.writeInt(4)
|
||||||
|
if (header.innerRandomStream == null)
|
||||||
|
throw IOException("Can't write innerRandomStream")
|
||||||
|
dataOutputStream.writeUInt(header.innerRandomStream!!.id)
|
||||||
|
|
||||||
|
val streamKeySize = header.innerRandomStreamKey.size
|
||||||
|
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey)
|
||||||
|
dataOutputStream.writeInt(streamKeySize)
|
||||||
|
dataOutputStream.write(header.innerRandomStreamKey)
|
||||||
|
|
||||||
|
database.binaryPool.doForEachOrderedBinary { _, keyBinary ->
|
||||||
|
val protectedBinary = keyBinary.binary
|
||||||
|
// Force decompression to add binary in header
|
||||||
|
protectedBinary.decompress()
|
||||||
|
// Write type binary
|
||||||
|
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
|
||||||
|
// Write size
|
||||||
|
dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length() + 1))
|
||||||
|
// Write protected flag
|
||||||
|
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
|
||||||
|
if (protectedBinary.isProtected) {
|
||||||
|
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
||||||
|
}
|
||||||
|
dataOutputStream.writeByte(flag)
|
||||||
|
|
||||||
|
protectedBinary.getInputDataStream().use { inputStream ->
|
||||||
|
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||||
|
dataOutputStream.write(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader)
|
||||||
|
dataOutputStream.writeInt(0)
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
private fun outputDatabase(outputStream: OutputStream) {
|
private fun outputDatabase(outputStream: OutputStream) {
|
||||||
|
|
||||||
@@ -282,9 +324,10 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
}
|
}
|
||||||
random.nextBytes(header.innerRandomStreamKey)
|
random.nextBytes(header.innerRandomStreamKey)
|
||||||
|
|
||||||
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
|
try {
|
||||||
if (randomStream == null) {
|
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
|
||||||
throw DatabaseOutputException("Invalid random cipher")
|
} catch (e: Exception) {
|
||||||
|
throw DatabaseOutputException(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
|
if (header.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
|
||||||
@@ -420,41 +463,56 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
writeObject(name, String(Base64.encode(data, BASE_64_FLAG)))
|
writeObject(name, String(Base64.encode(data, BASE_64_FLAG)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Normally used by a single entry but obsolete because binaries are in meta tag with kdbx3.1-
|
||||||
|
// or in file header with kdbx4
|
||||||
|
// binary.isProtected attribute is not used to create the XML
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
private fun writeBinary(binary : BinaryAttachment) {
|
private fun writeEntryBinary(binary : BinaryAttachment) {
|
||||||
val binaryLength = binary.length()
|
if (binary.length() > 0) {
|
||||||
if (binaryLength > 0) {
|
|
||||||
if (binary.isProtected) {
|
if (binary.isProtected) {
|
||||||
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
|
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
|
||||||
|
binary.getInputDataStream().use { inputStream ->
|
||||||
binary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||||
val encoded = ByteArray(buffer.size)
|
val encoded = ByteArray(buffer.size)
|
||||||
randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0)
|
randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0)
|
||||||
val charArray = String(Base64.encode(encoded, BASE_64_FLAG)).toCharArray()
|
xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
|
||||||
xml.text(charArray, 0, charArray.size)
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (binary.isCompressed) {
|
|
||||||
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
|
|
||||||
}
|
|
||||||
// Write the XML
|
// Write the XML
|
||||||
binary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
binary.getInputDataStream().use { inputStream ->
|
||||||
val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray()
|
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||||
xml.text(charArray, 0, charArray.size)
|
xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Only uses with kdbx3.1 to write binaries in meta tag
|
||||||
|
// With kdbx4, don't use this method because binaries are in header file
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
private fun writeMetaBinaries() {
|
private fun writeMetaBinaries() {
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
|
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
|
||||||
|
|
||||||
// Use indexes because necessarily in DatabaseV4 (binary header ref is the order)
|
// Use indexes because necessarily (binary header ref is the order)
|
||||||
mDatabaseKDBX.binaryPool.doForEachOrderedBinary { index, keyBinary ->
|
mDatabaseKDBX.binaryPool.doForEachOrderedBinary { index, keyBinary ->
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
||||||
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
|
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
|
||||||
writeBinary(keyBinary.binary)
|
val binary = keyBinary.binary
|
||||||
|
if (binary.length() > 0) {
|
||||||
|
if (binary.isCompressed) {
|
||||||
|
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
|
||||||
|
}
|
||||||
|
// Write the XML
|
||||||
|
binary.getInputDataStream().use { inputStream ->
|
||||||
|
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||||
|
xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,13 +581,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
|
|
||||||
if (protect) {
|
if (protect) {
|
||||||
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
|
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
|
||||||
|
val data = value.toString().toByteArray()
|
||||||
val data = value.toString().toByteArray(charset("UTF-8"))
|
val dataLength = data.size
|
||||||
val valLength = data.size
|
if (data.isNotEmpty()) {
|
||||||
|
val encoded = ByteArray(dataLength)
|
||||||
if (valLength > 0) {
|
randomStream!!.processBytes(data, 0, dataLength, encoded, 0)
|
||||||
val encoded = ByteArray(valLength)
|
|
||||||
randomStream!!.processBytes(data, 0, valLength, encoded, 0)
|
|
||||||
xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
|
xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -26,9 +26,12 @@ import android.graphics.*
|
|||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import androidx.core.widget.ImageViewCompat
|
import androidx.core.widget.ImageViewCompat
|
||||||
@@ -87,6 +90,22 @@ class IconDrawableFactory {
|
|||||||
remoteViews.setImageViewBitmap(imageId, bitmap)
|
remoteViews.setImageViewBitmap(imageId, bitmap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to assign a drawable to a icon and tint it
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun assignDrawableToIcon(superDrawable: SuperDrawable,
|
||||||
|
tintColor: Int = Color.BLACK): Icon {
|
||||||
|
val bitmap = superDrawable.drawable.toBitmap()
|
||||||
|
// Tint bitmap if it's not a custom icon
|
||||||
|
if (superDrawable.tintable && bitmap.isMutable) {
|
||||||
|
Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply {
|
||||||
|
colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return Icon.createWithBitmap(bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the [SuperDrawable] [icon] (from cache, or build it and add it to the cache if not exists yet), then [tint] it with [tintColor] if needed
|
* Get the [SuperDrawable] [icon] (from cache, or build it and add it to the cache if not exists yet), then [tint] it with [tintColor] if needed
|
||||||
*/
|
*/
|
||||||
@@ -309,3 +328,22 @@ fun RemoteViews.assignDatabaseIcon(context: Context,
|
|||||||
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun createIconFromDatabaseIcon(context: Context,
|
||||||
|
iconFactory: IconDrawableFactory,
|
||||||
|
icon: IconImage,
|
||||||
|
tintColor: Int = Color.BLACK): Icon? {
|
||||||
|
try {
|
||||||
|
return iconFactory.assignDrawableToIcon(
|
||||||
|
iconFactory.getIconSuperDrawable(context,
|
||||||
|
icon,
|
||||||
|
24,
|
||||||
|
true,
|
||||||
|
tintColor),
|
||||||
|
tintColor)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
|||||||
if (entryInfoKey != null) {
|
if (entryInfoKey != null) {
|
||||||
currentInputConnection.commitText(entryInfoKey!!.password, 1)
|
currentInputConnection.commitText(entryInfoKey!!.password, 1)
|
||||||
}
|
}
|
||||||
actionGoAutomatically()
|
val otpFieldExists = entryInfoKey?.containsCustomField(OTP_TOKEN_FIELD) ?: false
|
||||||
|
actionGoAutomatically(!otpFieldExists)
|
||||||
}
|
}
|
||||||
KEY_OTP -> {
|
KEY_OTP -> {
|
||||||
if (entryInfoKey != null) {
|
if (entryInfoKey != null) {
|
||||||
@@ -280,10 +281,11 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
|||||||
currentInputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB))
|
currentInputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun actionGoAutomatically() {
|
private fun actionGoAutomatically(switchToPreviousKeyboardIfAllowed: Boolean = true) {
|
||||||
if (PreferencesUtil.isAutoGoActionEnable(this)) {
|
if (PreferencesUtil.isAutoGoActionEnable(this)) {
|
||||||
currentInputConnection.performEditorAction(EditorInfo.IME_ACTION_GO)
|
currentInputConnection.performEditorAction(EditorInfo.IME_ACTION_GO)
|
||||||
if (PreferencesUtil.isKeyboardPreviousFillInEnable(this)) {
|
if (switchToPreviousKeyboardIfAllowed
|
||||||
|
&& PreferencesUtil.isKeyboardPreviousFillInEnable(this)) {
|
||||||
switchToPreviousKeyboard()
|
switchToPreviousKeyboard()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ class EntryInfo : Parcelable {
|
|||||||
var icon: IconImage = IconImageStandard()
|
var icon: IconImage = IconImageStandard()
|
||||||
var username: String = ""
|
var username: String = ""
|
||||||
var password: String = ""
|
var password: String = ""
|
||||||
|
var creationTime: DateInstant = DateInstant()
|
||||||
|
var modificationTime: DateInstant = DateInstant()
|
||||||
var expires: Boolean = false
|
var expires: Boolean = false
|
||||||
var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH
|
var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH
|
||||||
var url: String = ""
|
var url: String = ""
|
||||||
@@ -55,6 +57,8 @@ class EntryInfo : Parcelable {
|
|||||||
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
|
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
|
||||||
username = parcel.readString() ?: username
|
username = parcel.readString() ?: username
|
||||||
password = parcel.readString() ?: password
|
password = parcel.readString() ?: password
|
||||||
|
creationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: creationTime
|
||||||
|
modificationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: modificationTime
|
||||||
expires = parcel.readInt() != 0
|
expires = parcel.readInt() != 0
|
||||||
expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime
|
expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime
|
||||||
url = parcel.readString() ?: url
|
url = parcel.readString() ?: url
|
||||||
@@ -74,6 +78,8 @@ class EntryInfo : Parcelable {
|
|||||||
parcel.writeParcelable(icon, flags)
|
parcel.writeParcelable(icon, flags)
|
||||||
parcel.writeString(username)
|
parcel.writeString(username)
|
||||||
parcel.writeString(password)
|
parcel.writeString(password)
|
||||||
|
parcel.writeParcelable(creationTime, flags)
|
||||||
|
parcel.writeParcelable(modificationTime, flags)
|
||||||
parcel.writeInt(if (expires) 1 else 0)
|
parcel.writeInt(if (expires) 1 else 0)
|
||||||
parcel.writeParcelable(expiryTime, flags)
|
parcel.writeParcelable(expiryTime, flags)
|
||||||
parcel.writeString(url)
|
parcel.writeString(url)
|
||||||
@@ -91,8 +97,8 @@ class EntryInfo : Parcelable {
|
|||||||
return customFields.any { !it.protectedValue.isProtected }
|
return customFields.any { !it.protectedValue.isProtected }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isAutoGeneratedField(field: Field): Boolean {
|
fun containsCustomField(label: String): Boolean {
|
||||||
return field.name == OTP_TOKEN_FIELD
|
return customFields.lastOrNull { it.name == label } != null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGeneratedFieldValue(label: String): String {
|
fun getGeneratedFieldValue(label: String): String {
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.text.format.Formatter
|
||||||
|
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility data class to get FileDatabaseInfo at a `t` time
|
||||||
|
*/
|
||||||
|
data class SnapFileDatabaseInfo(var fileUri: Uri?,
|
||||||
|
var exists: Boolean,
|
||||||
|
var lastModification: Long?,
|
||||||
|
var size: Long?): Parcelable {
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) : this(
|
||||||
|
parcel.readParcelable(Uri::class.java.classLoader),
|
||||||
|
parcel.readByte() != 0.toByte(),
|
||||||
|
parcel.readValue(Long::class.java.classLoader) as? Long,
|
||||||
|
parcel.readValue(Long::class.java.classLoader) as? Long) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toString(context: Context): String {
|
||||||
|
val lastModificationString = DateFormat.getDateTimeInstance()
|
||||||
|
.format(Date(lastModification ?: 0))
|
||||||
|
return "$lastModificationString, " +
|
||||||
|
Formatter.formatFileSize(context, size ?: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeParcelable(fileUri, flags)
|
||||||
|
parcel.writeByte(if (exists) 1 else 0)
|
||||||
|
parcel.writeValue(lastModification)
|
||||||
|
parcel.writeValue(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is SnapFileDatabaseInfo) return false
|
||||||
|
|
||||||
|
if (fileUri != other.fileUri) return false
|
||||||
|
if (exists != other.exists) return false
|
||||||
|
if (lastModification != other.lastModification) return false
|
||||||
|
if (size != other.size) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = fileUri?.hashCode() ?: 0
|
||||||
|
result = 31 * result + exists.hashCode()
|
||||||
|
result = 31 * result + (lastModification?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (size?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<SnapFileDatabaseInfo> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): SnapFileDatabaseInfo {
|
||||||
|
return SnapFileDatabaseInfo(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<SnapFileDatabaseInfo?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromFileDatabaseInfo(fileDatabaseInfo: FileDatabaseInfo): SnapFileDatabaseInfo {
|
||||||
|
return SnapFileDatabaseInfo(
|
||||||
|
fileDatabaseInfo.fileUri,
|
||||||
|
fileDatabaseInfo.exists,
|
||||||
|
fileDatabaseInfo.getLastModification(),
|
||||||
|
fileDatabaseInfo.getSize())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,12 +62,12 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
|||||||
action = ACTION_REMOVE_KEYS
|
action = ACTION_REMOVE_KEYS
|
||||||
}
|
}
|
||||||
val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
val deviceCredential = PreferencesUtil.isDeviceCredentialUnlockEnable(this)
|
val biometricUnlockEnabled = PreferencesUtil.isBiometricUnlockEnable(this)
|
||||||
val notificationBuilder = buildNewNotification().apply {
|
val notificationBuilder = buildNewNotification().apply {
|
||||||
setSmallIcon(if (deviceCredential) {
|
setSmallIcon(if (biometricUnlockEnabled) {
|
||||||
R.drawable.notification_ic_device_unlock_24dp
|
|
||||||
} else {
|
|
||||||
R.drawable.notification_ic_fingerprint_unlock_24dp
|
R.drawable.notification_ic_fingerprint_unlock_24dp
|
||||||
|
} else {
|
||||||
|
R.drawable.notification_ic_device_unlock_24dp
|
||||||
})
|
})
|
||||||
intent?.let {
|
intent?.let {
|
||||||
setContentTitle(getString(R.string.advanced_unlock))
|
setContentTitle(getString(R.string.advanced_unlock))
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import android.os.Parcel
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,6 +55,7 @@ class ClipboardEntryNotificationField : Parcelable {
|
|||||||
NotificationFieldId.UNKNOWN -> ""
|
NotificationFieldId.UNKNOWN -> ""
|
||||||
NotificationFieldId.USERNAME -> entryInfo?.username ?: ""
|
NotificationFieldId.USERNAME -> entryInfo?.username ?: ""
|
||||||
NotificationFieldId.PASSWORD -> entryInfo?.password ?: ""
|
NotificationFieldId.PASSWORD -> entryInfo?.password ?: ""
|
||||||
|
NotificationFieldId.OTP -> entryInfo?.getGeneratedFieldValue(OTP_TOKEN_FIELD) ?: ""
|
||||||
NotificationFieldId.FIELD_A,
|
NotificationFieldId.FIELD_A,
|
||||||
NotificationFieldId.FIELD_B,
|
NotificationFieldId.FIELD_B,
|
||||||
NotificationFieldId.FIELD_C -> entryInfo?.getGeneratedFieldValue(label) ?: ""
|
NotificationFieldId.FIELD_C -> entryInfo?.getGeneratedFieldValue(label) ?: ""
|
||||||
@@ -81,7 +83,7 @@ class ClipboardEntryNotificationField : Parcelable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class NotificationFieldId {
|
enum class NotificationFieldId {
|
||||||
UNKNOWN, USERNAME, PASSWORD, FIELD_A, FIELD_B, FIELD_C;
|
UNKNOWN, USERNAME, PASSWORD, OTP, FIELD_A, FIELD_B, FIELD_C;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val anonymousFieldId: Array<NotificationFieldId>
|
val anonymousFieldId: Array<NotificationFieldId>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import android.content.Intent
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper.NEVER
|
import com.kunzisoft.keepass.timeout.TimeoutHelper.NEVER
|
||||||
@@ -250,6 +251,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
val containsUsernameToCopy = entry.username.isNotEmpty()
|
val containsUsernameToCopy = entry.username.isNotEmpty()
|
||||||
val containsPasswordToCopy = entry.password.isNotEmpty()
|
val containsPasswordToCopy = entry.password.isNotEmpty()
|
||||||
&& PreferencesUtil.allowCopyPasswordAndProtectedFields(context)
|
&& PreferencesUtil.allowCopyPasswordAndProtectedFields(context)
|
||||||
|
val containsOTPToCopy = entry.containsCustomField(OTP_TOKEN_FIELD)
|
||||||
val containsExtraFieldToCopy = entry.customFields.isNotEmpty()
|
val containsExtraFieldToCopy = entry.customFields.isNotEmpty()
|
||||||
&& (entry.containsCustomFieldsNotProtected()
|
&& (entry.containsCustomFieldsNotProtected()
|
||||||
||
|
||
|
||||||
@@ -262,7 +264,10 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
// If notifications enabled in settings
|
// If notifications enabled in settings
|
||||||
// Don't if application timeout
|
// Don't if application timeout
|
||||||
if (PreferencesUtil.isClipboardNotificationsEnable(context)) {
|
if (PreferencesUtil.isClipboardNotificationsEnable(context)) {
|
||||||
if (containsUsernameToCopy || containsPasswordToCopy || containsExtraFieldToCopy) {
|
if (containsUsernameToCopy
|
||||||
|
|| containsPasswordToCopy
|
||||||
|
|| containsOTPToCopy
|
||||||
|
|| containsExtraFieldToCopy) {
|
||||||
|
|
||||||
// username already copied, waiting for user's action before copy password.
|
// username already copied, waiting for user's action before copy password.
|
||||||
intent.action = ACTION_NEW_NOTIFICATION
|
intent.action = ACTION_NEW_NOTIFICATION
|
||||||
@@ -282,14 +287,22 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
ClipboardEntryNotificationField.NotificationFieldId.PASSWORD,
|
ClipboardEntryNotificationField.NotificationFieldId.PASSWORD,
|
||||||
context.getString(R.string.entry_password)))
|
context.getString(R.string.entry_password)))
|
||||||
}
|
}
|
||||||
|
// Add OTP
|
||||||
|
if (containsOTPToCopy) {
|
||||||
|
notificationFields.add(
|
||||||
|
ClipboardEntryNotificationField(
|
||||||
|
ClipboardEntryNotificationField.NotificationFieldId.OTP,
|
||||||
|
OTP_TOKEN_FIELD))
|
||||||
|
}
|
||||||
// Add extra fields
|
// Add extra fields
|
||||||
if (containsExtraFieldToCopy) {
|
if (containsExtraFieldToCopy) {
|
||||||
try {
|
try {
|
||||||
var anonymousFieldNumber = 0
|
var anonymousFieldNumber = 0
|
||||||
entry.customFields.forEach { field ->
|
entry.customFields.forEach { field ->
|
||||||
//If value is not protected or allowed
|
//If value is not protected or allowed
|
||||||
if (!field.protectedValue.isProtected
|
if ((!field.protectedValue.isProtected
|
||||||
|| PreferencesUtil.allowCopyPasswordAndProtectedFields(context)) {
|
|| PreferencesUtil.allowCopyPasswordAndProtectedFields(context))
|
||||||
|
&& field.name != OTP_TOKEN_FIELD) {
|
||||||
notificationFields.add(
|
notificationFields.add(
|
||||||
ClipboardEntryNotificationField(
|
ClipboardEntryNotificationField(
|
||||||
ClipboardEntryNotificationField.NotificationFieldId.anonymousFieldId[anonymousFieldNumber],
|
ClipboardEntryNotificationField.NotificationFieldId.anonymousFieldId[anonymousFieldNumber],
|
||||||
|
|||||||
@@ -22,9 +22,8 @@ package com.kunzisoft.keepass.notifications
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Binder
|
import android.os.*
|
||||||
import android.os.Bundle
|
import android.util.Log
|
||||||
import android.os.IBinder
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.GroupActivity
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
@@ -40,6 +39,7 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
|||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
@@ -47,6 +47,7 @@ import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
|||||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
import com.kunzisoft.keepass.utils.closeDatabase
|
import com.kunzisoft.keepass.utils.closeDatabase
|
||||||
|
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
@@ -65,6 +66,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
private var mAllowFinishAction = AtomicBoolean()
|
private var mAllowFinishAction = AtomicBoolean()
|
||||||
private var mActionRunning = false
|
private var mActionRunning = false
|
||||||
|
|
||||||
|
private var mDatabaseInfoListeners = LinkedList<DatabaseInfoListener>()
|
||||||
|
|
||||||
private var mIconId: Int = R.drawable.notification_ic_database_load
|
private var mIconId: Int = R.drawable.notification_ic_database_load
|
||||||
private var mTitleId: Int = R.string.database_opened
|
private var mTitleId: Int = R.string.database_opened
|
||||||
private var mMessageId: Int? = null
|
private var mMessageId: Int? = null
|
||||||
@@ -93,6 +96,14 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
mAllowFinishAction.set(false)
|
mAllowFinishAction.set(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addDatabaseFileInfoListener(databaseInfoListener: DatabaseInfoListener) {
|
||||||
|
mDatabaseInfoListeners.add(databaseInfoListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeDatabaseFileInfoListener(databaseInfoListener: DatabaseInfoListener) {
|
||||||
|
mDatabaseInfoListeners.remove(databaseInfoListener)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActionTaskListener {
|
interface ActionTaskListener {
|
||||||
@@ -101,6 +112,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
fun onStopAction(actionTask: String, result: ActionRunnable.Result)
|
fun onStopAction(actionTask: String, result: ActionRunnable.Result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DatabaseInfoListener {
|
||||||
|
fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
newDatabaseInfo: SnapFileDatabaseInfo)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force to call [ActionTaskListener.onStartAction] if the action is still running
|
* Force to call [ActionTaskListener.onStartAction] if the action is still running
|
||||||
*/
|
*/
|
||||||
@@ -112,6 +128,53 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun checkDatabaseInfo() {
|
||||||
|
try {
|
||||||
|
mDatabase.fileUri?.let {
|
||||||
|
val previousDatabaseInfo = mSnapFileDatabaseInfo
|
||||||
|
val lastFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo(
|
||||||
|
FileDatabaseInfo(applicationContext, it))
|
||||||
|
|
||||||
|
val oldDatabaseModification = previousDatabaseInfo?.lastModification
|
||||||
|
val newDatabaseModification = lastFileDatabaseInfo.lastModification
|
||||||
|
|
||||||
|
val conditionExists = previousDatabaseInfo != null
|
||||||
|
&& previousDatabaseInfo.exists != lastFileDatabaseInfo.exists
|
||||||
|
// To prevent dialog opening too often
|
||||||
|
val conditionLastModification = (oldDatabaseModification != null && newDatabaseModification != null
|
||||||
|
&& oldDatabaseModification < newDatabaseModification
|
||||||
|
&& mLastLocalSaveTime + 5000 < newDatabaseModification)
|
||||||
|
|
||||||
|
if (conditionExists || conditionLastModification) {
|
||||||
|
// Show the dialog only if it's real new info and not a delay after a save
|
||||||
|
Log.i(TAG, "Database file modified " +
|
||||||
|
"$previousDatabaseInfo != $lastFileDatabaseInfo ")
|
||||||
|
// Call listener to indicate a change in database info
|
||||||
|
if (previousDatabaseInfo != null) {
|
||||||
|
mDatabaseInfoListeners.forEach { listener ->
|
||||||
|
listener.onDatabaseInfoChanged(previousDatabaseInfo, lastFileDatabaseInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mSnapFileDatabaseInfo = lastFileDatabaseInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to check database info", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveDatabaseInfo() {
|
||||||
|
try {
|
||||||
|
mDatabase.fileUri?.let {
|
||||||
|
mSnapFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo(
|
||||||
|
FileDatabaseInfo(applicationContext, it))
|
||||||
|
Log.i(TAG, "Database file saved $mSnapFileDatabaseInfo")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to check database info", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
super.onBind(intent)
|
super.onBind(intent)
|
||||||
return mActionTaskBinder
|
return mActionTaskBinder
|
||||||
@@ -138,6 +201,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
val actionRunnable: ActionRunnable? = when (intentAction) {
|
val actionRunnable: ActionRunnable? = when (intentAction) {
|
||||||
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent)
|
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent)
|
||||||
ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent)
|
ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent)
|
||||||
|
ACTION_DATABASE_RELOAD_TASK -> buildDatabaseReloadActionTask()
|
||||||
ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent)
|
ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent)
|
||||||
ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent)
|
ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent)
|
||||||
ACTION_DATABASE_UPDATE_GROUP_TASK -> buildDatabaseUpdateGroupActionTask(intent)
|
ACTION_DATABASE_UPDATE_GROUP_TASK -> buildDatabaseUpdateGroupActionTask(intent)
|
||||||
@@ -192,6 +256,20 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
actionTaskListener.onStopAction(intentAction!!, result)
|
actionTaskListener.onStopAction(intentAction!!, result)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
// Save the database info before performing action
|
||||||
|
if (intentAction == ACTION_DATABASE_LOAD_TASK) {
|
||||||
|
saveDatabaseInfo()
|
||||||
|
}
|
||||||
|
// Save the database info after performing save action
|
||||||
|
if (intentAction == ACTION_DATABASE_SAVE
|
||||||
|
|| intent?.getBooleanExtra(SAVE_DATABASE_KEY, false) == true) {
|
||||||
|
mDatabase.fileUri?.let {
|
||||||
|
val newSnapFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo(
|
||||||
|
FileDatabaseInfo(applicationContext, it))
|
||||||
|
mLastLocalSaveTime = System.currentTimeMillis()
|
||||||
|
mSnapFileDatabaseInfo = newSnapFileDatabaseInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
removeIntentData(intent)
|
removeIntentData(intent)
|
||||||
TimeoutHelper.releaseTemporarilyDisableTimeout()
|
TimeoutHelper.releaseTemporarilyDisableTimeout()
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) {
|
||||||
@@ -214,7 +292,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
}
|
}
|
||||||
|
|
||||||
return when (intentAction) {
|
return when (intentAction) {
|
||||||
ACTION_DATABASE_LOAD_TASK, null -> {
|
ACTION_DATABASE_LOAD_TASK,
|
||||||
|
ACTION_DATABASE_RELOAD_TASK,
|
||||||
|
null -> {
|
||||||
START_STICKY
|
START_STICKY
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -248,7 +328,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
else -> {
|
else -> {
|
||||||
when (intentAction) {
|
when (intentAction) {
|
||||||
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
|
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
|
||||||
ACTION_DATABASE_LOAD_TASK -> R.string.loading_database
|
ACTION_DATABASE_LOAD_TASK,
|
||||||
|
ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database
|
||||||
ACTION_DATABASE_SAVE -> R.string.saving_database
|
ACTION_DATABASE_SAVE -> R.string.saving_database
|
||||||
else -> {
|
else -> {
|
||||||
R.string.command_execution
|
R.string.command_execution
|
||||||
@@ -258,13 +339,15 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
}
|
}
|
||||||
|
|
||||||
mMessageId = when (intentAction) {
|
mMessageId = when (intentAction) {
|
||||||
ACTION_DATABASE_LOAD_TASK -> null
|
ACTION_DATABASE_LOAD_TASK,
|
||||||
|
ACTION_DATABASE_RELOAD_TASK -> null
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
mWarningId =
|
mWarningId =
|
||||||
if (!saveAction
|
if (!saveAction
|
||||||
|| intentAction == ACTION_DATABASE_LOAD_TASK)
|
|| intentAction == ACTION_DATABASE_LOAD_TASK
|
||||||
|
|| intentAction == ACTION_DATABASE_RELOAD_TASK)
|
||||||
null
|
null
|
||||||
else
|
else
|
||||||
R.string.do_not_kill_app
|
R.string.do_not_kill_app
|
||||||
@@ -342,9 +425,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
* Execute action with a coroutine
|
* Execute action with a coroutine
|
||||||
*/
|
*/
|
||||||
private suspend fun executeAction(progressTaskUpdater: ProgressTaskUpdater,
|
private suspend fun executeAction(progressTaskUpdater: ProgressTaskUpdater,
|
||||||
onPreExecute: () -> Unit,
|
onPreExecute: () -> Unit,
|
||||||
onExecute: (ProgressTaskUpdater?) -> ActionRunnable?,
|
onExecute: (ProgressTaskUpdater?) -> ActionRunnable?,
|
||||||
onPostExecute: (result: ActionRunnable.Result) -> Unit) {
|
onPostExecute: (result: ActionRunnable.Result) -> Unit) {
|
||||||
mAllowFinishAction.set(false)
|
mAllowFinishAction.set(false)
|
||||||
|
|
||||||
TimeoutHelper.temporarilyDisableTimeout()
|
TimeoutHelper.temporarilyDisableTimeout()
|
||||||
@@ -465,6 +548,17 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildDatabaseReloadActionTask(): ActionRunnable {
|
||||||
|
return ReloadDatabaseRunnable(
|
||||||
|
this,
|
||||||
|
mDatabase,
|
||||||
|
this
|
||||||
|
) { result ->
|
||||||
|
// No need to add each info to reload database
|
||||||
|
result.data = Bundle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildDatabaseAssignPasswordActionTask(intent: Intent): ActionRunnable? {
|
private fun buildDatabaseAssignPasswordActionTask(intent: Intent): ActionRunnable? {
|
||||||
return if (intent.hasExtra(DATABASE_URI_KEY)
|
return if (intent.hasExtra(DATABASE_URI_KEY)
|
||||||
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
|
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
|
||||||
@@ -770,6 +864,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
|
|
||||||
const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK"
|
const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK"
|
||||||
const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK"
|
const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK"
|
||||||
|
const val ACTION_DATABASE_RELOAD_TASK = "ACTION_DATABASE_RELOAD_TASK"
|
||||||
const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_TASK"
|
const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_TASK"
|
||||||
const val ACTION_DATABASE_CREATE_GROUP_TASK = "ACTION_DATABASE_CREATE_GROUP_TASK"
|
const val ACTION_DATABASE_CREATE_GROUP_TASK = "ACTION_DATABASE_CREATE_GROUP_TASK"
|
||||||
const val ACTION_DATABASE_UPDATE_GROUP_TASK = "ACTION_DATABASE_UPDATE_GROUP_TASK"
|
const val ACTION_DATABASE_UPDATE_GROUP_TASK = "ACTION_DATABASE_UPDATE_GROUP_TASK"
|
||||||
@@ -822,6 +917,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
const val OLD_ELEMENT_KEY = "OLD_ELEMENT_KEY" // Warning type of this thing change every time
|
const val OLD_ELEMENT_KEY = "OLD_ELEMENT_KEY" // Warning type of this thing change every time
|
||||||
const val NEW_ELEMENT_KEY = "NEW_ELEMENT_KEY" // Warning type of this thing change every time
|
const val NEW_ELEMENT_KEY = "NEW_ELEMENT_KEY" // Warning type of this thing change every time
|
||||||
|
|
||||||
|
private var mSnapFileDatabaseInfo: SnapFileDatabaseInfo? = null
|
||||||
|
private var mLastLocalSaveTime: Long = 0
|
||||||
|
|
||||||
fun getListNodesFromBundle(database: Database, bundle: Bundle): List<Node> {
|
fun getListNodesFromBundle(database: Database, bundle: Bundle): List<Node> {
|
||||||
val nodesAction = ArrayList<Node>()
|
val nodesAction = ArrayList<Node>()
|
||||||
bundle.getParcelableArrayList<NodeId<*>>(GROUPS_ID_KEY)?.forEach {
|
bundle.getParcelableArrayList<NodeId<*>>(GROUPS_ID_KEY)?.forEach {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
package com.kunzisoft.keepass.otp
|
package com.kunzisoft.keepass.otp
|
||||||
|
|
||||||
import com.kunzisoft.keepass.model.OtpModel
|
import com.kunzisoft.keepass.model.OtpModel
|
||||||
|
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
|
||||||
import org.apache.commons.codec.binary.Base32
|
import org.apache.commons.codec.binary.Base32
|
||||||
import org.apache.commons.codec.binary.Base64
|
import org.apache.commons.codec.binary.Base64
|
||||||
import org.apache.commons.codec.binary.Hex
|
import org.apache.commons.codec.binary.Hex
|
||||||
@@ -137,7 +138,7 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
|
|||||||
@Throws(IllegalArgumentException::class)
|
@Throws(IllegalArgumentException::class)
|
||||||
fun setHexSecret(secret: String) {
|
fun setHexSecret(secret: String) {
|
||||||
if (secret.isNotEmpty())
|
if (secret.isNotEmpty())
|
||||||
otpModel.secret = Hex.decodeHex(secret)
|
otpModel.secret = Hex.decodeHex(secret.toCharArray())
|
||||||
else
|
else
|
||||||
throw IllegalArgumentException()
|
throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
@@ -150,16 +151,16 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
|
|||||||
|
|
||||||
@Throws(IllegalArgumentException::class)
|
@Throws(IllegalArgumentException::class)
|
||||||
fun setBase32Secret(secret: String) {
|
fun setBase32Secret(secret: String) {
|
||||||
if (isValidBase32(secret))
|
if (isValidBase32(secret)) {
|
||||||
otpModel.secret = Base32().decode(replaceBase32Chars(secret).toByteArray())
|
otpModel.secret = Base32().decode(replaceBase32Chars(secret))
|
||||||
else
|
} else
|
||||||
throw IllegalArgumentException()
|
throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class)
|
@Throws(IllegalArgumentException::class)
|
||||||
fun setBase64Secret(secret: String) {
|
fun setBase64Secret(secret: String) {
|
||||||
if (isValidBase64(secret))
|
if (isValidBase64(secret))
|
||||||
otpModel.secret = Base64().decode(secret.toByteArray())
|
otpModel.secret = Base64().decode(secret)
|
||||||
else
|
else
|
||||||
throw IllegalArgumentException()
|
throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
@@ -208,38 +209,24 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
|
|||||||
|
|
||||||
fun isValidBase32(secret: String): Boolean {
|
fun isValidBase32(secret: String): Boolean {
|
||||||
val secretChars = replaceBase32Chars(secret)
|
val secretChars = replaceBase32Chars(secret)
|
||||||
return secretChars.isNotEmpty() && checkBase32Secret(secretChars)
|
return secret.isNotEmpty()
|
||||||
|
&& (Pattern.matches("^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=)?$", secretChars))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isValidBase64(secret: String): Boolean {
|
fun isValidBase64(secret: String): Boolean {
|
||||||
// TODO replace base 64 chars
|
// TODO replace base 64 chars
|
||||||
return secret.isNotEmpty() && checkBase64Secret(secret)
|
return secret.isNotEmpty()
|
||||||
}
|
&& (Pattern.matches("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", secret))
|
||||||
|
|
||||||
fun removeLineChars(parameter: String): String {
|
|
||||||
return parameter.replace("[\\r|\\n|\\t|\\u00A0]+".toRegex(), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeSpaceChars(parameter: String): String {
|
|
||||||
return parameter.replace("[\\r|\\n|\\t|\\s|\\u00A0]+".toRegex(), "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun replaceBase32Chars(parameter: String): String {
|
fun replaceBase32Chars(parameter: String): String {
|
||||||
// Add 'A' at end if not Base32 length
|
// Add padding '=' at end if not Base32 length
|
||||||
var parameterNewSize = removeSpaceChars(parameter.toUpperCase(Locale.ENGLISH))
|
var parameterNewSize = parameter.toUpperCase(Locale.ENGLISH).removeSpaceChars()
|
||||||
while (parameterNewSize.length % 8 != 0) {
|
while (parameterNewSize.length % 8 != 0) {
|
||||||
parameterNewSize += 'A'
|
parameterNewSize += '='
|
||||||
}
|
}
|
||||||
return parameterNewSize
|
return parameterNewSize
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkBase32Secret(secret: String): Boolean {
|
|
||||||
return (Pattern.matches("^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=)?$", secret))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkBase64Secret(secret: String): Boolean {
|
|
||||||
return (Pattern.matches("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", secret))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ import android.net.Uri
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.model.Field
|
import com.kunzisoft.keepass.model.Field
|
||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.removeLineChars
|
|
||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.removeSpaceChars
|
|
||||||
import com.kunzisoft.keepass.otp.TokenCalculator.*
|
import com.kunzisoft.keepass.otp.TokenCalculator.*
|
||||||
|
import com.kunzisoft.keepass.utils.StringUtil.removeLineChars
|
||||||
|
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
@@ -57,13 +57,25 @@ object OtpEntryFields {
|
|||||||
private const val DIGITS_KEY = "size"
|
private const val DIGITS_KEY = "size"
|
||||||
private const val STEP_KEY = "step"
|
private const val STEP_KEY = "step"
|
||||||
|
|
||||||
// HmacOtp KeePass2 values (https://keepass.info/help/base/placeholders.html#hmacotp)
|
// HmacOtp KeePass2 values (https://keepass.info/help/base/placeholders.html#otp)
|
||||||
private const val HMACOTP_SECRET_FIELD = "HmacOtp-Secret"
|
private const val HMACOTP_SECRET_FIELD = "HmacOtp-Secret"
|
||||||
private const val HMACOTP_SECRET_HEX_FIELD = "HmacOtp-Secret-Hex"
|
private const val HMACOTP_SECRET_HEX_FIELD = "HmacOtp-Secret-Hex"
|
||||||
private const val HMACOTP_SECRET_BASE32_FIELD = "HmacOtp-Secret-Base32"
|
private const val HMACOTP_SECRET_BASE32_FIELD = "HmacOtp-Secret-Base32"
|
||||||
private const val HMACOTP_SECRET_BASE64_FIELD = "HmacOtp-Secret-Base64"
|
private const val HMACOTP_SECRET_BASE64_FIELD = "HmacOtp-Secret-Base64"
|
||||||
private const val HMACOTP_SECRET_COUNTER_FIELD = "HmacOtp-Counter"
|
private const val HMACOTP_SECRET_COUNTER_FIELD = "HmacOtp-Counter"
|
||||||
|
|
||||||
|
// TimeOtp KeePass2 values
|
||||||
|
private const val TIMEOTP_SECRET_FIELD = "TimeOtp-Secret"
|
||||||
|
private const val TIMEOTP_SECRET_HEX_FIELD = "TimeOtp-Secret-Hex"
|
||||||
|
private const val TIMEOTP_SECRET_BASE32_FIELD = "TimeOtp-Secret-Base32"
|
||||||
|
private const val TIMEOTP_SECRET_BASE64_FIELD = "TimeOtp-Secret-Base64"
|
||||||
|
private const val TIMEOTP_LENGTH_FIELD = "TimeOtp-Length"
|
||||||
|
private const val TIMEOTP_PERIOD_FIELD = "TimeOtp-Period"
|
||||||
|
private const val TIMEOTP_ALGORITHM_FIELD = "TimeOtp-Algorithm"
|
||||||
|
private const val TIMEOTP_ALGORITHM_SHA1_VALUE = "HMAC-SHA-1"
|
||||||
|
private const val TIMEOTP_ALGORITHM_SHA256_VALUE = "HMAC-SHA-256"
|
||||||
|
private const val TIMEOTP_ALGORITHM_SHA512_VALUE = "HMAC-SHA-512"
|
||||||
|
|
||||||
// Custom fields (maybe from plugin)
|
// Custom fields (maybe from plugin)
|
||||||
private const val TOTP_SEED_FIELD = "TOTP Seed"
|
private const val TOTP_SEED_FIELD = "TOTP Seed"
|
||||||
private const val TOTP_SETTING_FIELD = "TOTP Settings"
|
private const val TOTP_SETTING_FIELD = "TOTP Settings"
|
||||||
@@ -85,14 +97,17 @@ object OtpEntryFields {
|
|||||||
// OTP (HOTP/TOTP) from URL and field from KeePassXC
|
// OTP (HOTP/TOTP) from URL and field from KeePassXC
|
||||||
if (parseOTPUri(getField, otpElement))
|
if (parseOTPUri(getField, otpElement))
|
||||||
return otpElement
|
return otpElement
|
||||||
|
// TOTP from KeePass 2.47
|
||||||
|
if (parseTOTPFromOfficialField(getField, otpElement))
|
||||||
|
return otpElement
|
||||||
// TOTP from key values (maybe plugin or old KeePassXC)
|
// TOTP from key values (maybe plugin or old KeePassXC)
|
||||||
if (parseTOTPKeyValues(getField, otpElement))
|
if (parseTOTPKeyValues(getField, otpElement))
|
||||||
return otpElement
|
return otpElement
|
||||||
// TOTP from custom field
|
// TOTP from custom field
|
||||||
if (parseTOTPFromField(getField, otpElement))
|
if (parseTOTPFromPluginField(getField, otpElement))
|
||||||
return otpElement
|
return otpElement
|
||||||
// HOTP fields from KeePass 2
|
// HOTP fields from KeePass 2
|
||||||
if (parseHOTPFromField(getField, otpElement))
|
if (parseHOTPFromOfficialField(getField, otpElement))
|
||||||
return otpElement
|
return otpElement
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -126,7 +141,7 @@ object OtpEntryFields {
|
|||||||
private fun parseOTPUri(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
|
private fun parseOTPUri(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
|
||||||
val otpPlainText = getField(OTP_FIELD)
|
val otpPlainText = getField(OTP_FIELD)
|
||||||
if (otpPlainText != null && otpPlainText.isNotEmpty() && isOTPUri(otpPlainText)) {
|
if (otpPlainText != null && otpPlainText.isNotEmpty() && isOTPUri(otpPlainText)) {
|
||||||
val uri = Uri.parse(removeSpaceChars(otpPlainText))
|
val uri = Uri.parse(otpPlainText.removeSpaceChars())
|
||||||
|
|
||||||
if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase(Locale.ENGLISH)) {
|
if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase(Locale.ENGLISH)) {
|
||||||
Log.e(TAG, "Invalid or missing scheme in uri")
|
Log.e(TAG, "Invalid or missing scheme in uri")
|
||||||
@@ -159,16 +174,16 @@ object OtpEntryFields {
|
|||||||
if (nameParam != null && nameParam.isNotEmpty()) {
|
if (nameParam != null && nameParam.isNotEmpty()) {
|
||||||
val userIdArray = nameParam.split(":", "%3A")
|
val userIdArray = nameParam.split(":", "%3A")
|
||||||
if (userIdArray.size > 1) {
|
if (userIdArray.size > 1) {
|
||||||
otpElement.issuer = removeLineChars(userIdArray[0])
|
otpElement.issuer = userIdArray[0].removeLineChars()
|
||||||
otpElement.name = removeLineChars(userIdArray[1])
|
otpElement.name = userIdArray[1].removeLineChars()
|
||||||
} else {
|
} else {
|
||||||
otpElement.name = removeLineChars(nameParam)
|
otpElement.name = nameParam.removeLineChars()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val issuerParam = uri.getQueryParameter(ISSUER_URL_PARAM)
|
val issuerParam = uri.getQueryParameter(ISSUER_URL_PARAM)
|
||||||
if (issuerParam != null && issuerParam.isNotEmpty())
|
if (issuerParam != null && issuerParam.isNotEmpty())
|
||||||
otpElement.issuer = removeLineChars(issuerParam)
|
otpElement.issuer = issuerParam.removeLineChars()
|
||||||
|
|
||||||
val secretParam = uri.getQueryParameter(SECRET_URL_PARAM)
|
val secretParam = uri.getQueryParameter(SECRET_URL_PARAM)
|
||||||
if (secretParam != null && secretParam.isNotEmpty()) {
|
if (secretParam != null && secretParam.isNotEmpty()) {
|
||||||
@@ -247,8 +262,9 @@ object OtpEntryFields {
|
|||||||
encodeParameter(username)
|
encodeParameter(username)
|
||||||
else
|
else
|
||||||
encodeParameter(otpElement.name)
|
encodeParameter(otpElement.name)
|
||||||
|
val secret = encodeParameter(otpElement.getBase32Secret())
|
||||||
val uriString = StringBuilder("otpauth://$otpAuthority/$issuer%3A$accountName" +
|
val uriString = StringBuilder("otpauth://$otpAuthority/$issuer%3A$accountName" +
|
||||||
"?$SECRET_URL_PARAM=${otpElement.getBase32Secret()}" +
|
"?$SECRET_URL_PARAM=${secret}" +
|
||||||
"&$counterOrPeriodLabel=$counterOrPeriodValue" +
|
"&$counterOrPeriodLabel=$counterOrPeriodValue" +
|
||||||
"&$DIGITS_URL_PARAM=${otpElement.digits}" +
|
"&$DIGITS_URL_PARAM=${otpElement.digits}" +
|
||||||
"&$ISSUER_URL_PARAM=$issuer")
|
"&$ISSUER_URL_PARAM=$issuer")
|
||||||
@@ -262,7 +278,40 @@ object OtpEntryFields {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun encodeParameter(parameter: String): String {
|
private fun encodeParameter(parameter: String): String {
|
||||||
return Uri.encode(OtpElement.removeLineChars(parameter))
|
return Uri.encode(parameter.removeLineChars())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseTOTPFromOfficialField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
|
||||||
|
val secretField = getField(TIMEOTP_SECRET_FIELD)
|
||||||
|
val secretHexField = getField(TIMEOTP_SECRET_HEX_FIELD)
|
||||||
|
val secretBase32Field = getField(TIMEOTP_SECRET_BASE32_FIELD)
|
||||||
|
val secretBase64Field = getField(TIMEOTP_SECRET_BASE64_FIELD)
|
||||||
|
val lengthField = getField(TIMEOTP_LENGTH_FIELD)
|
||||||
|
val periodField = getField(TIMEOTP_PERIOD_FIELD)
|
||||||
|
val algorithmField = getField(TIMEOTP_ALGORITHM_FIELD)
|
||||||
|
try {
|
||||||
|
when {
|
||||||
|
secretField != null -> otpElement.setUTF8Secret(secretField)
|
||||||
|
secretHexField != null -> otpElement.setHexSecret(secretHexField)
|
||||||
|
secretBase32Field != null -> otpElement.setBase32Secret(secretBase32Field)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
otpElement.type = OtpType.TOTP
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseTOTPKeyValues(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
|
private fun parseTOTPKeyValues(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
|
||||||
@@ -290,7 +339,7 @@ object OtpEntryFields {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseTOTPFromField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
|
private fun parseTOTPFromPluginField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
|
||||||
val seedField = getField(TOTP_SEED_FIELD) ?: return false
|
val seedField = getField(TOTP_SEED_FIELD) ?: return false
|
||||||
try {
|
try {
|
||||||
otpElement.setBase32Secret(seedField)
|
otpElement.setBase32Secret(seedField)
|
||||||
@@ -316,7 +365,7 @@ object OtpEntryFields {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseHOTPFromField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
|
private fun parseHOTPFromOfficialField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
|
||||||
val secretField = getField(HMACOTP_SECRET_FIELD)
|
val secretField = getField(HMACOTP_SECRET_FIELD)
|
||||||
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)
|
||||||
@@ -382,25 +431,43 @@ object OtpEntryFields {
|
|||||||
val totpSeedField = Field(TOTP_SEED_FIELD)
|
val totpSeedField = Field(TOTP_SEED_FIELD)
|
||||||
val totpSettingField = Field(TOTP_SETTING_FIELD)
|
val totpSettingField = Field(TOTP_SETTING_FIELD)
|
||||||
val hmacOtpSecretField = Field(HMACOTP_SECRET_FIELD)
|
val hmacOtpSecretField = Field(HMACOTP_SECRET_FIELD)
|
||||||
val hmacOtpSecretHewField = Field(HMACOTP_SECRET_HEX_FIELD)
|
val hmacOtpSecretHexField = Field(HMACOTP_SECRET_HEX_FIELD)
|
||||||
val hmacOtpSecretBase32Field = Field(HMACOTP_SECRET_BASE32_FIELD)
|
val hmacOtpSecretBase32Field = Field(HMACOTP_SECRET_BASE32_FIELD)
|
||||||
val hmacOtpSecretBase64Field = Field(HMACOTP_SECRET_BASE64_FIELD)
|
val hmacOtpSecretBase64Field = Field(HMACOTP_SECRET_BASE64_FIELD)
|
||||||
val hmacOtpSecretCounterField = Field(HMACOTP_SECRET_COUNTER_FIELD)
|
val hmacOtpSecretCounterField = Field(HMACOTP_SECRET_COUNTER_FIELD)
|
||||||
|
val timeOtpSecretField = Field(TIMEOTP_SECRET_FIELD)
|
||||||
|
val timeOtpSecretHexField = Field(TIMEOTP_SECRET_HEX_FIELD)
|
||||||
|
val timeOtpSecretBase32Field = Field(TIMEOTP_SECRET_BASE32_FIELD)
|
||||||
|
val timeOtpSecretBase64Field = Field(TIMEOTP_SECRET_BASE64_FIELD)
|
||||||
|
val timeOtpLengthField = Field(TIMEOTP_LENGTH_FIELD)
|
||||||
|
val timeOtpPeriodField = Field(TIMEOTP_PERIOD_FIELD)
|
||||||
|
val timeOtpAlgorithmField = Field(TIMEOTP_ALGORITHM_FIELD)
|
||||||
newCustomFields.remove(otpField)
|
newCustomFields.remove(otpField)
|
||||||
newCustomFields.remove(totpSeedField)
|
newCustomFields.remove(totpSeedField)
|
||||||
newCustomFields.remove(totpSettingField)
|
newCustomFields.remove(totpSettingField)
|
||||||
newCustomFields.remove(hmacOtpSecretField)
|
newCustomFields.remove(hmacOtpSecretField)
|
||||||
newCustomFields.remove(hmacOtpSecretHewField)
|
newCustomFields.remove(hmacOtpSecretHexField)
|
||||||
newCustomFields.remove(hmacOtpSecretBase32Field)
|
newCustomFields.remove(hmacOtpSecretBase32Field)
|
||||||
newCustomFields.remove(hmacOtpSecretBase64Field)
|
newCustomFields.remove(hmacOtpSecretBase64Field)
|
||||||
newCustomFields.remove(hmacOtpSecretCounterField)
|
newCustomFields.remove(hmacOtpSecretCounterField)
|
||||||
|
newCustomFields.remove(timeOtpSecretField)
|
||||||
|
newCustomFields.remove(timeOtpSecretHexField)
|
||||||
|
newCustomFields.remove(timeOtpSecretBase32Field)
|
||||||
|
newCustomFields.remove(timeOtpSecretBase64Field)
|
||||||
|
newCustomFields.remove(timeOtpLengthField)
|
||||||
|
newCustomFields.remove(timeOtpPeriodField)
|
||||||
|
newCustomFields.remove(timeOtpAlgorithmField)
|
||||||
// Empty auto generated OTP Token field
|
// Empty auto generated OTP Token field
|
||||||
if (fieldsToParse.contains(otpField)
|
if (fieldsToParse.contains(otpField)
|
||||||
|| fieldsToParse.contains(totpSeedField)
|
|| fieldsToParse.contains(totpSeedField)
|
||||||
|| fieldsToParse.contains(hmacOtpSecretField)
|
|| fieldsToParse.contains(hmacOtpSecretField)
|
||||||
|| fieldsToParse.contains(hmacOtpSecretHewField)
|
|| fieldsToParse.contains(hmacOtpSecretHexField)
|
||||||
|| fieldsToParse.contains(hmacOtpSecretBase32Field)
|
|| fieldsToParse.contains(hmacOtpSecretBase32Field)
|
||||||
|| fieldsToParse.contains(hmacOtpSecretBase64Field)
|
|| fieldsToParse.contains(hmacOtpSecretBase64Field)
|
||||||
|
|| fieldsToParse.contains(timeOtpSecretField)
|
||||||
|
|| fieldsToParse.contains(timeOtpSecretHexField)
|
||||||
|
|| fieldsToParse.contains(timeOtpSecretBase32Field)
|
||||||
|
|| fieldsToParse.contains(timeOtpSecretBase64Field)
|
||||||
)
|
)
|
||||||
newCustomFields.add(Field(OTP_TOKEN_FIELD))
|
newCustomFields.add(Field(OTP_TOKEN_FIELD))
|
||||||
return newCustomFields
|
return newCustomFields
|
||||||
|
|||||||
@@ -19,10 +19,12 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.settings
|
package com.kunzisoft.keepass.settings
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import androidx.preference.SwitchPreference
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistAppIdPreferenceDialogFragmentCompat
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistAppIdPreferenceDialogFragmentCompat
|
||||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistWebDomainPreferenceDialogFragmentCompat
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistWebDomainPreferenceDialogFragmentCompat
|
||||||
@@ -32,6 +34,11 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
|
|||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
// Load the preferences from an XML resource
|
// Load the preferences from an XML resource
|
||||||
setPreferencesFromResource(R.xml.preferences_autofill, rootKey)
|
setPreferencesFromResource(R.xml.preferences_autofill, rootKey)
|
||||||
|
|
||||||
|
val autofillInlineSuggestionsPreference: SwitchPreference? = findPreference(getString(R.string.autofill_inline_suggestions_key))
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
|
autofillInlineSuggestionsPreference?.isVisible = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDisplayPreferenceDialog(preference: Preference?) {
|
override fun onDisplayPreferenceDialog(preference: Preference?) {
|
||||||
|
|||||||
@@ -103,6 +103,6 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen)
|
fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen, reload: Boolean = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment
|
|||||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper
|
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.notifications.AdvancedUnlockNotificationService
|
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
|
||||||
@@ -218,7 +218,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
val tempAdvancedUnlockPreference: SwitchPreference? = findPreference(getString(R.string.temp_advanced_unlock_enable_key))
|
val tempAdvancedUnlockPreference: SwitchPreference? = findPreference(getString(R.string.temp_advanced_unlock_enable_key))
|
||||||
|
|
||||||
val biometricUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
val biometricUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
BiometricUnlockDatabaseHelper.biometricUnlockSupported(activity)
|
AdvancedUnlockManager.biometricUnlockSupported(activity)
|
||||||
} else false
|
} else false
|
||||||
biometricUnlockEnablePreference?.apply {
|
biometricUnlockEnablePreference?.apply {
|
||||||
// False if under Marshmallow
|
// False if under Marshmallow
|
||||||
@@ -258,15 +258,18 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
BiometricUnlockDatabaseHelper.deviceCredentialUnlockSupported(activity)
|
AdvancedUnlockManager.deviceCredentialUnlockSupported(activity)
|
||||||
} else false
|
} else false
|
||||||
deviceCredentialUnlockEnablePreference?.apply {
|
deviceCredentialUnlockEnablePreference?.apply {
|
||||||
|
// Biometric unlock already checked
|
||||||
|
if (biometricUnlockEnablePreference?.isChecked == true)
|
||||||
|
isChecked = false
|
||||||
if (!deviceCredentialUnlockSupported) {
|
if (!deviceCredentialUnlockSupported) {
|
||||||
isChecked = false
|
isChecked = false
|
||||||
setOnPreferenceClickListener { preference ->
|
setOnPreferenceClickListener { preference ->
|
||||||
(preference as SwitchPreference).isChecked = false
|
(preference as SwitchPreference).isChecked = false
|
||||||
UnavailableFeatureDialogFragment.getInstance(Build.VERSION_CODES.R)
|
UnavailableFeatureDialogFragment.getInstance(Build.VERSION_CODES.M)
|
||||||
.show(parentFragmentManager, "unavailableFeatureDialog")
|
.show(parentFragmentManager, "unavailableFeatureDialog")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -337,9 +340,9 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
validate?.invoke()
|
validate?.invoke()
|
||||||
deleteKeysAlertDialog?.setOnDismissListener(null)
|
deleteKeysAlertDialog?.setOnDismissListener(null)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
BiometricUnlockDatabaseHelper.deleteEntryKeyInKeystoreForBiometric(
|
AdvancedUnlockManager.deleteEntryKeyInKeystoreForBiometric(
|
||||||
activity,
|
activity,
|
||||||
object : BiometricUnlockDatabaseHelper.BiometricUnlockErrorCallback {
|
object : AdvancedUnlockManager.AdvancedUnlockErrorCallback {
|
||||||
fun showException(e: Exception) {
|
fun showException(e: Exception) {
|
||||||
Toast.makeText(context,
|
Toast.makeText(context,
|
||||||
getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
|
getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
|
||||||
@@ -350,7 +353,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
showException(e)
|
showException(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBiometricException(e: Exception) {
|
override fun onGenericException(e: Exception) {
|
||||||
showException(e)
|
showException(e)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -383,7 +386,13 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
if (styleEnabled) {
|
if (styleEnabled) {
|
||||||
Stylish.assignStyle(styleIdString)
|
Stylish.assignStyle(styleIdString)
|
||||||
activity.recreate()
|
// Relaunch the current activity to redraw theme
|
||||||
|
(activity as? SettingsActivity?)?.apply {
|
||||||
|
keepCurrentScreen()
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
styleEnabled
|
styleEnabled
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
|||||||
private var mEncryptionAlgorithmPref: DialogListExplanationPreference? = null
|
private var mEncryptionAlgorithmPref: DialogListExplanationPreference? = null
|
||||||
private var mKeyDerivationPref: DialogListExplanationPreference? = null
|
private var mKeyDerivationPref: DialogListExplanationPreference? = null
|
||||||
private var mRoundPref: InputKdfNumberPreference? = null
|
private var mRoundPref: InputKdfNumberPreference? = null
|
||||||
private var mMemoryPref: InputKdfNumberPreference? = null
|
private var mMemoryPref: InputKdfSizePreference? = null
|
||||||
private var mParallelismPref: InputKdfNumberPreference? = null
|
private var mParallelismPref: InputKdfNumberPreference? = null
|
||||||
|
|
||||||
override fun onCreateScreenPreference(screen: Screen, savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreateScreenPreference(screen: Screen, savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
@@ -231,7 +231,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Memory Usage
|
// Memory Usage
|
||||||
mMemoryPref = findPreference<InputKdfNumberPreference>(getString(R.string.memory_usage_key))?.apply {
|
mMemoryPref = findPreference<InputKdfSizePreference>(getString(R.string.memory_usage_key))?.apply {
|
||||||
summary = mDatabase.memoryUsage.toString()
|
summary = mDatabase.memoryUsage.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,6 +552,13 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
|||||||
settingActivity?.mProgressDatabaseTaskProvider?.startDatabaseSave(!mDatabaseReadOnly)
|
settingActivity?.mProgressDatabaseTaskProvider?.startDatabaseSave(!mDatabaseReadOnly)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.menu_reload_database -> {
|
||||||
|
settingActivity?.apply {
|
||||||
|
keepCurrentScreen()
|
||||||
|
mProgressDatabaseTaskProvider?.startDatabaseReload(false)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
// Check the time lock before launching settings
|
// Check the time lock before launching settings
|
||||||
|
|||||||
@@ -35,9 +35,13 @@ abstract class NestedSettingsFragment : PreferenceFragmentCompat() {
|
|||||||
APPLICATION, FORM_FILLING, ADVANCED_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY
|
APPLICATION, FORM_FILLING, ADVANCED_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getScreen(): Screen {
|
||||||
|
return Screen.values()[requireArguments().getInt(TAG_KEY)]
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
onCreateScreenPreference(
|
onCreateScreenPreference(
|
||||||
Screen.values()[requireArguments().getInt(TAG_KEY)],
|
getScreen(),
|
||||||
savedInstanceState,
|
savedInstanceState,
|
||||||
rootKey)
|
rootKey)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import android.net.Uri
|
|||||||
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.biometric.AdvancedUnlockManager
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -240,14 +241,23 @@ object PreferencesUtil {
|
|||||||
|
|
||||||
fun isBiometricUnlockEnable(context: Context): Boolean {
|
fun isBiometricUnlockEnable(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
val biometricSupported = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||||
|
AdvancedUnlockManager.biometricUnlockSupported(context)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
|
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
|
||||||
context.resources.getBoolean(R.bool.biometric_unlock_enable_default))
|
context.resources.getBoolean(R.bool.biometric_unlock_enable_default))
|
||||||
|
&& biometricSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isDeviceCredentialUnlockEnable(context: Context): Boolean {
|
fun isDeviceCredentialUnlockEnable(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
// Priority to biometric unlock
|
||||||
|
val biometricAlreadySupported = isBiometricUnlockEnable(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.device_credential_unlock_enable_key),
|
return prefs.getBoolean(context.getString(R.string.device_credential_unlock_enable_key),
|
||||||
context.resources.getBoolean(R.bool.device_credential_unlock_enable_default))
|
context.resources.getBoolean(R.bool.device_credential_unlock_enable_default))
|
||||||
|
&& !biometricAlreadySupported
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isTempAdvancedUnlockEnable(context: Context): Boolean {
|
fun isTempAdvancedUnlockEnable(context: Context): Boolean {
|
||||||
@@ -426,13 +436,18 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.autofill_close_database_default))
|
context.resources.getBoolean(R.bool.autofill_close_database_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun isAutofillAutoSearchEnable(context: Context): Boolean {
|
fun isAutofillAutoSearchEnable(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.autofill_auto_search_key),
|
return prefs.getBoolean(context.getString(R.string.autofill_auto_search_key),
|
||||||
context.resources.getBoolean(R.bool.autofill_auto_search_default))
|
context.resources.getBoolean(R.bool.autofill_auto_search_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isAutofillInlineSuggestionsEnable(context: Context): Boolean {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return prefs.getBoolean(context.getString(R.string.autofill_inline_suggestions_key),
|
||||||
|
context.resources.getBoolean(R.bool.autofill_inline_suggestions_default))
|
||||||
|
}
|
||||||
|
|
||||||
fun isAutofillSaveSearchInfoEnable(context: Context): Boolean {
|
fun isAutofillSaveSearchInfoEnable(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.autofill_save_search_info_key),
|
return prefs.getBoolean(context.getString(R.string.autofill_save_search_info_key),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePassDX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.settings
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.backup.BackupManager
|
import android.app.backup.BackupManager
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -35,7 +34,9 @@ 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.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.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.view.showActionError
|
import com.kunzisoft.keepass.view.showActionError
|
||||||
|
|
||||||
@@ -81,7 +82,7 @@ open class SettingsActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Focus view to reinitialize timeout
|
// Focus view to reinitialize timeout
|
||||||
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
|
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
@@ -94,12 +95,30 @@ open class SettingsActivity
|
|||||||
backupManager = BackupManager(this)
|
backupManager = BackupManager(this)
|
||||||
|
|
||||||
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
||||||
// Call result in fragment
|
when (actionTask) {
|
||||||
(supportFragmentManager
|
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
||||||
.findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?)
|
// Reload the current activity
|
||||||
?.onProgressDialogThreadResult(actionTask, result)
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Call result in fragment
|
||||||
|
(supportFragmentManager
|
||||||
|
.findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?)
|
||||||
|
?.onProgressDialogThreadResult(actionTask, result)
|
||||||
|
coordinatorLayout?.showActionError(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
coordinatorLayout?.showActionError(result)
|
// To reload the current screen
|
||||||
|
if (intent.extras?.containsKey(FRAGMENT_ARG) == true) {
|
||||||
|
intent.extras?.getString(FRAGMENT_ARG)?.let { fragmentScreenName ->
|
||||||
|
onNestedPreferenceSelected(NestedSettingsFragment.Screen.valueOf(fragmentScreenName), true)
|
||||||
|
}
|
||||||
|
// Eat state
|
||||||
|
intent.removeExtra(FRAGMENT_ARG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,25 +211,41 @@ open class SettingsActivity
|
|||||||
hideOrShowLockButton(NestedSettingsFragment.Screen.APPLICATION)
|
hideOrShowLockButton(NestedSettingsFragment.Screen.APPLICATION)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun replaceFragment(key: NestedSettingsFragment.Screen) {
|
private fun replaceFragment(key: NestedSettingsFragment.Screen, reload: Boolean) {
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction().apply {
|
||||||
.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left,
|
if (reload) {
|
||||||
|
setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out,
|
||||||
R.anim.slide_in_left, R.anim.slide_out_right)
|
R.anim.slide_in_left, R.anim.slide_out_right)
|
||||||
.replace(R.id.fragment_container, NestedSettingsFragment.newInstance(key, mReadOnly), TAG_NESTED)
|
} else {
|
||||||
.addToBackStack(TAG_NESTED)
|
setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left,
|
||||||
.commit()
|
R.anim.slide_in_left, R.anim.slide_out_right)
|
||||||
|
}
|
||||||
|
replace(R.id.fragment_container, NestedSettingsFragment.newInstance(key, mReadOnly), TAG_NESTED)
|
||||||
|
addToBackStack(TAG_NESTED)
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
|
||||||
toolbar?.title = NestedSettingsFragment.retrieveTitle(resources, key)
|
toolbar?.title = NestedSettingsFragment.retrieveTitle(resources, key)
|
||||||
hideOrShowLockButton(key)
|
hideOrShowLockButton(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen) {
|
/**
|
||||||
|
* To keep the current screen when activity is reloaded
|
||||||
|
*/
|
||||||
|
fun keepCurrentScreen() {
|
||||||
|
(supportFragmentManager.findFragmentByTag(TAG_NESTED) as? NestedSettingsFragment?)
|
||||||
|
?.getScreen()?.let { fragmentKey ->
|
||||||
|
intent.putExtra(FRAGMENT_ARG, fragmentKey.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen, reload: Boolean) {
|
||||||
if (mTimeoutEnable)
|
if (mTimeoutEnable)
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
|
||||||
replaceFragment(key)
|
replaceFragment(key, reload)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
replaceFragment(key)
|
replaceFragment(key, reload)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
@@ -225,6 +260,7 @@ open class SettingsActivity
|
|||||||
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"
|
||||||
|
private const val FRAGMENT_ARG = "FRAGMENT_ARG"
|
||||||
|
|
||||||
fun launch(activity: Activity, readOnly: Boolean, timeoutEnable: Boolean) {
|
fun launch(activity: Activity, readOnly: Boolean, timeoutEnable: Boolean) {
|
||||||
val intent = Intent(activity, SettingsActivity::class.java)
|
val intent = Intent(activity, SettingsActivity::class.java)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import androidx.preference.DialogPreference
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||||
|
|
||||||
class InputKdfNumberPreference @JvmOverloads constructor(context: Context,
|
open class InputKdfNumberPreference @JvmOverloads constructor(context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
|
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
|
||||||
defStyleRes: Int = defStyleAttr)
|
defStyleRes: Int = defStyleAttr)
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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.util.AttributeSet
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.utils.DataByte
|
||||||
|
|
||||||
|
class InputKdfSizePreference @JvmOverloads constructor(context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
|
||||||
|
defStyleRes: Int = defStyleAttr)
|
||||||
|
: InputKdfNumberPreference(context, attrs, defStyleAttr, defStyleRes) {
|
||||||
|
|
||||||
|
override fun setSummary(summary: CharSequence) {
|
||||||
|
if (summary == UNKNOWN_VALUE_STRING) {
|
||||||
|
super.setSummary("")
|
||||||
|
} else {
|
||||||
|
var summaryString = summary
|
||||||
|
try {
|
||||||
|
val memorySize = summary.toString().toLong()
|
||||||
|
summaryString = if (memorySize > 0) {
|
||||||
|
// To convert bytes to mebibytes
|
||||||
|
DataByte(memorySize, DataByte.ByteFormat.BYTE)
|
||||||
|
.toBetterByteFormat().toString(context)
|
||||||
|
} else {
|
||||||
|
memorySize.toString()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
} finally {
|
||||||
|
super.setSummary(summaryString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,7 +36,7 @@ open class InputNumberPreference @JvmOverloads constructor(context: Context,
|
|||||||
|
|
||||||
override fun setSummary(summary: CharSequence) {
|
override fun setSummary(summary: CharSequence) {
|
||||||
if (summary == INFINITE_VALUE_STRING) {
|
if (summary == INFINITE_VALUE_STRING) {
|
||||||
super.setSummary("")
|
super.setSummary("∞")
|
||||||
} else {
|
} else {
|
||||||
super.setSummary(summary)
|
super.setSummary(summary)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* 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.util.AttributeSet
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.utils.DataByte
|
||||||
|
|
||||||
|
open class InputSizePreference @JvmOverloads constructor(context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
|
||||||
|
defStyleRes: Int = defStyleAttr)
|
||||||
|
: InputNumberPreference(context, attrs, defStyleAttr, defStyleRes) {
|
||||||
|
|
||||||
|
override fun setSummary(summary: CharSequence) {
|
||||||
|
var summaryString = summary
|
||||||
|
try {
|
||||||
|
val memorySize = summary.toString().toLong()
|
||||||
|
summaryString = if (memorySize >= 0) {
|
||||||
|
// To convert bytes to mebibytes
|
||||||
|
DataByte(memorySize, DataByte.ByteFormat.BYTE)
|
||||||
|
.toBetterByteFormat().toString(context)
|
||||||
|
} else {
|
||||||
|
memorySize.toString()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
} finally {
|
||||||
|
super.setSummary(summaryString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ import com.kunzisoft.keepass.R
|
|||||||
abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCompat() {
|
abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCompat() {
|
||||||
|
|
||||||
private var inputTextView: EditText? = null
|
private var inputTextView: EditText? = null
|
||||||
|
private var textUnitView: TextView? = null
|
||||||
private var textExplanationView: TextView? = null
|
private var textExplanationView: TextView? = null
|
||||||
private var switchElementView: CompoundButton? = null
|
private var switchElementView: CompoundButton? = null
|
||||||
|
|
||||||
@@ -47,6 +48,14 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setInoutText(@StringRes inputTextId: Int) {
|
||||||
|
inputText = getString(inputTextId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showInputText(show: Boolean) {
|
||||||
|
inputTextView?.visibility = if (show) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
fun setInputTextError(error: CharSequence) {
|
fun setInputTextError(error: CharSequence) {
|
||||||
this.inputTextView?.error = error
|
this.inputTextView?.error = error
|
||||||
}
|
}
|
||||||
@@ -55,6 +64,24 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
|
|||||||
this.mOnInputTextEditorActionListener = onEditorActionListener
|
this.mOnInputTextEditorActionListener = onEditorActionListener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var unitText: String?
|
||||||
|
get() = textUnitView?.text?.toString() ?: ""
|
||||||
|
set(unitText) {
|
||||||
|
textUnitView?.apply {
|
||||||
|
if (unitText != null && unitText.isNotEmpty()) {
|
||||||
|
text = unitText
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
text = ""
|
||||||
|
visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUnitText(@StringRes unitTextId: Int) {
|
||||||
|
unitText = getString(unitTextId)
|
||||||
|
}
|
||||||
|
|
||||||
var explanationText: String?
|
var explanationText: String?
|
||||||
get() = textExplanationView?.text?.toString() ?: ""
|
get() = textExplanationView?.text?.toString() ?: ""
|
||||||
set(explanationText) {
|
set(explanationText) {
|
||||||
@@ -69,6 +96,10 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setExplanationText(@StringRes explanationTextId: Int) {
|
||||||
|
explanationText = getString(explanationTextId)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBindDialogView(view: View) {
|
override fun onBindDialogView(view: View) {
|
||||||
super.onBindDialogView(view)
|
super.onBindDialogView(view)
|
||||||
|
|
||||||
@@ -93,6 +124,8 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
textUnitView = view.findViewById(R.id.input_text_unit)
|
||||||
|
textUnitView?.visibility = View.GONE
|
||||||
textExplanationView = view.findViewById(R.id.explanation_text)
|
textExplanationView = view.findViewById(R.id.explanation_text)
|
||||||
textExplanationView?.visibility = View.GONE
|
textExplanationView?.visibility = View.GONE
|
||||||
switchElementView = view.findViewById(R.id.switch_element)
|
switchElementView = view.findViewById(R.id.switch_element)
|
||||||
@@ -113,18 +146,6 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setInoutText(@StringRes inputTextId: Int) {
|
|
||||||
inputText = getString(inputTextId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showInputText(show: Boolean) {
|
|
||||||
inputTextView?.visibility = if (show) View.VISIBLE else View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setExplanationText(@StringRes explanationTextId: Int) {
|
|
||||||
explanationText = getString(explanationTextId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSwitchAction(onCheckedChange: ((isChecked: Boolean)-> Unit)?, defaultChecked: Boolean) {
|
fun setSwitchAction(onCheckedChange: ((isChecked: Boolean)-> Unit)?, defaultChecked: Boolean) {
|
||||||
switchElementView?.visibility = if (onCheckedChange == null) View.GONE else View.VISIBLE
|
switchElementView?.visibility = if (onCheckedChange == null) View.GONE else View.VISIBLE
|
||||||
switchElementView?.isChecked = defaultChecked
|
switchElementView?.isChecked = defaultChecked
|
||||||
|
|||||||
@@ -22,50 +22,76 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.utils.DataByte
|
||||||
|
|
||||||
class MaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
class MaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||||
|
|
||||||
|
private var dataByte = DataByte(2L, DataByte.ByteFormat.MEBIBYTE)
|
||||||
|
|
||||||
override fun onBindDialogView(view: View) {
|
override fun onBindDialogView(view: View) {
|
||||||
super.onBindDialogView(view)
|
super.onBindDialogView(view)
|
||||||
|
|
||||||
setExplanationText(R.string.max_history_size_summary)
|
setExplanationText(R.string.max_history_size_summary)
|
||||||
database?.historyMaxSize?.let { maxItemsDatabase ->
|
database?.historyMaxSize?.let { maxItemsDatabase ->
|
||||||
inputText = maxItemsDatabase.toString()
|
dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE)
|
||||||
|
.toBetterByteFormat()
|
||||||
|
inputText = dataByte.number.toString()
|
||||||
|
if (dataByte.number >= 0) {
|
||||||
|
setUnitText(dataByte.format.stringId)
|
||||||
|
} else {
|
||||||
|
unitText = null
|
||||||
|
}
|
||||||
|
|
||||||
setSwitchAction({ isChecked ->
|
setSwitchAction({ isChecked ->
|
||||||
inputText = if (!isChecked) {
|
if (!isChecked) {
|
||||||
INFINITE_MAX_HISTORY_SIZE.toString()
|
dataByte = INFINITE_MAX_HISTORY_SIZE_DATA_BYTE
|
||||||
} else
|
inputText = INFINITE_MAX_HISTORY_SIZE.toString()
|
||||||
DEFAULT_MAX_HISTORY_SIZE.toString()
|
unitText = null
|
||||||
|
} else {
|
||||||
|
dataByte = DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE
|
||||||
|
inputText = dataByte.number.toString()
|
||||||
|
setUnitText(dataByte.format.stringId)
|
||||||
|
}
|
||||||
showInputText(isChecked)
|
showInputText(isChecked)
|
||||||
}, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
|
}, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(positiveResult: Boolean) {
|
override fun onDialogClosed(positiveResult: Boolean) {
|
||||||
if (positiveResult) {
|
if (positiveResult) {
|
||||||
database?.let { database ->
|
database?.let { database ->
|
||||||
var maxHistorySize: Long = try {
|
val maxHistorySize: Long = try {
|
||||||
inputText.toLong()
|
inputText.toLong()
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
DEFAULT_MAX_HISTORY_SIZE
|
DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE.toBytes()
|
||||||
}
|
}
|
||||||
if (maxHistorySize < INFINITE_MAX_HISTORY_SIZE) {
|
val numberOfBytes = if (maxHistorySize >= 0) {
|
||||||
maxHistorySize = INFINITE_MAX_HISTORY_SIZE
|
val dataByteConversion = DataByte(maxHistorySize, dataByte.format)
|
||||||
|
var bytes = dataByteConversion.toBytes()
|
||||||
|
if (bytes > Long.MAX_VALUE) {
|
||||||
|
bytes = Long.MAX_VALUE
|
||||||
|
}
|
||||||
|
bytes
|
||||||
|
} else {
|
||||||
|
INFINITE_MAX_HISTORY_SIZE
|
||||||
}
|
}
|
||||||
|
|
||||||
val oldMaxHistorySize = database.historyMaxSize
|
val oldMaxHistorySize = database.historyMaxSize
|
||||||
database.historyMaxSize = maxHistorySize
|
database.historyMaxSize = numberOfBytes
|
||||||
|
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(oldMaxHistorySize, maxHistorySize, mDatabaseAutoSaveEnable)
|
mProgressDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(oldMaxHistorySize, numberOfBytes, mDatabaseAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val DEFAULT_MAX_HISTORY_SIZE = 134217728L
|
|
||||||
const val INFINITE_MAX_HISTORY_SIZE = -1L
|
const val INFINITE_MAX_HISTORY_SIZE = -1L
|
||||||
|
|
||||||
|
private val INFINITE_MAX_HISTORY_SIZE_DATA_BYTE = DataByte(INFINITE_MAX_HISTORY_SIZE, DataByte.ByteFormat.MEBIBYTE)
|
||||||
|
private val DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE = DataByte(6L, DataByte.ByteFormat.MEBIBYTE)
|
||||||
|
|
||||||
fun newInstance(key: String): MaxHistorySizePreferenceDialogFragmentCompat {
|
fun newInstance(key: String): MaxHistorySizePreferenceDialogFragmentCompat {
|
||||||
val fragment = MaxHistorySizePreferenceDialogFragmentCompat()
|
val fragment = MaxHistorySizePreferenceDialogFragmentCompat()
|
||||||
val bundle = Bundle(1)
|
val bundle = Bundle(1)
|
||||||
|
|||||||
@@ -22,33 +22,46 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.utils.DataByte
|
||||||
|
|
||||||
class MemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
class MemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||||
|
|
||||||
|
private var dataByte = DataByte(MIN_MEMORY_USAGE, DataByte.ByteFormat.BYTE)
|
||||||
|
|
||||||
override fun onBindDialogView(view: View) {
|
override fun onBindDialogView(view: View) {
|
||||||
super.onBindDialogView(view)
|
super.onBindDialogView(view)
|
||||||
|
|
||||||
setExplanationText(R.string.memory_usage_explanation)
|
setExplanationText(R.string.memory_usage_explanation)
|
||||||
inputText = database?.memoryUsage?.toString()?: MIN_MEMORY_USAGE.toString()
|
|
||||||
|
val memoryBytes = database?.memoryUsage ?: MIN_MEMORY_USAGE
|
||||||
|
dataByte = DataByte(memoryBytes, DataByte.ByteFormat.BYTE)
|
||||||
|
.toBetterByteFormat()
|
||||||
|
inputText = dataByte.number.toString()
|
||||||
|
setUnitText(dataByte.format.stringId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(positiveResult: Boolean) {
|
override fun onDialogClosed(positiveResult: Boolean) {
|
||||||
if (positiveResult) {
|
if (positiveResult) {
|
||||||
database?.let { database ->
|
database?.let { database ->
|
||||||
var memoryUsage: Long = try {
|
var newMemoryUsage: Long = try {
|
||||||
inputText.toLong()
|
inputText.toLong()
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
MIN_MEMORY_USAGE
|
MIN_MEMORY_USAGE
|
||||||
}
|
}
|
||||||
if (memoryUsage < MIN_MEMORY_USAGE) {
|
if (newMemoryUsage < MIN_MEMORY_USAGE) {
|
||||||
memoryUsage = MIN_MEMORY_USAGE
|
newMemoryUsage = MIN_MEMORY_USAGE
|
||||||
|
}
|
||||||
|
// To transform in bytes
|
||||||
|
dataByte.number = newMemoryUsage
|
||||||
|
var numberOfBytes = dataByte.toBytes()
|
||||||
|
if (numberOfBytes > Long.MAX_VALUE) {
|
||||||
|
numberOfBytes = Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
// TODO Max Memory
|
|
||||||
|
|
||||||
val oldMemoryUsage = database.memoryUsage
|
val oldMemoryUsage = database.memoryUsage
|
||||||
database.memoryUsage = memoryUsage
|
database.memoryUsage = numberOfBytes
|
||||||
|
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(oldMemoryUsage, memoryUsage, mDatabaseAutoSaveEnable)
|
mProgressDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(oldMemoryUsage, numberOfBytes, mDatabaseAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,5 +138,5 @@ fun Context.closeDatabase() {
|
|||||||
cancelAll()
|
cancelAll()
|
||||||
}
|
}
|
||||||
// Clear data
|
// Clear data
|
||||||
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
|
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
|
||||||
}
|
}
|
||||||
84
app/src/main/java/com/kunzisoft/keepass/utils/DataByte.kt
Normal file
84
app/src/main/java/com/kunzisoft/keepass/utils/DataByte.kt
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* 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.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
|
class DataByte(var number: Long, var format: ByteFormat) {
|
||||||
|
|
||||||
|
fun toBetterByteFormat(): DataByte {
|
||||||
|
return when (this.format) {
|
||||||
|
ByteFormat.BYTE -> {
|
||||||
|
when {
|
||||||
|
//this.number % GIBIBYTES == 0L -> {
|
||||||
|
// DataByte((this.number / GIBIBYTES), ByteFormat.GIBIBYTE)
|
||||||
|
//}
|
||||||
|
this.number % MEBIBYTES == 0L -> {
|
||||||
|
DataByte((this.number / MEBIBYTES), ByteFormat.MEBIBYTE)
|
||||||
|
}
|
||||||
|
this.number % KIBIBYTES == 0L -> {
|
||||||
|
DataByte((this.number / KIBIBYTES), ByteFormat.KIBIBYTE)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
DataByte(this.number, ByteFormat.BYTE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
DataByte(toBytes(), ByteFormat.BYTE).toBetterByteFormat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of bytes in current DataByte
|
||||||
|
*/
|
||||||
|
fun toBytes(): Long {
|
||||||
|
return when (this.format) {
|
||||||
|
ByteFormat.BYTE -> this.number
|
||||||
|
ByteFormat.KIBIBYTE -> this.number * KIBIBYTES
|
||||||
|
ByteFormat.MEBIBYTE -> this.number * MEBIBYTES
|
||||||
|
//ByteFormat.GIBIBYTE -> this.number * GIBIBYTES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$number ${format.name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toString(context: Context): String {
|
||||||
|
return "$number ${context.getString(format.stringId)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ByteFormat(@StringRes var stringId: Int) {
|
||||||
|
BYTE(R.string.unit_byte),
|
||||||
|
KIBIBYTE(R.string.unit_kibibyte),
|
||||||
|
MEBIBYTE(R.string.unit_mebibyte)
|
||||||
|
//GIBIBYTE(R.string.unit_gibibyte)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KIBIBYTES = 1024L
|
||||||
|
const val MEBIBYTES = 1048576L
|
||||||
|
const val GIBIBYTES = 1073741824L
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,23 +53,19 @@ object MenuUtil {
|
|||||||
fun onDefaultMenuOptionsItemSelected(activity: Activity,
|
fun onDefaultMenuOptionsItemSelected(activity: Activity,
|
||||||
item: MenuItem,
|
item: MenuItem,
|
||||||
readOnly: Boolean = READ_ONLY_DEFAULT,
|
readOnly: Boolean = READ_ONLY_DEFAULT,
|
||||||
timeoutEnable: Boolean = false): Boolean {
|
timeoutEnable: Boolean = false) {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.menu_contribute -> {
|
R.id.menu_contribute -> {
|
||||||
onContributionItemSelected(activity)
|
onContributionItemSelected(activity)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
R.id.menu_app_settings -> {
|
R.id.menu_app_settings -> {
|
||||||
// To avoid flickering when launch settings in a LockingActivity
|
// To avoid flickering when launch settings in a LockingActivity
|
||||||
SettingsActivity.launch(activity, readOnly, timeoutEnable)
|
SettingsActivity.launch(activity, readOnly, timeoutEnable)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
R.id.menu_about -> {
|
R.id.menu_about -> {
|
||||||
val intent = Intent(activity, AboutActivity::class.java)
|
val intent = Intent(activity, AboutActivity::class.java)
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
else -> return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
app/src/main/java/com/kunzisoft/keepass/utils/StringUtil.kt
Normal file
14
app/src/main/java/com/kunzisoft/keepass/utils/StringUtil.kt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package com.kunzisoft.keepass.utils
|
||||||
|
|
||||||
|
object StringUtil {
|
||||||
|
|
||||||
|
fun String.removeLineChars(): String {
|
||||||
|
return this.replace("[\\r|\\n|\\t|\\u00A0]+".toRegex(), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.removeSpaceChars(): String {
|
||||||
|
return this.replace("[\\r|\\n|\\t|\\s|\\u00A0]+".toRegex(), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) }
|
||||||
|
}
|
||||||
@@ -48,8 +48,8 @@ import java.util.*
|
|||||||
|
|
||||||
|
|
||||||
class EntryContentsView @JvmOverloads constructor(context: Context,
|
class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||||
var attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
var defStyle: Int = 0)
|
defStyle: Int = 0)
|
||||||
: LinearLayout(context, attrs, defStyle) {
|
: LinearLayout(context, attrs, defStyle) {
|
||||||
|
|
||||||
private var fontInVisibility: Boolean = false
|
private var fontInVisibility: Boolean = false
|
||||||
@@ -67,7 +67,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
|
|
||||||
private val creationDateView: TextView
|
private val creationDateView: TextView
|
||||||
private val modificationDateView: TextView
|
private val modificationDateView: TextView
|
||||||
private val lastAccessDateView: TextView
|
|
||||||
private val expiresImageView: ImageView
|
private val expiresImageView: ImageView
|
||||||
private val expiresDateView: TextView
|
private val expiresDateView: TextView
|
||||||
|
|
||||||
@@ -117,7 +116,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
|
|
||||||
creationDateView = findViewById(R.id.entry_created)
|
creationDateView = findViewById(R.id.entry_created)
|
||||||
modificationDateView = findViewById(R.id.entry_modified)
|
modificationDateView = findViewById(R.id.entry_modified)
|
||||||
lastAccessDateView = findViewById(R.id.entry_accessed)
|
|
||||||
expiresImageView = findViewById(R.id.entry_expires_image)
|
expiresImageView = findViewById(R.id.entry_expires_image)
|
||||||
expiresDateView = findViewById(R.id.entry_expires_date)
|
expiresDateView = findViewById(R.id.entry_expires_date)
|
||||||
|
|
||||||
@@ -258,20 +256,13 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
modificationDateView.text = date.getDateTimeString(resources)
|
modificationDateView.text = date.getDateTimeString(resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assignLastAccessDate(date: DateInstant) {
|
fun setExpires(isExpires: Boolean, expiryTime: DateInstant) {
|
||||||
lastAccessDateView.text = date.getDateTimeString(resources)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setExpires(isExpires: Boolean) {
|
|
||||||
expiresImageView.visibility = if (isExpires) View.VISIBLE else View.GONE
|
expiresImageView.visibility = if (isExpires) View.VISIBLE else View.GONE
|
||||||
}
|
expiresDateView.text = if (isExpires) {
|
||||||
|
expiryTime.getDateTimeString(resources)
|
||||||
fun assignExpiresDate(date: DateInstant) {
|
} else {
|
||||||
assignExpiresDate(date.getDateTimeString(resources))
|
resources.getString(R.string.never)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assignExpiresDate(constString: String) {
|
|
||||||
expiresDateView.text = constString
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assignUUID(uuid: UUID) {
|
fun assignUUID(uuid: UUID) {
|
||||||
@@ -279,7 +270,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
uuidReferenceView.text = UuidUtil.toHexString(uuid)
|
uuidReferenceView.text = UuidUtil.toHexString(uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun setHiddenProtectedValue(hiddenProtectedValue: Boolean) {
|
fun setHiddenProtectedValue(hiddenProtectedValue: Boolean) {
|
||||||
passwordFieldView.hiddenProtectedValue = hiddenProtectedValue
|
passwordFieldView.hiddenProtectedValue = hiddenProtectedValue
|
||||||
// Hidden style for custom fields
|
// Hidden style for custom fields
|
||||||
@@ -306,7 +296,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
allowCopy: Boolean,
|
allowCopy: Boolean,
|
||||||
onCopyButtonClickListener: OnClickListener?) {
|
onCopyButtonClickListener: OnClickListener?) {
|
||||||
|
|
||||||
val entryCustomField: EntryField? = EntryField(context, attrs, defStyle)
|
val entryCustomField: EntryField? = EntryField(context)
|
||||||
entryCustomField?.apply {
|
entryCustomField?.apply {
|
||||||
setLabel(title)
|
setLabel(title)
|
||||||
setValue(value.toString(), value.isProtected)
|
setValue(value.toString(), value.isProtected)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.content.Context
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
@@ -20,7 +19,6 @@ class KeyFileSelectionView @JvmOverloads constructor(context: Context,
|
|||||||
|
|
||||||
private val keyFileNameInputLayout: TextInputLayout
|
private val keyFileNameInputLayout: TextInputLayout
|
||||||
private val keyFileNameView: TextView
|
private val keyFileNameView: TextView
|
||||||
private val keyFileOpenView: ImageView
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
@@ -28,7 +26,6 @@ class KeyFileSelectionView @JvmOverloads constructor(context: Context,
|
|||||||
|
|
||||||
keyFileNameInputLayout = findViewById(R.id.input_entry_keyfile)
|
keyFileNameInputLayout = findViewById(R.id.input_entry_keyfile)
|
||||||
keyFileNameView = findViewById(R.id.keyfile_name)
|
keyFileNameView = findViewById(R.id.keyfile_name)
|
||||||
keyFileOpenView = findViewById(R.id.keyfile_open_button)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setOnClickListener(l: OnClickListener?) {
|
override fun setOnClickListener(l: OnClickListener?) {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import android.content.Context
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
@@ -58,7 +57,11 @@ class FileDatabaseInfo : Serializable {
|
|||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
|
|
||||||
fun getModificationString(): String? {
|
fun getLastModification(): Long? {
|
||||||
|
return documentFile?.lastModified()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLastModificationString(): String? {
|
||||||
return documentFile?.lastModified()?.let {
|
return documentFile?.lastModified()?.let {
|
||||||
if (it != 0L) {
|
if (it != 0L) {
|
||||||
DateFormat.getDateTimeInstance()
|
DateFormat.getDateTimeInstance()
|
||||||
@@ -69,6 +72,10 @@ class FileDatabaseInfo : Serializable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSize(): Long? {
|
||||||
|
return documentFile?.length()
|
||||||
|
}
|
||||||
|
|
||||||
fun getSizeString(): String? {
|
fun getSizeString(): String? {
|
||||||
return documentFile?.let {
|
return documentFile?.let {
|
||||||
Formatter.formatFileSize(context, it.length())
|
Formatter.formatFileSize(context, it.length())
|
||||||
|
|||||||
7
app/src/main/res/drawable/ic_reload_white_24dp.xml
Normal file
7
app/src/main/res/drawable/ic_reload_white_24dp.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:viewportWidth="24" >
|
||||||
|
<path android:fillColor="#FFFFFF" android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z"/>
|
||||||
|
</vector>
|
||||||
@@ -68,17 +68,10 @@
|
|||||||
android:padding="0dp"
|
android:padding="0dp"
|
||||||
android:contentDescription="@string/about"
|
android:contentDescription="@string/about"
|
||||||
android:src="@drawable/ic_launcher_foreground"/>
|
android:src="@drawable/ic_launcher_foreground"/>
|
||||||
<FrameLayout
|
<androidx.fragment.app.FragmentContainerView
|
||||||
|
android:id="@+id/fragment_advanced_unlock_container_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent" />
|
||||||
<com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
|
||||||
android:id="@+id/biometric_info"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:background="?attr/colorPrimary"
|
|
||||||
android:visibility="gone"/>
|
|
||||||
</FrameLayout>
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
<androidx.appcompat.widget.Toolbar
|
||||||
@@ -164,8 +157,8 @@
|
|||||||
android:inputType="textPassword"
|
android:inputType="textPassword"
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
android:importantForAutofill="yes"
|
android:importantForAutofill="yes"
|
||||||
android:autofillHints="password|"
|
android:autofillHints="password"
|
||||||
android:imeOptions="actionDone"
|
android:imeOptions="actionDone|flagNoPersonalizedLearning"
|
||||||
android:maxLines="1"/>
|
android:maxLines="1"/>
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|||||||
13
app/src/main/res/layout/fragment_advanced_unlock.xml
Normal file
13
app/src/main/res/layout/fragment_advanced_unlock.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
<com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||||
|
android:id="@+id/advanced_unlock_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="?attr/colorPrimary"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
</FrameLayout>
|
||||||
@@ -106,6 +106,7 @@
|
|||||||
android:inputType="textPassword|textMultiLine"
|
android:inputType="textPassword|textMultiLine"
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
android:importantForAutofill="no"
|
android:importantForAutofill="no"
|
||||||
|
android:imeOptions="flagNoPersonalizedLearning"
|
||||||
android:ems="10"
|
android:ems="10"
|
||||||
android:maxLines="10"
|
android:maxLines="10"
|
||||||
android:hint="@string/entry_password"/>
|
android:hint="@string/entry_password"/>
|
||||||
|
|||||||
@@ -29,6 +29,18 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:importantForAutofill="noExcludeDescendants"
|
android:importantForAutofill="noExcludeDescendants"
|
||||||
tools:targetApi="o">
|
tools:targetApi="o">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/setup_otp_type_message"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/card_view_margin"
|
||||||
|
android:layout_marginLeft="@dimen/card_view_margin"
|
||||||
|
android:layout_marginEnd="@dimen/card_view_margin"
|
||||||
|
android:layout_marginRight="@dimen/card_view_margin"
|
||||||
|
android:text="@string/error_otp_type"
|
||||||
|
style="@style/KeepassDXStyle.TextAppearance.WarningTextStyle"/>
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
android:id="@+id/card_view_otp_selection"
|
android:id="@+id/card_view_otp_selection"
|
||||||
android:layout_margin="4dp"
|
android:layout_margin="4dp"
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="textMultiLine"
|
android:inputType="textMultiLine"
|
||||||
|
android:imeOptions="flagNoPersonalizedLearning"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:focusableInTouchMode="true"
|
android:focusableInTouchMode="true"
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
@@ -46,9 +47,9 @@
|
|||||||
android:id="@+id/entry_extra_field_edit"
|
android:id="@+id/entry_extra_field_edit"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
|
||||||
android:layout_alignParentRight="true"
|
android:layout_alignParentRight="true"
|
||||||
android:layout_alignParentEnd="true"
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_alignTop="@+id/entry_extra_field_value_container"
|
||||||
android:src="@drawable/ic_more_white_24dp"
|
android:src="@drawable/ic_more_white_24dp"
|
||||||
android:contentDescription="@string/menu_edit"
|
android:contentDescription="@string/menu_edit"
|
||||||
style="@style/KeepassDXStyle.ImageButton.Simple"/>
|
style="@style/KeepassDXStyle.ImageButton.Simple"/>
|
||||||
|
|||||||
@@ -46,14 +46,24 @@
|
|||||||
app:layout_constraintTop_toBottomOf="@+id/explanation_text"
|
app:layout_constraintTop_toBottomOf="@+id/explanation_text"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:minHeight="48dp"/>
|
android:minHeight="48dp"/>
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatEditText
|
<androidx.appcompat.widget.AppCompatEditText
|
||||||
android:id="@+id/input_text"
|
android:id="@+id/input_text"
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/switch_element"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
android:minHeight="48dp"
|
|
||||||
android:digits="0123456789"
|
android:digits="0123456789"
|
||||||
android:inputType="number"/>
|
android:inputType="number"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/input_text_unit"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/switch_element" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/input_text_unit"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/input_text"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/input_text" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -175,12 +175,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/entry_accessed"
|
android:text="@string/entry_accessed"
|
||||||
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
|
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
|
||||||
android:visibility="gone"
|
|
||||||
android:id="@+id/entry_accessed"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
|
|
||||||
|
|
||||||
<!-- Expires -->
|
<!-- Expires -->
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/keyfile_open_button">
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/keyfile_name"
|
android:id="@+id/keyfile_name"
|
||||||
@@ -33,16 +33,4 @@
|
|||||||
android:imeOptions="actionDone"
|
android:imeOptions="actionDone"
|
||||||
android:maxLines="1"/>
|
android:maxLines="1"/>
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
|
||||||
android:id="@+id/keyfile_open_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
android:contentDescription="@string/content_description_open_file"
|
|
||||||
android:focusable="true"
|
|
||||||
android:background="@drawable/background_item_selection"
|
|
||||||
android:src="@drawable/ic_folder_white_24dp"
|
|
||||||
style="@style/KeepassDXStyle.ImageButton.Simple" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -22,6 +22,6 @@
|
|||||||
<item android:id="@+id/menu_contribute"
|
<item android:id="@+id/menu_contribute"
|
||||||
android:icon="@drawable/ic_heart_white_24dp"
|
android:icon="@drawable/ic_heart_white_24dp"
|
||||||
android:title="@string/contribute"
|
android:title="@string/contribute"
|
||||||
android:orderInCategory="95"
|
android:orderInCategory="99"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
</menu>
|
</menu>
|
||||||
@@ -24,4 +24,9 @@
|
|||||||
android:title="@string/menu_save_database"
|
android:title="@string/menu_save_database"
|
||||||
android:orderInCategory="95"
|
android:orderInCategory="95"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
|
<item android:id="@+id/menu_reload_database"
|
||||||
|
android:icon="@drawable/ic_reload_white_24dp"
|
||||||
|
android:title="@string/menu_reload_database"
|
||||||
|
android:orderInCategory="96"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
</menu>
|
</menu>
|
||||||
@@ -248,7 +248,7 @@
|
|||||||
<string name="content_description_background">الخلفية</string>
|
<string name="content_description_background">الخلفية</string>
|
||||||
<string name="rounds">دورات التحويل</string>
|
<string name="rounds">دورات التحويل</string>
|
||||||
<string name="rounds_explanation">توفر الدورات الاضافية ضد هجوم توليد التركيبات ،لكنها تبطئ التحميل والحفظ.</string>
|
<string name="rounds_explanation">توفر الدورات الاضافية ضد هجوم توليد التركيبات ،لكنها تبطئ التحميل والحفظ.</string>
|
||||||
<string name="memory_usage_explanation">مقدار الذاكرة (بالبايت) لاستخدامها في دالة اشتقاق المفتاح.</string>
|
<string name="memory_usage_explanation">مقدار الذاكرة لاستخدامها في دالة اشتقاق المفتاح.</string>
|
||||||
<string name="parallelism_explanation">درجة التوازي (عدد العمليات) لدالة اشتقاق المفتاح.</string>
|
<string name="parallelism_explanation">درجة التوازي (عدد العمليات) لدالة اشتقاق المفتاح.</string>
|
||||||
<string name="sort_groups_before">مجموعات قبل</string>
|
<string name="sort_groups_before">مجموعات قبل</string>
|
||||||
<string name="selection_mode">نمط التحديد</string>
|
<string name="selection_mode">نمط التحديد</string>
|
||||||
@@ -404,7 +404,7 @@
|
|||||||
<string name="settings_database_force_changing_master_key_summary">يطالبك بتغيير المفتاح الرئيسي (بالأيام)</string>
|
<string name="settings_database_force_changing_master_key_summary">يطالبك بتغيير المفتاح الرئيسي (بالأيام)</string>
|
||||||
<string name="settings_database_recommend_changing_master_key_summary">اقترح تجديد المفتاح الرئيسي (بالأيام)</string>
|
<string name="settings_database_recommend_changing_master_key_summary">اقترح تجديد المفتاح الرئيسي (بالأيام)</string>
|
||||||
<string name="settings_database_recommend_changing_master_key_title">اقترح التجديد</string>
|
<string name="settings_database_recommend_changing_master_key_title">اقترح التجديد</string>
|
||||||
<string name="max_history_size_summary">حجم التأريخ ( بالبايت) لكل مدخل</string>
|
<string name="max_history_size_summary">حجم التأريخ لكل مدخل</string>
|
||||||
<string name="max_history_size_title">الحجم الأقصى</string>
|
<string name="max_history_size_title">الحجم الأقصى</string>
|
||||||
<string name="max_history_items_summary">عدد عناصر التأريخ لكل مدخل</string>
|
<string name="max_history_items_summary">عدد عناصر التأريخ لكل مدخل</string>
|
||||||
<string name="education_generate_password_title">أنشئ كلمة سر قوية</string>
|
<string name="education_generate_password_title">أنشئ كلمة سر قوية</string>
|
||||||
|
|||||||
@@ -175,7 +175,7 @@
|
|||||||
<string name="settings_database_force_changing_master_key_title">Forsiraj obnovu</string>
|
<string name="settings_database_force_changing_master_key_title">Forsiraj obnovu</string>
|
||||||
<string name="settings_database_recommend_changing_master_key_summary">Preporučiti promenu glavnog ključa (u danima)</string>
|
<string name="settings_database_recommend_changing_master_key_summary">Preporučiti promenu glavnog ključa (u danima)</string>
|
||||||
<string name="settings_database_recommend_changing_master_key_title">Preporučiti obnavljanje</string>
|
<string name="settings_database_recommend_changing_master_key_title">Preporučiti obnavljanje</string>
|
||||||
<string name="max_history_size_summary">Ograniči veličinu istorije (u bajtovima) po unosu</string>
|
<string name="max_history_size_summary">Ograniči veličinu istorije po unosu</string>
|
||||||
<string name="max_history_size_title">Maksimalna veličina</string>
|
<string name="max_history_size_title">Maksimalna veličina</string>
|
||||||
<string name="max_history_items_summary">Ograniči broja stavki istorije po unosu</string>
|
<string name="max_history_items_summary">Ograniči broja stavki istorije po unosu</string>
|
||||||
<string name="max_history_items_title">Maksimalan broj</string>
|
<string name="max_history_items_title">Maksimalan broj</string>
|
||||||
|
|||||||
12
app/src/main/res/values-bg/strings.xml
Normal file
12
app/src/main/res/values-bg/strings.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="feedback">Обратна връзка</string>
|
||||||
|
<string name="encryption_algorithm">Алгоритъм за криптиране</string>
|
||||||
|
<string name="encryption">Криптиране</string>
|
||||||
|
<string name="security">Сигурност</string>
|
||||||
|
<string name="master_key">Главен ключ</string>
|
||||||
|
<string name="add_group">Добави група</string>
|
||||||
|
<string name="edit_entry">Редактирай</string>
|
||||||
|
<string name="add_entry">Добави</string>
|
||||||
|
<string name="accept">Приемам</string>
|
||||||
|
</resources>
|
||||||
@@ -207,7 +207,7 @@
|
|||||||
<string name="command_execution">S\'executa l\'ordre…</string>
|
<string name="command_execution">S\'executa l\'ordre…</string>
|
||||||
<string name="parallelism_explanation">Grau de paral·lelisme (és a dir, nombre de fils) fets servir per la funció de derivació de la clau.</string>
|
<string name="parallelism_explanation">Grau de paral·lelisme (és a dir, nombre de fils) fets servir per la funció de derivació de la clau.</string>
|
||||||
<string name="parallelism">Paral·lelisme</string>
|
<string name="parallelism">Paral·lelisme</string>
|
||||||
<string name="memory_usage_explanation">Quantitat de memòria (en bytes) usada per la funció de derivació de la clau.</string>
|
<string name="memory_usage_explanation">Quantitat de memòria usada per la funció de derivació de la clau.</string>
|
||||||
<string name="memory_usage">Ús de la memòria</string>
|
<string name="memory_usage">Ús de la memòria</string>
|
||||||
<string name="hide_broken_locations_summary">Amaga els enllaços trencats en la llista de bases de dades recents</string>
|
<string name="hide_broken_locations_summary">Amaga els enllaços trencats en la llista de bases de dades recents</string>
|
||||||
<string name="show_recent_files_summary">Mostra la ubicació de les bases de dades recents</string>
|
<string name="show_recent_files_summary">Mostra la ubicació de les bases de dades recents</string>
|
||||||
|
|||||||
@@ -19,12 +19,12 @@
|
|||||||
Czech translation by Jan Vaněk
|
Czech translation by Jan Vaněk
|
||||||
--><resources>
|
--><resources>
|
||||||
<string name="homepage">Domovská stránka</string>
|
<string name="homepage">Domovská stránka</string>
|
||||||
<string name="about_description">Androidová verze správce hesel KeePass</string>
|
<string name="about_description">Implementace správce hesel KeePass pro Android</string>
|
||||||
<string name="accept">Přijmout</string>
|
<string name="accept">Přijmout</string>
|
||||||
<string name="add_entry">Přidat záznam</string>
|
<string name="add_entry">Přidat záznam</string>
|
||||||
<string name="add_group">Přidat skupinu</string>
|
<string name="add_group">Přidat skupinu</string>
|
||||||
<string name="encryption_algorithm">Šifrovací algoritmus</string>
|
<string name="encryption_algorithm">Šifrovací algoritmus</string>
|
||||||
<string name="app_timeout">Časový limit aplikace</string>
|
<string name="app_timeout">Časový limit</string>
|
||||||
<string name="app_timeout_summary">Doba nečinnosti, po které se aplikace zamkne</string>
|
<string name="app_timeout_summary">Doba nečinnosti, po které se aplikace zamkne</string>
|
||||||
<string name="application">Aplikace</string>
|
<string name="application">Aplikace</string>
|
||||||
<string name="menu_app_settings">Nastavení aplikace</string>
|
<string name="menu_app_settings">Nastavení aplikace</string>
|
||||||
@@ -35,16 +35,16 @@
|
|||||||
<string name="clipboard_error">Některá zařízení nedovolují aplikacím používat schránku.</string>
|
<string name="clipboard_error">Některá zařízení nedovolují aplikacím používat schránku.</string>
|
||||||
<string name="clipboard_error_clear">Nelze vyprázdnit schránku</string>
|
<string name="clipboard_error_clear">Nelze vyprázdnit schránku</string>
|
||||||
<string name="clipboard_timeout">Časový limit schránky</string>
|
<string name="clipboard_timeout">Časový limit schránky</string>
|
||||||
<string name="clipboard_timeout_summary">Doba uchování ve schránce</string>
|
<string name="clipboard_timeout_summary">Doba uchování ve schránce (je-li podporována zařízením)</string>
|
||||||
<string name="select_to_copy">Vyberte zkopírovat %1$s do schránky</string>
|
<string name="select_to_copy">Vyberte zkopírovat %1$s do schránky</string>
|
||||||
<string name="retrieving_db_key">Načítání klíče databáze…</string>
|
<string name="retrieving_db_key">Načítám klíč databáze…</string>
|
||||||
<string name="database">Databáze</string>
|
<string name="database">Databáze</string>
|
||||||
<string name="decrypting_db">Dešifrování obsahu databáze…</string>
|
<string name="decrypting_db">Dešifruji obsah databáze…</string>
|
||||||
<string name="default_checkbox">Použít jako výchozí databázi</string>
|
<string name="default_checkbox">Použít jako výchozí databázi</string>
|
||||||
<string name="digits">Číslice</string>
|
<string name="digits">Číslice</string>
|
||||||
<string name="select_database_file">Otevřít existující databázi</string>
|
<string name="select_database_file">Otevřít existující databázi</string>
|
||||||
<string name="entry_accessed">Poslední přístup</string>
|
<string name="entry_accessed">Poslední přístup</string>
|
||||||
<string name="entry_cancel">Storno</string>
|
<string name="entry_cancel">Zrušit</string>
|
||||||
<string name="entry_notes">Poznámky</string>
|
<string name="entry_notes">Poznámky</string>
|
||||||
<string name="entry_confpassword">Potvrďte heslo</string>
|
<string name="entry_confpassword">Potvrďte heslo</string>
|
||||||
<string name="entry_created">Vytvořeno</string>
|
<string name="entry_created">Vytvořeno</string>
|
||||||
@@ -55,27 +55,27 @@
|
|||||||
<string name="entry_password">Heslo</string>
|
<string name="entry_password">Heslo</string>
|
||||||
<string name="save">Uložit</string>
|
<string name="save">Uložit</string>
|
||||||
<string name="entry_title">Název</string>
|
<string name="entry_title">Název</string>
|
||||||
<string name="entry_url">URL adresa</string>
|
<string name="entry_url">URL</string>
|
||||||
<string name="entry_user_name">Uživatelské jméno</string>
|
<string name="entry_user_name">Uživatelské jméno</string>
|
||||||
<string name="error_arc4">Arcfour proudová šifra není podporována.</string>
|
<string name="error_arc4">Arcfour proudová šifra není podporována.</string>
|
||||||
<string name="error_can_not_handle_uri">KeePassDX nemůže zpracovat toto URI.</string>
|
<string name="error_can_not_handle_uri">KeePassDX nemůže zpracovat toto URI.</string>
|
||||||
<string name="error_file_not_create">Soubor se nedaří vytvořit</string>
|
<string name="error_file_not_create">Soubor se nepodařilo vytvořit</string>
|
||||||
<string name="error_invalid_db">Databázi nelze číst.</string>
|
<string name="error_invalid_db">Databázi se nepodařilo načíst.</string>
|
||||||
<string name="error_invalid_path">Ujistěte se, že cesta je správná.</string>
|
<string name="error_invalid_path">Ujistěte se, že cesta je správná.</string>
|
||||||
<string name="error_no_name">Zadejte jméno.</string>
|
<string name="error_no_name">Zadejte jméno.</string>
|
||||||
<string name="error_nokeyfile">Vyberte soubor s klíčem.</string>
|
<string name="error_nokeyfile">Vyberte soubor s klíčem.</string>
|
||||||
<string name="error_out_of_memory">Nedostatek paměti k načtení celé databáze.</string>
|
<string name="error_out_of_memory">Nedostatek paměti k načtení celé databáze.</string>
|
||||||
<string name="error_pass_gen_type">Je třeba zvolit alespoň jeden způsob vytváření hesla.</string>
|
<string name="error_pass_gen_type">Je třeba zvolit alespoň jeden způsob vytváření hesla.</string>
|
||||||
<string name="error_pass_match">Zadaná hesla se neshodují.</string>
|
<string name="error_pass_match">Hesla se neshodují.</string>
|
||||||
<string name="error_rounds_too_large">Příliš vysoký „Počet průchodů“. Nastavuji na 2147483648.</string>
|
<string name="error_rounds_too_large">Příliš vysoký „Počet průchodů“. Nastavuji na 2147483648.</string>
|
||||||
<string name="error_string_key">Je třeba, aby každý řetězec měl název kolonky.</string>
|
<string name="error_string_key">Je třeba, aby každý řetězec měl název kolonky.</string>
|
||||||
<string name="error_wrong_length">Do pole „Délka“ zadejte celé kladné číslo.</string>
|
<string name="error_wrong_length">Do pole „Délka“ zadejte celé kladné číslo.</string>
|
||||||
<string name="field_name">Název pole</string>
|
<string name="field_name">Název kolonky</string>
|
||||||
<string name="field_value">Hodnota pole</string>
|
<string name="field_value">Hodnota kolonky</string>
|
||||||
<string name="file_browser">Správce souborů</string>
|
<string name="file_browser">Správce souborů</string>
|
||||||
<string name="generate_password">Vytvoř heslo</string>
|
<string name="generate_password">Generovat heslo</string>
|
||||||
<string name="hint_conf_pass">Potvrdit heslo</string>
|
<string name="hint_conf_pass">Potvrdit heslo</string>
|
||||||
<string name="hint_generated_password">Vytvořené heslo</string>
|
<string name="hint_generated_password">Generované heslo</string>
|
||||||
<string name="hint_group_name">Název skupiny</string>
|
<string name="hint_group_name">Název skupiny</string>
|
||||||
<string name="hint_keyfile">Soubor s klíčem</string>
|
<string name="hint_keyfile">Soubor s klíčem</string>
|
||||||
<string name="hint_length">Délka</string>
|
<string name="hint_length">Délka</string>
|
||||||
@@ -83,8 +83,8 @@
|
|||||||
<string name="password">Heslo</string>
|
<string name="password">Heslo</string>
|
||||||
<string name="invalid_credentials">Nebylo možno načíst autentizační údaje.</string>
|
<string name="invalid_credentials">Nebylo možno načíst autentizační údaje.</string>
|
||||||
<string name="invalid_algorithm">Nesprávný algoritmus.</string>
|
<string name="invalid_algorithm">Nesprávný algoritmus.</string>
|
||||||
<string name="invalid_db_sig">Nedaří se rozpoznat formát databáze.</string>
|
<string name="invalid_db_sig">Nepodařilo se rozpoznat formát databáze.</string>
|
||||||
<string name="keyfile_is_empty">Soubor s klíčem je prázdný.</string>
|
<string name="keyfile_is_empty">Soubor klíče je prázdný.</string>
|
||||||
<string name="length">Délka</string>
|
<string name="length">Délka</string>
|
||||||
<string name="list_size_title">Velikost položek seznamu</string>
|
<string name="list_size_title">Velikost položek seznamu</string>
|
||||||
<string name="list_size_summary">Velikost textu v seznamu prvků</string>
|
<string name="list_size_summary">Velikost textu v seznamu prvků</string>
|
||||||
@@ -97,28 +97,28 @@
|
|||||||
<string name="settings">Nastavení</string>
|
<string name="settings">Nastavení</string>
|
||||||
<string name="menu_database_settings">Nastavení databáze</string>
|
<string name="menu_database_settings">Nastavení databáze</string>
|
||||||
<string name="menu_delete">Smazat</string>
|
<string name="menu_delete">Smazat</string>
|
||||||
<string name="menu_donate">Podpořit vývoj darem</string>
|
<string name="menu_donate">Přispět darem</string>
|
||||||
<string name="menu_edit">Upravit</string>
|
<string name="menu_edit">Upravit</string>
|
||||||
<string name="menu_hide_password">Skrýt heslo</string>
|
<string name="menu_hide_password">Skrýt heslo</string>
|
||||||
<string name="menu_lock">Zamknout databázi</string>
|
<string name="menu_lock">Zamknout databázi</string>
|
||||||
<string name="menu_open">Otevřít</string>
|
<string name="menu_open">Otevřít</string>
|
||||||
<string name="menu_search">Hledat</string>
|
<string name="menu_search">Hledat</string>
|
||||||
<string name="menu_showpass">Ukaž heslo</string>
|
<string name="menu_showpass">Ukázat heslo</string>
|
||||||
<string name="menu_url">Jít na URL</string>
|
<string name="menu_url">Přejít na URL</string>
|
||||||
<string name="minus">Mínus</string>
|
<string name="minus">Mínus</string>
|
||||||
<string name="never">Nikdy</string>
|
<string name="never">Nikdy</string>
|
||||||
<string name="no_results">Žádné výsledky hledání</string>
|
<string name="no_results">Žádné výsledky hledání</string>
|
||||||
<string name="no_url_handler">Pro otevření tohoto URL nainstalujte webový prohlížeč.</string>
|
<string name="no_url_handler">Pro otevření tohoto URL nainstalujte webový prohlížeč.</string>
|
||||||
<string name="omit_backup_search_title">Neprohledávat položky v záloze</string>
|
<string name="omit_backup_search_title">Neprohledávat položky v záloze</string>
|
||||||
<string name="omit_backup_search_summary">Vynechat skupiny „Záloha“ a \"Koš\" z výsledků vyhledávání</string>
|
<string name="omit_backup_search_summary">Vynechat skupiny „Záloha“ a \"Koš\" z výsledků vyhledávání</string>
|
||||||
<string name="progress_create">Vytvářím novou databázi…</string>
|
<string name="progress_create">Zakládám novou databázi…</string>
|
||||||
<string name="progress_title">Zpracování…</string>
|
<string name="progress_title">Pracuji…</string>
|
||||||
<string name="protection">Ochrana</string>
|
<string name="protection">Ochrana</string>
|
||||||
<string name="read_only_warning">Ke změně v databázi potřebuje KeePassDX oprávnění pro zápis.</string>
|
<string name="read_only_warning">Ke změně v databáze potřebuje KeePassDX oprávnění pro zápis.</string>
|
||||||
<string name="content_description_remove_from_list">Odstranit</string>
|
<string name="content_description_remove_from_list">Odstranit</string>
|
||||||
<string name="encryption_rijndael">Rijndael (AES)</string>
|
<string name="encryption_rijndael">Rijndael (AES)</string>
|
||||||
<string name="root">Kořen</string>
|
<string name="root">Kořen</string>
|
||||||
<string name="rounds">Počet šifrovacích průchodů</string>
|
<string name="rounds">Transformační průchody</string>
|
||||||
<string name="rounds_explanation">Vyšší počet šifrovacích průchodů zvýší odolnost proti útoku zkoušením všech možných hesel, ale může výrazně zpomalit načítání a ukládání.</string>
|
<string name="rounds_explanation">Vyšší počet šifrovacích průchodů zvýší odolnost proti útoku zkoušením všech možných hesel, ale může výrazně zpomalit načítání a ukládání.</string>
|
||||||
<string name="saving_database">Ukládám databázi…</string>
|
<string name="saving_database">Ukládám databázi…</string>
|
||||||
<string name="space">Místo</string>
|
<string name="space">Místo</string>
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
<string name="version_label">Verze %1$s</string>
|
<string name="version_label">Verze %1$s</string>
|
||||||
<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 si po každé úpravě zazá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">
|
<string-array name="timeout_options">
|
||||||
<item>5 sekund</item>
|
<item>5 sekund</item>
|
||||||
<item>10 sekund</item>
|
<item>10 sekund</item>
|
||||||
@@ -155,41 +155,41 @@
|
|||||||
<string name="encryption">Šifrování</string>
|
<string name="encryption">Šifrování</string>
|
||||||
<string name="key_derivation_function">Funkce pro tvorbu klíče</string>
|
<string name="key_derivation_function">Funkce pro tvorbu klíče</string>
|
||||||
<string name="extended_ASCII">Rozšířené ASCII</string>
|
<string name="extended_ASCII">Rozšířené ASCII</string>
|
||||||
<string name="allow">Umožnit</string>
|
<string name="allow">Povolit</string>
|
||||||
<string name="error_load_database">Databázi se nedaří 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 nedaří načíst, zkuste snížit množství paměti, využívané funkcí pro tvorbu klíče.</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 nedaří 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="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 položek 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="copy_field">Kopie %1$s</string>
|
<string name="copy_field">Kopie %1$s</string>
|
||||||
<string name="menu_form_filling_settings">Vyplňování formulářů</string>
|
<string name="menu_form_filling_settings">Vyplňování formulářů</string>
|
||||||
<string name="menu_copy">Zkopírovat</string>
|
<string name="menu_copy">Zkopírovat</string>
|
||||||
<string name="menu_move">Přesunout</string>
|
<string name="menu_move">Přesunout</string>
|
||||||
<string name="menu_paste">Vložit</string>
|
<string name="menu_paste">Vložit</string>
|
||||||
<string name="menu_cancel">Storno</string>
|
<string name="menu_cancel">Zrušit</string>
|
||||||
<string name="menu_file_selection_read_only">Chráněno před zápisem</string>
|
<string name="menu_file_selection_read_only">Chráněno před zápisem</string>
|
||||||
<string name="menu_open_file_read_and_write">Čtení a zápis</string>
|
<string name="menu_open_file_read_and_write">Čtení a zápis</string>
|
||||||
<string name="read_only">Chráněno před zápisem</string>
|
<string name="read_only">Chráněno před zápisem</string>
|
||||||
<string name="encryption_explanation">Algoritmus šifrování databáze užitý pro všechna data.</string>
|
<string name="encryption_explanation">Algoritmus šifrování databáze použit pro všechna data.</string>
|
||||||
<string name="kdf_explanation">Klíč pro šifrovací algoritmus je vytvořen transformací hlavního klíče skrze odvozovací funkci klíče s náhodně přidanou složkou, tzv. solí.</string>
|
<string name="kdf_explanation">Klíč pro šifrovací algoritmus je vytvořen transformací hlavního klíče pomocí funkce odvození klíče s náhodně přidanou složkou, tzv. solí.</string>
|
||||||
<string name="memory_usage">Využití paměti</string>
|
<string name="memory_usage">Využití paměti</string>
|
||||||
<string name="memory_usage_explanation">Množství paměti (v bajtech) použitých funkcí pro odvození klíče.</string>
|
<string name="memory_usage_explanation">Množství paměti použitých funkcí pro odvození klíče.</string>
|
||||||
<string name="parallelism">Souběžné zpracovávání</string>
|
<string name="parallelism">Souběžné zpracovávání</string>
|
||||||
<string name="parallelism_explanation">Stupeň souběžného zpracovávání (počet vláken) použitý funkcí pro vytvoření klíče.</string>
|
<string name="parallelism_explanation">Stupeň souběžného zpracovávání (počet vláken) použitý funkcí pro odvození klíče.</string>
|
||||||
<string name="sort_menu">Seřadit</string>
|
<string name="sort_menu">Seřadit</string>
|
||||||
<string name="sort_ascending">Nejnižší první ↓</string>
|
<string name="sort_ascending">Nejnižší první ↓</string>
|
||||||
<string name="sort_groups_before">Skupiny první</string>
|
<string name="sort_groups_before">Skupiny první</string>
|
||||||
<string name="sort_recycle_bin_bottom">Koš jako poslední</string>
|
<string name="sort_recycle_bin_bottom">Koš jako poslední</string>
|
||||||
<string name="sort_title">Nadpis</string>
|
<string name="sort_title">Nadpis</string>
|
||||||
<string name="sort_username">Uživatelské jméno</string>
|
<string name="sort_username">Uživatelské jméno</string>
|
||||||
<string name="sort_creation_time">Vytvoření</string>
|
<string name="sort_creation_time">Založeno</string>
|
||||||
<string name="sort_last_modify_time">Změna</string>
|
<string name="sort_last_modify_time">Změněno</string>
|
||||||
<string name="sort_last_access_time">Přístup</string>
|
<string name="sort_last_access_time">Přístup</string>
|
||||||
<string name="warning">Varování</string>
|
<string name="warning">Varování</string>
|
||||||
<string name="warning_password_encoding">Nepoužívejte v hesle pro databázový soubor znaky mimo znakovou sadu Latin-1 (nepoužívejte znaky s diakritikou).</string>
|
<string name="warning_password_encoding">Nepoužívejte v hesle pro databázový soubor znaky mimo znaky kódování textu (nerozpoznané znaky budou konvertovány na stejné písmeno).</string>
|
||||||
<string name="warning_empty_password">Pokračovat bez ochrany odemknutím heslem\?</string>
|
<string name="warning_empty_password">Pokračovat bez ochrany odemknutí heslem\?</string>
|
||||||
<string name="warning_no_encryption_key">Pokračovat bez šifrovacího klíče\?</string>
|
<string name="warning_no_encryption_key">Pokračovat bez šifrovacího klíče\?</string>
|
||||||
<string name="encrypted_value_stored">Šifrované heslo uloženo</string>
|
<string name="encrypted_value_stored">Šifrované heslo uloženo</string>
|
||||||
<string name="no_credentials_stored">Tato databáze zatím nemá uložené heslo.</string>
|
<string name="no_credentials_stored">Tato databáze zatím nemá uložené heslo.</string>
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
<string name="general">Obecné</string>
|
<string name="general">Obecné</string>
|
||||||
<string name="autofill">Automatické vyplnění</string>
|
<string name="autofill">Automatické vyplnění</string>
|
||||||
<string name="autofill_service_name">KeePassDX automatické vyplňování formulářů</string>
|
<string name="autofill_service_name">KeePassDX automatické vyplňování formulářů</string>
|
||||||
<string name="autofill_sign_in_prompt">Přihlásit se pomocí KeePassDX</string>
|
<string name="autofill_sign_in_prompt">Přihlásit se s KeePassDX</string>
|
||||||
<string name="set_autofill_service_title">Nastavit výchozí službu automatického vyplňování</string>
|
<string name="set_autofill_service_title">Nastavit výchozí službu automatického vyplňování</string>
|
||||||
<string name="autofill_explanation_summary">Povolit rychlé automatické vyplňování formulářů v ostatních aplikacích</string>
|
<string name="autofill_explanation_summary">Povolit rychlé automatické vyplňování formulářů v ostatních aplikacích</string>
|
||||||
<string name="password_size_title">Délka generovaného hesla</string>
|
<string name="password_size_title">Délka generovaného hesla</string>
|
||||||
@@ -206,28 +206,28 @@
|
|||||||
<string name="list_password_generator_options_title">Znaky hesla</string>
|
<string name="list_password_generator_options_title">Znaky hesla</string>
|
||||||
<string name="list_password_generator_options_summary">Nastavit povolené znaky pro generátor hesel</string>
|
<string name="list_password_generator_options_summary">Nastavit povolené znaky pro generátor hesel</string>
|
||||||
<string name="clipboard">Schránka</string>
|
<string name="clipboard">Schránka</string>
|
||||||
<string name="clipboard_notifications_title">Oznamování schránky</string>
|
<string name="clipboard_notifications_title">Oznámení schránky</string>
|
||||||
<string name="clipboard_notifications_summary">Ukázat oznamení schránky o kopírování pole při prohlížení záznamu</string>
|
<string name="clipboard_notifications_summary">Ukázat oznámení schránky o kopírování pole při prohlížení záznamu</string>
|
||||||
<string name="clipboard_warning">Vymazat historii schránky manuálně, pokud automatické vymazání schránky selže.</string>
|
<string name="clipboard_warning">Vymazat historii schránky manuálně, pokud automatické vymazání schránky selže.</string>
|
||||||
<string name="lock">Zamknout</string>
|
<string name="lock">Zamknout</string>
|
||||||
<string name="lock_database_screen_off_title">Zámek obrazovky</string>
|
<string name="lock_database_screen_off_title">Zámek obrazovky</string>
|
||||||
<string name="lock_database_screen_off_summary">Při zhasnutí obrazovky uzamknout databázi</string>
|
<string name="lock_database_screen_off_summary">Při zhasnutí obrazovky uzamknout databázi</string>
|
||||||
<string name="advanced_unlock">Pokročilé odemčení</string>
|
<string name="advanced_unlock">Rozšířené odemknutí</string>
|
||||||
<string name="biometric_unlock_enable_title">Biometrické odemčení</string>
|
<string name="biometric_unlock_enable_title">Biometrické odemknutí</string>
|
||||||
<string name="biometric_unlock_enable_summary">Nechá otevřít databázi snímáním biometrického údaje</string>
|
<string name="biometric_unlock_enable_summary">Nechá otevřít databázi snímáním biometrického údaje</string>
|
||||||
<string name="biometric_delete_all_key_title">Smazat šifrovací klíče</string>
|
<string name="biometric_delete_all_key_title">Smazat šifrovací klíče</string>
|
||||||
<string name="biometric_delete_all_key_summary">Smazat všechny šifrovací klíče související s rozpoznáním pokročilého odemknutí</string>
|
<string name="biometric_delete_all_key_summary">Smazat všechny šifrovací klíče související s rozpoznáním rozšířeného odemknutí</string>
|
||||||
<string name="unavailable_feature_text">Tuto funkci se nedaří spustit.</string>
|
<string name="unavailable_feature_text">Tuto funkci se nedaří spustit.</string>
|
||||||
<string name="unavailable_feature_version">V zařízení je instalován Android %1$s, ale potřebná je verze %2$s a novější.</string>
|
<string name="unavailable_feature_version">V zařízení je instalován Android %1$s, ale potřebná je verze %2$s a novější.</string>
|
||||||
<string name="unavailable_feature_hardware">Hardware nebyl rozpoznán.</string>
|
<string name="unavailable_feature_hardware">Odpovídající hardware nebyl rozpoznán.</string>
|
||||||
<string name="file_name">Název souboru</string>
|
<string name="file_name">Název souboru</string>
|
||||||
<string name="path">Cesta</string>
|
<string name="path">Cesta</string>
|
||||||
<string name="assign_master_key">Přiřadit hlavní klíč</string>
|
<string name="assign_master_key">Přiřadit hlavní klíč</string>
|
||||||
<string name="create_keepass_file">Vytvořit novou databázi</string>
|
<string name="create_keepass_file">Založit novou databázi</string>
|
||||||
<string name="recycle_bin_title">Využití koše</string>
|
<string name="recycle_bin_title">Využití koše</string>
|
||||||
<string name="recycle_bin_summary">Před smazáním přesune skupiny a položky do skupiny „Koš“</string>
|
<string name="recycle_bin_summary">Před smazáním přesune skupiny a položky do skupiny „Koš“</string>
|
||||||
<string name="monospace_font_fields_enable_title">Písmo položek</string>
|
<string name="monospace_font_fields_enable_title">Písmo kolonek</string>
|
||||||
<string name="monospace_font_fields_enable_summary">Čitelnost znaků v položkách můžete přizpůsobit změnou písma</string>
|
<string name="monospace_font_fields_enable_summary">Čitelnost znaků v kolonkách můžete přizpůsobit změnou písma</string>
|
||||||
<string name="allow_copy_password_title">Důvěřovat schránce</string>
|
<string name="allow_copy_password_title">Důvěřovat schránce</string>
|
||||||
<string name="allow_copy_password_summary">Povolit kopírování hesla záznamu a chráněných položek do schránky</string>
|
<string name="allow_copy_password_summary">Povolit kopírování hesla záznamu a chráněných položek do schránky</string>
|
||||||
<string name="allow_copy_password_warning">Varování: Schránka je sdílena všemi aplikacemi. Pokud jsou do ní zkopírovány citlivé údaje, mohl by se k nim dostat další software.</string>
|
<string name="allow_copy_password_warning">Varování: Schránka je sdílena všemi aplikacemi. Pokud jsou do ní zkopírovány citlivé údaje, mohl by se k nim dostat další software.</string>
|
||||||
@@ -253,47 +253,47 @@
|
|||||||
<string name="education_create_database_summary">Založte svůj první soubor pro správu hesel.</string>
|
<string name="education_create_database_summary">Založte svůj první soubor pro správu hesel.</string>
|
||||||
<string name="education_select_database_title">Otevřít existující databázi</string>
|
<string name="education_select_database_title">Otevřít existující databázi</string>
|
||||||
<string name="education_select_database_summary">Otevřete svou dříve používanou databázi ze správce souborů a pokračujte v jejím používání.</string>
|
<string name="education_select_database_summary">Otevřete svou dříve používanou databázi ze správce souborů a pokračujte v jejím používání.</string>
|
||||||
<string name="education_new_node_title">Přidejte položky do databáze</string>
|
<string name="education_new_node_title">Přidejte záznamy do databáze</string>
|
||||||
<string name="education_new_node_summary">Položky pomáhají se správou vašich digitálních identit.
|
<string name="education_new_node_summary">Záznamy pomáhají se správou Vašich digitálních identit.
|
||||||
\n
|
\n
|
||||||
\nSkupiny (ekvivalent složek) organizují záznamy v databázi.</string>
|
\nSkupiny (ekvivalent složek) organizují záznamy v databázi.</string>
|
||||||
<string name="education_search_title">Hledejte v položkách</string>
|
<string name="education_search_title">Hledat v záznamech</string>
|
||||||
<string name="education_search_summary">Zadejte název, uživatelské jméno nebo jiné položky k nalezení svých hesel.</string>
|
<string name="education_search_summary">Zadejte název, uživatelské jméno nebo jiné kolonky k nalezení svých hesel.</string>
|
||||||
<string name="education_entry_edit_title">Upravit položku</string>
|
<string name="education_entry_edit_title">Upravit záznam</string>
|
||||||
<string name="education_entry_edit_summary">Přidejte ke své položce vlastní kolonky. Společná data mohou být sdílena mezi více různými kolonkami.</string>
|
<string name="education_entry_edit_summary">Přidejte ke svému záznamu vlastní kolonky. Společná data mohou být sdílena mezi různými kolonkami záznamu odkazem.</string>
|
||||||
<string name="education_generate_password_title">Vytvořit silné heslo</string>
|
<string name="education_generate_password_title">Vytvořit silné heslo</string>
|
||||||
<string name="education_generate_password_summary">Vygenerujte silné heslo pro svou položku, definujte je podle kritérií formuláře, a nezapomeňte na bezpečné heslo.</string>
|
<string name="education_generate_password_summary">Generujte silné heslo pro svůj záznam, definujte je podle kritérií formuláře, a nezapomeňte na bezpečné heslo.</string>
|
||||||
<string name="education_entry_new_field_title">Přidat vlastní kolonky</string>
|
<string name="education_entry_new_field_title">Přidat vlastní kolonky</string>
|
||||||
<string name="education_entry_new_field_summary">Registrovat další kolonku, zadat hodnotu a volitelně ji ochránit.</string>
|
<string name="education_entry_new_field_summary">Registrovat další kolonku, zadat hodnotu a volitelně ji ochránit.</string>
|
||||||
<string name="education_unlock_title">Odemknout databázi</string>
|
<string name="education_unlock_title">Odemknout databázi</string>
|
||||||
<string name="education_read_only_title">Ochraňte svou databázi před zápisem</string>
|
<string name="education_read_only_title">Ochraňte svou databázi před zápisem</string>
|
||||||
<string name="education_read_only_summary">Změnit režim otevírání pro dané sezení.
|
<string name="education_read_only_summary">Změnit režim otevírání pro dané sezení.
|
||||||
\n
|
\n
|
||||||
\nV režimu \"pouze pro čtení\" zabráníte nechtěným změnám do databáze.
|
\nV režimu \"pouze pro čtení\" zabráníte nechtěným změnám v databázi.
|
||||||
\nV režimu \"zápisu\" je možné přidávat, mazat nebo měnit všechny prvky podle libosti.</string>
|
\nV režimu \"zápisu\" je možné přidávat, mazat nebo měnit všechny prvky podle libosti.</string>
|
||||||
<string name="education_field_copy_title">Zkopírujte kolonku</string>
|
<string name="education_field_copy_title">Zkopírovat kolonku</string>
|
||||||
<string name="education_field_copy_summary">Zkopírované kolonky lze vkládat kam chcete
|
<string name="education_field_copy_summary">Zkopírované kolonky lze vkládat podle libosti.
|
||||||
\n
|
\n
|
||||||
\nK vyplňování formulářů použijte svou oblíbenou metodu.</string>
|
\nK vyplňování formulářů použijte svou oblíbenou metodu.</string>
|
||||||
<string name="education_lock_title">Uzamkni databáze</string>
|
<string name="education_lock_title">Uzamknout databázi</string>
|
||||||
<string name="education_lock_summary">Rychlé uzamkni databázi. Je možné nastavit, aby se databáze zamkla po určitém čase a také po zhasnutí obrazovky.</string>
|
<string name="education_lock_summary">Rychle uzamknout databázi. Je možné nastavit, aby se databáze zamkla po určitém čase a také po zhasnutí obrazovky.</string>
|
||||||
<string name="education_sort_title">Řazení položek</string>
|
<string name="education_sort_title">Řazení položek</string>
|
||||||
<string name="education_sort_summary">Vyberte řazení položek a skupin.</string>
|
<string name="education_sort_summary">Vyberte řazení položek a skupin.</string>
|
||||||
<string name="education_donation_title">Zapojit se</string>
|
<string name="education_donation_title">Zapojit se</string>
|
||||||
<string name="education_donation_summary">Zapojte se a pomozte zvýšit stabilitu, bezpečnost a přidávání dalších funkcí.</string>
|
<string name="education_donation_summary">Zapojte se a pomozte zvýšit stabilitu, bezpečnost a doplnění dalších funkcí.</string>
|
||||||
<string name="html_text_ad_free">Na rozdíl od mnoha aplikací pro správu hesel, tato je <strong>bez reklam</strong>", je "<strong>svobodný software pod copyleft licencí</strong> a nesbírá žádné osobní údaje na svých serverech bez ohledu na to, jakou verzi používáte.</string>
|
<string name="html_text_ad_free">Na rozdíl od mnoha aplikací pro správu hesel je tato <strong>bez reklam</strong>, je \u0020<strong>svobodný software pod copyleft licencí</strong> a nesbírá žádné osobní údaje na svých serverech bez ohledu na to, jakou verzi používáte.</string>
|
||||||
<string name="html_text_buy_pro">Zakoupením varianty „pro“ získáte přístup k tomuto <strong>vizuálnímu stylu</strong> a hlavně pomůžete <strong>uskutečnění komunitních projektů.</strong></string>
|
<string name="html_text_buy_pro">Zakoupením varianty \"pro\" získáte přístup k tomuto <strong>vizuálnímu stylu</strong> a hlavně pomůžete <strong>uskutečnění komunitních projektů.</strong></string>
|
||||||
<string name="html_text_feature_generosity">Tento <strong>vizuální styl</strong> je k dispozici díky vaší štědrosti.</string>
|
<string name="html_text_feature_generosity">Tento <strong>vizuální styl</strong> je k dispozici díky vaší štědrosti.</string>
|
||||||
<string name="html_text_donation">Pro zajištění svobody nás všech a pokračování aktivity, počítáme s vaším <strong>přispěním.</strong></string>
|
<string name="html_text_donation">Pro zajištění svobody nás všech a pokračování aktivity počítáme s Vaším <strong>přispěním.</strong></string>
|
||||||
<string name="html_text_dev_feature">Tato funkce je <strong>ve vývoji</strong> a potřebuje váš <strong>příspěvek</strong>, aby byla brzy k dispozici.</string>
|
<string name="html_text_dev_feature">Tato funkce je <strong>ve vývoji</strong> a potřebuje Váš <strong>příspěvek</strong>, aby byla brzy k dispozici.</string>
|
||||||
<string name="html_text_dev_feature_buy_pro">Zakoupením <strong>pro</strong> varianty,</string>
|
<string name="html_text_dev_feature_buy_pro">Zakoupením <strong>pro</strong> varianty,</string>
|
||||||
<string name="html_text_dev_feature_contibute"><strong>Zapojením se</strong>,</string>
|
<string name="html_text_dev_feature_contibute"><strong>Zapojením se</strong>,</string>
|
||||||
<string name="html_text_dev_feature_encourage">povzbudíte vývojáře k přidávání <strong>nových funkcí</strong> a <strong>opravování chyb</strong> dle vašich připomínek.</string>
|
<string name="html_text_dev_feature_encourage">povzbudíte vývojáře k doplnění <strong>nových funkcí</strong> a <strong>opravám chyb</strong> dle vašich připomínek.</string>
|
||||||
<string name="html_text_dev_feature_thanks">Mnohé díky za vaše přispění.</string>
|
<string name="html_text_dev_feature_thanks">Mockrát děkujeme za Váš příspěvek.</string>
|
||||||
<string name="html_text_dev_feature_work_hard">Tvrdě pracujeme na brzkém vydání této funkce.</string>
|
<string name="html_text_dev_feature_work_hard">Tvrdě pracujeme na brzkém vydání této funkce.</string>
|
||||||
<string name="html_text_dev_feature_upgrade">Pamatujte na aktualizaci aplikace instalováním nových verzí.</string>
|
<string name="html_text_dev_feature_upgrade">Pamatujte na aktualizaci aplikace instalováním nových verzí.</string>
|
||||||
<string name="download">Stáhnout</string>
|
<string name="download">Stáhnout</string>
|
||||||
<string name="contribute">Zapojit se</string>
|
<string name="contribute">Přispět</string>
|
||||||
<string name="encryption_chacha20">ChaCha20</string>
|
<string name="encryption_chacha20">ChaCha20</string>
|
||||||
<string name="kdf_AES">AES</string>
|
<string name="kdf_AES">AES</string>
|
||||||
<string name="style_choose_title">Vzhled aplikace</string>
|
<string name="style_choose_title">Vzhled aplikace</string>
|
||||||
@@ -304,16 +304,16 @@
|
|||||||
<string name="keyboard_name">Magikeyboard</string>
|
<string name="keyboard_name">Magikeyboard</string>
|
||||||
<string name="keyboard_label">Magikeyboard (KeePassDX)</string>
|
<string name="keyboard_label">Magikeyboard (KeePassDX)</string>
|
||||||
<string name="keyboard_setting_label">Magikeyboard nastavení</string>
|
<string name="keyboard_setting_label">Magikeyboard nastavení</string>
|
||||||
<string name="keyboard_entry_category">Položka</string>
|
<string name="keyboard_entry_category">Záznam</string>
|
||||||
<string name="keyboard_entry_timeout_title">Časový limit</string>
|
<string name="keyboard_entry_timeout_title">Časový limit</string>
|
||||||
<string name="keyboard_entry_timeout_summary">Doba uchování položky v Magikeyboardu</string>
|
<string name="keyboard_entry_timeout_summary">Doba uchování položky v Magikeyboardu</string>
|
||||||
<string name="keyboard_notification_entry_title">Informace o oznámení</string>
|
<string name="keyboard_notification_entry_title">Informace o oznámení</string>
|
||||||
<string name="keyboard_notification_entry_summary">Zobrazit oznámení, když je položka dostupná</string>
|
<string name="keyboard_notification_entry_summary">Zobrazit oznámení, když je položka dostupná</string>
|
||||||
<string name="keyboard_notification_entry_content_title_text">Položka</string>
|
<string name="keyboard_notification_entry_content_title_text">Záznam</string>
|
||||||
<string name="keyboard_notification_entry_content_title">%1$s dostupné v Magikeyboardu</string>
|
<string name="keyboard_notification_entry_content_title">%1$s dostupné v Magikeyboardu</string>
|
||||||
<string name="keyboard_notification_entry_content_text">%1$s</string>
|
<string name="keyboard_notification_entry_content_text">%1$s</string>
|
||||||
<string name="keyboard_notification_entry_clear_close_title">Vymazat při zavření</string>
|
<string name="keyboard_notification_entry_clear_close_title">Vymazat při zavření</string>
|
||||||
<string name="keyboard_notification_entry_clear_close_summary">Zavři databázi při zavření oznámení</string>
|
<string name="keyboard_notification_entry_clear_close_summary">Zavřít databázi při zavření oznámení</string>
|
||||||
<string name="keyboard_appearance_category">Vzhled</string>
|
<string name="keyboard_appearance_category">Vzhled</string>
|
||||||
<string name="keyboard_theme_title">Vzhled klávesnice</string>
|
<string name="keyboard_theme_title">Vzhled klávesnice</string>
|
||||||
<string name="keyboard_keys_category">Klávesy</string>
|
<string name="keyboard_keys_category">Klávesy</string>
|
||||||
@@ -326,37 +326,37 @@
|
|||||||
<string name="clear_clipboard_notification_title">Vymazat při ukončení</string>
|
<string name="clear_clipboard_notification_title">Vymazat při ukončení</string>
|
||||||
<string name="clear_clipboard_notification_summary">Uzamknout databázi, jakmile trvání schránky vyprší nebo po uzavření oznámení</string>
|
<string name="clear_clipboard_notification_summary">Uzamknout databázi, jakmile trvání schránky vyprší nebo po uzavření oznámení</string>
|
||||||
<string name="recycle_bin">Koš</string>
|
<string name="recycle_bin">Koš</string>
|
||||||
<string name="keyboard_selection_entry_title">Výběr položky</string>
|
<string name="keyboard_selection_entry_title">Výběr záznamu</string>
|
||||||
<string name="keyboard_selection_entry_summary">Při prohlížení záznamu ukázat na Magikeyboard pole položek</string>
|
<string name="keyboard_selection_entry_summary">Při prohlížení záznamu ukázat na Magikeyboard kolonky</string>
|
||||||
<string name="delete_entered_password_title">Smazat heslo</string>
|
<string name="delete_entered_password_title">Smazat heslo</string>
|
||||||
<string name="delete_entered_password_summary">Smaže heslo zadané po pokusu o připojení k databázi</string>
|
<string name="delete_entered_password_summary">Smaže heslo zadané po pokusu o připojení k databázi</string>
|
||||||
<string name="content_description_open_file">Otevřít soubor</string>
|
<string name="content_description_open_file">Otevřít soubor</string>
|
||||||
<string name="content_description_node_children">Potomci uzlu</string>
|
<string name="content_description_node_children">Podřazené prvky uzlu</string>
|
||||||
<string name="content_description_add_node">Přidej uzel</string>
|
<string name="content_description_add_node">Přidat uzel</string>
|
||||||
<string name="content_description_add_entry">Přidej záznam</string>
|
<string name="content_description_add_entry">Přidat záznam</string>
|
||||||
<string name="content_description_add_group">Přidat skupinu</string>
|
<string name="content_description_add_group">Přidat skupinu</string>
|
||||||
<string name="content_description_file_information">Informace o souboru</string>
|
<string name="content_description_file_information">Informace o souboru</string>
|
||||||
<string name="content_description_password_checkbox">Checkbox hesla</string>
|
<string name="content_description_password_checkbox">Checkbox hesla</string>
|
||||||
<string name="content_description_keyfile_checkbox">Checkbox souboru s klíčem</string>
|
<string name="content_description_keyfile_checkbox">Checkbox souboru s klíčem</string>
|
||||||
<string name="content_description_repeat_toggle_password_visibility">Přepni ukázání hesla</string>
|
<string name="content_description_repeat_toggle_password_visibility">Opakovat přepnutí viditelnosti hesla</string>
|
||||||
<string name="content_description_entry_icon">Ikona záznamu</string>
|
<string name="content_description_entry_icon">Ikona záznamu</string>
|
||||||
<string name="entry_password_generator">Generátor hesel</string>
|
<string name="entry_password_generator">Generátor hesel</string>
|
||||||
<string name="content_description_password_length">Délka hesla</string>
|
<string name="content_description_password_length">Délka hesla</string>
|
||||||
<string name="entry_add_field">Přidej pole</string>
|
<string name="entry_add_field">Přidat pole</string>
|
||||||
<string name="content_description_remove_field">Odeber pole</string>
|
<string name="content_description_remove_field">Odebrat pole</string>
|
||||||
<string name="entry_UUID">UUID</string>
|
<string name="entry_UUID">UUID</string>
|
||||||
<string name="error_move_entry_here">Sem záznam přesunout nelze.</string>
|
<string name="error_move_entry_here">Sem záznam přesunout nelze.</string>
|
||||||
<string name="error_copy_entry_here">Sem záznam zkopírovat nelze.</string>
|
<string name="error_copy_entry_here">Sem záznam zkopírovat nelze.</string>
|
||||||
<string name="list_groups_show_number_entries_title">Ukaž počet záznamů</string>
|
<string name="list_groups_show_number_entries_title">Ukázat počet záznamů</string>
|
||||||
<string name="list_groups_show_number_entries_summary">Ukaž počet záznamů ve skupině</string>
|
<string name="list_groups_show_number_entries_summary">Ukázat počet záznamů ve skupině</string>
|
||||||
<string name="content_description_background">Pozadí</string>
|
<string name="content_description_background">Pozadí</string>
|
||||||
<string name="content_description_update_from_list">Aktualizovat</string>
|
<string name="content_description_update_from_list">Aktualizovat</string>
|
||||||
<string name="content_description_keyboard_close_fields">Zavři kolonky</string>
|
<string name="content_description_keyboard_close_fields">Zavřít pole</string>
|
||||||
<string name="error_create_database_file">Nelze vytvořit databázi s tímto heslem a klíčem ze souboru.</string>
|
<string name="error_create_database_file">Nelze vytvořit databázi s tímto heslem a souborem klíče.</string>
|
||||||
<string name="menu_advanced_unlock_settings">Pokročilé odemčení</string>
|
<string name="menu_advanced_unlock_settings">Rozšířené odemknutí</string>
|
||||||
<string name="biometric">Biometrika</string>
|
<string name="biometric">Biometrika</string>
|
||||||
<string name="biometric_auto_open_prompt_title">Automaticky otevřít pobídku</string>
|
<string name="biometric_auto_open_prompt_title">Automaticky otevřít pobídku</string>
|
||||||
<string name="biometric_auto_open_prompt_summary">Automaticky žádat pokročilé odemknutí, je-li databáze nastavena k jejímu použití</string>
|
<string name="biometric_auto_open_prompt_summary">Automaticky žádat rozšířené odemknutí, je-li databáze nastavena k jejímu použití</string>
|
||||||
<string name="enable">Zapnout</string>
|
<string name="enable">Zapnout</string>
|
||||||
<string name="disable">Vypnout</string>
|
<string name="disable">Vypnout</string>
|
||||||
<string name="master_key">Hlavní klíč</string>
|
<string name="master_key">Hlavní klíč</string>
|
||||||
@@ -372,7 +372,7 @@
|
|||||||
<string name="entry_otp">OTP</string>
|
<string name="entry_otp">OTP</string>
|
||||||
<string name="error_invalid_OTP">Neplatná OTP tajnost.</string>
|
<string name="error_invalid_OTP">Neplatná OTP tajnost.</string>
|
||||||
<string name="error_disallow_no_credentials">Nejméně jeden přihlašovací údaj musí být zadán.</string>
|
<string name="error_disallow_no_credentials">Nejméně jeden přihlašovací údaj musí být zadán.</string>
|
||||||
<string name="error_copy_group_here">Sem skupinu kopírovat nemůžete.</string>
|
<string name="error_copy_group_here">Sem skupinu kopírovat nelze.</string>
|
||||||
<string name="error_otp_secret_key">Tajný klíč musí mít formát Base32.</string>
|
<string name="error_otp_secret_key">Tajný klíč musí mít formát Base32.</string>
|
||||||
<string name="error_otp_counter">Čítač musít být mezi %1$d a %2$d.</string>
|
<string name="error_otp_counter">Čítač musít být mezi %1$d a %2$d.</string>
|
||||||
<string name="error_otp_period">Interval musít být mezi %1$d a %2$d vteřinami.</string>
|
<string name="error_otp_period">Interval musít být mezi %1$d a %2$d vteřinami.</string>
|
||||||
@@ -384,14 +384,14 @@
|
|||||||
<string name="contains_duplicate_uuid">Databáze obsahuje duplikátní UUID.</string>
|
<string name="contains_duplicate_uuid">Databáze obsahuje duplikátní UUID.</string>
|
||||||
<string name="contains_duplicate_uuid_procedure">Opravit chybu založením nového UUID pro duplikáty a pokračovat\?</string>
|
<string name="contains_duplicate_uuid_procedure">Opravit chybu založením nového UUID pro duplikáty a pokračovat\?</string>
|
||||||
<string name="database_opened">Databáze otevřena</string>
|
<string name="database_opened">Databáze otevřena</string>
|
||||||
<string name="clipboard_explanation_summary">Kopírujte pole záznamů pomocí schránky Vašeho zařízení</string>
|
<string name="clipboard_explanation_summary">Kopírovat kolonky záznamů pomocí schránky svého zařízení</string>
|
||||||
<string name="advanced_unlock_explanation_summary">K snadnějšímu otevření databáze použijte pokročilé odemknutí</string>
|
<string name="advanced_unlock_explanation_summary">K snadnějšímu otevření databáze použijte rozšířené odemknutí</string>
|
||||||
<string name="database_data_compression_title">Komprese dat</string>
|
<string name="database_data_compression_title">Komprese dat</string>
|
||||||
<string name="database_data_compression_summary">Komprese dat snižuje velikost databáze</string>
|
<string name="database_data_compression_summary">Komprese dat snižuje velikost databáze</string>
|
||||||
<string name="max_history_items_title">Maximální počet</string>
|
<string name="max_history_items_title">Maximální počet</string>
|
||||||
<string name="max_history_items_summary">Omezit počet položek v historii záznamu</string>
|
<string name="max_history_items_summary">Omezit počet položek v historii záznamu</string>
|
||||||
<string name="max_history_size_title">Maximální velikost</string>
|
<string name="max_history_size_title">Maximální velikost</string>
|
||||||
<string name="max_history_size_summary">Omezit velikost historie na záznam (v bajtech)</string>
|
<string name="max_history_size_summary">Omezit velikost historie na záznam</string>
|
||||||
<string name="settings_database_recommend_changing_master_key_title">Doporučit změnu</string>
|
<string name="settings_database_recommend_changing_master_key_title">Doporučit změnu</string>
|
||||||
<string name="settings_database_recommend_changing_master_key_summary">Dporučit změnu hlavního klíče (dny)</string>
|
<string name="settings_database_recommend_changing_master_key_summary">Dporučit změnu hlavního klíče (dny)</string>
|
||||||
<string name="settings_database_force_changing_master_key_title">Vynutit změnu</string>
|
<string name="settings_database_force_changing_master_key_title">Vynutit změnu</string>
|
||||||
@@ -413,20 +413,20 @@
|
|||||||
<string name="recycle_bin_group_title">Skupina Koš</string>
|
<string name="recycle_bin_group_title">Skupina Koš</string>
|
||||||
<string name="enable_auto_save_database_title">Uložit databázi automaticky</string>
|
<string name="enable_auto_save_database_title">Uložit databázi automaticky</string>
|
||||||
<string name="enable_auto_save_database_summary">Uložit databázi po každé důležité akci (v režimu \"Zápis\")</string>
|
<string name="enable_auto_save_database_summary">Uložit databázi po každé důležité akci (v režimu \"Zápis\")</string>
|
||||||
<string name="entry_attachments">Připojené soubory</string>
|
<string name="entry_attachments">Přílohy</string>
|
||||||
<string name="menu_restore_entry_history">Obnovit historii</string>
|
<string name="menu_restore_entry_history">Obnovit historii</string>
|
||||||
<string name="menu_delete_entry_history">Smazat historii</string>
|
<string name="menu_delete_entry_history">Smazat historii</string>
|
||||||
<string name="keyboard_auto_go_action_title">Akce auto-klávesy</string>
|
<string name="keyboard_auto_go_action_title">Akce auto-klávesy</string>
|
||||||
<string name="keyboard_auto_go_action_summary">Akce klávesy \"Jít\" po stisknutí klávesy \"Položka\"</string>
|
<string name="keyboard_auto_go_action_summary">Akce klávesy \"Jít\" po stisknutí klávesy \"Kolonka\"</string>
|
||||||
<string name="download_attachment">Stáhnout %1$s</string>
|
<string name="download_attachment">Stáhnout %1$s</string>
|
||||||
<string name="download_initialization">Zahajuji…</string>
|
<string name="download_initialization">Zahajuji…</string>
|
||||||
<string name="download_progression">Probíhá: %1$d%%</string>
|
<string name="download_progression">Probíhá: %1$d%%</string>
|
||||||
<string name="download_finalization">Dokončuji…</string>
|
<string name="download_finalization">Dokončuji…</string>
|
||||||
<string name="download_complete">Kompletní!</string>
|
<string name="download_complete">Kompletní!</string>
|
||||||
<string name="hide_expired_entries_title">Skrýt propadlé záznamy</string>
|
<string name="hide_expired_entries_title">Skrýt propadlé záznamy</string>
|
||||||
<string name="hide_expired_entries_summary">Propadlé záznamy nejsou ukázány</string>
|
<string name="hide_expired_entries_summary">Propadlé záznamy nebudou ukázány</string>
|
||||||
<string name="contact">Kontakt</string>
|
<string name="contact">Kontakt</string>
|
||||||
<string name="contribution">Příspěvky</string>
|
<string name="contribution">Příspění</string>
|
||||||
<string name="feedback">Feedback</string>
|
<string name="feedback">Feedback</string>
|
||||||
<string name="auto_focus_search_title">Snadné hledání</string>
|
<string name="auto_focus_search_title">Snadné hledání</string>
|
||||||
<string name="auto_focus_search_summary">Při otevření databáze žádat hledání</string>
|
<string name="auto_focus_search_summary">Při otevření databáze žádat hledání</string>
|
||||||
@@ -436,23 +436,23 @@
|
|||||||
<string name="remember_keyfile_locations_summary">Uchová informaci o tom, kde jsou uloženy soubory s klíči</string>
|
<string name="remember_keyfile_locations_summary">Uchová informaci o tom, kde jsou uloženy soubory s klíči</string>
|
||||||
<string name="show_recent_files_title">Ukázat nedávné soubory</string>
|
<string name="show_recent_files_title">Ukázat nedávné soubory</string>
|
||||||
<string name="show_recent_files_summary">Ukázat umístění nedávných databází</string>
|
<string name="show_recent_files_summary">Ukázat umístění nedávných databází</string>
|
||||||
<string name="hide_broken_locations_title">Skrýt špatné odkazy na databáze</string>
|
<string name="hide_broken_locations_title">Skrýt chybné odkazy na databáze</string>
|
||||||
<string name="hide_broken_locations_summary">Skrýt nesprávné odkazy v seznamu nedávných databází</string>
|
<string name="hide_broken_locations_summary">Skrýt chybné odkazy v seznamu nedávných databází</string>
|
||||||
<string name="warning_database_read_only">Udělit právo zápisu pro uložení změn v databázi</string>
|
<string name="warning_database_read_only">Udělit právo zápisu pro uložení změn v databázi</string>
|
||||||
<string name="html_about_licence">KeePassDX © %1$d Kunzisoft je <strong>open source</strong> a <strong>bey reklam</strong>.
|
<string name="html_about_licence">KeePassDX © %1$d Kunzisoft je <strong>open source</strong> a <strong>bey reklam</strong>.
|
||||||
\nJe poskytován jak je, pod licencí <strong>GPLv3</strong>, bez jakékoli záruky.</string>
|
\nJe poskytován jak je, pod licencí <strong>GPLv3</strong>, bez jakékoli záruky.</string>
|
||||||
<string name="html_about_contribution">Abychom si <strong>udrželi svoji svobodu</strong>, <strong>opravili chyby</strong>,<strong>doplnili funkce</strong> a <strong>byli vždy aktivní</strong>, počítáme s Vaším <strong>přispěním</strong>.</string>
|
<string name="html_about_contribution">Abychom si <strong>udrželi svoji svobodu</strong>, <strong>opravili chyby</strong>,<strong>doplnili funkce</strong> a <strong>byli vždy aktivní</strong>, počítáme s Vaším <strong>přispěním</strong>.</string>
|
||||||
<string name="error_create_database">Nedaří se vytvořit soubor s databází.</string>
|
<string name="error_create_database">Nepodařilo se vytvořit soubor databáze.</string>
|
||||||
<string name="entry_add_attachment">Přidat přílohu</string>
|
<string name="entry_add_attachment">Přidat přílohu</string>
|
||||||
<string name="discard">Zahodit</string>
|
<string name="discard">Zavrhnout</string>
|
||||||
<string name="discard_changes">Zahodit změny\?</string>
|
<string name="discard_changes">Zavrhnout změny\?</string>
|
||||||
<string name="validate">Ověřit</string>
|
<string name="validate">Zkontrolovat</string>
|
||||||
<string name="education_setup_OTP_summary">Nastavit správu One-Time hesla (HOTP / TOTP) pro založení tokenu požadovaného pro dvoufázové ověření (2FA).</string>
|
<string name="education_setup_OTP_summary">Nastavit správu One-Time hesla (HOTP / TOTP) pro založení tokenu požadovaného pro dvoufázové ověření (2FA).</string>
|
||||||
<string name="education_setup_OTP_title">Nastavit OTP</string>
|
<string name="education_setup_OTP_title">Nastavit OTP</string>
|
||||||
<string name="autofill_auto_search_summary">Automaticky navrhnout výsledky hledání z webové domény nebo ID aplikace</string>
|
<string name="autofill_auto_search_summary">Automaticky navrhnout výsledky hledání z webové domény nebo ID aplikace</string>
|
||||||
<string name="autofill_auto_search_title">Samočinné hledání</string>
|
<string name="autofill_auto_search_title">Samočinné hledání</string>
|
||||||
<string name="lock_database_show_button_summary">Ukáže tlačítko zámku v uživatelském rozhraní</string>
|
<string name="lock_database_show_button_summary">Zobrazí tlačítko zámku v uživatelském rozhraní</string>
|
||||||
<string name="lock_database_show_button_title">Ukázat tlačítko zámku</string>
|
<string name="lock_database_show_button_title">Zobrazit tlačítko zámku</string>
|
||||||
<string name="autofill_preference_title">Nastavení samovyplnění</string>
|
<string name="autofill_preference_title">Nastavení samovyplnění</string>
|
||||||
<string name="warning_database_link_revoked">Přístup k souboru zrušenému správcem souborů</string>
|
<string name="warning_database_link_revoked">Přístup k souboru zrušenému správcem souborů</string>
|
||||||
<string name="error_label_exists">Tento štítek již existuje.</string>
|
<string name="error_label_exists">Tento štítek již existuje.</string>
|
||||||
@@ -469,14 +469,14 @@
|
|||||||
<string name="subdomain_search_title">Hledat v subdoméně</string>
|
<string name="subdomain_search_title">Hledat v subdoméně</string>
|
||||||
<string name="error_string_type">Tento text se s požadovanou položkou neshoduje.</string>
|
<string name="error_string_type">Tento text se s požadovanou položkou neshoduje.</string>
|
||||||
<string name="content_description_add_item">Přidat položku</string>
|
<string name="content_description_add_item">Přidat položku</string>
|
||||||
<string name="keyboard_previous_fill_in_summary">Automaticky přepnout na předchozí klávesnici po provedení Akce auto-klávesy</string>
|
<string name="keyboard_previous_fill_in_summary">Automaticky přepnout na předchozí klávesnici po provedení \"Akce auto-klávesy\"</string>
|
||||||
<string name="keyboard_previous_fill_in_title">Akce auto-klávesy</string>
|
<string name="keyboard_previous_fill_in_title">Akce auto-klávesy</string>
|
||||||
<string name="keyboard_previous_database_credentials_summary">Automaticky přepnout zpět na předchozí klávesnici na obrazovce ověřovacích údajů databáze</string>
|
<string name="keyboard_previous_database_credentials_summary">Automaticky přepnout zpět na předchozí klávesnici na obrazovce ověřovacích údajů databáze</string>
|
||||||
<string name="keyboard_previous_database_credentials_title">Obrazovka ověřovacích údajů databáze</string>
|
<string name="keyboard_previous_database_credentials_title">Obrazovka ověřovacích údajů databáze</string>
|
||||||
<string name="keyboard_change">Přepnout klávesnici</string>
|
<string name="keyboard_change">Přepnout klávesnici</string>
|
||||||
<string name="upload_attachment">Nahrát %1$s</string>
|
<string name="upload_attachment">Nahrát %1$s</string>
|
||||||
<string name="content_description_credentials_information">Info o údajích</string>
|
<string name="content_description_credentials_information">Informace o údajích</string>
|
||||||
<string name="warning_file_too_big">Databáze KeePassu předpokládá uchovávat jen malé pomocné sobory (např. PGP soubory).
|
<string name="warning_file_too_big">Databáze KeePassu předpokládá uchovávat jen malé pomocné sobory (např. PGP soubory s klíči).
|
||||||
\n
|
\n
|
||||||
\nVaše databáze by se mohla značně zvětšit a tímto nahráním tak ztratit na výkonnosti.</string>
|
\nVaše databáze by se mohla značně zvětšit a tímto nahráním tak ztratit na výkonnosti.</string>
|
||||||
<string name="warning_replace_file">Nahráním tohoto souboru nahradíte existující soubor.</string>
|
<string name="warning_replace_file">Nahráním tohoto souboru nahradíte existující soubor.</string>
|
||||||
@@ -497,41 +497,60 @@
|
|||||||
<string name="autofill_ask_to_save_data_title">Zeptat se před uložením</string>
|
<string name="autofill_ask_to_save_data_title">Zeptat se před uložením</string>
|
||||||
<string name="autofill_save_search_info_summary">Pokuste se uložit údaje hledání, když manuálně vybíráte položku</string>
|
<string name="autofill_save_search_info_summary">Pokuste se uložit údaje hledání, když manuálně vybíráte položku</string>
|
||||||
<string name="autofill_save_search_info_title">Uložit info hledání</string>
|
<string name="autofill_save_search_info_title">Uložit info hledání</string>
|
||||||
<string name="autofill_close_database_summary">Zavřít databázi po samodoplnění polí</string>
|
<string name="autofill_close_database_summary">Zavřít databázi po samovyplnění polí</string>
|
||||||
<string name="autofill_close_database_title">Zavřít databázi</string>
|
<string name="autofill_close_database_title">Zavřít databázi</string>
|
||||||
<string name="keyboard_previous_lock_summary">Po uzamknutí databáze automaticky přepnout zpět na předchozí klávesnici</string>
|
<string name="keyboard_previous_lock_summary">Po uzamknutí databáze automaticky přepnout zpět na předchozí klávesnici</string>
|
||||||
<string name="keyboard_previous_lock_title">Uzamknout databázi</string>
|
<string name="keyboard_previous_lock_title">Uzamknout databázi</string>
|
||||||
<string name="keyboard_save_search_info_summary">Pokuste se uložit sdílené info, když manuálné vybíráte položku</string>
|
<string name="keyboard_save_search_info_summary">Pokuste se uložit sdílené info, když manuálné vybíráte položku</string>
|
||||||
<string name="keyboard_save_search_info_title">Uložit sdílené info</string>
|
<string name="keyboard_save_search_info_title">Uložit sdílené info</string>
|
||||||
<string name="notification">Oznámení</string>
|
<string name="notification">Oznámení</string>
|
||||||
<string name="crypto_object_not_initialized">Krypto objekt nelze načíst.</string>
|
|
||||||
<string name="biometric_security_update_required">Vyžadována aktualizace biometrického zabezpečení.</string>
|
<string name="biometric_security_update_required">Vyžadována aktualizace biometrického zabezpečení.</string>
|
||||||
<string name="configure_biometric">Žádné přihlašovací ani biometrické údaje nejsou registrovány.</string>
|
<string name="configure_biometric">Žádné přihlašovací ani biometrické údaje nejsou registrovány.</string>
|
||||||
<string name="warning_empty_recycle_bin">Trvale odstranit všechny položky z koše\?</string>
|
<string name="warning_empty_recycle_bin">Trvale odstranit všechny uzly z koše\?</string>
|
||||||
<string name="registration_mode">Režim registrace</string>
|
<string name="registration_mode">Režim registrace</string>
|
||||||
<string name="save_mode">Režim ukládání</string>
|
<string name="save_mode">Režim ukládání</string>
|
||||||
<string name="search_mode">Režim vyhledávání</string>
|
<string name="search_mode">Režim vyhledávání</string>
|
||||||
<string name="error_field_name_already_exists">Jméno položky již existuje.</string>
|
<string name="error_field_name_already_exists">Jméno kolonky již existuje.</string>
|
||||||
<string name="error_registration_read_only">Uložení nové položky v režimu databáze pouze pro čtení není povoleno</string>
|
<string name="error_registration_read_only">Uložení nové položky v režimu databáze pouze pro čtení není povoleno</string>
|
||||||
<string name="enter">Enter</string>
|
<string name="enter">Enter</string>
|
||||||
<string name="backspace">Backspace</string>
|
<string name="backspace">Backspace</string>
|
||||||
<string name="select_entry">Vybrat záznam</string>
|
<string name="select_entry">Vybrat záznam</string>
|
||||||
<string name="back_to_previous_keyboard">Zpět na předchozí klávesnici</string>
|
<string name="back_to_previous_keyboard">Zpět na předchozí klávesnici</string>
|
||||||
<string name="custom_fields">Vlastní položky</string>
|
<string name="custom_fields">Vlastní kolonky</string>
|
||||||
<string name="advanced_unlock_delete_all_key_warning">Smazat všechny šifrovací klíče související s rozpoznáním pokročilého odemknutí\?</string>
|
<string name="advanced_unlock_delete_all_key_warning">Smazat všechny šifrovací klíče související s rozpoznáním rozšířeného odemknutí\?</string>
|
||||||
<string name="device_credential_unlock_enable_summary">Dovolí pro otevření databáze použít heslo Vašeho zařízení</string>
|
<string name="device_credential_unlock_enable_summary">Dovolí pro otevření databáze použít heslo Vašeho zařízení</string>
|
||||||
<string name="device_credential_unlock_enable_title">Odemknutí heslem zařízení</string>
|
<string name="device_credential_unlock_enable_title">Odemknutí heslem zařízení</string>
|
||||||
<string name="device_credential">Heslo zařízení</string>
|
<string name="device_credential">Heslo zařízení</string>
|
||||||
<string name="credential_before_click_advanced_unlock_button">Zadejte heslo a klikněte na tlačítko \"Pokročilé odemknutí\".</string>
|
<string name="credential_before_click_advanced_unlock_button">Zadejte heslo a klikněte na tlačítko \"Rozšířené odemknutí\".</string>
|
||||||
<string name="advanced_unlock_prompt_not_initialized">Nelze inicializovat pobídku pro pokročilé odemknutí.</string>
|
<string name="advanced_unlock_prompt_not_initialized">Nelze inicializovat pobídku pro rozšířené odemknutí.</string>
|
||||||
<string name="advanced_unlock_scanning_error">Chyba při pokročilém odemknutí: %1$s</string>
|
<string name="advanced_unlock_scanning_error">Chyba při rozšířeném odemknutí: %1$s</string>
|
||||||
<string name="advanced_unlock_not_recognized">Otisk pro pokročilé odemknutí nebyl rozpoznán</string>
|
<string name="advanced_unlock_not_recognized">Otisk pro rozšířené odemknutí nebyl rozpoznán</string>
|
||||||
<string name="advanced_unlock_invalid_key">Nelze načíst klíč pokročilého odemknutí. Prosím, smažte jej a opakujte proces rozpoznání uzamknutí.</string>
|
<string name="advanced_unlock_invalid_key">Nelze načíst klíč rozšířeného odemknutí. Prosím, smažte jej a opakujte proces rozpoznání odemknutí.</string>
|
||||||
<string name="advanced_unlock_prompt_extract_credential_message">Načíst důvěrný údaj pomocí dat pokročilého odemknutí</string>
|
<string name="advanced_unlock_prompt_extract_credential_message">Načíst důvěrný údaj pomocí dat rozšířeného odemknutí</string>
|
||||||
<string name="advanced_unlock_prompt_extract_credential_title">Otevřít databázi pomocí rozpoznání pokročilého odemknutí</string>
|
<string name="advanced_unlock_prompt_extract_credential_title">Otevřít databázi pomocí rozpoznání rozšířeného odemknutí</string>
|
||||||
<string name="advanced_unlock_prompt_store_credential_message">Varování: Pokud použijete rozpoznání pokročilého odemknutí, musíte si i nadále pamatovat hlavní heslo.</string>
|
<string name="advanced_unlock_prompt_store_credential_message">Varování: Pokud použijete rozpoznání rozšířeného odemknutí, musíte si i nadále pamatovat hlavní heslo.</string>
|
||||||
<string name="advanced_unlock_prompt_store_credential_title">Rozpoznání pokročilého odemknutí</string>
|
<string name="advanced_unlock_prompt_store_credential_title">Rozpoznání rozšířeného odemknutí</string>
|
||||||
<string name="open_advanced_unlock_prompt_store_credential">Pro uložení důvěrných údajů otevřete pobídku pokročilého odemknutí</string>
|
<string name="open_advanced_unlock_prompt_store_credential">Pro uložení důvěrných údajů otevřete pobídku rozšířeného odemknutí</string>
|
||||||
<string name="open_advanced_unlock_prompt_unlock_database">Pro odemknutí databáze otevřete pobídku pokročilého odemknutí</string>
|
<string name="open_advanced_unlock_prompt_unlock_database">Pro odemknutí databáze otevřete pobídku rozšířeného odemknutí</string>
|
||||||
<string name="menu_keystore_remove_key">Smazat klíč pokročilého odemknutí</string>
|
<string name="menu_keystore_remove_key">Smazat klíč rozšířeného odemknutí</string>
|
||||||
|
<string name="education_advanced_unlock_title">Rozšířené odemknutí databáze</string>
|
||||||
|
<string name="advanced_unlock_timeout">Časový limit rozšířeného odemknutí</string>
|
||||||
|
<string name="temp_advanced_unlock_timeout_summary">Trvání použití rozšířeného odemknutí než bude obsah téhož smazán</string>
|
||||||
|
<string name="temp_advanced_unlock_enable_summary">Za účelem rozšířeného odemknutí neukládat žádný šifrovaný obsah</string>
|
||||||
|
<string name="temp_advanced_unlock_enable_title">Přechodné rozšířené odemknutí</string>
|
||||||
|
<string name="advanced_unlock_tap_delete">Pro odstranění klíčů rozšířeného odemknutí klepnout</string>
|
||||||
|
<string name="kdf_Argon2id">Argon2id</string>
|
||||||
|
<string name="kdf_Argon2d">Argon2d</string>
|
||||||
|
<string name="education_advanced_unlock_summary">Abyste rychle odemknuli databázi, propojte své heslo s naskenovanou biometrikou nebo údaji zámku zařízení.</string>
|
||||||
|
<string name="temp_advanced_unlock_timeout_title">Vypršení pokročilého odemknutí</string>
|
||||||
|
<string name="content">Obsah</string>
|
||||||
|
<string name="error_rebuild_list">Seznam nelze řádně sestavit.</string>
|
||||||
|
<string name="error_database_uri_null">URI databáze nelze načíst.</string>
|
||||||
|
<string name="autofill_inline_suggestions_keyboard">Návrhy samovyplnění přidány.</string>
|
||||||
|
<string name="autofill_inline_suggestions_title">Návrhy inline</string>
|
||||||
|
<string name="autofill_inline_suggestions_summary">Pokusí ze zobrazit návrhy samovyplnění přímo z kompatibilní klávesnice</string>
|
||||||
|
<string name="warning_database_revoked">Přístup k souboru odebrán správcem souborů, uzavřete databázi a nově ji otevřete z jejího adresáře.</string>
|
||||||
|
<string name="menu_reload_database">Databázi nově načíst</string>
|
||||||
|
<string name="warning_database_info_changed_options">Přepsat externí změny uložením databáze nebo databázi včetně posledních změn nově načíst.</string>
|
||||||
|
<string name="warning_database_info_changed">Informace obsažená ve Vašem databázovém souboru by změněna mimo aplikaci.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
<string name="encryption_explanation">Databasekrypteringsalgoritme anvendt for alle data.</string>
|
<string name="encryption_explanation">Databasekrypteringsalgoritme anvendt for alle data.</string>
|
||||||
<string name="kdf_explanation">For at generere nøglen til krypteringsalgoritmen, omdannes hovednøglen ved hjælp af en tilfældigt saltet nøgleafledningsfunktion.</string>
|
<string name="kdf_explanation">For at generere nøglen til krypteringsalgoritmen, omdannes hovednøglen ved hjælp af en tilfældigt saltet nøgleafledningsfunktion.</string>
|
||||||
<string name="memory_usage">Hukommelsesforbrug</string>
|
<string name="memory_usage">Hukommelsesforbrug</string>
|
||||||
<string name="memory_usage_explanation">Hukommelse (i bytes), som anvendes af nøgleafledningsfunktion.</string>
|
<string name="memory_usage_explanation">Hukommelse, som anvendes af nøgleafledningsfunktion.</string>
|
||||||
<string name="parallelism">Parallelitet</string>
|
<string name="parallelism">Parallelitet</string>
|
||||||
<string name="parallelism_explanation">Grad af parallelitet (dvs. antallet af tråde), som anvendes af nøgleafledningsfunktion.</string>
|
<string name="parallelism_explanation">Grad af parallelitet (dvs. antallet af tråde), som anvendes af nøgleafledningsfunktion.</string>
|
||||||
<string name="sort_menu">Sorter</string>
|
<string name="sort_menu">Sorter</string>
|
||||||
@@ -391,7 +391,7 @@
|
|||||||
<string name="max_history_items_title">Max. antal</string>
|
<string name="max_history_items_title">Max. antal</string>
|
||||||
<string name="max_history_items_summary">Begræns antallet af historikposter pr. indtastning</string>
|
<string name="max_history_items_summary">Begræns antallet af historikposter pr. indtastning</string>
|
||||||
<string name="max_history_size_title">Max. størrelse</string>
|
<string name="max_history_size_title">Max. størrelse</string>
|
||||||
<string name="max_history_size_summary">Begræns historikstørrelsen (i bytes) pr. post</string>
|
<string name="max_history_size_summary">Begræns historikstørrelsen pr. post</string>
|
||||||
<string name="settings_database_recommend_changing_master_key_title">Anbefalet fornyelse</string>
|
<string name="settings_database_recommend_changing_master_key_title">Anbefalet fornyelse</string>
|
||||||
<string name="settings_database_recommend_changing_master_key_summary">Anbefal ændring af hovednøglen (dage)</string>
|
<string name="settings_database_recommend_changing_master_key_summary">Anbefal ændring af hovednøglen (dage)</string>
|
||||||
<string name="settings_database_force_changing_master_key_title">Gennemtving fornyelse</string>
|
<string name="settings_database_force_changing_master_key_title">Gennemtving fornyelse</string>
|
||||||
@@ -491,7 +491,6 @@
|
|||||||
<string name="database_data_remove_unlinked_attachments_summary">Fjerner vedhæftede filer indeholdt i databasen, men ikke knyttet til en post</string>
|
<string name="database_data_remove_unlinked_attachments_summary">Fjerner vedhæftede filer indeholdt i databasen, men ikke knyttet til en post</string>
|
||||||
<string name="database_data_remove_unlinked_attachments_title">Fjern ikke-sammenkædede data</string>
|
<string name="database_data_remove_unlinked_attachments_title">Fjern ikke-sammenkædede data</string>
|
||||||
<string name="data">Data</string>
|
<string name="data">Data</string>
|
||||||
<string name="crypto_object_not_initialized">Kryptoobjektet kunne ikke hentes.</string>
|
|
||||||
<string name="biometric_security_update_required">Biometrisk sikkerhedsopdatering påkrævet.</string>
|
<string name="biometric_security_update_required">Biometrisk sikkerhedsopdatering påkrævet.</string>
|
||||||
<string name="warning_empty_keyfile_explanation">Indholdet af nøglefilen bør aldrig ændres og bør i bedste fald indeholde tilfældigt genererede data.</string>
|
<string name="warning_empty_keyfile_explanation">Indholdet af nøglefilen bør aldrig ændres og bør i bedste fald indeholde tilfældigt genererede data.</string>
|
||||||
<string name="warning_empty_keyfile">Det anbefales ikke at tilføje en tom nøglefil.</string>
|
<string name="warning_empty_keyfile">Det anbefales ikke at tilføje en tom nøglefil.</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">Papierkorb/Sicherung nicht durchsuchen</string>
|
<string name="omit_backup_search_title">Recycle bin und Backup nicht durchsuchen</string>
|
||||||
<string name="omit_backup_search_summary">Die Gruppen „Sicherung“ und „Papierkorb“ werden bei der Suche nicht berücksichtigt</string>
|
<string name="omit_backup_search_summary">Die Gruppen „Backup“ und „Recycle bin“ 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>
|
||||||
@@ -199,7 +199,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 (in Bytes) 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>
|
||||||
@@ -218,7 +218,7 @@
|
|||||||
<string name="autofill_explanation_summary">Automatisches Ausfüllen aktivieren, um schnell Formulare in anderen Apps auszufüllen</string>
|
<string name="autofill_explanation_summary">Automatisches Ausfüllen aktivieren, um schnell Formulare in anderen Apps auszufüllen</string>
|
||||||
<string name="clipboard">Zwischenablage</string>
|
<string name="clipboard">Zwischenablage</string>
|
||||||
<string name="biometric_delete_all_key_title">Verschlüsselungsschlüssel löschen</string>
|
<string name="biometric_delete_all_key_title">Verschlüsselungsschlüssel löschen</string>
|
||||||
<string name="biometric_delete_all_key_summary">Alle mit der biometrischen Erkennung verbundenen Verschlüsselungsschlüssel löschen.</string>
|
<string name="biometric_delete_all_key_summary">Lösche alle Verschlüsselungsschlüssel, die mit der erweiterten Entsperrerkennung zusammenhängen</string>
|
||||||
<string name="unavailable_feature_version">Auf dem Gerät läuft Android %1$s, eine Version ab %2$s wäre aber nötig.</string>
|
<string name="unavailable_feature_version">Auf dem Gerät läuft Android %1$s, eine Version ab %2$s wäre aber nötig.</string>
|
||||||
<string name="unavailable_feature_hardware">Keine entsprechende Hardware.</string>
|
<string name="unavailable_feature_hardware">Keine entsprechende Hardware.</string>
|
||||||
<string name="recycle_bin_title">Papierkorb-Nutzung</string>
|
<string name="recycle_bin_title">Papierkorb-Nutzung</string>
|
||||||
@@ -231,7 +231,7 @@
|
|||||||
<string name="database_description_title">Datenbankbeschreibung</string>
|
<string name="database_description_title">Datenbankbeschreibung</string>
|
||||||
<string name="database_version_title">Datenbankversion</string>
|
<string name="database_version_title">Datenbankversion</string>
|
||||||
<string name="text_appearance">Text</string>
|
<string name="text_appearance">Text</string>
|
||||||
<string name="application_appearance">App</string>
|
<string name="application_appearance">Interface</string>
|
||||||
<string name="other">Andere</string>
|
<string name="other">Andere</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>
|
||||||
@@ -267,7 +267,7 @@
|
|||||||
<string name="education_donation_title">Mitmachen</string>
|
<string name="education_donation_title">Mitmachen</string>
|
||||||
<string name="education_donation_summary">Mithelfen, um Stabilität und Sicherheit zu verbessern und weitere Funktionen zu ermöglichen.</string>
|
<string name="education_donation_summary">Mithelfen, um Stabilität und Sicherheit zu verbessern und weitere Funktionen zu ermöglichen.</string>
|
||||||
<string name="html_text_ad_free">Anders als viele andere Passwortmanager ist dieser <strong>werbefrei</strong>, <strong>quelloffen</strong> und unter einer <strong>Copyleft-Lizenz</strong>. Es werden keine persönlichen Daten gesammelt, in welcher Form auch immer, unabhängig von der verwendeten Version (kostenlos oder Pro).</string>
|
<string name="html_text_ad_free">Anders als viele andere Passwortmanager ist dieser <strong>werbefrei</strong>, <strong>quelloffen</strong> und unter einer <strong>Copyleft-Lizenz</strong>. Es werden keine persönlichen Daten gesammelt, in welcher Form auch immer, unabhängig von der verwendeten Version (kostenlos oder Pro).</string>
|
||||||
<string name="html_text_buy_pro">Mit dem Kauf der Pro-Version erhält man Zugang zu diesem <strong>visuellen Stil</strong> und unterstützt insbesondere <strong>die Umsetzung gemeinschaftlicher Projektarbeiten.</strong></string>
|
<string name="html_text_buy_pro">Mit dem Kauf der Pro-Version erhalten Sie Zugriff auf diesen <strong>visuellen Stil</strong> und unterstützen insbesondere <strong>die Umsetzung gemeinschaftlicher Projekte.</strong></string>
|
||||||
<string name="html_text_feature_generosity">Dieser <strong>visuelle Stil</strong> wurde wegen Ihrer Großzügigkeit freigeschaltet.</string>
|
<string name="html_text_feature_generosity">Dieser <strong>visuelle Stil</strong> wurde wegen Ihrer Großzügigkeit freigeschaltet.</string>
|
||||||
<string name="html_text_donation">Um unsere Freiheit zu bewahren und immer aktiv zu bleiben, zählen wir auf Ihren <strong>Beitrag.</strong></string>
|
<string name="html_text_donation">Um unsere Freiheit zu bewahren und immer aktiv zu bleiben, zählen wir auf Ihren <strong>Beitrag.</strong></string>
|
||||||
<string name="html_text_dev_feature">Diese Funktion ist <strong>in Entwicklung</strong> und erfordert <strong>Ihren Beitrag</strong>, um bald verfügbar zu sein.</string>
|
<string name="html_text_dev_feature">Diese Funktion ist <strong>in Entwicklung</strong> und erfordert <strong>Ihren Beitrag</strong>, um bald verfügbar zu sein.</string>
|
||||||
@@ -276,7 +276,7 @@
|
|||||||
<string name="html_text_dev_feature_encourage">Sie ermutigen die Entwickler:innen, <strong>neue Funktionen</strong> einzuführen und gemäß Ihren Anmerkungen <strong>Fehler zu beheben</strong>.</string>
|
<string name="html_text_dev_feature_encourage">Sie ermutigen die Entwickler:innen, <strong>neue Funktionen</strong> einzuführen und gemäß Ihren Anmerkungen <strong>Fehler zu beheben</strong>.</string>
|
||||||
<string name="html_text_dev_feature_thanks">Vielen Dank für Ihre Unterstützung.</string>
|
<string name="html_text_dev_feature_thanks">Vielen Dank für Ihre Unterstützung.</string>
|
||||||
<string name="html_text_dev_feature_work_hard">Wir bemühen uns, diese Funktion bald zu veröffentlichen.</string>
|
<string name="html_text_dev_feature_work_hard">Wir bemühen uns, diese Funktion bald zu veröffentlichen.</string>
|
||||||
<string name="html_text_dev_feature_upgrade">Vergessen Sie nicht, Ihre Anwendung aktuell zu halten, indem Sie neue Versionen installieren.</string>
|
<string name="html_text_dev_feature_upgrade">Denken Sie daran, Ihre App auf dem neuesten Stand zu halten, indem Sie neue Versionen installieren.</string>
|
||||||
<string name="download">Download</string>
|
<string name="download">Download</string>
|
||||||
<string name="contribute">Unterstützen</string>
|
<string name="contribute">Unterstützen</string>
|
||||||
<string name="encryption_chacha20">ChaCha20</string>
|
<string name="encryption_chacha20">ChaCha20</string>
|
||||||
@@ -302,7 +302,7 @@
|
|||||||
<string name="education_read_only_summary">Den Öffnungsmodus für die Sitzung ändern.
|
<string name="education_read_only_summary">Den Öffnungsmodus für die Sitzung ändern.
|
||||||
\n
|
\n
|
||||||
\n„Schreibgeschützt“ verhindert unbeabsichtigte Änderungen an der Datenbank.
|
\n„Schreibgeschützt“ verhindert unbeabsichtigte Änderungen an der Datenbank.
|
||||||
\nMit „Änderbar“ können alle Elemente hinzugefügt, gelöscht oder geändert werden.</string>
|
\nMit „Änderbar“ können Sie alle Elemente nach Belieben hinzufügen, löschen oder ändern.</string>
|
||||||
<string name="edit_entry">Eintrag bearbeiten</string>
|
<string name="edit_entry">Eintrag bearbeiten</string>
|
||||||
<string name="error_load_database">Datenbank kann nicht geladen werden.</string>
|
<string name="error_load_database">Datenbank kann nicht geladen werden.</string>
|
||||||
<string name="error_load_database_KDF_memory">Laden des Schlüssels fehlgeschlagen. Bitte versuchen, die „Speicherplatznutzung“ von KDF zu verringern.</string>
|
<string name="error_load_database_KDF_memory">Laden des Schlüssels fehlgeschlagen. Bitte versuchen, die „Speicherplatznutzung“ von KDF zu verringern.</string>
|
||||||
@@ -336,7 +336,7 @@
|
|||||||
<string name="show_recent_files_summary">Speicherort zuletzt verwendeter Datenbanken anzeigen</string>
|
<string name="show_recent_files_summary">Speicherort zuletzt verwendeter Datenbanken anzeigen</string>
|
||||||
<string name="hide_broken_locations_title">Defekte Datenbankverknüpfungen ausblenden</string>
|
<string name="hide_broken_locations_title">Defekte Datenbankverknüpfungen ausblenden</string>
|
||||||
<string name="hide_broken_locations_summary">Nicht funktionierende Verknüpfungen in der Liste der zuletzt verwendeten Datenbanken ausblenden</string>
|
<string name="hide_broken_locations_summary">Nicht funktionierende Verknüpfungen in der Liste der zuletzt verwendeten Datenbanken ausblenden</string>
|
||||||
<string name="do_not_kill_app">Nicht die Anwendung beenden …</string>
|
<string name="do_not_kill_app">App nicht beenden …</string>
|
||||||
<string name="lock_database_back_root_summary">Datenbank sperren, wenn auf dem Hauptbildschirm der Zurück-Button gedrückt wird</string>
|
<string name="lock_database_back_root_summary">Datenbank sperren, wenn auf dem Hauptbildschirm der Zurück-Button gedrückt wird</string>
|
||||||
<string name="clear_clipboard_notification_title">Beim Schließen löschen</string>
|
<string name="clear_clipboard_notification_title">Beim Schließen löschen</string>
|
||||||
<string name="recycle_bin">Papierkorb</string>
|
<string name="recycle_bin">Papierkorb</string>
|
||||||
@@ -373,8 +373,8 @@
|
|||||||
<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">Biometrische Abfrage automatisch öffnen</string>
|
<string name="biometric_auto_open_prompt_title">Abfrage automatisch öffnen</string>
|
||||||
<string name="biometric_auto_open_prompt_summary">Automatisch nach Biometrie fragen, wenn die Datenbank dafür eingerichtet ist</string>
|
<string name="biometric_auto_open_prompt_summary">Automatisch nach der erweiterten Entsperrung fragen, 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>
|
||||||
@@ -407,7 +407,7 @@
|
|||||||
<string name="max_history_items_title">Maximale Anzahl</string>
|
<string name="max_history_items_title">Maximale Anzahl</string>
|
||||||
<string name="max_history_items_summary">Anzahl der Verlaufseinträge pro Eintrag begrenzen</string>
|
<string name="max_history_items_summary">Anzahl der Verlaufseinträge pro Eintrag begrenzen</string>
|
||||||
<string name="max_history_size_title">Maximale Größe</string>
|
<string name="max_history_size_title">Maximale Größe</string>
|
||||||
<string name="max_history_size_summary">Verlaufsumfang (in Bytes) pro Eintrag begrenzen</string>
|
<string name="max_history_size_summary">Verlaufsumfang pro Eintrag begrenzen</string>
|
||||||
<string name="settings_database_recommend_changing_master_key_title">Erneuerung empfehlen</string>
|
<string name="settings_database_recommend_changing_master_key_title">Erneuerung empfehlen</string>
|
||||||
<string name="settings_database_recommend_changing_master_key_summary">(Nach Tagen) Änderung des Hauptschlüssels empfehlen</string>
|
<string name="settings_database_recommend_changing_master_key_summary">(Nach Tagen) Änderung des Hauptschlüssels empfehlen</string>
|
||||||
<string name="settings_database_force_changing_master_key_title">Erneuerung erzwingen</string>
|
<string name="settings_database_force_changing_master_key_title">Erneuerung erzwingen</string>
|
||||||
@@ -500,7 +500,7 @@
|
|||||||
<string name="upload_attachment">%1$s hochladen</string>
|
<string name="upload_attachment">%1$s hochladen</string>
|
||||||
<string name="education_add_attachment_title">Anhang hinzufügen</string>
|
<string name="education_add_attachment_title">Anhang hinzufügen</string>
|
||||||
<string name="database_data_remove_unlinked_attachments_summary">Entfernt Anhänge die in der Datenbank enthalten, aber keinem Eintrag zugeordnet sind</string>
|
<string name="database_data_remove_unlinked_attachments_summary">Entfernt Anhänge die in der Datenbank enthalten, aber keinem Eintrag zugeordnet sind</string>
|
||||||
<string name="warning_sure_add_file">Die Datei trotzdem hinzufügen\?</string>
|
<string name="warning_sure_add_file">Soll die Datei trotzdem hinzugefügt werden\?</string>
|
||||||
<string name="show_uuid_summary">Zeigt die mit einem Eintrag verknüpfte UUID an</string>
|
<string name="show_uuid_summary">Zeigt die mit einem Eintrag verknüpfte UUID an</string>
|
||||||
<string name="show_uuid_title">UUID anzeigen</string>
|
<string name="show_uuid_title">UUID anzeigen</string>
|
||||||
<string name="autofill_read_only_save">Das Speichern von Daten ist für eine als schreibgeschützt geöffnete Datenbank nicht zulässig.</string>
|
<string name="autofill_read_only_save">Das Speichern von Daten ist für eine als schreibgeschützt geöffnete Datenbank nicht zulässig.</string>
|
||||||
@@ -508,7 +508,6 @@
|
|||||||
<string name="keyboard_previous_lock_summary">Automatisches Zurückschalten zur vorherigen Tastatur nach dem Sperren der Datenbank</string>
|
<string name="keyboard_previous_lock_summary">Automatisches Zurückschalten zur vorherigen Tastatur nach dem Sperren der Datenbank</string>
|
||||||
<string name="keyboard_previous_lock_title">Datenbank sperren</string>
|
<string name="keyboard_previous_lock_title">Datenbank sperren</string>
|
||||||
<string name="notification">Benachrichtigung</string>
|
<string name="notification">Benachrichtigung</string>
|
||||||
<string name="crypto_object_not_initialized">Kryptoobjekt kann nicht abgerufen werden.</string>
|
|
||||||
<string name="biometric_security_update_required">Biometrisches Sicherheitsupdate erforderlich.</string>
|
<string name="biometric_security_update_required">Biometrisches Sicherheitsupdate erforderlich.</string>
|
||||||
<string name="configure_biometric">Es sind keine biometrischen oder Geräteanmeldeinformationen registriert.</string>
|
<string name="configure_biometric">Es sind keine biometrischen oder Geräteanmeldeinformationen registriert.</string>
|
||||||
<string name="registration_mode">Registrierungsmodus</string>
|
<string name="registration_mode">Registrierungsmodus</string>
|
||||||
@@ -528,5 +527,35 @@
|
|||||||
<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">Öffne den erweiterten Entsperrdialog zum Speichern von Anmeldeinformationen</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">Öffnen des erweiterten Entsperrdialogs zum Entsperren der Datenbank</string>
|
||||||
<string name="menu_keystore_remove_key">Löschen des Schlüssels zum erweiterten Entsperren</string>
|
<string name="menu_keystore_remove_key">Löschen des Schlüssels zum erweiterten Entsperren</string>
|
||||||
<string name="advanced_unlock_prompt_store_credential_title">Fortschrittliche Entsperrerkennung</string>
|
<string name="advanced_unlock_prompt_store_credential_title">Erweiterte 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_title">Erweiterte Entsperrung der Datenbank</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_title">Verfall der erweiterten 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_title">Temporäre erweiterte Entsperrung</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="content">Inhalt</string>
|
||||||
|
<string name="advanced_unlock_prompt_extract_credential_title">Öffne Datenbank mit erweiterter Entsperrerkennung</string>
|
||||||
|
<string name="enter">Eingabetaste</string>
|
||||||
|
<string name="backspace">Rücktaste</string>
|
||||||
|
<string name="select_entry">Wähle Eintrag</string>
|
||||||
|
<string name="back_to_previous_keyboard">Zurück zur vorherigen Tastatur</string>
|
||||||
|
<string name="custom_fields">Benutzerdefinierte Felder</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">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="advanced_unlock_prompt_not_initialized">Initialisieren des erweitertes Entsperren Dialogs fehlgeschlagen.</string>
|
||||||
|
<string name="advanced_unlock_scanning_error">Erweitertes Entsperren Fehler: %1$s</string>
|
||||||
|
<string name="advanced_unlock_not_recognized">Konnte den Abdruck des erweiterten Entsperrens nicht erkennen</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_prompt_extract_credential_message">Extrahiere Datenbankanmeldedaten mit Daten aus erweitertem Entsperren</string>
|
||||||
|
<string name="kdf_Argon2id">Argon2id</string>
|
||||||
|
<string name="kdf_Argon2d">Argon2d</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="menu_reload_database">Datenbank neu laden</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -165,7 +165,7 @@
|
|||||||
<string name="encryption_explanation">Αλγόριθμος κρυπτογράφησης βάσης δεδομένων που χρησιμοποιείται για όλα τα δεδομένα.</string>
|
<string name="encryption_explanation">Αλγόριθμος κρυπτογράφησης βάσης δεδομένων που χρησιμοποιείται για όλα τα δεδομένα.</string>
|
||||||
<string name="kdf_explanation">Για να δημιουργηθεί το κλειδί για τον αλγόριθμο κρυπτογράφησης, το κύριο κλειδί μετασχηματίζεται χρησιμοποιώντας μια τυχαία αλατισμένη λειτουργία εξαγωγής κλειδιών.</string>
|
<string name="kdf_explanation">Για να δημιουργηθεί το κλειδί για τον αλγόριθμο κρυπτογράφησης, το κύριο κλειδί μετασχηματίζεται χρησιμοποιώντας μια τυχαία αλατισμένη λειτουργία εξαγωγής κλειδιών.</string>
|
||||||
<string name="memory_usage">Χρήση μνήμης</string>
|
<string name="memory_usage">Χρήση μνήμης</string>
|
||||||
<string name="memory_usage_explanation">Ποσότητα μνήμης (σε bytes) που θα χρησιμοποιηθεί από τη λειτουργία εξαγωγής κλειδιών.</string>
|
<string name="memory_usage_explanation">Ποσότητα μνήμης που θα χρησιμοποιηθεί από τη λειτουργία εξαγωγής κλειδιών.</string>
|
||||||
<string name="parallelism">Παραλληλισμός</string>
|
<string name="parallelism">Παραλληλισμός</string>
|
||||||
<string name="parallelism_explanation">Βαθμός παραλληλισμού (δηλ. Αριθμός νημάτων) που χρησιμοποιείται από τη συνάρτηση εξαγωγής κλειδιών.</string>
|
<string name="parallelism_explanation">Βαθμός παραλληλισμού (δηλ. Αριθμός νημάτων) που χρησιμοποιείται από τη συνάρτηση εξαγωγής κλειδιών.</string>
|
||||||
<string name="sort_menu">Ταξινόμηση</string>
|
<string name="sort_menu">Ταξινόμηση</string>
|
||||||
@@ -392,7 +392,7 @@
|
|||||||
<string name="max_history_items_title">Μέγιστος αριθμός</string>
|
<string name="max_history_items_title">Μέγιστος αριθμός</string>
|
||||||
<string name="max_history_items_summary">Περιορίστε τον αριθμό των στοιχείων ιστορικού ανά καταχώριση</string>
|
<string name="max_history_items_summary">Περιορίστε τον αριθμό των στοιχείων ιστορικού ανά καταχώριση</string>
|
||||||
<string name="max_history_size_title">Μέγιστο μέγεθος</string>
|
<string name="max_history_size_title">Μέγιστο μέγεθος</string>
|
||||||
<string name="max_history_size_summary">Περιορίστε το μέγεθος ιστορικού (σε bytes) ανά καταχώριση</string>
|
<string name="max_history_size_summary">Περιορίστε το μέγεθος ιστορικού ανά καταχώριση</string>
|
||||||
<string name="settings_database_recommend_changing_master_key_title">Συστήστε ανανέωση</string>
|
<string name="settings_database_recommend_changing_master_key_title">Συστήστε ανανέωση</string>
|
||||||
<string name="settings_database_recommend_changing_master_key_summary">Προτεινόμενη αλλαγή του κύριου κλειδιού (ημέρες)</string>
|
<string name="settings_database_recommend_changing_master_key_summary">Προτεινόμενη αλλαγή του κύριου κλειδιού (ημέρες)</string>
|
||||||
<string name="settings_database_force_changing_master_key_title">Εξαναγκαστική ανανέωση</string>
|
<string name="settings_database_force_changing_master_key_title">Εξαναγκαστική ανανέωση</string>
|
||||||
@@ -503,7 +503,6 @@
|
|||||||
<string name="keyboard_save_search_info_summary">Προσπαθήστε να αποθηκεύσετε κοινόχρηστες πληροφορίες όταν κάνετε μια χειροκίνητη επιλογή καταχώρησης</string>
|
<string name="keyboard_save_search_info_summary">Προσπαθήστε να αποθηκεύσετε κοινόχρηστες πληροφορίες όταν κάνετε μια χειροκίνητη επιλογή καταχώρησης</string>
|
||||||
<string name="keyboard_save_search_info_title">Αποθήκευση κοινόχρηστων πληροφοριών</string>
|
<string name="keyboard_save_search_info_title">Αποθήκευση κοινόχρηστων πληροφοριών</string>
|
||||||
<string name="notification">Ειδοποίηση</string>
|
<string name="notification">Ειδοποίηση</string>
|
||||||
<string name="crypto_object_not_initialized">Δεν είναι δυνατή η ανάκτηση κρυπτογραφικού αντικειμένου.</string>
|
|
||||||
<string name="biometric_security_update_required">Απαιτείται ενημέρωση βιομετρικής ασφάλειας.</string>
|
<string name="biometric_security_update_required">Απαιτείται ενημέρωση βιομετρικής ασφάλειας.</string>
|
||||||
<string name="configure_biometric">Κανένα πιστοποιητικό βιομετρίας ή συσκευής δεν είναι εγγεγραμμένο.</string>
|
<string name="configure_biometric">Κανένα πιστοποιητικό βιομετρίας ή συσκευής δεν είναι εγγεγραμμένο.</string>
|
||||||
<string name="warning_empty_recycle_bin">Να διαγραφούν οριστικά όλοι οι κόμβοι από τον κάδο ανακύκλωσης;</string>
|
<string name="warning_empty_recycle_bin">Να διαγραφούν οριστικά όλοι οι κόμβοι από τον κάδο ανακύκλωσης;</string>
|
||||||
@@ -542,4 +541,15 @@
|
|||||||
<string name="temp_advanced_unlock_timeout_title">Λήξη προηγμένου ξεκλειδώματος</string>
|
<string name="temp_advanced_unlock_timeout_title">Λήξη προηγμένου ξεκλειδώματος</string>
|
||||||
<string name="advanced_unlock_tap_delete">Πατήστε για διαγραφή προηγμένων κλειδιών ξεκλειδώματος</string>
|
<string name="advanced_unlock_tap_delete">Πατήστε για διαγραφή προηγμένων κλειδιών ξεκλειδώματος</string>
|
||||||
<string name="content">Περιεχόμενα</string>
|
<string name="content">Περιεχόμενα</string>
|
||||||
|
<string name="kdf_Argon2id">Argon2id</string>
|
||||||
|
<string name="kdf_Argon2d">Argon2d</string>
|
||||||
|
<string name="error_rebuild_list">Δεν είναι δυνατή η σωστή αναδημιουργία της λίστας.</string>
|
||||||
|
<string name="error_database_uri_null">Δεν είναι δυνατή η ανάκτηση του URI βάσης δεδομένων.</string>
|
||||||
|
<string name="autofill_inline_suggestions_keyboard">Προστέθηκαν προτάσεις αυτόματης συμπλήρωσης.</string>
|
||||||
|
<string name="autofill_inline_suggestions_summary">Απόπειρα εμφάνισης προτάσεων αυτόματης συμπλήρωσης απευθείας από ένα συμβατό πληκτρολόγιο</string>
|
||||||
|
<string name="autofill_inline_suggestions_title">Προτάσεις στην ίδια γραμμή</string>
|
||||||
|
<string name="warning_database_revoked">Πρόσβαση στο αρχείο που ανακλήθηκε από το διαχειριστή αρχείων, κλείστε τη βάση δεδομένων και ανοίξτε το ξανά από τη θέση του.</string>
|
||||||
|
<string name="warning_database_info_changed_options">Αντικαταστήστε τις εξωτερικές τροποποιήσεις αποθηκεύοντας τη βάση δεδομένων ή φορτώστε την ξανά με τις πιο πρόσφατες αλλαγές.</string>
|
||||||
|
<string name="warning_database_info_changed">Οι πληροφορίες που περιέχονται στο αρχείο της βάσης δεδομένων σας έχουν τροποποιηθεί εκτός της εφαρμογής.</string>
|
||||||
|
<string name="menu_reload_database">Επαναφόρτωση βάσης δεδομένων</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -19,9 +19,9 @@
|
|||||||
|
|
||||||
Spanish translation by José I. Paños. Updated by David García-Abad (23-09-2013)
|
Spanish translation by José I. Paños. Updated by David García-Abad (23-09-2013)
|
||||||
--><resources>
|
--><resources>
|
||||||
<string name="feedback">Commentario</string>
|
<string name="feedback">Comentarios</string>
|
||||||
<string name="homepage">Página de inicio</string>
|
<string name="homepage">Página de inicio</string>
|
||||||
<string name="about_description">Implementación para Android del gestor de contraseñas KeePass</string>
|
<string name="about_description">Implementación en Android del gestor de contraseñas KeePass</string>
|
||||||
<string name="accept">Aceptar</string>
|
<string name="accept">Aceptar</string>
|
||||||
<string name="add_entry">Añadir entrada</string>
|
<string name="add_entry">Añadir entrada</string>
|
||||||
<string name="add_group">Añadir grupo</string>
|
<string name="add_group">Añadir grupo</string>
|
||||||
@@ -31,30 +31,30 @@
|
|||||||
<string name="application">Aplicación</string>
|
<string name="application">Aplicación</string>
|
||||||
<string name="menu_app_settings">Configuración de la aplicación</string>
|
<string name="menu_app_settings">Configuración de la aplicación</string>
|
||||||
<string name="brackets">Paréntesis</string>
|
<string name="brackets">Paréntesis</string>
|
||||||
<string name="file_manager_install_description">Explora ficheros con OpenIntents File Manager</string>
|
<string name="file_manager_install_description">Se requiere un administrador de archivos que acepte la acciones de intención ACTION_CREATE_DOCUMENT y ACTION_OPEN_DOCUMENT para crear, abrir y guardar los archivos de la base de datos.</string>
|
||||||
<string name="clipboard_cleared">Portapapeles limpiado</string>
|
<string name="clipboard_cleared">Portapapeles limpiado</string>
|
||||||
<string name="clipboard_timeout">Portapapeles caducado</string>
|
<string name="clipboard_timeout">Portapapeles caducado</string>
|
||||||
<string name="clipboard_timeout_summary">Duración del almacenamiento en el portapapeles (si lo admite el dispositivo)</string>
|
<string name="clipboard_timeout_summary">Duración del almacenamiento en el portapapeles (si lo admite su dispositivo)</string>
|
||||||
<string name="select_to_copy">Seleccionar para copiar %1$s en el portapapeles</string>
|
<string name="select_to_copy">Seleccionar para copiar %1$s en el portapapeles</string>
|
||||||
<string name="retrieving_db_key">Creando clave de la base de datos…</string>
|
<string name="retrieving_db_key">Recuperando la clave de la base de datos…</string>
|
||||||
<string name="database">Base de datos</string>
|
<string name="database">Base de datos</string>
|
||||||
<string name="decrypting_db">Descifrando el contenido de la base de datos…</string>
|
<string name="decrypting_db">Descifrando el contenido de la base de datos…</string>
|
||||||
<string name="default_checkbox">Utilice como base de datos por defecto</string>
|
<string name="default_checkbox">Utilizar como base de datos por defecto</string>
|
||||||
<string name="digits">Dígitos</string>
|
<string name="digits">Dígitos</string>
|
||||||
<string name="html_about_licence">KeePassDX © %1$d Kunzisoft es \u0020de <strong>código abierto</strong> y <strong>sin publicidad</strong>.
|
<string name="html_about_licence">KeePassDX © %1$d Kunzisoft es de <strong>código abierto</strong> y <strong>sin publicidad</strong>.
|
||||||
\nSe proporciona tal cual, bajo licencia <strong>GPLv3</strong>, sin ninguna garantía.</string>
|
\nSe proporciona tal cual, bajo licencia <strong>GPLv3</strong>, sin ninguna garantía.</string>
|
||||||
<string name="select_database_file">Abrir base de datos existente</string>
|
<string name="select_database_file">Abrir base de datos existente</string>
|
||||||
<string name="entry_accessed">Acceso</string>
|
<string name="entry_accessed">Accedido</string>
|
||||||
<string name="entry_cancel">Cancelar</string>
|
<string name="entry_cancel">Cancelar</string>
|
||||||
<string name="entry_notes">Notas</string>
|
<string name="entry_notes">Notas</string>
|
||||||
<string name="entry_confpassword">Confirmar contraseña</string>
|
<string name="entry_confpassword">Confirmar contraseña</string>
|
||||||
<string name="entry_created">Creación</string>
|
<string name="entry_created">Creado</string>
|
||||||
<string name="entry_expires">Caducidad</string>
|
<string name="entry_expires">Caduca</string>
|
||||||
<string name="entry_keyfile">Archivo de clave</string>
|
<string name="entry_keyfile">Archivo de clave</string>
|
||||||
<string name="entry_modified">Modificación</string>
|
<string name="entry_modified">Modificada</string>
|
||||||
<string name="entry_password">Contraseña</string>
|
<string name="entry_password">Contraseña</string>
|
||||||
<string name="save">Guardar</string>
|
<string name="save">Guardar</string>
|
||||||
<string name="entry_title">Nombre</string>
|
<string name="entry_title">Título</string>
|
||||||
<string name="entry_url">URL</string>
|
<string name="entry_url">URL</string>
|
||||||
<string name="entry_user_name">Nombre de usuario</string>
|
<string name="entry_user_name">Nombre de usuario</string>
|
||||||
<string name="error_arc4">No se admite el cifrador de flujo Arcfour.</string>
|
<string name="error_arc4">No se admite el cifrador de flujo Arcfour.</string>
|
||||||
@@ -64,11 +64,11 @@
|
|||||||
<string name="error_invalid_path">Asegúrese de que la ruta sea correcta.</string>
|
<string name="error_invalid_path">Asegúrese de que la ruta sea correcta.</string>
|
||||||
<string name="error_no_name">Proporcione un nombre.</string>
|
<string name="error_no_name">Proporcione un nombre.</string>
|
||||||
<string name="error_nokeyfile">Seleccione un archivo de clave.</string>
|
<string name="error_nokeyfile">Seleccione un archivo de clave.</string>
|
||||||
<string name="error_out_of_memory">No queda memoria para cargar toda la base de datos.</string>
|
<string name="error_out_of_memory">No hay memoria para cargar toda la base de datos.</string>
|
||||||
<string name="error_pass_gen_type">Debe seleccionar al menos un tipo de generación de contraseñas.</string>
|
<string name="error_pass_gen_type">Debe seleccionar al menos un tipo de generación de contraseñas.</string>
|
||||||
<string name="error_pass_match">Las contraseñas no coinciden.</string>
|
<string name="error_pass_match">Las contraseñas no coinciden.</string>
|
||||||
<string name="error_rounds_too_large">Pasadas demasiado grande. Establecido a 2147483648.</string>
|
<string name="error_rounds_too_large">Pasadas demasiado grande. Establecido a 2147483648.</string>
|
||||||
<string name="error_wrong_length">Proporcione un número entero positivo en el campo «Longitud».</string>
|
<string name="error_wrong_length">Introduzca un número entero positivo en el campo \"Longitud\".</string>
|
||||||
<string name="file_browser">Explorador de archivos</string>
|
<string name="file_browser">Explorador de archivos</string>
|
||||||
<string name="generate_password">Generar contraseña</string>
|
<string name="generate_password">Generar contraseña</string>
|
||||||
<string name="hint_conf_pass">Confirmar contraseña</string>
|
<string name="hint_conf_pass">Confirmar contraseña</string>
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
<string name="hint_length">Longitud</string>
|
<string name="hint_length">Longitud</string>
|
||||||
<string name="password">Contraseña</string>
|
<string name="password">Contraseña</string>
|
||||||
<string name="hint_pass">Contraseña</string>
|
<string name="hint_pass">Contraseña</string>
|
||||||
<string name="invalid_credentials">Contraseña o archivo de clave no válido.</string>
|
<string name="invalid_credentials">No se pudieron leer las credenciales.</string>
|
||||||
<string name="invalid_db_sig">No se pudo reconocer el formato de la base de datos.</string>
|
<string name="invalid_db_sig">No se pudo reconocer el formato de la base de datos.</string>
|
||||||
<string name="length">Longitud</string>
|
<string name="length">Longitud</string>
|
||||||
<string name="list_size_title">Tamaño de los elementos de la lista</string>
|
<string name="list_size_title">Tamaño de los elementos de la lista</string>
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
<string name="minus">Menos</string>
|
<string name="minus">Menos</string>
|
||||||
<string name="never">Nunca</string>
|
<string name="never">Nunca</string>
|
||||||
<string name="no_results">Sin resultado de búsqueda</string>
|
<string name="no_results">Sin resultado de búsqueda</string>
|
||||||
<string name="no_url_handler">Instale un navegador web para abrir este URL.</string>
|
<string name="no_url_handler">Instale un navegador web para abrir esta URL.</string>
|
||||||
<string name="omit_backup_search_title">No buscar en las entradas de respaldo</string>
|
<string name="omit_backup_search_title">No buscar en las entradas de respaldo</string>
|
||||||
<string name="omit_backup_search_summary">Omite los grupos «Respaldo» y «Papelera de reciclaje» de los resultados de búsqueda</string>
|
<string name="omit_backup_search_summary">Omite los grupos «Respaldo» y «Papelera de reciclaje» de los resultados de búsqueda</string>
|
||||||
<string name="progress_create">Creando nueva base de datos…</string>
|
<string name="progress_create">Creando nueva base de datos…</string>
|
||||||
@@ -124,7 +124,9 @@
|
|||||||
<string name="unsupported_db_version">No se admite esta versión de la base de datos.</string>
|
<string name="unsupported_db_version">No se admite esta versión de la base de datos.</string>
|
||||||
<string name="uppercase">Mayúsculas</string>
|
<string name="uppercase">Mayúsculas</string>
|
||||||
<string name="version_label">Versión %1$s</string>
|
<string name="version_label">Versión %1$s</string>
|
||||||
<string name="education_unlock_summary">Introduzca una contraseña y/o un archivo de clave para desbloquear su base de datos.</string>
|
<string name="education_unlock_summary">Introduzca la contraseña y/o el archivo clave para desbloquear su base de datos.
|
||||||
|
\n
|
||||||
|
\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">
|
<string-array name="timeout_options">
|
||||||
<item>5 segundos</item>
|
<item>5 segundos</item>
|
||||||
<item>10 segundos</item>
|
<item>10 segundos</item>
|
||||||
@@ -146,28 +148,28 @@
|
|||||||
<string name="extended_ASCII">ASCII extendido</string>
|
<string name="extended_ASCII">ASCII extendido</string>
|
||||||
<string name="allow">Permitir</string>
|
<string name="allow">Permitir</string>
|
||||||
<string name="clipboard_error_title">Error del portapapeles</string>
|
<string name="clipboard_error_title">Error del portapapeles</string>
|
||||||
<string name="clipboard_error">Algunos dispositivos Android tienen un error en la implementación del portapapeles que provoca fallos al copiar desde las aplicaciones.</string>
|
<string name="clipboard_error">Algunos dispositivos no permiten que las aplicaciones usen el portapapeles.</string>
|
||||||
<string name="clipboard_error_clear">Falló la limpieza del portapapeles</string>
|
<string name="clipboard_error_clear">No se pudo limpiar el portapapeles</string>
|
||||||
<string name="error_string_key">Cada cadena debe tener un nombre de campo.</string>
|
<string name="error_string_key">Cada cadena debe tener un nombre de campo.</string>
|
||||||
<string name="error_autofill_enable_service">El servicio de autocompletado no se ha podido habilitar.</string>
|
<string name="error_autofill_enable_service">El servicio de autocompletado no se ha podido habilitar.</string>
|
||||||
<string name="field_name">Nombre del campo</string>
|
<string name="field_name">Nombre del campo</string>
|
||||||
<string name="field_value">Valor del campo</string>
|
<string name="field_value">Valor del campo</string>
|
||||||
<string name="file_not_found_content">No se pudo encontrar el archivo. Intente volver a abrirlo en el explorador de archivos.</string>
|
<string name="file_not_found_content">No se pudo encontrar el archivo. Intente volver a abrirlo en el explorador de archivos.</string>
|
||||||
<string name="invalid_algorithm">El algoritmo es incorrecto.</string>
|
<string name="invalid_algorithm">Algoritmo incorrecto.</string>
|
||||||
<string name="keyfile_is_empty">El archivo de clave está vacío.</string>
|
<string name="keyfile_is_empty">El archivo de clave está vacío.</string>
|
||||||
<string name="copy_field">Copia de %1$s</string>
|
<string name="copy_field">Copia de %1$s</string>
|
||||||
<string name="menu_form_filling_settings">Llenado de formulario</string>
|
<string name="menu_form_filling_settings">Llenado de formulario</string>
|
||||||
<string name="protection">Protección</string>
|
<string name="protection">Protección</string>
|
||||||
<string name="read_only">Protegida contra escritura</string>
|
<string name="read_only">Protegida contra escritura</string>
|
||||||
<string name="read_only_warning">KeePassDX necesita permiso de escritura para modificar la base de datos.</string>
|
<string name="read_only_warning">Dependiendo del administrador de archivos, puede que KeePassDX no permita escribir en su almacenamiento.</string>
|
||||||
<string name="encryption_explanation">Algoritmo de cifrado utilizado en todos los datos.</string>
|
<string name="encryption_explanation">Algoritmo de cifrado utilizado para todos los datos.</string>
|
||||||
<string name="kdf_explanation">Para generar la clave del algoritmo de cifrado, la clave maestra se transforma mediante una función de derivación de claves con una sal aleatoria.</string>
|
<string name="kdf_explanation">Para generar la clave del algoritmo de cifrado, la clave maestra se transforma mediante una función de derivación de claves con una sal aleatoria.</string>
|
||||||
<string name="memory_usage">Uso de memoria</string>
|
<string name="memory_usage">Uso de memoria</string>
|
||||||
<string name="memory_usage_explanation">Cantidad de memoria (en bytes) que usará la función de derivación de clave.</string>
|
<string name="memory_usage_explanation">Cantidad de memoria que usará la función de derivación de clave.</string>
|
||||||
<string name="parallelism">Paralelismo</string>
|
<string name="parallelism">Paralelismo</string>
|
||||||
<string name="parallelism_explanation">Grado de paralelismo (p. ej. número de hilos) usados por la función de derivación de clave.</string>
|
<string name="parallelism_explanation">Grado de paralelismo (p. ej. número de hilos) usados por la función de derivación de clave.</string>
|
||||||
<string name="sort_menu">Ordenar</string>
|
<string name="sort_menu">Ordenar</string>
|
||||||
<string name="sort_ascending">Inferiores primero ↓</string>
|
<string name="sort_ascending">Más bajo primero ↓</string>
|
||||||
<string name="sort_groups_before">Agrupar antes</string>
|
<string name="sort_groups_before">Agrupar antes</string>
|
||||||
<string name="sort_recycle_bin_bottom">Papelera debajo</string>
|
<string name="sort_recycle_bin_bottom">Papelera debajo</string>
|
||||||
<string name="sort_title">Título</string>
|
<string name="sort_title">Título</string>
|
||||||
@@ -177,8 +179,8 @@
|
|||||||
<string name="sort_last_access_time">Acceso</string>
|
<string name="sort_last_access_time">Acceso</string>
|
||||||
<string name="search_results">Resultados de búsqueda</string>
|
<string name="search_results">Resultados de búsqueda</string>
|
||||||
<string name="warning">Atención</string>
|
<string name="warning">Atención</string>
|
||||||
<string name="entry_not_found">Datos de entrada no encontrados.</string>
|
<string name="entry_not_found">No se pudieron encontrar los datos de entrada.</string>
|
||||||
<string name="warning_password_encoding">Evite emplear en la base de datos caracteres que no pertenezcan al formato de codificación del texto (los caracteres no reconocidos se convierten a la misma letra).</string>
|
<string name="warning_password_encoding">Evite los caracteres de la contraseña fuera del formato de codificación de texto en el archivo de la base de datos (los caracteres no reconocidos se convierten a la misma letra).</string>
|
||||||
<string name="warning_empty_password">¿Continuar sin la protección de desbloqueo de contraseña\?</string>
|
<string name="warning_empty_password">¿Continuar sin la protección de desbloqueo de contraseña\?</string>
|
||||||
<string name="warning_no_encryption_key">¿Continuar sin clave de cifrado\?</string>
|
<string name="warning_no_encryption_key">¿Continuar sin clave de cifrado\?</string>
|
||||||
<string name="encrypted_value_stored">Contraseña cifrada almacenada</string>
|
<string name="encrypted_value_stored">Contraseña cifrada almacenada</string>
|
||||||
@@ -197,25 +199,25 @@
|
|||||||
<string name="list_password_generator_options_summary">Establecer los caracteres permitidos del generador de contraseñas</string>
|
<string name="list_password_generator_options_summary">Establecer los caracteres permitidos del generador de contraseñas</string>
|
||||||
<string name="clipboard">Portapapeles</string>
|
<string name="clipboard">Portapapeles</string>
|
||||||
<string name="clipboard_notifications_title">Notificaciones del portapapeles</string>
|
<string name="clipboard_notifications_title">Notificaciones del portapapeles</string>
|
||||||
<string name="clipboard_notifications_summary">Mostrar notificaciones del portapapeles para copiar campos al examinar una entrada</string>
|
<string name="clipboard_notifications_summary">Mostrar las notificaciones del portapapeles para copiar campos al examinar una entrada</string>
|
||||||
<string name="lock">Bloquear</string>
|
<string name="lock">Bloquear</string>
|
||||||
<string name="lock_database_screen_off_title">Bloqueo de pantalla</string>
|
<string name="lock_database_screen_off_title">Bloqueo de pantalla</string>
|
||||||
<string name="lock_database_screen_off_summary">Bloquear la base de datos cuando la pantalla esté apagada</string>
|
<string name="lock_database_screen_off_summary">Bloquear la base de datos cuando la pantalla esté apagada</string>
|
||||||
<string name="advanced_unlock">Desbloqueo avanzado</string>
|
<string name="advanced_unlock">Desbloqueo avanzado</string>
|
||||||
<string name="biometric_unlock_enable_title">Desbloqueo biométrico</string>
|
<string name="biometric_unlock_enable_title">Desbloqueo biométrico</string>
|
||||||
<string name="biometric_unlock_enable_summary">Le permite escanear su huella dactilar u otro dato biométrico para abrir la base de datos</string>
|
<string name="biometric_unlock_enable_summary">Le permite escanear sus datos biométricos para abrir la base de datos</string>
|
||||||
<string name="biometric_delete_all_key_title">Eliminar claves de cifrado</string>
|
<string name="biometric_delete_all_key_title">Eliminar claves de cifrado</string>
|
||||||
<string name="biometric_delete_all_key_summary">Eliminar todas las claves de cifrado relacionadas con el reconocimiento biométrico</string>
|
<string name="biometric_delete_all_key_summary">Eliminar todas las claves de cifrado relacionadas con el reconocimiento de desbloqueo avanzado</string>
|
||||||
<string name="unavailable_feature_text">No se pudo iniciar esta funcionalidad.</string>
|
<string name="unavailable_feature_text">No se pudo iniciar esta funcionalidad.</string>
|
||||||
<string name="unavailable_feature_version">La versión de Android del dispositivo es %1$s, pero es necesaria la %2$s o posterior.</string>
|
<string name="unavailable_feature_version">El dispositivo funciona con Android %1$s, pero necesita %2$s o posterior.</string>
|
||||||
<string name="unavailable_feature_hardware">No se encontró el hardware correspondiente.</string>
|
<string name="unavailable_feature_hardware">No se pudo encontrar el hardware correspondiente.</string>
|
||||||
<string name="file_name">Nombre del archivo</string>
|
<string name="file_name">Nombre del archivo</string>
|
||||||
<string name="path">Ruta</string>
|
<string name="path">Ruta</string>
|
||||||
<string name="assign_master_key">Asignar una clave maestra</string>
|
<string name="assign_master_key">Asignar una clave maestra</string>
|
||||||
<string name="create_keepass_file">Crear base de datos nueva</string>
|
<string name="create_keepass_file">Crear una base de datos nueva</string>
|
||||||
<string name="recycle_bin_title">Uso de la papelera de reciclaje</string>
|
<string name="recycle_bin_title">Uso de la papelera de reciclaje</string>
|
||||||
<string name="recycle_bin_summary">Mueve los grupos y las entradas al grupo \"Papelera de reciclaje\" antes de eliminarlos</string>
|
<string name="recycle_bin_summary">Mueve los grupos y las entradas al grupo \"Papelera de reciclaje\" antes de eliminarlos</string>
|
||||||
<string name="monospace_font_fields_enable_title">Fuente de los campos</string>
|
<string name="monospace_font_fields_enable_title">Tipografía del campo</string>
|
||||||
<string name="monospace_font_fields_enable_summary">Cambiar la fuente de los campos para una mejor visibilidad del carácter</string>
|
<string name="monospace_font_fields_enable_summary">Cambiar la fuente de los campos para una mejor visibilidad del carácter</string>
|
||||||
<string name="allow_copy_password_title">Portapapeles de confianza</string>
|
<string name="allow_copy_password_title">Portapapeles de confianza</string>
|
||||||
<string name="allow_copy_password_summary">Permitir copiar la contraseña de entrada y los campos protegidos al portapapeles</string>
|
<string name="allow_copy_password_summary">Permitir copiar la contraseña de entrada y los campos protegidos al portapapeles</string>
|
||||||
@@ -223,11 +225,11 @@
|
|||||||
<string name="database_description_title">Descripción de la base de datos</string>
|
<string name="database_description_title">Descripción de la base de datos</string>
|
||||||
<string name="database_version_title">Versión de la base de datos</string>
|
<string name="database_version_title">Versión de la base de datos</string>
|
||||||
<string name="text_appearance">Texto</string>
|
<string name="text_appearance">Texto</string>
|
||||||
<string name="application_appearance">Aplicación</string>
|
<string name="application_appearance">Interfaz</string>
|
||||||
<string name="other">Otro</string>
|
<string name="other">Otro</string>
|
||||||
<string name="keyboard">Teclado</string>
|
<string name="keyboard">Teclado</string>
|
||||||
<string name="magic_keyboard_title">Teclado mágico</string>
|
<string name="magic_keyboard_title">Teclado mágico</string>
|
||||||
<string name="magic_keyboard_explanation_summary">Active un teclado personalizado que llene sus contraseñas y todos los campos de identidad fácilmente.</string>
|
<string name="magic_keyboard_explanation_summary">Active un teclado personalizado que llene sus contraseñas y todos los campos de identidad fácilmente</string>
|
||||||
<string name="reset_education_screens_title">Restablecer sugerencias didácticas</string>
|
<string name="reset_education_screens_title">Restablecer sugerencias didácticas</string>
|
||||||
<string name="reset_education_screens_summary">Mostrar de nuevo toda la información didáctica</string>
|
<string name="reset_education_screens_summary">Mostrar de nuevo toda la información didáctica</string>
|
||||||
<string name="reset_education_screens_text">Se restablecieron las sugerencias didácticas</string>
|
<string name="reset_education_screens_text">Se restablecieron las sugerencias didácticas</string>
|
||||||
@@ -259,28 +261,28 @@
|
|||||||
<string name="education_donation_title">Participar</string>
|
<string name="education_donation_title">Participar</string>
|
||||||
<string name="education_donation_summary">Participe para aumentar la estabilidad, la seguridad y agregar más funciones.</string>
|
<string name="education_donation_summary">Participe para aumentar la estabilidad, la seguridad y agregar más funciones.</string>
|
||||||
<string name="html_text_ad_free">A diferencia de muchas aplicaciones de administración de contraseñas, esta es <strong>sin publicidad</strong>, <strong>software libre con copyleft</strong> y no recopila datos personales en sus servidores, sin importar la versión que use.</string>
|
<string name="html_text_ad_free">A diferencia de muchas aplicaciones de administración de contraseñas, esta es <strong>sin publicidad</strong>, <strong>software libre con copyleft</strong> y no recopila datos personales en sus servidores, sin importar la versión que use.</string>
|
||||||
<string name="html_text_buy_pro">Al comprar la versión pro, tendrá acceso a <strong>la característica visual </strong>y usted ayudará especialmente a <strong>la realización de proyectos comunitarios.</strong></string>
|
<string name="html_text_buy_pro">Al comprar la versión pro, tendrá acceso al <strong>estilo visual </strong>y ayudará especialmente a <strong>la realización de proyectos comunitarios.</strong></string>
|
||||||
<string name="html_text_feature_generosity">Esta <strong>característica visual </strong>está disponible gracias a tu generosidad.</string>
|
<string name="html_text_feature_generosity">Este <strong>estilo visual </strong>está disponible gracias a tu generosidad.</string>
|
||||||
<string name="html_text_donation">Para mantener nuestra libertad y estar siempre vigente, contamos con tu <strong>contribución.</strong></string>
|
<string name="html_text_donation">Para mantener nuestra libertad y estar siempre vigente, contamos con tu <strong>contribución.</strong></string>
|
||||||
<string name="html_text_dev_feature">Esta función está <strong>en desarrollo</strong> y requiere de tu <strong>contribución</strong> para estar disponible dentro de poco.</string>
|
<string name="html_text_dev_feature">Esta función está <strong>en desarrollo</strong> y requiere de tu <strong>contribución</strong> para estar disponible dentro de poco.</string>
|
||||||
<string name="html_text_dev_feature_buy_pro">Al comprar la versión <strong>pro</strong>,</string>
|
<string name="html_text_dev_feature_buy_pro">Al comprar la versión <strong>pro</strong>,</string>
|
||||||
<string name="html_text_dev_feature_contibute">Al <strong>contribuir</strong>,</string>
|
<string name="html_text_dev_feature_contibute">Al <strong>contribuir</strong>,</string>
|
||||||
<string name="html_text_dev_feature_encourage">usted alienta a los desarrolladores a crear <strong>nuevas funciones</strong> y a <strong>errores de configuración</strong> de acuerdo con tus comentarios.</string>
|
<string name="html_text_dev_feature_encourage">anima a los desarrolladores a crear <strong>nuevas funciones</strong> y a <strong>corregir errores</strong> de acuerdo con sus comentarios.</string>
|
||||||
<string name="html_text_dev_feature_thanks">Muchas gracias por tu contribución.</string>
|
<string name="html_text_dev_feature_thanks">Muchas gracias por tu contribución.</string>
|
||||||
<string name="html_text_dev_feature_work_hard">Estamos trabajando duro para lanzar esta característica rápidamente.</string>
|
<string name="html_text_dev_feature_work_hard">Estamos trabajando duro para lanzar esta característica rápidamente.</string>
|
||||||
<string name="html_text_dev_feature_upgrade">No olvide mantener su aplicación actualizada.</string>
|
<string name="html_text_dev_feature_upgrade">Recuerde mantener su aplicación actualizada instalando nuevas versiones.</string>
|
||||||
<string name="download">Descargar</string>
|
<string name="download">Descargar</string>
|
||||||
<string name="contribute">Contribuir</string>
|
<string name="contribute">Contribuir</string>
|
||||||
<string name="encryption_chacha20">ChaCha20</string>
|
<string name="encryption_chacha20">ChaCha20</string>
|
||||||
<string name="kdf_AES">AES-KDF</string>
|
<string name="kdf_AES">AES</string>
|
||||||
<string name="style_choose_title">Tema de aplicación</string>
|
<string name="style_choose_title">Tema de la aplicación</string>
|
||||||
<string name="style_choose_summary">Tema utilizado en la aplicación</string>
|
<string name="style_choose_summary">Tema utilizado en la aplicación</string>
|
||||||
<string name="icon_pack_choose_title">Seleccione un paquete de iconos</string>
|
<string name="icon_pack_choose_title">Seleccione un paquete de iconos</string>
|
||||||
<string name="icon_pack_choose_summary">Cambiar el paquete de iconos en la aplicación</string>
|
<string name="icon_pack_choose_summary">Cambiar el paquete de iconos en la aplicación</string>
|
||||||
<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 se puede mover un grupo dentro de sí mismo.</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>
|
||||||
@@ -291,13 +293,13 @@
|
|||||||
<string name="menu_open_file_read_and_write">Modificable</string>
|
<string name="menu_open_file_read_and_write">Modificable</string>
|
||||||
<string name="build_label">Compilación %1$s</string>
|
<string name="build_label">Compilación %1$s</string>
|
||||||
<string name="clipboard_warning">Si la eliminación del cortapapeles falla, elimine su historial manualmente.</string>
|
<string name="clipboard_warning">Si la eliminación del cortapapeles falla, elimine su historial manualmente.</string>
|
||||||
<string name="allow_copy_password_warning">ATENCIÓN: todas las aplicaciones comparten el portapapeles. Si copia datos confidenciales, otros programas podrían recuperarlos.</string>
|
<string name="allow_copy_password_warning">Advertencia: El portapapeles es compartido por todas las aplicaciones. Si se copian datos sensibles, otros programas pueden recuperarlos.</string>
|
||||||
<string name="allow_no_password_title">No permitir claves maestras</string>
|
<string name="allow_no_password_title">No permitir claves maestras</string>
|
||||||
<string name="allow_no_password_summary">Activar el botón «Abrir» si no se selecciona ninguna credencial</string>
|
<string name="allow_no_password_summary">Permite pulsar el botón \"Abrir\" si no se seleccionan credenciales</string>
|
||||||
<string name="enable_education_screens_title">Pantallas didácticas</string>
|
<string name="enable_education_screens_title">Sugerencias educativas</string>
|
||||||
<string name="enable_education_screens_summary">Resaltar los elementos para conocer cómo funciona la aplicación</string>
|
<string name="enable_education_screens_summary">Destacar elementos para aprender cómo funciona la aplicación</string>
|
||||||
<string name="enable_read_only_title">Protegida contra escritura</string>
|
<string name="enable_read_only_title">Protegida contra escritura</string>
|
||||||
<string name="enable_read_only_summary">Abrir su base de datos protegida contra escritura de manera predeterminada</string>
|
<string name="enable_read_only_summary">Abrir la base de datos de solo lectura por defecto</string>
|
||||||
<string name="education_read_only_title">Proteja la base de datos contra escritura</string>
|
<string name="education_read_only_title">Proteja la base de datos contra escritura</string>
|
||||||
<string name="keyboard_name">Teclado mágico</string>
|
<string name="keyboard_name">Teclado mágico</string>
|
||||||
<string name="keyboard_label">Teclado mágico (KeePassDX)</string>
|
<string name="keyboard_label">Teclado mágico (KeePassDX)</string>
|
||||||
@@ -315,56 +317,56 @@
|
|||||||
<string name="keyboard_appearance_category">Apariencia</string>
|
<string name="keyboard_appearance_category">Apariencia</string>
|
||||||
<string name="keyboard_theme_title">Tema del teclado</string>
|
<string name="keyboard_theme_title">Tema del teclado</string>
|
||||||
<string name="keyboard_keys_category">Teclas</string>
|
<string name="keyboard_keys_category">Teclas</string>
|
||||||
<string name="keyboard_key_vibrate_title">Vibrar al pulsar</string>
|
<string name="keyboard_key_vibrate_title">Vibrar al pulsar tecla</string>
|
||||||
<string name="keyboard_key_sound_title">Emitir sonido al pulsar</string>
|
<string name="keyboard_key_sound_title">Sonar al pulsar tecla</string>
|
||||||
<string name="selection_mode">Modo de selección</string>
|
<string name="selection_mode">Modo de selección</string>
|
||||||
<string name="do_not_kill_app">No cierre la aplicación…</string>
|
<string name="do_not_kill_app">No cierre la aplicación…</string>
|
||||||
<string name="lock_database_back_root_summary">Bloquear la base de datos cuando se pulsa el botón Atrás en la pantalla inicial</string>
|
<string name="lock_database_back_root_summary">Bloquear la base de datos cuando el usuario pulse el botón atrás en la pantalla inicial</string>
|
||||||
<string name="clear_clipboard_notification_title">Vaciar al cerrar</string>
|
<string name="clear_clipboard_notification_title">Vaciar al cerrar</string>
|
||||||
<string name="clear_clipboard_notification_summary">Bloquear la base de datos cuando la duración del portapapeles expire o la notificación sea cerrada</string>
|
<string name="clear_clipboard_notification_summary">Bloquear la base de datos cuando expire la duración del portapapeles o cuando se cierre la notificación después de empezar a utilizarla</string>
|
||||||
<string name="recycle_bin">Papelera de reciclaje</string>
|
<string name="recycle_bin">Papelera de reciclaje</string>
|
||||||
<string name="keyboard_selection_entry_title">Selección de entrada</string>
|
<string name="keyboard_selection_entry_title">Selección de entrada</string>
|
||||||
<string name="keyboard_selection_entry_summary">Mostrar campos de entrada en el Teclado mágico al ver una entrada</string>
|
<string name="keyboard_selection_entry_summary">Mostrar campos de entrada en el Teclado mágico al ver una entrada</string>
|
||||||
<string name="delete_entered_password_title">Eliminar contraseña</string>
|
<string name="delete_entered_password_title">Eliminar contraseña</string>
|
||||||
<string name="delete_entered_password_summary">Elimina la contraseña proporcionada tras un intento de conexión</string>
|
<string name="delete_entered_password_summary">Elimina la contraseña introducida tras un intento de conexión a una bse de datos</string>
|
||||||
<string name="content_description_open_file">Abrir archivo</string>
|
<string name="content_description_open_file">Abrir archivo</string>
|
||||||
<string name="content_description_node_children">Elementos secundarios del nodo</string>
|
<string name="content_description_node_children">Nodos hijo</string>
|
||||||
<string name="content_description_add_node">Añadir nodo</string>
|
<string name="content_description_add_node">Añadir nodo</string>
|
||||||
<string name="content_description_add_entry">Añadir entrada</string>
|
<string name="content_description_add_entry">Añadir entrada</string>
|
||||||
<string name="content_description_add_group">Añadir grupo</string>
|
<string name="content_description_add_group">Añadir grupo</string>
|
||||||
<string name="content_description_file_information">Información de archivo</string>
|
<string name="content_description_file_information">Información del archivo</string>
|
||||||
<string name="content_description_password_checkbox">Casilla de contraseña</string>
|
<string name="content_description_password_checkbox">Casilla de verificación de la contraseña</string>
|
||||||
<string name="content_description_keyfile_checkbox">Casilla de archivo de clave</string>
|
<string name="content_description_keyfile_checkbox">Casilla de verificación del archivo de clave</string>
|
||||||
<string name="content_description_entry_icon">Icono de entrada</string>
|
<string name="content_description_entry_icon">Icono de entrada</string>
|
||||||
<string name="entry_password_generator">Generador de contraseñas</string>
|
<string name="entry_password_generator">Generador de contraseñas</string>
|
||||||
<string name="content_description_password_length">Longitud de contraseña</string>
|
<string name="content_description_password_length">Longitud de contraseña</string>
|
||||||
<string name="entry_add_field">Añadir campo</string>
|
<string name="entry_add_field">Añadir campo</string>
|
||||||
<string name="content_description_remove_field">Eliminar campo</string>
|
<string name="content_description_remove_field">Eliminar el campo</string>
|
||||||
<string name="entry_UUID">UUID</string>
|
<string name="entry_UUID">UUID</string>
|
||||||
<string name="error_move_entry_here">No se puede mover una entrada aquí.</string>
|
<string name="error_move_entry_here">No puede mover una entrada aquí.</string>
|
||||||
<string name="error_copy_entry_here">No se puede copiar una entrada aquí.</string>
|
<string name="error_copy_entry_here">No puede copiar una entrada aquí.</string>
|
||||||
<string name="list_groups_show_number_entries_title">Mostrar cantidad de entradas</string>
|
<string name="list_groups_show_number_entries_title">Mostrar el número de entradas</string>
|
||||||
<string name="list_groups_show_number_entries_summary">Mostrar la cantidad de entradas en un grupo</string>
|
<string name="list_groups_show_number_entries_summary">Mostrar el número de entradas en un grupo</string>
|
||||||
<string name="content_description_background">Fondo</string>
|
<string name="content_description_background">Fondo</string>
|
||||||
<string name="content_description_update_from_list">Actualizar</string>
|
<string name="content_description_update_from_list">Actualizar</string>
|
||||||
<string name="content_description_keyboard_close_fields">Cerrar campos</string>
|
<string name="content_description_keyboard_close_fields">Cerrar campos</string>
|
||||||
<string name="error_create_database_file">No se puede crear la base de datos con esta contraseña y este archivo de clave.</string>
|
<string name="error_create_database_file">No se puede crear la base de datos con esta contraseña y este archivo de clave.</string>
|
||||||
<string name="menu_advanced_unlock_settings">Desbloqueo avanzado</string>
|
<string name="menu_advanced_unlock_settings">Desbloqueo avanzado</string>
|
||||||
<string name="biometric">Biometría</string>
|
<string name="biometric">Biometría</string>
|
||||||
<string name="biometric_auto_open_prompt_title">Abrir petición de datos biométricos automáticamente</string>
|
<string name="biometric_auto_open_prompt_title">Abrir petición automáticamente</string>
|
||||||
<string name="biometric_auto_open_prompt_summary">Abrir automáticamente la petición de datos biométricos cuando se define una clave biométrica para una base de datos</string>
|
<string name="biometric_auto_open_prompt_summary">Solicitar automáticamente el desbloqueo avanzado si la base de datos está configurada para utilizarlo</string>
|
||||||
<string name="enable">Activar</string>
|
<string name="enable">Activar</string>
|
||||||
<string name="disable">Desactivar</string>
|
<string name="disable">Desactivar</string>
|
||||||
<string name="education_read_only_summary">Cambiar el modo de apertura de la sesión.
|
<string name="education_read_only_summary">Cambiar el modo de apertura de la sesión.
|
||||||
\n
|
\n
|
||||||
\n\"Protegido contra escritura\" evita cambios no deseados en la base de datos.
|
\n\"Protegido contra escritura\" evita cambios no deseados en la base de datos.
|
||||||
\n\"Modificable\" le permite agregar, eliminar o modificar todos los elementos.</string>
|
\n\"Modificable\" le permite agregar, eliminar o modificar todos los elementos como desee.</string>
|
||||||
<string name="lock_database_back_root_title">Presione hacia atrás en la raíz para bloquear</string>
|
<string name="lock_database_back_root_title">Presione \'Atrás\' para bloquear</string>
|
||||||
<string name="content_description_repeat_toggle_password_visibility">Repetir la visibilidad de la contraseña</string>
|
<string name="content_description_repeat_toggle_password_visibility">Repetir la visibilidad de la contraseña</string>
|
||||||
<string name="master_key">Llave maestra</string>
|
<string name="master_key">Clave maestra</string>
|
||||||
<string name="security">Seguridad</string>
|
<string name="security">Seguridad</string>
|
||||||
<string name="entry_history">Historial</string>
|
<string name="entry_history">Historial</string>
|
||||||
<string name="entry_setup_otp">Configuración de contraseña de un solo uso</string>
|
<string name="entry_setup_otp">Establecer una contraseña de un solo uso</string>
|
||||||
<string name="otp_type">Tipo de contraseña de un solo uso</string>
|
<string name="otp_type">Tipo de contraseña de un solo uso</string>
|
||||||
<string name="otp_secret">Secreto</string>
|
<string name="otp_secret">Secreto</string>
|
||||||
<string name="otp_period">Período (segundos)</string>
|
<string name="otp_period">Período (segundos)</string>
|
||||||
@@ -377,16 +379,16 @@
|
|||||||
<string name="error_otp_secret_key">Clave secreta debe estar en formato Base32.</string>
|
<string name="error_otp_secret_key">Clave secreta debe estar en formato Base32.</string>
|
||||||
<string name="error_otp_counter">Contador debe estar entre %1$d y %2$d.</string>
|
<string name="error_otp_counter">Contador debe estar entre %1$d y %2$d.</string>
|
||||||
<string name="error_save_database">No se puede guardar la base de datos.</string>
|
<string name="error_save_database">No se puede guardar la base de datos.</string>
|
||||||
<string name="error_string_type">Este texto no encaja con la información requerida.</string>
|
<string name="error_string_type">Este texto no coincide con el elemento requerido.</string>
|
||||||
<string name="error_create_database">No fue posible crear el archivo de base de datos.</string>
|
<string name="error_create_database">No fue posible crear el archivo de base de datos.</string>
|
||||||
<string name="html_about_contribution">Parar lograr <strong>mantener nuestra libertad</strong>, <strong>corregir errores</strong>, <strong>agregar características</strong> y <strong>siempre estar activos</strong>, contamos con tu <strong>contribución</strong>.</string>
|
<string name="html_about_contribution">Parar lograr <strong>mantener nuestra libertad</strong>, <strong>corregir errores</strong>, <strong>agregar características</strong> y <strong>estar siempre activos</strong>, contamos con tu <strong>contribución</strong>.</string>
|
||||||
<string name="content_description_add_item">Añadir elemento</string>
|
<string name="content_description_add_item">Añadir elemento</string>
|
||||||
<string name="download_complete">Descarga completa!</string>
|
<string name="download_complete">¡Completado!</string>
|
||||||
<string name="download_finalization">Finalizando…</string>
|
<string name="download_finalization">Finalizando…</string>
|
||||||
<string name="download_progression">En progreso: %1$d%%</string>
|
<string name="download_progression">En progreso: %1$d%%</string>
|
||||||
<string name="download_initialization">Inicializando…</string>
|
<string name="download_initialization">Inicializando…</string>
|
||||||
<string name="download_attachment">Descargar %1$s</string>
|
<string name="download_attachment">Descargar %1$s</string>
|
||||||
<string name="enable_auto_save_database_summary">Guardar la base de datos tras acciones importantes (en modo \"Modificable\")</string>
|
<string name="enable_auto_save_database_summary">Guardar la base de datos después de cada acción importante (en modo \"Modificable\")</string>
|
||||||
<string name="enable_auto_save_database_title">Guardar base de datos automáticamente</string>
|
<string name="enable_auto_save_database_title">Guardar base de datos automáticamente</string>
|
||||||
<string name="autofill_block">Bloquear autocompletado</string>
|
<string name="autofill_block">Bloquear autocompletado</string>
|
||||||
<string name="keyboard_change">Cambiar teclado</string>
|
<string name="keyboard_change">Cambiar teclado</string>
|
||||||
@@ -395,12 +397,12 @@
|
|||||||
<string name="compression_none">Ninguna</string>
|
<string name="compression_none">Ninguna</string>
|
||||||
<string name="compression">Compresión</string>
|
<string name="compression">Compresión</string>
|
||||||
<string name="database_default_username_title">Nombre de usuario predeterminado</string>
|
<string name="database_default_username_title">Nombre de usuario predeterminado</string>
|
||||||
<string name="settings_database_force_changing_master_key_next_time_summary">Requerir cambiar la contraseña maestra la próxima vez (una sola vez)</string>
|
<string name="settings_database_force_changing_master_key_next_time_summary">Requerir cambiar la contraseña maestra la próxima vez (una vez)</string>
|
||||||
<string name="settings_database_force_changing_master_key_next_time_title">Forzar renovación la próxima vez</string>
|
<string name="settings_database_force_changing_master_key_next_time_title">Forzar renovación la próxima vez</string>
|
||||||
<string name="settings_database_force_changing_master_key_summary">Requerir un cambio de contraseña maestra (días)</string>
|
<string name="settings_database_force_changing_master_key_summary">Requerir un cambio de la contraseña maestra (días)</string>
|
||||||
<string name="settings_database_force_changing_master_key_title">Forzar renovación</string>
|
<string name="settings_database_force_changing_master_key_title">Forzar renovación</string>
|
||||||
<string name="max_history_size_title">Tamaño máximo</string>
|
<string name="max_history_size_title">Tamaño máximo</string>
|
||||||
<string name="advanced_unlock_explanation_summary">Usar desbloqueo avanzado para abrir una base de datos de manera más fácil</string>
|
<string name="advanced_unlock_explanation_summary">Usar el desbloqueo avanzado para abrir una base de datos más fácilmente</string>
|
||||||
<string name="lock_database_show_button_summary">Muestra el botón de bloqueo en la interfaz</string>
|
<string name="lock_database_show_button_summary">Muestra el botón de bloqueo en la interfaz</string>
|
||||||
<string name="lock_database_show_button_title">Mostrar botón de bloqueo</string>
|
<string name="lock_database_show_button_title">Mostrar botón de bloqueo</string>
|
||||||
<string name="autofill_preference_title">Configuración de autocompletado</string>
|
<string name="autofill_preference_title">Configuración de autocompletado</string>
|
||||||
@@ -409,8 +411,8 @@
|
|||||||
<string name="warning_database_read_only">Otorga acceso de escritura para guardar cambios en la base de datos</string>
|
<string name="warning_database_read_only">Otorga acceso de escritura para guardar cambios en la base de datos</string>
|
||||||
<string name="show_recent_files_summary">Mostrar ubicaciones de bases de datos recientes</string>
|
<string name="show_recent_files_summary">Mostrar ubicaciones de bases de datos recientes</string>
|
||||||
<string name="show_recent_files_title">Mostrar archivos recientes</string>
|
<string name="show_recent_files_title">Mostrar archivos recientes</string>
|
||||||
<string name="remember_database_locations_summary">Recordar la ubicación de las bases de datos</string>
|
<string name="remember_database_locations_summary">Lleva un registro de los lugares donde se almacenan las bases de datos</string>
|
||||||
<string name="remember_database_locations_title">Guardar ubicaciones de bases de datos</string>
|
<string name="remember_database_locations_title">Recordar las ubicaciones de las bases de datos</string>
|
||||||
<string name="contains_duplicate_uuid">La base de datos contiene UUIDs duplicados.</string>
|
<string name="contains_duplicate_uuid">La base de datos contiene UUIDs duplicados.</string>
|
||||||
<string name="menu_restore_entry_history">Restaurar historial</string>
|
<string name="menu_restore_entry_history">Restaurar historial</string>
|
||||||
<string name="menu_empty_recycle_bin">Vaciar papelera de reciclaje</string>
|
<string name="menu_empty_recycle_bin">Vaciar papelera de reciclaje</string>
|
||||||
@@ -419,12 +421,12 @@
|
|||||||
<string name="invalid_db_same_uuid">%1$s con la misma UUID %2$s ya existe.</string>
|
<string name="invalid_db_same_uuid">%1$s con la misma UUID %2$s ya existe.</string>
|
||||||
<string name="error_label_exists">Esta etiqueta ya existe.</string>
|
<string name="error_label_exists">Esta etiqueta ya existe.</string>
|
||||||
<string name="discard">Descartar</string>
|
<string name="discard">Descartar</string>
|
||||||
<string name="discard_changes">¿Descartar cambios\?</string>
|
<string name="discard_changes">¿Descartar los cambios\?</string>
|
||||||
<string name="validate">Validar</string>
|
<string name="validate">Validar</string>
|
||||||
<string name="contribution">Contribución</string>
|
<string name="contribution">Contribución</string>
|
||||||
<string name="contact">Contactar</string>
|
<string name="contact">Contacto</string>
|
||||||
<string name="error_otp_period">Período debe estar entre %1$d y %2$d segundos.</string>
|
<string name="error_otp_period">El período debe estar entre %1$d y %2$d segundos.</string>
|
||||||
<string name="error_copy_group_here">No se puede copiar un grupo aquí.</string>
|
<string name="error_copy_group_here">No puede copiar un grupo aquí.</string>
|
||||||
<string name="database_data_compression_summary">La compresión de datos reduce el tamaño de la base de datos</string>
|
<string name="database_data_compression_summary">La compresión de datos reduce el tamaño de la base de datos</string>
|
||||||
<string name="database_data_compression_title">Compresión de datos</string>
|
<string name="database_data_compression_title">Compresión de datos</string>
|
||||||
<string name="warning_empty_keyfile">No se recomienda agregar un archivo de claves vacío.</string>
|
<string name="warning_empty_keyfile">No se recomienda agregar un archivo de claves vacío.</string>
|
||||||
@@ -433,19 +435,19 @@
|
|||||||
<string name="warning_replace_file">Al cargar este archivo reemplazará el existente.</string>
|
<string name="warning_replace_file">Al cargar este archivo reemplazará el existente.</string>
|
||||||
<string name="warning_permanently_delete_nodes">¿Borrar los nodos seleccionados de forma permanente\?</string>
|
<string name="warning_permanently_delete_nodes">¿Borrar los nodos seleccionados de forma permanente\?</string>
|
||||||
<string name="warning_database_link_revoked">El acceso al archivo fue revocado por el administrador de archivos</string>
|
<string name="warning_database_link_revoked">El acceso al archivo fue revocado por el administrador de archivos</string>
|
||||||
<string name="command_execution">Ejecutando el comando …</string>
|
<string name="command_execution">Ejecutando el comando…</string>
|
||||||
<string name="hide_broken_locations_summary">Ocultar enlaces rotos en la lista de bases de datos recientes</string>
|
<string name="hide_broken_locations_summary">Ocultar enlaces rotos en la lista de bases de datos recientes</string>
|
||||||
<string name="hide_broken_locations_title">Ocultar enlaces de bases de datos rotos</string>
|
<string name="hide_broken_locations_title">Ocultar enlaces de bases de datos rotos</string>
|
||||||
<string name="remember_keyfile_locations_summary">Realiza un seguimiento de dónde se almacenan los archivos de claves</string>
|
<string name="remember_keyfile_locations_summary">Realiza un seguimiento de dónde se almacenan los archivos de claves</string>
|
||||||
<string name="remember_keyfile_locations_title">Recuerdar las ubicaciones de los archivos de claves</string>
|
<string name="remember_keyfile_locations_title">Recordar las ubicaciones de los archivos de clave</string>
|
||||||
<string name="subdomain_search_summary">Buscar dominios web con restricciones de subdominios</string>
|
<string name="subdomain_search_summary">Buscar dominios web con restricciones de subdominios</string>
|
||||||
<string name="subdomain_search_title">Búsqueda de subdominio</string>
|
<string name="subdomain_search_title">Búsqueda de subdominio</string>
|
||||||
<string name="auto_focus_search_summary">Solicite una búsqueda al abrir una base de datos</string>
|
<string name="auto_focus_search_summary">Solicite una búsqueda al abrir una base de datos</string>
|
||||||
<string name="auto_focus_search_title">Búsqueda rápida</string>
|
<string name="auto_focus_search_title">Búsqueda rápida</string>
|
||||||
<string name="menu_delete_entry_history">Borrar historial</string>
|
<string name="menu_delete_entry_history">Eliminar el historial</string>
|
||||||
<string name="error_otp_digits">El token debe tener de %1$d a %2$d dígitos.</string>
|
<string name="error_otp_digits">El token debe contener de %1$d a %2$d dígitos.</string>
|
||||||
<string name="entry_attachments">Archivos adjuntos</string>
|
<string name="entry_attachments">Archivos adjuntos</string>
|
||||||
<string name="entry_add_attachment">Adjuntar</string>
|
<string name="entry_add_attachment">Añadir el archivo adjunto</string>
|
||||||
<string name="content_description_credentials_information">Información de credenciales</string>
|
<string name="content_description_credentials_information">Información de credenciales</string>
|
||||||
<string name="database_opened">Base de datos abierta</string>
|
<string name="database_opened">Base de datos abierta</string>
|
||||||
<string name="education_add_attachment_title">Adjuntar</string>
|
<string name="education_add_attachment_title">Adjuntar</string>
|
||||||
@@ -455,4 +457,94 @@
|
|||||||
<string name="warning_file_too_big">Se supone que una base de datos KeePass contiene solo pequeños archivos de utilidad (como archivos de clave PGP).
|
<string name="warning_file_too_big">Se supone que una base de datos KeePass contiene solo pequeños archivos de utilidad (como archivos de clave PGP).
|
||||||
\n
|
\n
|
||||||
\nLa base de datos puede volverse muy grande y reducir su rendimiento con esta subida.</string>
|
\nLa base de datos puede volverse muy grande y reducir su rendimiento con esta subida.</string>
|
||||||
|
<string name="recycle_bin_group_title">Grupo de la papelera de reciclaje</string>
|
||||||
|
<string name="filter">Filtrar</string>
|
||||||
|
<string name="database_data_remove_unlinked_attachments_summary">Elimina los archivos adjuntos contenidos en la base de datos pero no vinculados a una entrada</string>
|
||||||
|
<string name="database_data_remove_unlinked_attachments_title">Eliminar los datos no vinculados</string>
|
||||||
|
<string name="data">Datos</string>
|
||||||
|
<string name="advanced_unlock_delete_all_key_warning">¿Borrar todas las claves de encriptación relacionadas con el reconocimiento de desbloqueo avanzado\?</string>
|
||||||
|
<string name="advanced_unlock_timeout">Tiempo límite de desbloqueo avanzado</string>
|
||||||
|
<string name="temp_advanced_unlock_timeout_summary">Duración del uso de desbloqueo avanzado antes de borrar su contenido</string>
|
||||||
|
<string name="temp_advanced_unlock_timeout_title">Expiración de desbloqueo avanzado</string>
|
||||||
|
<string name="temp_advanced_unlock_enable_summary">No almacenar ningún contenido encriptado para utilizar el desbloqueo avanzado</string>
|
||||||
|
<string name="temp_advanced_unlock_enable_title">Desbloqueo avanzado temporal</string>
|
||||||
|
<string name="device_credential_unlock_enable_summary">Le permite usar la credenciales de su dispositivo para abrir la base de datos</string>
|
||||||
|
<string name="device_credential_unlock_enable_title">Desbloqueo de las credenciales del dispositivo</string>
|
||||||
|
<string name="advanced_unlock_tap_delete">Toque para eliminar las teclas de desbloqueo avanzadas</string>
|
||||||
|
<string name="content">Contenido</string>
|
||||||
|
<string name="clipboard_explanation_summary">Copiar los campos de entrada usando el portapapeles de su dispositivo</string>
|
||||||
|
<string name="device_credential">Credenciales del dispositivo</string>
|
||||||
|
<string name="credential_before_click_advanced_unlock_button">Introduzca la contraseña y luego haga clic en el botón \"Desbloqueo avanzado\".</string>
|
||||||
|
<string name="advanced_unlock_prompt_not_initialized">No se pudo inicializar el indicador de desbloqueo avanzado.</string>
|
||||||
|
<string name="advanced_unlock_scanning_error">Error de desbloqueo avanzado: %1$s</string>
|
||||||
|
<string name="advanced_unlock_not_recognized">No se pudo reconocer la impresión de desbloqueo avanzado</string>
|
||||||
|
<string name="advanced_unlock_invalid_key">No se pudo leer la llave de desbloqueo avanzada. Por favor, bórrela y repita el procedimiento de reconocimiento de desbloqueo.</string>
|
||||||
|
<string name="advanced_unlock_prompt_extract_credential_message">Extraer la credencial de la base de datos con datos de desbloqueo avanzado</string>
|
||||||
|
<string name="advanced_unlock_prompt_extract_credential_title">Abrir la base de datos con reconocimiento de desbloqueo avanzado</string>
|
||||||
|
<string name="advanced_unlock_prompt_store_credential_message">Advertencia: Aún debes recordar tu contraseña maestra si usas el reconocimiento de desbloqueo avanzado.</string>
|
||||||
|
<string name="advanced_unlock_prompt_store_credential_title">Reconocimiento de desbloqueo avanzado</string>
|
||||||
|
<string name="open_advanced_unlock_prompt_store_credential">Abrir el indicador de desbloqueo avanzado para almacenar las credenciales</string>
|
||||||
|
<string name="open_advanced_unlock_prompt_unlock_database">Abrir el aviso de desbloqueo avanzado para desbloquear la base de datos</string>
|
||||||
|
<string name="keystore_not_accessible">El almacén de claves no está debidamente inicializado.</string>
|
||||||
|
<string name="biometric_security_update_required">Se requiere una actualización de la seguridad biométrica.</string>
|
||||||
|
<string name="configure_biometric">No se ha inscrito ninguna credencial biométrica o del dispositivo.</string>
|
||||||
|
<string name="warning_empty_keyfile_explanation">El contenido del archivo clave nunca debe modificarse y, en el mejor de los casos, debe contener datos generados al azar.</string>
|
||||||
|
<string name="warning_empty_recycle_bin">¿Borrar permanentemente todos los nodos de la papelera de reciclaje\?</string>
|
||||||
|
<string name="registration_mode">Modo de registro</string>
|
||||||
|
<string name="save_mode">Modo de guardado</string>
|
||||||
|
<string name="search_mode">Modo de búsqueda</string>
|
||||||
|
<string name="contains_duplicate_uuid_procedure">¿Resolver el problema generando nuevos UUID para que los duplicados continúen\?</string>
|
||||||
|
<string name="menu_keystore_remove_key">Borrar la clave de desbloqueo avanzado</string>
|
||||||
|
<string name="error_field_name_already_exists">El nombre del campo ya existe.</string>
|
||||||
|
<string name="error_registration_read_only">Guardar un nuevo elemento no está permitido en una base de datos de sólo lectura</string>
|
||||||
|
<string name="settings_database_recommend_changing_master_key_title">Recomendar la renovación</string>
|
||||||
|
<string name="max_history_size_summary">Limitar el tamaño del historial por entrada</string>
|
||||||
|
<string name="max_history_items_summary">Limitar el número de elementos del historial por entrada</string>
|
||||||
|
<string name="max_history_items_title">Número máximo</string>
|
||||||
|
<string name="device_keyboard_setting_title">Configuración del teclado del dispositivo</string>
|
||||||
|
<string name="database_custom_color_title">Color personalizado de la base de datos</string>
|
||||||
|
<string name="settings_database_recommend_changing_master_key_summary">Recomendar cambiar la contraseña maestra (días)</string>
|
||||||
|
<string name="notification">Notificación</string>
|
||||||
|
<string name="hide_expired_entries_title">Ocultar las entradas expiradas</string>
|
||||||
|
<string name="keyboard_search_share_title">Buscar información compartida</string>
|
||||||
|
<string name="kdf_Argon2id">Argon2id</string>
|
||||||
|
<string name="kdf_Argon2d">Argon2d</string>
|
||||||
|
<string name="upload_attachment">Subir %1$s</string>
|
||||||
|
<string name="education_setup_OTP_summary">Configurar la gestión de contraseñas de una sola vez (HOTP / TOTP) para generar un token solicitado para la autenticación de dos factores (2FA).</string>
|
||||||
|
<string name="education_setup_OTP_title">Establecer la contaseña de un solo uso</string>
|
||||||
|
<string name="education_advanced_unlock_summary">Vincule su contraseña con su credencial biométrica o del dispositivo escaneada para desbloquear rápidamente su base de datos.</string>
|
||||||
|
<string name="education_advanced_unlock_title">Desbloqueo avanzado de la base de datos</string>
|
||||||
|
<string name="autofill_read_only_save">No se permite guardar datos en una base de datos abierta como de sólo lectura.</string>
|
||||||
|
<string name="autofill_block_restart">Reiniciar la aplicación que contiene el formulario para activar el bloqueo.</string>
|
||||||
|
<string name="autofill_web_domain_blocklist_summary">Lista de bloqueo que impide el llenado automático de los dominios web</string>
|
||||||
|
<string name="autofill_web_domain_blocklist_title">Lista de bloqueo de los dominios web</string>
|
||||||
|
<string name="autofill_application_id_blocklist_summary">Lista de bloqueo que impide el llenado automático de las aplicaciones</string>
|
||||||
|
<string name="autofill_application_id_blocklist_title">Lista de bloqueo de las aplicaciones</string>
|
||||||
|
<string name="autofill_ask_to_save_data_summary">Pedir que se guarden los datos cuando se valide un formulario</string>
|
||||||
|
<string name="autofill_ask_to_save_data_title">Pedir que se guarden los datos</string>
|
||||||
|
<string name="autofill_save_search_info_summary">Intente guardar la información de la búsqueda cuando haga una selección de entrada manual</string>
|
||||||
|
<string name="autofill_save_search_info_title">Guardar la información de la búsqueda</string>
|
||||||
|
<string name="autofill_auto_search_summary">Sugerir automáticamente los resultados de la búsqueda desde el dominio web o el ID de la aplicación</string>
|
||||||
|
<string name="autofill_auto_search_title">Búsqueda automática</string>
|
||||||
|
<string name="autofill_close_database_summary">Cerrar la base de datos después de una selección de autocompletado</string>
|
||||||
|
<string name="autofill_close_database_title">Cerrar la base de datos</string>
|
||||||
|
<string name="enter">Entrar</string>
|
||||||
|
<string name="backspace">Retroceder</string>
|
||||||
|
<string name="select_entry">Seleccionar la entrada</string>
|
||||||
|
<string name="back_to_previous_keyboard">Volver al teclado anterior</string>
|
||||||
|
<string name="custom_fields">Campos personalizados</string>
|
||||||
|
<string name="keyboard_previous_lock_summary">Cambiar automáticamente al teclado anterior después de bloquear la base de datos</string>
|
||||||
|
<string name="keyboard_previous_lock_title">Bloquear la base de datos</string>
|
||||||
|
<string name="keyboard_previous_fill_in_summary">Cambiar automáticamente al teclado anterior después de ejecutar \"Acción de la tecla automática\"</string>
|
||||||
|
<string name="keyboard_previous_fill_in_title">Acción de la tecla automática</string>
|
||||||
|
<string name="keyboard_previous_database_credentials_summary">Cambiar automáticamente al teclado anterior en la pantalla de credenciales de la base de datos</string>
|
||||||
|
<string name="keyboard_previous_database_credentials_title">Pantalla de credenciales de la base de datos</string>
|
||||||
|
<string name="keyboard_auto_go_action_title">Acción de la tecla automática</string>
|
||||||
|
<string name="keyboard_save_search_info_summary">Intentar guardar la información compartida cuando hagas una selección de entrada manual</string>
|
||||||
|
<string name="keyboard_save_search_info_title">Guardar información compartida</string>
|
||||||
|
<string name="keyboard_search_share_summary">Buscar automáticamente la información compartida para llenar el teclado</string>
|
||||||
|
<string name="show_uuid_summary">Muestra el UUID vinculado a una entrada</string>
|
||||||
|
<string name="show_uuid_title">Mostrar UUID</string>
|
||||||
|
<string name="error_rebuild_list">No es posible reconstruir adecuadamente la lista.</string>
|
||||||
|
<string name="error_database_uri_null">La URI de la base de datos no puede ser recuperada.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
<string name="saving_database">در حال ذخیره پایگاه داده</string>
|
<string name="saving_database">در حال ذخیره پایگاه داده</string>
|
||||||
<string name="parallelism_explanation">درجه موازی سازی (یعنی تعداد موضوعات) که توسط عملکرد مشتق کلیدی استفاده می شود.</string>
|
<string name="parallelism_explanation">درجه موازی سازی (یعنی تعداد موضوعات) که توسط عملکرد مشتق کلیدی استفاده می شود.</string>
|
||||||
<string name="parallelism">موازی کاری</string>
|
<string name="parallelism">موازی کاری</string>
|
||||||
<string name="memory_usage_explanation">مقدار حافظه (در بایت) که توسط تابع مشتق کلید مورد استفاده قرار گیرد.</string>
|
<string name="memory_usage_explanation">مقدار حافظه که توسط تابع مشتق کلید مورد استفاده قرار گیرد.</string>
|
||||||
<string name="memory_usage">استفاده از حافظه</string>
|
<string name="memory_usage">استفاده از حافظه</string>
|
||||||
<string name="rounds_explanation">دورهای رمزگذاری اضافی محافظت بالاتری در برابر حملات نیروی وحشی ایجاد می کنند ، اما در واقع می توانند سرعت و بارگذاری را کاهش دهند.</string>
|
<string name="rounds_explanation">دورهای رمزگذاری اضافی محافظت بالاتری در برابر حملات نیروی وحشی ایجاد می کنند ، اما در واقع می توانند سرعت و بارگذاری را کاهش دهند.</string>
|
||||||
<string name="rounds">دور تحول</string>
|
<string name="rounds">دور تحول</string>
|
||||||
|
|||||||
@@ -249,7 +249,7 @@
|
|||||||
<string name="list_password_generator_options_title">Salasanamerkit</string>
|
<string name="list_password_generator_options_title">Salasanamerkit</string>
|
||||||
<string name="password_size_summary">Asettaa oletuspituuden generoiduille salasanoille</string>
|
<string name="password_size_summary">Asettaa oletuspituuden generoiduille salasanoille</string>
|
||||||
<string name="parallelism">Rinnakkaisuus</string>
|
<string name="parallelism">Rinnakkaisuus</string>
|
||||||
<string name="memory_usage_explanation">Avaimen johtamisfunktion käyttämän muistin (tavuina) määrä.</string>
|
<string name="memory_usage_explanation">Avaimen johtamisfunktion käyttämän muistin määrä.</string>
|
||||||
<string name="memory_usage">Muistin käyttö</string>
|
<string name="memory_usage">Muistin käyttö</string>
|
||||||
<string name="kdf_explanation">Pääavain muunnetaan käyttäen satunnaista suolattua avaimen johtamisfunktiota, jotta salausalgoritmin avain voidaan generoida.</string>
|
<string name="kdf_explanation">Pääavain muunnetaan käyttäen satunnaista suolattua avaimen johtamisfunktiota, jotta salausalgoritmin avain voidaan generoida.</string>
|
||||||
<string name="encryption_explanation">Salasanatietokannan salausalgoritmi, jota käytetään kaikelle datalle.</string>
|
<string name="encryption_explanation">Salasanatietokannan salausalgoritmi, jota käytetään kaikelle datalle.</string>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user