diff --git a/.gitignore b/.gitignore
index d02280737..3cd74fd62 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,9 @@ bin/
gen/
out/
+# Kotlin folder
+.kotlin/
+
# Gradle files
.gradle/
build/
diff --git a/CHANGELOG b/CHANGELOG
index 89ffb7d36..4734503f2 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,13 @@
+KeePassDX(4.2.0)
+ * Passkeys management #1421 #2097 (@cali-95)
+ * Confirm usage of passkey #2165 #2124
+ * Dialog to manage missing signature #2152 #2155 #2161 #2160
+ * Capture error #2159 #2215
+ * Change Passkey Backup Eligibility & Backup State #2135 #2150 #2212
+ * Search settings #2112 #2181 #2187 #2204
+ * Autofill refactoring #765 #2196
+ * Small fixes #2157 #2164 #2171 #2122 #2180 #2209 #2214
+
KeePassDX(4.1.9)
* Fix landscape UI #2198 #2200 (@chenxiaolong)
* Fix start loop and flash screen #2201
diff --git a/app/build.gradle b/app/build.gradle
index 81802b957..895d76061 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass"
minSdkVersion 19
targetSdkVersion 35
- versionCode = 143
- versionName = "4.1.9"
+ versionCode = 145
+ versionName = "4.2.0"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
@@ -35,6 +35,10 @@ android {
}
}
+ buildFeatures {
+ buildConfig true
+ }
+
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
@@ -101,6 +105,11 @@ android {
buildFeatures {
buildConfig true
}
+
+ packaging {
+ // Bouncy castle bug https://github.com/bcgit/bc-java/issues/1685
+ resources.pickFirsts.add('META-INF/versions/9/OSGI-INF/MANIFEST.MF')
+ }
}
def room_version = "2.5.1"
@@ -140,7 +149,10 @@ dependencies {
implementation 'commons-codec:commons-codec:1.15'
// Password generator
implementation 'me.gosimple:nbvcxz:1.5.0'
-
+
+ // Credentials Provider
+ implementation "androidx.credentials:credentials:1.2.2"
+
// Modules import
implementation project(path: ':database')
implementation project(path: ':icon-pack')
diff --git a/app/src/free/assets/passkeys_privileged_apps_community.json b/app/src/free/assets/passkeys_privileged_apps_community.json
new file mode 100644
index 000000000..27eb8b5bc
--- /dev/null
+++ b/app/src/free/assets/passkeys_privileged_apps_community.json
@@ -0,0 +1,64 @@
+{
+ "apps": [
+ {
+ "type": "android",
+ "info": {
+ "package_name": "io.github.forkmaintainers.iceraven",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "9C:0D:22:37:9F:48:7B:70:A4:F9:F8:BE:C0:17:3C:F9:1A:16:44:F0:8F:93:38:5B:5B:78:2C:E3:76:60:BA:81"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.chromium.chrome",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "A8:56:48:50:79:BC:B3:57:BF:BE:69:BA:19:A9:BA:43:CD:0A:D9:AB:22:67:52:C7:80:B6:88:8A:FD:48:21:6B"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.cromite.cromite",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "63:3F:A4:1D:82:11:D6:D0:91:6A:81:9B:89:66:8C:6D:E9:2E:64:23:2D:A6:7F:9D:16:FD:81:C3:B7:E9:23:FF"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.ironfoxoss.ironfox",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "C5:E2:91:B5:A5:71:F9:C8:CD:9A:97:99:C2:C9:4E:02:EC:97:03:94:88:93:F2:CA:75:6D:67:B9:42:04:F9:04"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.fennec_fdroid",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "06:66:53:58:EF:D8:BA:05:BE:23:6A:47:A1:2C:B0:95:8D:7D:75:DD:93:9D:77:C2:B3:1F:53:98:53:7E:BD:C5"
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/app/src/free/assets/passkeys_privileged_apps_google.json b/app/src/free/assets/passkeys_privileged_apps_google.json
new file mode 100644
index 000000000..47ada3cdf
--- /dev/null
+++ b/app/src/free/assets/passkeys_privileged_apps_google.json
@@ -0,0 +1,820 @@
+{
+ "apps": [
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.android.chrome",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
+ },
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.chrome.beta",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "DA:63:3D:34:B6:9E:63:AE:21:03:B4:9D:53:CE:05:2F:C5:F7:F3:C5:3A:AB:94:FD:C2:A2:08:BD:FD:14:24:9C"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.chrome.dev",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "90:44:EE:5F:EE:4B:BC:5E:21:DD:44:66:54:31:C4:EB:1F:1F:71:A3:27:16:A0:BC:92:7B:CB:B3:92:33:CA:BF"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.chrome.canary",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "20:19:DF:A1:FB:23:EF:BF:70:C5:BC:D1:44:3C:5B:EA:B0:4F:3F:2F:F4:36:6E:9A:C1:E3:45:76:39:A2:4C:FC"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.chromium.chrome",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
+ },
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.google.android.apps.chrome",
+ "signatures": [
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.fennec_webauthndebug",
+ "signatures": [
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "BD:AE:82:02:80:D2:AF:B7:74:94:EF:22:58:AA:78:A9:AE:A1:36:41:7E:8B:C2:3D:C9:87:75:2E:6F:48:E8:48"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.firefox",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.firefox_beta",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.focus",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.fennec_aurora",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "BC:04:88:83:8D:06:F4:CA:6B:F3:23:86:DA:AB:0D:D8:EB:CF:3E:77:30:78:74:59:F6:2F:B3:CD:14:A1:BA:AA"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.rocket",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "86:3A:46:F0:97:39:32:B7:D0:19:9B:54:91:12:74:1C:2D:27:31:AC:72:EA:11:B7:52:3A:A9:0A:11:BF:56:91"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.fenix",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "50:04:77:90:88:E7:F9:88:D5:BC:5C:C5:F8:79:8F:EB:F4:F8:CD:08:4A:1B:2A:46:EF:D4:C8:EE:4A:EA:F2:11"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.fenix.debug",
+ "signatures": [
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "BD:AE:82:02:80:D2:AF:B7:74:94:EF:22:58:AA:78:A9:AE:A1:36:41:7E:8B:C2:3D:C9:87:75:2E:6F:48:E8:48"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.focus.beta",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.focus.nightly",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.klar",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.reference.browser",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "B0:09:90:E3:0F:9D:81:5D:2E:BC:7B:9B:B2:21:CE:47:E5:C9:D5:17:AA:C7:0E:7F:D5:95:B1:E5:3E:9A:4B:14"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.microsoft.emmx.canary",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.microsoft.emmx.dev",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.microsoft.emmx.beta",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.microsoft.emmx",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.microsoft.emmx.rolling",
+ "signatures": [
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.microsoft.emmx.local",
+ "signatures": [
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.brave.browser",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.brave.browser_beta",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.brave.browser_nightly",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "app.vanadium.browser",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.vivaldi.browser",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.vivaldi.browser.snapshot",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.vivaldi.browser.sopranos",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.citrix.Receiver",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "3D:D1:12:67:10:69:AB:36:4E:F9:BE:73:9A:B7:B5:EE:15:E1:CD:E9:D8:75:7B:1B:F0:64:F5:0C:55:68:9A:49"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "CE:B2:23:D7:77:09:F2:B6:BC:0B:3A:78:36:F5:A5:AF:4C:E1:D3:55:F4:A7:28:86:F7:9D:F8:0D:C9:D6:12:2E"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "AA:D0:D4:57:E6:33:C3:78:25:77:30:5B:C1:B2:D9:E3:81:41:C7:21:DF:0D:AA:6E:29:07:2F:C4:1D:34:F0:AB"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.android.browser",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "C9:00:9D:01:EB:F9:F5:D0:30:2B:C7:1B:2F:E9:AA:9A:47:A4:32:BB:A1:73:08:A3:11:1B:75:D7:B2:14:90:25"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.sec.android.app.sbrowser",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.sec.android.app.sbrowser.beta",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.google.android.gms",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "7C:E8:3C:1B:71:F3:D5:72:FE:D0:4C:8D:40:C5:CB:10:FF:75:E6:D8:7D:9D:F6:FB:D5:3F:04:68:C2:90:50:53"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "D2:2C:C5:00:29:9F:B2:28:73:A0:1A:01:0D:E1:C8:2F:BE:4D:06:11:19:B9:48:14:DD:30:1D:AB:50:CB:76:78"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.yandex.browser",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.yandex.browser.beta",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.yandex.browser.alpha",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.yandex.browser.corp",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.yandex.browser.canary",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.yandex.browser.broteam",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.talonsec.talon",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "A3:66:03:44:A6:F6:AF:CA:81:8C:BF:43:96:A2:3C:CF:D5:ED:7A:78:1B:B4:A3:D1:85:03:01:E2:F4:6D:23:83"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "E2:A5:64:74:EA:23:7B:06:67:B6:F5:2C:DC:E9:04:5E:24:88:3B:AE:D0:82:59:9A:A2:DF:0B:60:3A:CF:6A:3B"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.talonsec.talon_beta",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "F5:86:62:7A:32:C8:9F:E6:7E:00:6D:B1:8C:34:31:9E:01:7F:B3:B2:BE:D6:9D:01:01:B7:F9:43:E7:7C:48:AE"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "9A:A1:25:D5:E5:5E:3F:B0:DE:96:72:D9:A9:5D:04:65:3F:49:4A:1E:C3:EE:76:1E:94:C4:4E:5D:2F:65:8E:2F"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.duckduckgo.mobile.android.debug",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "C4:F0:9E:2B:D7:25:AD:F5:AD:92:0B:A2:80:27:66:AC:16:4A:C1:53:B3:EA:9E:08:48:B0:57:98:37:F7:6A:29"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.duckduckgo.mobile.android",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "BB:7B:B3:1C:57:3C:46:A1:DA:7F:C5:C5:28:A6:AC:F4:32:10:84:56:FE:EC:50:81:0C:7F:33:69:4E:B3:D2:D4"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.naver.whale",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "0B:8B:85:23:BB:4A:EF:FA:34:6E:4B:DD:4F:BF:7D:19:34:50:56:9A:A1:4A:AA:D4:AD:FD:94:A3:F7:B2:27:BB"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.fido.fido2client",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "FC:98:DA:E6:3A:D3:96:26:C8:C6:7F:BE:83:F2:F0:6F:74:93:2A:9C:D1:46:B9:2C:EC:FC:6A:04:7A:90:43:86"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.heytap.browser",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "AF:F8:A7:49:CF:0E:7D:75:44:65:D0:FB:FA:7B:8D:0C:64:5E:22:5C:10:C6:E2:32:AD:A0:D9:74:88:36:B8:E5"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "A8:FE:A4:CA:FB:93:32:DA:26:B8:E6:81:08:17:C1:DA:90:A5:03:0E:35:A6:0A:79:E0:6C:90:97:AA:C6:A4:42"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "io.island.Island",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "D9:C3:39:AC:9C:3A:EE:E1:75:1D:85:8C:35:D9:BA:C5:CC:87:B3:CE:76:30:93:F0:F5:10:64:F5:A2:F6:9B:04"
+ },
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "io.island.IslandCanary",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "90:17:13:23:45:6E:6F:39:CB:FD:CF:B2:56:BE:1D:CF:F3:BC:1C:59:8A:15:93:30:E4:97:73:D0:4C:B9:C9:05"
+ },
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "io.island.IslandBeta",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "35:31:83:1A:9E:2B:21:1D:E6:AA:C3:69:4B:45:83:6E:56:09:B9:D7:D0:04:C3:1B:21:87:40:FB:77:17:38:D1"
+ },
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "io.island.IslandDev",
+ "signatures": [
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "io.island.island.intune",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "C2:38:24:15:41:20:A0:8F:C3:95:42:AC:D8:2A:E9:24:94:78:80:1E:47:FD:6C:66:2B:18:1C:28:CA:7E:59:4E"
+ },
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "io.island.island.canary.intune",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "1E:16:74:BB:79:EA:09:FB:37:CF:9F:1B:07:1B:1D:51:8D:46:03:0E:D3:EE:F2:C1:4E:AD:93:9E:C6:EE:3A:4C"
+ },
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "io.island.island.beta.intune",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "D2:5E:AD:F6:1C:E6:36:6C:A4:23:A4:7F:C4:DB:9B:8C:9C:8A:35:B4:B0:19:E8:D9:82:FB:D0:8A:D9:DB:49:5A"
+ },
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "io.island.island.dev.intune",
+ "signatures": [
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "net.quetta.browser",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "BE:FE:E7:31:12:6A:A5:6E:7E:FD:AE:AF:5E:F3:FA:EA:44:1C:19:CC:E0:CA:EC:42:6B:65:BB:F8:2C:59:46:80"
+ },
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "F1:38:00:4F:38:04:51:D4:8A:05:2B:B3:A3:EF:17:24:23:D4:B0:D0:C8:A3:AA:DD:FB:DB:66:30:31:48:EC:A4"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "cz.seznam.sbrowser",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "DB:95:40:66:10:78:83:6E:4E:B1:66:F6:9E:F4:07:30:9E:8D:AE:33:34:68:5E:C8:F6:FA:2F:13:81:B9:AC:F6"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.opera.mini.native",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "57:AC:BC:52:5F:1B:2E:BD:19:19:6C:D6:F0:14:39:7C:C9:10:FD:18:84:1E:0A:E8:50:FE:BC:3E:1E:59:3F:F2"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.opera.mini.native.beta",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "57:AC:BC:52:5F:1B:2E:BD:19:19:6C:D6:F0:14:39:7C:C9:10:FD:18:84:1E:0A:E8:50:FE:BC:3E:1E:59:3F:F2"
+ }
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 900abdea7..66cb1d194 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -45,7 +45,8 @@
android:resizeableActivity="true"
android:supportsRtl="true"
android:theme="@style/KeepassDXStyle.Night"
- tools:targetApi="s">
+ tools:targetApi="s"
+ tools:ignore="CredentialDependency">
@@ -158,25 +159,41 @@
android:label="@string/about" />
-
+ android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
+ android:label="@string/keyboard_setting_label"
+ android:exported="true">
+
+
+
+
+
+
+ android:name="com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity"
+ android:theme="@style/Theme.Transparent"
+ android:exported="false"
+ android:excludeFromRecents="true" />
+
+ android:exported="true"
+ android:excludeFromRecents="true">
@@ -192,14 +209,12 @@
-
-
-
-
-
+ android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
+ android:theme="@style/Theme.Transparent"
+ android:configChanges="keyboardHidden"
+ android:exported="false"
+ android:excludeFromRecents="true"
+ tools:targetApi="upside_down_cake" />
@@ -249,6 +264,22 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/AboutActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/AboutActivity.kt
index d117418f4..c07cc8c4a 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/AboutActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/AboutActivity.kt
@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.activities
import android.content.pm.PackageManager.NameNotFoundException
-import android.os.Build
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.util.Log
@@ -32,7 +31,7 @@ import androidx.core.text.HtmlCompat
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishActivity
-import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
+import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
import com.kunzisoft.keepass.utils.getPackageInfoCompat
import org.joda.time.DateTime
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt
deleted file mode 100644
index f69d1475f..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt
+++ /dev/null
@@ -1,287 +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 .
- *
- */
-package com.kunzisoft.keepass.activities
-
-import android.app.Activity
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import android.os.Build
-import android.os.Bundle
-import android.util.Log
-import android.widget.Toast
-import androidx.activity.result.ActivityResultLauncher
-import androidx.annotation.RequiresApi
-import com.kunzisoft.keepass.R
-import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
-import com.kunzisoft.keepass.activities.helpers.SpecialMode
-import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
-import com.kunzisoft.keepass.autofill.AutofillComponent
-import com.kunzisoft.keepass.autofill.AutofillHelper
-import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
-import com.kunzisoft.keepass.autofill.KeeAutofillService
-import com.kunzisoft.keepass.database.ContextualDatabase
-import com.kunzisoft.keepass.database.helper.SearchHelper
-import com.kunzisoft.keepass.model.RegisterInfo
-import com.kunzisoft.keepass.model.SearchInfo
-import com.kunzisoft.keepass.utils.WebDomain
-import com.kunzisoft.keepass.utils.getParcelableCompat
-import com.kunzisoft.keepass.utils.getParcelableExtraCompat
-
-@RequiresApi(api = Build.VERSION_CODES.O)
-class AutofillLauncherActivity : DatabaseModeActivity() {
-
- private var mAutofillActivityResultLauncher: ActivityResultLauncher? =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
- AutofillHelper.buildActivityResultLauncher(this, true)
- else null
-
- override fun applyCustomStyle(): Boolean {
- return false
- }
-
- override fun finishActivityIfReloadRequested(): Boolean {
- return true
- }
-
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
-
- // Retrieve selection mode
- EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
- when (specialMode) {
- SpecialMode.SELECTION -> {
- intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle ->
- // To pass extra inline request
- var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- compatInlineSuggestionsRequest = bundle.getParcelableCompat(KEY_INLINE_SUGGESTION)
- }
- // Build search param
- bundle.getParcelableCompat(KEY_SEARCH_INFO)?.let { searchInfo ->
- WebDomain.getConcreteWebDomain(
- this,
- searchInfo.webDomain
- ) { concreteWebDomain ->
- // Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
- val assistStructure = AutofillHelper
- .retrieveAutofillComponent(intent)
- ?.assistStructure
- val newAutofillComponent = if (assistStructure != null) {
- AutofillComponent(
- assistStructure,
- compatInlineSuggestionsRequest
- )
- } else {
- null
- }
- searchInfo.webDomain = concreteWebDomain
- launchSelection(database, newAutofillComponent, searchInfo)
- }
- }
- }
- // Remove bundle
- intent.removeExtra(KEY_SELECTION_BUNDLE)
- }
- SpecialMode.REGISTRATION -> {
- // To register info
- val registerInfo = intent.getParcelableExtraCompat(KEY_REGISTER_INFO)
- val searchInfo = SearchInfo(registerInfo?.searchInfo)
- WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
- searchInfo.webDomain = concreteWebDomain
- launchRegistration(database, searchInfo, registerInfo)
- }
- }
- else -> {
- // Not an autofill call
- setResult(Activity.RESULT_CANCELED)
- finish()
- }
- }
- }
- }
-
- private fun launchSelection(database: ContextualDatabase?,
- autofillComponent: AutofillComponent?,
- searchInfo: SearchInfo) {
- if (autofillComponent == null) {
- setResult(Activity.RESULT_CANCELED)
- finish()
- } else if (KeeAutofillService.autofillAllowedFor(
- applicationId = searchInfo.applicationId,
- webDomain = searchInfo.webDomain,
- context = this
- )) {
- // If database is open
- SearchHelper.checkAutoSearchInfo(this,
- database,
- searchInfo,
- { openedDatabase, items ->
- // Items found
- AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
- finish()
- },
- { openedDatabase ->
- // Show the database UI to select the entry
- GroupActivity.launchForAutofillResult(this,
- openedDatabase,
- mAutofillActivityResultLauncher,
- autofillComponent,
- searchInfo,
- false)
- },
- {
- // If database not open
- FileDatabaseSelectActivity.launchForAutofillResult(this,
- mAutofillActivityResultLauncher,
- autofillComponent,
- searchInfo)
- }
- )
- } else {
- showBlockRestartMessage()
- setResult(Activity.RESULT_CANCELED)
- finish()
- }
- }
-
- private fun launchRegistration(database: ContextualDatabase?,
- searchInfo: SearchInfo,
- registerInfo: RegisterInfo?) {
- if (KeeAutofillService.autofillAllowedFor(
- applicationId = searchInfo.applicationId,
- webDomain = searchInfo.webDomain,
- context = this
- )) {
- val readOnly = database?.isReadOnly != false
- SearchHelper.checkAutoSearchInfo(this,
- database,
- searchInfo,
- { openedDatabase, _ ->
- if (!readOnly) {
- // Show the database UI to select the entry
- GroupActivity.launchForRegistration(this,
- openedDatabase,
- registerInfo)
- } else {
- showReadOnlySaveMessage()
- }
- },
- { openedDatabase ->
- if (!readOnly) {
- // Show the database UI to select the entry
- GroupActivity.launchForRegistration(this,
- openedDatabase,
- registerInfo)
- } else {
- showReadOnlySaveMessage()
- }
- },
- {
- // If database not open
- FileDatabaseSelectActivity.launchForRegistration(this,
- registerInfo)
- }
- )
- } else {
- showBlockRestartMessage()
- setResult(Activity.RESULT_CANCELED)
- }
- finish()
- }
-
- private fun showBlockRestartMessage() {
- // If item not allowed, show a toast
- Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
- }
-
- private fun showReadOnlySaveMessage() {
- Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
- }
-
- companion object {
-
- private val TAG = AutofillLauncherActivity::class.java.name
-
- private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
- private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
- private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
-
- private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
-
- fun getPendingIntentForSelection(context: Context,
- searchInfo: SearchInfo? = null,
- compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent? {
- try {
- return PendingIntent.getActivity(
- context, 0,
- // Doesn't work with direct extra Parcelable (don't know why?)
- // Wrap into a bundle to bypass the problem
- Intent(context, AutofillLauncherActivity::class.java).apply {
- putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
- putParcelable(KEY_SEARCH_INFO, searchInfo)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
- }
- })
- },
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
- } else {
- PendingIntent.FLAG_CANCEL_CURRENT
- }
- )
- } catch (e: RuntimeException) {
- Log.e(TAG, "Unable to create pending intent for selection", e)
- return null
- }
- }
-
- fun getPendingIntentForRegistration(context: Context,
- registerInfo: RegisterInfo): PendingIntent? {
- try {
- return PendingIntent.getActivity(
- context, 0,
- Intent(context, AutofillLauncherActivity::class.java).apply {
- EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
- putExtra(KEY_REGISTER_INFO, registerInfo)
- },
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
- } else {
- PendingIntent.FLAG_CANCEL_CURRENT
- }
- )
- } catch (e: RuntimeException) {
- Log.e(TAG, "Unable to create pending intent for registration", e)
- return null
- }
- }
-
- fun launchForRegistration(context: Context,
- registerInfo: RegisterInfo) {
- val intent = Intent(context, AutofillLauncherActivity::class.java)
- EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
- intent.putExtra(KEY_REGISTER_INFO, registerInfo)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- context.startActivity(intent)
- }
- }
-}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt
index 186288919..e061a27c2 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt
@@ -50,15 +50,15 @@ import com.google.android.material.tabs.TabLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.fragments.EntryFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
-import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TagsAdapter
+import com.kunzisoft.keepass.credentialprovider.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryActivityEducation
-import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
@@ -69,7 +69,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper
-import com.kunzisoft.keepass.utils.UuidUtil
+import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.view.WindowInsetPosition
import com.kunzisoft.keepass.view.applyWindowInsets
@@ -264,7 +264,7 @@ class EntryActivity : DatabaseLockActivity() {
mIcon = entryInfo.icon
// Assign title text
val entryTitle =
- if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
+ entryInfo.title.ifEmpty { entryInfo.id.asHexString() }
collapsingToolbarLayout?.title = entryTitle
toolbar?.title = entryTitle
// Assign tags
@@ -312,11 +312,11 @@ class EntryActivity : DatabaseLockActivity() {
mEntryViewModel.historySelected.observe(this) { historySelected ->
mDatabase?.let { database ->
launch(
- this,
- database,
- historySelected.nodeId,
- historySelected.historyPosition,
- mEntryActivityResultLauncher
+ activity = this,
+ database = database,
+ entryId = historySelected.nodeId,
+ historyPosition = historySelected.historyPosition,
+ activityResultLauncher = mEntryActivityResultLauncher
)
}
}
@@ -330,9 +330,8 @@ class EntryActivity : DatabaseLockActivity() {
return coordinatorLayout
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
-
mEntryViewModel.loadDatabase(database)
}
@@ -478,11 +477,12 @@ class EntryActivity : DatabaseLockActivity() {
R.id.menu_edit -> {
mDatabase?.let { database ->
mMainEntryId?.let { entryId ->
- EntryEditActivity.launchToUpdate(
- this,
- database,
- entryId,
- mEntryActivityResultLauncher
+ EntryEditActivity.launch(
+ activity = this,
+ database = database,
+ registrationType = EntryEditActivity.RegistrationType.UPDATE,
+ nodeId = entryId,
+ activityResultLauncher = mEntryActivityResultLauncher
)
}
}
@@ -520,7 +520,7 @@ class EntryActivity : DatabaseLockActivity() {
// Transit data in previous Activity after an update
Intent().apply {
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
- setResult(Activity.RESULT_OK, this)
+ setResult(RESULT_OK, this)
}
super.finish()
}
@@ -534,34 +534,22 @@ class EntryActivity : DatabaseLockActivity() {
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
/**
- * Open standard Entry activity
+ * Open standard or history Entry activity
*/
- fun launch(activity: Activity,
- database: ContextualDatabase,
- entryId: NodeId,
- activityResultLauncher: ActivityResultLauncher) {
+ fun launch(
+ activity: Activity,
+ database: ContextualDatabase,
+ entryId: NodeId,
+ historyPosition: Int? = null,
+ activityResultLauncher: ActivityResultLauncher
+ ) {
if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
- activityResultLauncher.launch(intent)
- }
- }
- }
-
- /**
- * Open history Entry activity
- */
- fun launch(activity: Activity,
- database: ContextualDatabase,
- entryId: NodeId,
- historyPosition: Int,
- activityResultLauncher: ActivityResultLauncher) {
- if (database.loaded) {
- if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
- val intent = Intent(activity, EntryActivity::class.java)
- intent.putExtra(KEY_ENTRY, entryId)
- intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
+ historyPosition?.let {
+ intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
+ }
activityResultLauncher.launch(intent)
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt
index f5b2d6bcb..f339459fa 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt
@@ -36,14 +36,14 @@ import android.widget.Spinner
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
-import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
-import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.timepicker.MaterialTimePicker
@@ -55,22 +55,24 @@ import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Compani
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
-import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
-import com.kunzisoft.keepass.autofill.AutofillComponent
-import com.kunzisoft.keepass.autofill.AutofillHelper
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildSpecialModeResponseAndSetResult
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
+import com.kunzisoft.keepass.credentialprovider.TypeMode
+import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Field
-import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.education.EntryEditActivityEducation
-import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.model.EntryAttachmentState
@@ -79,9 +81,9 @@ import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
-import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getNewEntry
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -100,6 +102,7 @@ import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
+import kotlinx.coroutines.launch
import java.util.EnumSet
import java.util.UUID
@@ -155,9 +158,6 @@ class EntryEditActivity : DatabaseLockActivity(),
}
}
- // To ask data lost only one time
- private var backPressedAlreadyApproved = false
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_entry_edit)
@@ -210,8 +210,8 @@ class EntryEditActivity : DatabaseLockActivity(),
mDatabase,
entryId,
parentId,
- EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent),
- EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
+ intent.retrieveRegisterInfo()
+ ?: intent.retrieveSearchInfo()?.toRegisterInfo()
)
// To retrieve attachment
@@ -378,23 +378,30 @@ class EntryEditActivity : DatabaseLockActivity(),
} ?: run {
updateEntry(entrySave.oldEntry, entrySave.newEntry)
}
+ }
- // Don't wait for saving if it's to provide autofill
- mDatabase?.let { database ->
- EntrySelectionHelper.doSpecialAction(intent,
- {},
- {},
- {},
- {
- entryValidatedForKeyboardSelection(database, entrySave.newEntry)
- },
- { _, _ ->
- entryValidatedForAutofillSelection(database, entrySave.newEntry)
- },
- {
- entryValidatedForAutofillRegistration(entrySave.newEntry)
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ mEntryEditViewModel.uiState.collect { uiState ->
+ when (uiState) {
+ EntryEditViewModel.UIState.Loading -> {}
+ EntryEditViewModel.UIState.ShowOverwriteMessage -> {
+ if (mEntryEditViewModel.warningOverwriteDataAlreadyApproved.not()) {
+ AlertDialog.Builder(this@EntryEditActivity)
+ .setTitle(R.string.warning_overwrite_data_title)
+ .setMessage(R.string.warning_overwrite_data_description)
+ .setNegativeButton(android.R.string.cancel) { _, _ ->
+ mEntryEditViewModel.backPressedAlreadyApproved = true
+ onCancelSpecialMode()
+ }
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ mEntryEditViewModel.warningOverwriteDataAlreadyApproved = true
+ }
+ .create().show()
+ }
+ }
}
- )
+ }
}
}
}
@@ -407,13 +414,13 @@ class EntryEditActivity : DatabaseLockActivity(),
return true
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
- mAllowCustomFields = database?.allowEntryCustomFields() == true
- mAllowOTP = database?.allowOTP == true
- mEntryEditViewModel.loadDatabase(database)
+ mAllowCustomFields = database.allowEntryCustomFields() == true
+ mAllowOTP = database.allowOTP == true
+ mEntryEditViewModel.loadTemplateEntry(database)
mTemplatesSelectorAdapter?.apply {
- iconDrawableFactory = mDatabase?.iconDrawableFactory
+ iconDrawableFactory = database.iconDrawableFactory
notifyDataSetChanged()
}
}
@@ -424,39 +431,45 @@ class EntryEditActivity : DatabaseLockActivity(),
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
+ mEntryEditViewModel.unlockAction()
when (actionTask) {
ACTION_DATABASE_CREATE_ENTRY_TASK,
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
try {
if (result.isSuccess) {
- var newNodes: List = ArrayList()
- result.data?.getBundle(DatabaseTaskNotificationService.NEW_NODES_KEY)?.let { newNodesBundle ->
- newNodes = DatabaseTaskNotificationService.getListNodesFromBundle(database, newNodesBundle)
- }
- if (newNodes.size == 1) {
- (newNodes[0] as? Entry?)?.let { entry ->
- EntrySelectionHelper.doSpecialAction(intent,
- {
- // Finish naturally
- finishForEntryResult(entry)
- },
- {
- // Nothing when search retrieved
- },
- {
- entryValidatedForSave(entry)
- },
- {
- entryValidatedForKeyboardSelection(database, entry)
- },
- { _, _ ->
- entryValidatedForAutofillSelection(database, entry)
- },
- {
- entryValidatedForAutofillRegistration(entry)
+ result.data?.getNewEntry(database)?.let { entry ->
+ EntrySelectionHelper.doSpecialAction(
+ intent = intent,
+ defaultAction = {
+ // Finish naturally
+ finishForEntryResult(entry)
+ },
+ searchAction = {
+ // Nothing when search retrieved
+ },
+ selectionAction = { intentSender, typeMode, searchInfo ->
+ when(typeMode) {
+ TypeMode.DEFAULT -> {}
+ TypeMode.MAGIKEYBOARD ->
+ entryValidatedForKeyboardSelection(database, entry)
+ TypeMode.PASSKEY ->
+ entryValidatedForPasskey(database, entry)
+ TypeMode.AUTOFILL ->
+ entryValidatedForAutofill(database, entry)
}
- )
- }
+ },
+ registrationAction = { _, typeMode, _ ->
+ when(typeMode) {
+ TypeMode.DEFAULT ->
+ entryValidatedForSave(entry)
+ TypeMode.MAGIKEYBOARD -> {}
+ TypeMode.PASSKEY ->
+ entryValidatedForPasskey(database, entry)
+ TypeMode.AUTOFILL ->
+ entryValidatedForAutofill(database, entry)
+ }
+ }
+ )
}
}
} catch (e: Exception) {
@@ -483,19 +496,25 @@ class EntryEditActivity : DatabaseLockActivity(),
finishForEntryResult(entry)
}
- private fun entryValidatedForAutofillSelection(database: ContextualDatabase, entry: Entry) {
+ private fun entryValidatedForAutofill(database: ContextualDatabase, entry: Entry) {
// Build Autofill response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity,
- database,
- entry.getEntryInfo(database))
+ this.buildSpecialModeResponseAndSetResult(
+ entryInfo = entry.getEntryInfo(database),
+ extras = buildEntryResult(entry)
+ )
}
onValidateSpecialMode()
}
- private fun entryValidatedForAutofillRegistration(entry: Entry) {
+ private fun entryValidatedForPasskey(database: ContextualDatabase, entry: Entry) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ this.buildPasskeyResponseAndSetResult(
+ entryInfo = entry.getEntryInfo(database),
+ extras = buildEntryResult(entry) // To update the previous screen
+ )
+ }
onValidateSpecialMode()
- finishForEntryResult(entry)
}
override fun onResume() {
@@ -729,13 +748,13 @@ class EntryEditActivity : DatabaseLockActivity(),
}
private fun onApprovedBackPressed(approved: () -> Unit) {
- if (!backPressedAlreadyApproved) {
+ if (mEntryEditViewModel.backPressedAlreadyApproved.not()) {
AlertDialog.Builder(this)
.setMessage(R.string.discard_changes)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.discard) { _, _ ->
mAttachmentFileBinderManager?.stopUploadAllAttachments()
- backPressedAlreadyApproved = true
+ mEntryEditViewModel.backPressedAlreadyApproved = true
approved.invoke()
}.create().show()
} else {
@@ -743,14 +762,19 @@ class EntryEditActivity : DatabaseLockActivity(),
}
}
+ private fun buildEntryResult(entry: Entry): Bundle {
+ return Bundle().apply {
+ putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
+ }
+ }
+
private fun finishForEntryResult(entry: Entry) {
// Assign entry callback as a result
try {
- val bundle = Bundle()
+ val bundle = buildEntryResult(entry)
val intentEntry = Intent()
- bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
intentEntry.putExtras(bundle)
- setResult(Activity.RESULT_OK, intentEntry)
+ setResult(RESULT_OK, intentEntry)
super.finish()
} catch (e: Exception) {
// Exception when parcelable can't be done
@@ -758,6 +782,10 @@ class EntryEditActivity : DatabaseLockActivity(),
}
}
+ enum class RegistrationType {
+ UPDATE, CREATE
+ }
+
companion object {
private val TAG = EntryEditActivity::class.java.name
@@ -767,23 +795,12 @@ class EntryEditActivity : DatabaseLockActivity(),
const val KEY_PARENT = "parent"
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
- fun registerForEntryResult(fragment: Fragment,
- entryAddedOrUpdatedListener: (NodeId?) -> Unit): ActivityResultLauncher {
- return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- if (result.resultCode == Activity.RESULT_OK) {
- entryAddedOrUpdatedListener.invoke(
- result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
- )
- } else {
- entryAddedOrUpdatedListener.invoke(null)
- }
- }
- }
-
- fun registerForEntryResult(activity: FragmentActivity,
- entryAddedOrUpdatedListener: (NodeId?) -> Unit): ActivityResultLauncher {
+ fun registerForEntryResult(
+ activity: FragmentActivity,
+ entryAddedOrUpdatedListener: (NodeId?) -> Unit
+ ): ActivityResultLauncher {
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- if (result.resultCode == Activity.RESULT_OK) {
+ if (result.resultCode == RESULT_OK) {
entryAddedOrUpdatedListener.invoke(
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
)
@@ -794,151 +811,78 @@ class EntryEditActivity : DatabaseLockActivity(),
}
/**
- * Launch EntryEditActivity to update an existing entry by his [entryId]
+ * Launch EntryEditActivity to update an existing entry or to add a new entry in an existing group
*/
- fun launchToUpdate(activity: Activity,
- database: ContextualDatabase,
- entryId: NodeId,
- activityResultLauncher: ActivityResultLauncher) {
+ fun launch(
+ activity: Activity,
+ database: ContextualDatabase,
+ registrationType: RegistrationType,
+ nodeId: NodeId<*>,
+ activityResultLauncher: ActivityResultLauncher
+ ) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
- intent.putExtra(KEY_ENTRY, entryId)
+ when (registrationType) {
+ RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
+ RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
+ }
activityResultLauncher.launch(intent)
}
}
}
/**
- * Launch EntryEditActivity to add a new entry in an existent group
+ * Launch EntryEditActivity to add a new entry in special selection
*/
- fun launchToCreate(activity: Activity,
- database: ContextualDatabase,
- groupId: NodeId<*>,
- activityResultLauncher: ActivityResultLauncher) {
- if (database.loaded && !database.isReadOnly) {
- if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
- val intent = Intent(activity, EntryEditActivity::class.java)
- intent.putExtra(KEY_PARENT, groupId)
- activityResultLauncher.launch(intent)
- }
- }
- }
-
- fun launchToUpdateForSave(context: Context,
- database: ContextualDatabase,
- entryId: NodeId,
- searchInfo: SearchInfo) {
- if (database.loaded && !database.isReadOnly) {
- if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
- val intent = Intent(context, EntryEditActivity::class.java)
- intent.putExtra(KEY_ENTRY, entryId)
- EntrySelectionHelper.startActivityForSaveModeResult(
- context,
- intent,
- searchInfo
- )
- }
- }
- }
-
- fun launchToCreateForSave(context: Context,
- database: ContextualDatabase,
- groupId: NodeId<*>,
- searchInfo: SearchInfo) {
+ fun launchForSelection(
+ context: Context,
+ database: ContextualDatabase,
+ typeMode: TypeMode,
+ groupId: NodeId<*>,
+ searchInfo: SearchInfo? = null,
+ activityResultLauncher: ActivityResultLauncher? = null,
+ ) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
- EntrySelectionHelper.startActivityForSaveModeResult(
- context,
- intent,
- searchInfo
+ EntrySelectionHelper.startActivityForSelectionModeResult(
+ context = context,
+ intent = intent,
+ typeMode = typeMode,
+ searchInfo = searchInfo,
+ activityResultLauncher = activityResultLauncher
)
}
}
}
/**
- * Launch EntryEditActivity to add a new entry in keyboard selection
+ * Launch EntryEditActivity to update an updated entry or register a new entry (from autofill)
*/
- fun launchForKeyboardSelectionResult(context: Context,
- database: ContextualDatabase,
- groupId: NodeId<*>,
- searchInfo: SearchInfo? = null) {
+ fun launchForRegistration(
+ context: Context,
+ database: ContextualDatabase,
+ nodeId: NodeId<*>,
+ registerInfo: RegisterInfo? = null,
+ typeMode: TypeMode,
+ registrationType: RegistrationType,
+ activityResultLauncher: ActivityResultLauncher? = null,
+ ) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
- intent.putExtra(KEY_PARENT, groupId)
- EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
+ when (registrationType) {
+ RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
+ RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
+ }
+ EntrySelectionHelper.startActivityForRegistrationModeResult(
context,
- intent,
- searchInfo
- )
- }
- }
- }
-
- /**
- * Launch EntryEditActivity to add a new entry in autofill selection
- */
- @RequiresApi(api = Build.VERSION_CODES.O)
- fun launchForAutofillResult(activity: AppCompatActivity,
- database: ContextualDatabase,
- activityResultLauncher: ActivityResultLauncher?,
- autofillComponent: AutofillComponent,
- groupId: NodeId<*>,
- searchInfo: SearchInfo? = null) {
- if (database.loaded && !database.isReadOnly) {
- if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
- val intent = Intent(activity, EntryEditActivity::class.java)
- intent.putExtra(KEY_PARENT, groupId)
- AutofillHelper.startActivityForAutofillResult(
- activity,
- intent,
activityResultLauncher,
- autofillComponent,
- searchInfo
- )
- }
- }
- }
-
- /**
- * Launch EntryEditActivity to register an updated entry (from autofill)
- */
- fun launchToUpdateForRegistration(context: Context,
- database: ContextualDatabase,
- entryId: NodeId,
- registerInfo: RegisterInfo? = null) {
- if (database.loaded && !database.isReadOnly) {
- if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
- val intent = Intent(context, EntryEditActivity::class.java)
- intent.putExtra(KEY_ENTRY, entryId)
- EntrySelectionHelper.startActivityForRegistrationModeResult(
- context,
intent,
- registerInfo
- )
- }
- }
- }
-
- /**
- * Launch EntryEditActivity to register a new entry (from autofill)
- */
- fun launchToCreateForRegistration(context: Context,
- database: ContextualDatabase,
- groupId: NodeId<*>,
- registerInfo: RegisterInfo? = null) {
- if (database.loaded && !database.isReadOnly) {
- if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
- val intent = Intent(context, EntryEditActivity::class.java)
- intent.putExtra(KEY_PARENT, groupId)
- EntrySelectionHelper.startActivityForRegistrationModeResult(
- context,
- intent,
- registerInfo
+ registerInfo,
+ typeMode
)
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt
index dffc3c586..3c8ebc179 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt
@@ -19,7 +19,6 @@
*/
package com.kunzisoft.keepass.activities
-import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
@@ -33,8 +32,6 @@ import android.view.MenuItem
import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
-import androidx.annotation.RequiresApi
-import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
@@ -44,15 +41,14 @@ import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
-import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
-import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
-import com.kunzisoft.keepass.autofill.AutofillComponent
-import com.kunzisoft.keepass.autofill.AutofillHelper
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
+import com.kunzisoft.keepass.credentialprovider.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
@@ -65,10 +61,10 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
+import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
import com.kunzisoft.keepass.utils.DexUtil
import com.kunzisoft.keepass.utils.MagikeyboardUtil
import com.kunzisoft.keepass.utils.MenuUtil
-import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
import com.kunzisoft.keepass.utils.UriUtil.openUrl
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.asError
@@ -98,11 +94,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
private var mExternalFileHelper: ExternalFileHelper? = null
- private var mAutofillActivityResultLauncher: ActivityResultLauncher? =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
- AutofillHelper.buildActivityResultLauncher(this)
- else null
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -132,7 +123,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let {
- launchPasswordActivityWithPath(uri)
+ launchMainCredentialActivityWithPath(uri)
}
}
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
@@ -161,7 +152,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
- launchPasswordActivity(
+ launchMainCredentialActivity(
databaseFileUri,
fileDatabaseHistoryEntityToOpen.keyFileUri,
fileDatabaseHistoryEntityToOpen.hardwareKey
@@ -180,7 +171,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
// Load default database the first time
databaseFilesViewModel.doForDefaultDatabase { databaseFileUri ->
- launchPasswordActivityWithPath(databaseFileUri)
+ launchMainCredentialActivityWithPath(databaseFileUri)
}
// Retrieve the database URI provided by file manager after an orientation change
@@ -225,11 +216,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
- if (database != null) {
- launchGroupActivityIfLoaded(database)
- }
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
+ launchGroupActivityIfLoaded(database)
}
override fun onDatabaseActionFinished(
@@ -237,8 +225,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
actionTask: String,
result: ActionRunnable.Result
) {
- super.onDatabaseActionFinished(database, actionTask, result)
-
if (result.isSuccess) {
// Update list
when (actionTask) {
@@ -288,17 +274,58 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
}
- private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
- MainCredentialActivity.launch(this,
- databaseUri,
- keyFile,
- hardwareKey,
- { exception ->
- fileNoFoundAction(exception)
+ private fun launchMainCredentialActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
+ try {
+ EntrySelectionHelper.doSpecialAction(
+ intent = this.intent,
+ defaultAction = {
+ MainCredentialActivity.launch(
+ activity = this,
+ databaseFile = databaseUri,
+ keyFile = keyFile,
+ hardwareKey = hardwareKey
+ )
},
- { onCancelSpecialMode() },
- { onLaunchActivitySpecialMode() },
- mAutofillActivityResultLauncher)
+ searchAction = { searchInfo ->
+ MainCredentialActivity.launchForSearchResult(
+ activity = this,
+ databaseFile = databaseUri,
+ keyFile = keyFile,
+ hardwareKey = hardwareKey,
+ searchInfo = searchInfo
+ )
+ onLaunchActivitySpecialMode()
+ },
+ selectionAction = { intentSenderMode, typeMode, searchInfo ->
+ MainCredentialActivity.launchForSelection(
+ activity = this,
+ activityResultLauncher = if (intentSenderMode)
+ mCredentialActivityResultLauncher else null,
+ databaseFile = databaseUri,
+ keyFile = keyFile,
+ hardwareKey = hardwareKey,
+ typeMode = typeMode,
+ searchInfo = searchInfo
+ )
+ onLaunchActivitySpecialMode()
+ },
+ registrationAction = { intentSenderMode, typeMode, registerInfo ->
+ MainCredentialActivity.launchForRegistration(
+ activity = this,
+ activityResultLauncher = if (intentSenderMode)
+ mCredentialActivityResultLauncher else null,
+ databaseFile = databaseUri,
+ keyFile = keyFile,
+ hardwareKey = hardwareKey,
+ typeMode = typeMode,
+ registerInfo = registerInfo
+ )
+ onLaunchActivitySpecialMode()
+ }
+ )
+ } catch (e: FileNotFoundException) {
+ fileNoFoundAction(e)
+ }
}
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
@@ -308,12 +335,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
{ onValidateSpecialMode() },
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() },
- mAutofillActivityResultLauncher)
+ mCredentialActivityResultLauncher
+ )
}
}
- private fun launchPasswordActivityWithPath(databaseUri: Uri) {
- launchPasswordActivity(databaseUri, null, null)
+ private fun launchMainCredentialActivityWithPath(databaseUri: Uri) {
+ launchMainCredentialActivity(databaseUri, null, null)
// Delete flickering for kitkat <=
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
@@ -337,10 +365,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
}
- mDatabase?.let { database ->
- launchGroupActivityIfLoaded(database)
- }
-
// Show recent files if allowed
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
databaseFilesViewModel.loadListOfDatabases()
@@ -359,7 +383,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
try {
mDatabaseFileUri?.let { databaseUri ->
// Create the new database
- createDatabase(databaseUri, mainCredential)
+ mDatabaseViewModel.createDatabase(databaseUri, mainCredential)
}
} catch (e: Exception) {
val error = getString(R.string.error_create_database_file)
@@ -443,55 +467,36 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
* -------------------------
*/
- fun launchForSearchResult(context: Context,
- searchInfo: SearchInfo) {
- EntrySelectionHelper.startActivityForSearchModeResult(context,
- Intent(context, FileDatabaseSelectActivity::class.java),
- searchInfo)
+ fun launchForSearchResult(
+ context: Context,
+ searchInfo: SearchInfo
+ ) {
+ EntrySelectionHelper.startActivityForSearchModeResult(
+ context = context,
+ intent = Intent(context, FileDatabaseSelectActivity::class.java),
+ searchInfo = searchInfo
+ )
}
/*
* -------------------------
- * Save Launch
+ * Selection Launch
* -------------------------
*/
- fun launchForSaveResult(context: Context,
- searchInfo: SearchInfo) {
- EntrySelectionHelper.startActivityForSaveModeResult(context,
- Intent(context, FileDatabaseSelectActivity::class.java),
- searchInfo)
- }
-
- /*
- * -------------------------
- * Keyboard Launch
- * -------------------------
- */
-
- fun launchForKeyboardSelectionResult(activity: Activity,
- searchInfo: SearchInfo? = null) {
- EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(activity,
- Intent(activity, FileDatabaseSelectActivity::class.java),
- searchInfo)
- }
-
- /*
- * -------------------------
- * Autofill Launch
- * -------------------------
- */
-
- @RequiresApi(api = Build.VERSION_CODES.O)
- fun launchForAutofillResult(activity: AppCompatActivity,
- activityResultLauncher: ActivityResultLauncher?,
- autofillComponent: AutofillComponent,
- searchInfo: SearchInfo? = null) {
- AutofillHelper.startActivityForAutofillResult(activity,
- Intent(activity, FileDatabaseSelectActivity::class.java),
- activityResultLauncher,
- autofillComponent,
- searchInfo)
+ fun launchForSelection(
+ context: Context,
+ typeMode: TypeMode,
+ searchInfo: SearchInfo? = null,
+ activityResultLauncher: ActivityResultLauncher? = null,
+ ) {
+ EntrySelectionHelper.startActivityForSelectionModeResult(
+ context = context,
+ intent = Intent(context, FileDatabaseSelectActivity::class.java),
+ searchInfo = searchInfo,
+ typeMode = typeMode,
+ activityResultLauncher = activityResultLauncher
+ )
}
/*
@@ -499,11 +504,19 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
* Registration Launch
* -------------------------
*/
- fun launchForRegistration(context: Context,
- registerInfo: RegisterInfo? = null) {
- EntrySelectionHelper.startActivityForRegistrationModeResult(context,
- Intent(context, FileDatabaseSelectActivity::class.java),
- registerInfo)
+ fun launchForRegistration(
+ context: Context,
+ typeMode: TypeMode,
+ registerInfo: RegisterInfo? = null,
+ activityResultLauncher: ActivityResultLauncher?,
+ ) {
+ EntrySelectionHelper.startActivityForRegistrationModeResult(
+ context = context,
+ activityResultLauncher = activityResultLauncher,
+ intent = Intent(context, FileDatabaseSelectActivity::class.java),
+ registerInfo = registerInfo,
+ typeMode = typeMode
+ )
}
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
index c82207b3e..f0bba2b56 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
@@ -18,7 +18,6 @@
*/
package com.kunzisoft.keepass.activities
-import android.app.Activity
import android.app.SearchManager
import android.content.ComponentName
import android.content.Context
@@ -39,10 +38,8 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
-import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
-import androidx.annotation.RequiresApi
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
@@ -64,13 +61,19 @@ import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
import com.kunzisoft.keepass.activities.dialogs.MainCredentialDialogFragment
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.fragments.GroupFragment
-import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
-import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.BreadcrumbAdapter
-import com.kunzisoft.keepass.autofill.AutofillComponent
-import com.kunzisoft.keepass.autofill.AutofillHelper
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildSpecialModeResponseAndSetResult
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
+import com.kunzisoft.keepass.credentialprovider.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.TypeMode
+import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.element.DateInstant
@@ -81,17 +84,17 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.Type
+import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper
+import com.kunzisoft.keepass.database.helper.SearchHelper.getSearchParametersFromSearchInfo
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.education.GroupActivityEducation
-import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
-import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
-import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getNewEntry
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.settings.SettingsActivity
import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -114,6 +117,7 @@ import com.kunzisoft.keepass.view.applyWindowInsets
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.setTransparentNavigationBar
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
+import com.kunzisoft.keepass.view.toastError
import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
import com.kunzisoft.keepass.viewmodels.GroupViewModel
@@ -170,6 +174,7 @@ class GroupActivity : DatabaseLockActivity(),
// Manage group
private var mSearchState: SearchState? = null
private var mAutoSearch: Boolean = false // To mainly manage keyboard
+ private var mTempSearchInfo: Boolean = false // To manage temp search
private var mMainGroupState: GroupState? = null // Group state, not a search
private var mRootGroup: Group? = null // Root group in the tree
private var mMainGroup: Group? = null // Main group currently in memory
@@ -211,6 +216,7 @@ class GroupActivity : DatabaseLockActivity(),
private val mOnSearchActionExpandListener = object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(p0: MenuItem): Boolean {
searchFiltersView?.visibility = View.VISIBLE
+ searchFiltersView?.showSearchExpandButton(!mTempSearchInfo)
searchView?.setOnQueryTextListener(mOnSearchQueryTextListener)
searchFiltersView?.onParametersChangeListener = mOnSearchFiltersChangeListener
@@ -255,6 +261,7 @@ class GroupActivity : DatabaseLockActivity(),
private fun removeSearch() {
mSearchState = null
+ mTempSearchInfo = false
intent.removeExtra(AUTO_SEARCH_KEY)
if (Intent.ACTION_SEARCH == intent.action) {
intent.action = Intent.ACTION_DEFAULT
@@ -267,11 +274,6 @@ class GroupActivity : DatabaseLockActivity(),
mGroupEditViewModel.selectIcon(icon)
}
- private var mAutofillActivityResultLauncher: ActivityResultLauncher? =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
- AutofillHelper.buildActivityResultLauncher(this)
- else null
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -499,59 +501,44 @@ class GroupActivity : DatabaseLockActivity(),
addNodeButtonView?.setAddEntryClickListener {
mDatabase?.let { database ->
mMainGroup?.let { currentGroup ->
- EntrySelectionHelper.doSpecialAction(intent,
- {
+ EntrySelectionHelper.doSpecialAction(
+ intent = intent,
+ defaultAction = {
mMainGroup?.nodeId?.let { currentParentGroupId ->
- EntryEditActivity.launchToCreate(
- this@GroupActivity,
- database,
- currentParentGroupId,
- mEntryActivityResultLauncher
+ EntryEditActivity.launch(
+ activity = this@GroupActivity,
+ database = database,
+ registrationType = EntryEditActivity.RegistrationType.CREATE,
+ nodeId = currentParentGroupId,
+ activityResultLauncher = mEntryActivityResultLauncher
)
}
},
- {
+ searchAction = {
// Search not used
},
- { searchInfo ->
- EntryEditActivity.launchToCreateForSave(
- this@GroupActivity,
- database,
- currentGroup.nodeId,
- searchInfo
+ selectionAction = { intentSenderMode, typeMode, searchInfo ->
+ EntryEditActivity.launchForSelection(
+ context = this@GroupActivity,
+ database = database,
+ typeMode = typeMode,
+ groupId = currentGroup.nodeId,
+ searchInfo = searchInfo,
+ activityResultLauncher = if (intentSenderMode)
+ mCredentialActivityResultLauncher else null
)
onLaunchActivitySpecialMode()
},
- { searchInfo ->
- EntryEditActivity.launchForKeyboardSelectionResult(
- this@GroupActivity,
- database,
- currentGroup.nodeId,
- searchInfo
- )
- onLaunchActivitySpecialMode()
- },
- { searchInfo, autofillComponent ->
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- EntryEditActivity.launchForAutofillResult(
- this@GroupActivity,
- database,
- mAutofillActivityResultLauncher,
- autofillComponent,
- currentGroup.nodeId,
- searchInfo
- )
- onLaunchActivitySpecialMode()
- } else {
- onCancelSpecialMode()
- }
- },
- { searchInfo ->
- EntryEditActivity.launchToCreateForRegistration(
- this@GroupActivity,
- database,
- currentGroup.nodeId,
- searchInfo
+ registrationAction = { intentSenderMode, typeMode, registerInfo ->
+ EntryEditActivity.launchForRegistration(
+ context = this@GroupActivity,
+ database = database,
+ nodeId = currentGroup.nodeId,
+ registerInfo = registerInfo,
+ typeMode = typeMode,
+ registrationType = EntryEditActivity.RegistrationType.CREATE,
+ activityResultLauncher = if (intentSenderMode)
+ mCredentialActivityResultLauncher else null
)
onLaunchActivitySpecialMode()
}
@@ -595,7 +582,7 @@ class GroupActivity : DatabaseLockActivity(),
}
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mBreadcrumbAdapter = BreadcrumbAdapter(this, database).apply {
@@ -635,18 +622,16 @@ class GroupActivity : DatabaseLockActivity(),
adapter = mBreadcrumbAdapter
}
- mGroupEditViewModel.setGroupNamesNotAllowed(database?.groupNamesNotAllowed)
+ mGroupEditViewModel.setGroupNamesNotAllowed(database.groupNamesNotAllowed)
mRecyclingBinEnabled = !mDatabaseReadOnly
- && database?.isRecycleBinEnabled == true
+ && database.isRecycleBinEnabled == true
- mRootGroup = database?.rootGroup
+ mRootGroup = database.rootGroup
loadGroup()
// Update view
- database?.let {
- mBreadcrumbAdapter?.iconDrawableFactory = it.iconDrawableFactory
- }
+ mBreadcrumbAdapter?.iconDrawableFactory = database.iconDrawableFactory
refreshDatabaseViews()
invalidateOptionsMenu()
}
@@ -684,9 +669,7 @@ class GroupActivity : DatabaseLockActivity(),
var entry: Entry? = null
try {
- result.data?.getBundle(NEW_NODES_KEY)?.let { newNodesBundle ->
- entry = getListNodesFromBundle(database, newNodesBundle)[0] as Entry
- }
+ entry = result.data?.getNewEntry(database)
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve entry action for selection", e)
}
@@ -694,30 +677,30 @@ class GroupActivity : DatabaseLockActivity(),
when (actionTask) {
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
if (result.isSuccess) {
- EntrySelectionHelper.doSpecialAction(intent,
- {
+ EntrySelectionHelper.doSpecialAction(
+ intent = intent,
+ defaultAction = {
// Standard not used after task
},
- {
+ searchAction = {
// Search not used
},
- {
+ selectionAction = { intentSenderMode, typeMode, searchInfo ->
+ when (typeMode) {
+ TypeMode.DEFAULT -> {}
+ TypeMode.MAGIKEYBOARD -> entry?.let {
+ entrySelectedForKeyboardSelection(database, it)
+ }
+ TypeMode.PASSKEY -> entry?.let {
+ entrySelectedForPasskeySelection(database, it)
+ }
+ TypeMode.AUTOFILL -> entry?.let {
+ entrySelectedForAutofillSelection(database, it)
+ }
+ }
+ },
+ registrationAction = { intentSenderMode, typeMode, searchInfo ->
// Save not used
- },
- {
- // Keyboard selection
- entry?.let {
- entrySelectedForKeyboardSelection(database, it)
- }
- },
- { _, _ ->
- // Autofill selection
- entry?.let {
- entrySelectedForAutofillSelection(database, it)
- }
- },
- {
- // Not use
}
)
}
@@ -731,37 +714,34 @@ class GroupActivity : DatabaseLockActivity(),
finishNodeAction()
}
- /**
- * Transform the AUTO_SEARCH_KEY in ACTION_SEARCH, return true if AUTO_SEARCH_KEY was present
- */
- private fun transformSearchInfoIntent(intent: Intent) {
- // To relaunch the activity as ACTION_SEARCH
- val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
- val autoSearch = intent.getBooleanExtra(AUTO_SEARCH_KEY, false)
- intent.removeExtra(AUTO_SEARCH_KEY)
- if (searchInfo != null && autoSearch) {
- intent.action = Intent.ACTION_SEARCH
- intent.putExtra(SearchManager.QUERY, searchInfo.toString())
- }
- }
-
private fun manageIntent(intent: Intent?) {
intent?.let {
if (intent.extras?.containsKey(GROUP_STATE_KEY) == true) {
mMainGroupState = intent.getParcelableExtraCompat(GROUP_STATE_KEY)
intent.removeExtra(GROUP_STATE_KEY)
}
- // To transform KEY_SEARCH_INFO in ACTION_SEARCH
- transformSearchInfoIntent(intent)
+ // To get the form filling search as temp search
+ val searchInfo: SearchInfo? = intent.retrieveSearchInfo()
+ val autoSearch = intent.getBooleanExtra(AUTO_SEARCH_KEY, false)
// Get search query
- if (intent.action == Intent.ACTION_SEARCH) {
+ if (searchInfo != null && autoSearch) {
mAutoSearch = true
- val stringQuery = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
- intent.action = Intent.ACTION_DEFAULT
- intent.removeExtra(SearchManager.QUERY)
- mSearchState = SearchState(PreferencesUtil.getDefaultSearchParameters(this).apply {
- searchQuery = stringQuery
- }, mSearchState?.firstVisibleItem ?: 0)
+ mTempSearchInfo = true
+ searchInfo.getSearchParametersFromSearchInfo(this) {
+ mSearchState = SearchState(
+ searchParameters = it,
+ firstVisibleItem = mSearchState?.firstVisibleItem ?: 0
+ )
+ }
+ } else if (intent.action == Intent.ACTION_SEARCH) {
+ mAutoSearch = true
+ mSearchState = SearchState(
+ searchParameters = PreferencesUtil.getDefaultSearchParameters(this).apply {
+ searchQuery = intent.getStringExtra(SearchManager.QUERY)
+ ?.trim { it <= ' ' } ?: ""
+ },
+ firstVisibleItem = mSearchState?.firstVisibleItem ?: 0
+ )
} else if (mRequestStartupSearch
&& PreferencesUtil.automaticallyFocusSearch(this@GroupActivity)) {
// Expand the search view if defined in settings
@@ -769,6 +749,8 @@ class GroupActivity : DatabaseLockActivity(),
mRequestStartupSearch = false
addSearch()
}
+ intent.action = Intent.ACTION_DEFAULT
+ intent.removeExtra(SearchManager.QUERY)
}
}
@@ -793,8 +775,9 @@ class GroupActivity : DatabaseLockActivity(),
// Assign title
if (group?.isVirtual == true) {
searchFiltersView?.setNumbers(group.numberOfChildEntries)
- searchFiltersView?.setCurrentGroupText(mMainGroup?.title ?: "")
+ searchFiltersView?.setCurrentGroupText(mMainGroup?.title ?: getString(R.string.search))
searchFiltersView?.availableOther(mDatabase?.allowEntryCustomFields() ?: false)
+ searchFiltersView?.availableApplicationIds(mDatabase?.allowEntryCustomFields() ?: false)
searchFiltersView?.availableTags(mDatabase?.allowTags() ?: false)
searchFiltersView?.enableTags(mDatabase?.tagPool?.isNotEmpty() ?: false)
searchFiltersView?.availableSearchableGroup(mDatabase?.allowCustomSearchableGroup() ?: false)
@@ -861,49 +844,65 @@ class GroupActivity : DatabaseLockActivity(),
Type.ENTRY -> try {
val entryVersioned = node as Entry
- EntrySelectionHelper.doSpecialAction(intent,
- {
+ EntrySelectionHelper.doSpecialAction(
+ intent = intent,
+ defaultAction = {
EntryActivity.launch(
- this@GroupActivity,
- database,
- entryVersioned.nodeId,
- mEntryActivityResultLauncher
+ activity = this@GroupActivity,
+ database = database,
+ entryId = entryVersioned.nodeId,
+ activityResultLauncher = mEntryActivityResultLauncher
)
// Do not reload group here
},
- {
+ searchAction = {
// Nothing here, a search is simply performed
},
- { searchInfo ->
- if (!database.isReadOnly) {
- entrySelectedForSave(database, entryVersioned, searchInfo)
- loadGroup()
- } else
- finish()
- },
- { searchInfo ->
- if (!database.isReadOnly
- && searchInfo != null
- && PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity)
- ) {
- updateEntryWithSearchInfo(database, entryVersioned, searchInfo)
+ selectionAction = { intentSenderMode, typeMode, searchInfo ->
+ when (typeMode) {
+ TypeMode.DEFAULT -> {}
+ TypeMode.MAGIKEYBOARD -> {
+ if (!database.isReadOnly
+ && searchInfo != null
+ && PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity)
+ ) {
+ updateEntryWithRegisterInfo(
+ database,
+ entryVersioned,
+ searchInfo.toRegisterInfo()
+ )
+ }
+ entrySelectedForKeyboardSelection(database, entryVersioned)
+ }
+ TypeMode.PASSKEY -> {
+ entrySelectedForPasskeySelection(database, entryVersioned)
+ }
+ TypeMode.AUTOFILL -> {
+ if (!database.isReadOnly
+ && searchInfo != null
+ && PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)
+ ) {
+ updateEntryWithRegisterInfo(
+ database,
+ entryVersioned,
+ searchInfo.toRegisterInfo()
+ )
+ }
+ entrySelectedForAutofillSelection(database, entryVersioned)
+ }
}
- entrySelectedForKeyboardSelection(database, entryVersioned)
loadGroup()
},
- { searchInfo, _ ->
- if (!database.isReadOnly
- && searchInfo != null
- && PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)
- ) {
- updateEntryWithSearchInfo(database, entryVersioned, searchInfo)
- }
- entrySelectedForAutofillSelection(database, entryVersioned)
- loadGroup()
- },
- { registerInfo ->
+ registrationAction = { intentSenderMode, typeMode, registerInfo ->
if (!database.isReadOnly) {
- entrySelectedForRegistration(database, entryVersioned, registerInfo)
+ entrySelectedForRegistration(
+ database = database,
+ entry = entryVersioned,
+ registerInfo = registerInfo,
+ typeMode = typeMode,
+ activityResultLauncher = if (intentSenderMode)
+ mCredentialActivityResultLauncher else null
+ )
loadGroup()
} else
finish()
@@ -914,18 +913,6 @@ class GroupActivity : DatabaseLockActivity(),
}
}
- private fun entrySelectedForSave(database: ContextualDatabase, entry: Entry, searchInfo: SearchInfo) {
- removeSearch()
- // Save to update the entry
- EntryEditActivity.launchToUpdateForSave(
- this@GroupActivity,
- database,
- entry.nodeId,
- searchInfo
- )
- onLaunchActivitySpecialMode()
- }
-
private fun entrySelectedForKeyboardSelection(database: ContextualDatabase, entry: Entry) {
removeSearch()
// Populate Magikeyboard with entry
@@ -940,10 +927,17 @@ class GroupActivity : DatabaseLockActivity(),
removeSearch()
// Build response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- AutofillHelper.buildResponseAndSetResult(
- this,
- database,
- entry.getEntryInfo(database)
+ this.buildSpecialModeResponseAndSetResult(entry.getEntryInfo(database))
+ }
+ onValidateSpecialMode()
+ }
+
+ private fun entrySelectedForPasskeySelection(database: ContextualDatabase, entry: Entry) {
+ removeSearch()
+ // Build response with the entry selected
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ buildPasskeyResponseAndSetResult(
+ entryInfo = entry.getEntryInfo(database)
)
}
onValidateSpecialMode()
@@ -952,23 +946,28 @@ class GroupActivity : DatabaseLockActivity(),
private fun entrySelectedForRegistration(
database: ContextualDatabase,
entry: Entry,
- registerInfo: RegisterInfo?
+ activityResultLauncher: ActivityResultLauncher?,
+ registerInfo: RegisterInfo?,
+ typeMode: TypeMode
) {
removeSearch()
// Registration to update the entry
- EntryEditActivity.launchToUpdateForRegistration(
- this@GroupActivity,
- database,
- entry.nodeId,
- registerInfo
+ EntryEditActivity.launchForRegistration(
+ context = this@GroupActivity,
+ database = database,
+ activityResultLauncher = activityResultLauncher,
+ nodeId = entry.nodeId,
+ registerInfo = registerInfo,
+ typeMode = typeMode,
+ registrationType = EntryEditActivity.RegistrationType.UPDATE
)
onLaunchActivitySpecialMode()
}
- private fun updateEntryWithSearchInfo(
+ private fun updateEntryWithRegisterInfo(
database: ContextualDatabase,
entry: Entry,
- searchInfo: SearchInfo
+ registerInfo: RegisterInfo
) {
val newEntry = Entry(entry)
val entryInfo = newEntry.getEntryInfo(
@@ -976,11 +975,9 @@ class GroupActivity : DatabaseLockActivity(),
raw = true,
removeTemplateConfiguration = false
)
- val modification = entryInfo.saveSearchInfo(database, searchInfo)
+ entryInfo.saveRegisterInfo(database, registerInfo)
newEntry.setEntryInfo(database, entryInfo)
- if (modification) {
- updateEntry(entry, newEntry)
- }
+ updateEntry(entry, newEntry)
}
private fun finishNodeAction() {
@@ -1032,11 +1029,12 @@ class GroupActivity : DatabaseLockActivity(),
launchDialogForGroupUpdate(node as Group)
}
Type.ENTRY -> {
- EntryEditActivity.launchToUpdate(
- this@GroupActivity,
- database,
- (node as Entry).nodeId,
- mEntryActivityResultLauncher
+ EntryEditActivity.launch(
+ activity = this@GroupActivity,
+ database = database,
+ registrationType = EntryEditActivity.RegistrationType.UPDATE,
+ nodeId = (node as Entry).nodeId,
+ activityResultLauncher = mEntryActivityResultLauncher
)
}
}
@@ -1155,7 +1153,9 @@ class GroupActivity : DatabaseLockActivity(),
finishNodeAction()
searchView?.setOnQueryTextListener(null)
- searchFiltersView?.saveSearchParameters()
+ if (!mTempSearchInfo) {
+ searchFiltersView?.saveSearchParameters()
+ }
}
private fun addSearchQueryInSearchView(searchQuery: String) {
@@ -1209,7 +1209,7 @@ class GroupActivity : DatabaseLockActivity(),
searchView = it.actionView as SearchView?
searchView?.apply {
setOnQueryTextFocusChangeListener(mOnSearchTextFocusChangeListener)
- val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager?
+ val searchManager = getSystemService(SEARCH_SERVICE) as SearchManager?
(searchManager?.getSearchableInfo(
ComponentName(this@GroupActivity, GroupActivity::class.java)
))?.let { searchableInfo ->
@@ -1220,7 +1220,9 @@ class GroupActivity : DatabaseLockActivity(),
if (searchState != null) {
it.expandActionView()
addSearchQueryInSearchView(searchState.searchParameters.searchQuery)
- searchFiltersView?.searchParameters = searchState.searchParameters
+ if (mTempSearchInfo.not()) {
+ searchFiltersView?.searchParameters = searchState.searchParameters
+ }
}
}
if (it.isActionViewExpanded) {
@@ -1396,8 +1398,8 @@ class GroupActivity : DatabaseLockActivity(),
// Else in root, lock if needed
else {
removeSearch()
- EntrySelectionHelper.removeModesFromIntent(intent)
- EntrySelectionHelper.removeInfoFromIntent(intent)
+ intent.removeModes()
+ intent.removeInfo()
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
lockAndExit()
} else {
@@ -1474,9 +1476,11 @@ class GroupActivity : DatabaseLockActivity(),
private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"
private const val AUTO_SEARCH_KEY = "AUTO_SEARCH_KEY"
- private fun buildIntent(context: Context,
- groupState: GroupState?,
- intentBuildLauncher: (Intent) -> Unit) {
+ private fun buildIntent(
+ context: Context,
+ groupState: GroupState?,
+ intentBuildLauncher: (Intent) -> Unit
+ ) {
val intent = Intent(context, GroupActivity::class.java)
if (groupState != null) {
intent.putExtra(GROUP_STATE_KEY, groupState)
@@ -1484,18 +1488,12 @@ class GroupActivity : DatabaseLockActivity(),
intentBuildLauncher.invoke(intent)
}
- private fun checkTimeAndBuildIntent(activity: Activity,
- groupState: GroupState?,
- intentBuildLauncher: (Intent) -> Unit) {
- if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
- buildIntent(activity, groupState, intentBuildLauncher)
- }
- }
-
- private fun checkTimeAndBuildIntent(context: Context,
- groupState: GroupState?,
- intentBuildLauncher: (Intent) -> Unit) {
- if (TimeoutHelper.checkTime(context)) {
+ private fun checkTimeAndBuildIntent(
+ context: Context,
+ groupState: GroupState?,
+ intentBuildLauncher: (Intent) -> Unit
+ ) {
+ if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
buildIntent(context, groupState, intentBuildLauncher)
}
}
@@ -1505,9 +1503,11 @@ class GroupActivity : DatabaseLockActivity(),
* Standard Launch
* -------------------------
*/
- fun launch(context: Context,
- database: ContextualDatabase,
- autoSearch: Boolean = false) {
+ fun launch(
+ context: Context,
+ database: ContextualDatabase,
+ autoSearch: Boolean = false
+ ) {
if (database.loaded) {
checkTimeAndBuildIntent(context, null) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
@@ -1521,85 +1521,38 @@ class GroupActivity : DatabaseLockActivity(),
* Search Launch
* -------------------------
*/
- fun launchForSearchResult(context: Context,
- database: ContextualDatabase,
- searchInfo: SearchInfo,
- autoSearch: Boolean = false) {
+ fun launchForSearchResult(
+ context: Context,
+ database: ContextualDatabase,
+ searchInfo: SearchInfo,
+ autoSearch: Boolean = false
+ ) {
if (database.loaded) {
checkTimeAndBuildIntent(context, null) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
- EntrySelectionHelper.addSearchInfoInIntent(
- intent,
- searchInfo
- )
+ intent.addSearchInfo(searchInfo)
context.startActivity(intent)
}
}
}
- /*
- * -------------------------
- * Search save Launch
- * -------------------------
- */
- fun launchForSaveResult(context: Context,
- database: ContextualDatabase,
- searchInfo: SearchInfo,
- autoSearch: Boolean = false) {
- if (database.loaded && !database.isReadOnly) {
- checkTimeAndBuildIntent(context, null) { intent ->
- intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
- EntrySelectionHelper.startActivityForSaveModeResult(
- context,
- intent,
- searchInfo
- )
- }
- }
- }
-
- /*
- * -------------------------
- * Keyboard Launch
- * -------------------------
- */
- fun launchForKeyboardSelectionResult(context: Context,
- database: ContextualDatabase,
- searchInfo: SearchInfo? = null,
- autoSearch: Boolean = false) {
+ fun launchForSelection(
+ context: Context,
+ database: ContextualDatabase,
+ typeMode: TypeMode,
+ searchInfo: SearchInfo? = null,
+ autoSearch: Boolean = false,
+ activityResultLauncher: ActivityResultLauncher? = null,
+ ) {
if (database.loaded) {
checkTimeAndBuildIntent(context, null) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
- EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
- context,
- intent,
- searchInfo
- )
- }
- }
- }
-
- /*
- * -------------------------
- * Autofill Launch
- * -------------------------
- */
- @RequiresApi(api = Build.VERSION_CODES.O)
- fun launchForAutofillResult(activity: AppCompatActivity,
- database: ContextualDatabase,
- activityResultLaunch: ActivityResultLauncher?,
- autofillComponent: AutofillComponent,
- searchInfo: SearchInfo? = null,
- autoSearch: Boolean = false) {
- if (database.loaded) {
- checkTimeAndBuildIntent(activity, null) { intent ->
- intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
- AutofillHelper.startActivityForAutofillResult(
- activity,
- intent,
- activityResultLaunch,
- autofillComponent,
- searchInfo
+ EntrySelectionHelper.startActivityForSelectionModeResult(
+ context = context,
+ intent = intent,
+ typeMode = typeMode,
+ searchInfo = searchInfo,
+ activityResultLauncher = activityResultLauncher
)
}
}
@@ -1610,16 +1563,22 @@ class GroupActivity : DatabaseLockActivity(),
* Registration Launch
* -------------------------
*/
- fun launchForRegistration(context: Context,
- database: ContextualDatabase,
- registerInfo: RegisterInfo? = null) {
+ fun launchForRegistration(
+ context: Context,
+ database: ContextualDatabase,
+ typeMode: TypeMode,
+ registerInfo: RegisterInfo? = null,
+ activityResultLauncher: ActivityResultLauncher?,
+ ) {
if (database.loaded && !database.isReadOnly) {
checkTimeAndBuildIntent(context, null) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, false)
EntrySelectionHelper.startActivityForRegistrationModeResult(
context,
+ activityResultLauncher,
intent,
- registerInfo
+ registerInfo,
+ typeMode
)
}
}
@@ -1630,65 +1589,49 @@ class GroupActivity : DatabaseLockActivity(),
* Global Launch
* -------------------------
*/
- fun launch(activity: AppCompatActivity,
- database: ContextualDatabase,
- onValidateSpecialMode: () -> Unit,
- onCancelSpecialMode: () -> Unit,
- onLaunchActivitySpecialMode: () -> Unit,
- autofillActivityResultLauncher: ActivityResultLauncher?) {
- EntrySelectionHelper.doSpecialAction(activity.intent,
- {
- // Default action
- launch(
- activity,
+ fun launch(
+ activity: AppCompatActivity,
+ database: ContextualDatabase,
+ onValidateSpecialMode: () -> Unit,
+ onCancelSpecialMode: () -> Unit,
+ onLaunchActivitySpecialMode: () -> Unit,
+ activityResultLauncher: ActivityResultLauncher?
+ ) {
+ EntrySelectionHelper.doSpecialAction(
+ intent = activity.intent,
+ defaultAction = {
+ // Default action
+ launch(
+ activity,
+ database,
+ true
+ )
+ },
+ searchAction = { searchInfo ->
+ // Search action
+ if (database.loaded) {
+ launchForSearchResult(activity,
database,
- true
- )
- },
- { searchInfo ->
- // Search action
- if (database.loaded) {
- launchForSearchResult(activity,
- database,
- searchInfo,
- true)
- onLaunchActivitySpecialMode()
- } else {
- // Simply close if database not opened
- onCancelSpecialMode()
- }
- },
- { searchInfo ->
- // Save info
- if (database.loaded) {
- if (!database.isReadOnly) {
- launchForSaveResult(
- activity,
- database,
- searchInfo,
- false
- )
- onLaunchActivitySpecialMode()
- } else {
- Toast.makeText(
- activity.applicationContext,
- R.string.autofill_read_only_save,
- Toast.LENGTH_LONG
- )
- .show()
- onCancelSpecialMode()
- }
- }
- },
- { searchInfo ->
- // Keyboard selection
- SearchHelper.checkAutoSearchInfo(activity,
- database,
- searchInfo,
- { _, items ->
+ searchInfo,
+ true)
+ onLaunchActivitySpecialMode()
+ } else {
+ // Simply close if database not opened
+ onCancelSpecialMode()
+ }
+ },
+ selectionAction = { intentSenderMode, typeMode, searchInfo ->
+ SearchHelper.checkAutoSearchInfo(
+ context = activity,
+ database = database,
+ searchInfo = searchInfo,
+ onItemsFound = { openedDatabase, items ->
+ when (typeMode) {
+ TypeMode.DEFAULT -> {}
+ TypeMode.MAGIKEYBOARD -> {
MagikeyboardService.performSelection(
- items,
- { entryInfo ->
+ items = items,
+ actionPopulateKeyboard = { entryInfo ->
// Keyboard populated
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
activity,
@@ -1696,92 +1639,89 @@ class GroupActivity : DatabaseLockActivity(),
)
onValidateSpecialMode()
},
- { autoSearch ->
- launchForKeyboardSelectionResult(activity,
- database,
- searchInfo,
- autoSearch)
+ actionEntrySelection = { autoSearch ->
+ launchForSelection(
+ context = activity,
+ database = database,
+ typeMode = TypeMode.MAGIKEYBOARD,
+ searchInfo = searchInfo,
+ autoSearch = autoSearch
+ )
onLaunchActivitySpecialMode()
}
)
- },
- {
- // Here no search info found, disable auto search
- launchForKeyboardSelectionResult(activity,
- database,
- searchInfo,
- false)
- onLaunchActivitySpecialMode()
- },
- {
- // Simply close if database not opened, normally not happened
- onCancelSpecialMode()
}
- )
- },
- { searchInfo, autofillComponent ->
- // Autofill selection
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- SearchHelper.checkAutoSearchInfo(activity,
- database,
- searchInfo,
- { openedDatabase, items ->
- // Response is build
- AutofillHelper.buildResponseAndSetResult(activity, openedDatabase, items)
- onValidateSpecialMode()
- },
- {
- // Here no search info found, disable auto search
- launchForAutofillResult(activity,
- database,
- autofillActivityResultLauncher,
- autofillComponent,
- searchInfo,
- false)
- onLaunchActivitySpecialMode()
- },
- {
- // Simply close if database not opened, normally not happened
- onCancelSpecialMode()
- }
+ TypeMode.PASSKEY -> {
+ // Response is build
+ EntrySelectionHelper.performSelection(
+ items = items,
+ actionPopulateCredentialProvider = { entryInfo ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ activity.buildPasskeyResponseAndSetResult(entryInfo)
+ }
+ onValidateSpecialMode()
+ },
+ actionEntrySelection = {
+ launchForSelection(
+ context = activity,
+ database = database,
+ typeMode = TypeMode.PASSKEY,
+ searchInfo = searchInfo,
+ activityResultLauncher = activityResultLauncher,
+ autoSearch = true
+ )
+ onLaunchActivitySpecialMode()
+ }
+ )
+ }
+ TypeMode.AUTOFILL -> {
+ // Response is build
+ activity.buildSpecialModeResponseAndSetResult(items)
+ onValidateSpecialMode()
+ }
+ }
+ },
+ onItemNotFound = {
+ // Here no search info found, disable auto search
+ launchForSelection(
+ context = activity,
+ database = database,
+ typeMode = typeMode,
+ searchInfo = searchInfo,
+ autoSearch = false,
+ activityResultLauncher = if (intentSenderMode)
+ activityResultLauncher else null
)
- } else {
+ onLaunchActivitySpecialMode()
+ },
+ onDatabaseClosed = {
+ // Simply close if database not opened, normally not happened
onCancelSpecialMode()
}
- },
- { registerInfo ->
- // Autofill registration
+ )
+ },
+ registrationAction = { intentSenderMode, typeMode, registerInfo ->
+ // Save info
+ if (database.loaded) {
if (!database.isReadOnly) {
- SearchHelper.checkAutoSearchInfo(activity,
- database,
- registerInfo?.searchInfo,
- { _, _ ->
- // No auto search, it's a registration
- launchForRegistration(activity,
- database,
- registerInfo)
- onLaunchActivitySpecialMode()
- },
- {
- // Here no search info found, disable auto search
- launchForRegistration(activity,
- database,
- registerInfo)
- onLaunchActivitySpecialMode()
- },
- {
- // Simply close if database not opened, normally not happened
- onCancelSpecialMode()
- }
+ launchForRegistration(
+ context = activity,
+ database = database,
+ registerInfo = registerInfo,
+ typeMode = typeMode,
+ activityResultLauncher = if (intentSenderMode)
+ activityResultLauncher else null
)
+ onLaunchActivitySpecialMode()
} else {
- Toast.makeText(activity.applicationContext,
- R.string.autofill_read_only_save,
- Toast.LENGTH_LONG)
- .show()
+ activity.toastError(RegisterInReadOnlyDatabaseException())
onCancelSpecialMode()
}
- })
+ } else {
+ onCancelSpecialMode()
+ }
+ }
+ )
}
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt
index d471b5e6d..9ff62a512 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt
@@ -174,10 +174,10 @@ class IconPickerActivity : DatabaseLockActivity() {
return true
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
- if (database?.allowCustomIcons == true) {
+ if (database.allowCustomIcons) {
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
} else {
uploadButton.visibility = View.GONE
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/ImageViewerActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/ImageViewerActivity.kt
index 6cd495a17..af258b61d 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/ImageViewerActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/ImageViewerActivity.kt
@@ -101,7 +101,7 @@ class ImageViewerActivity : DatabaseLockActivity() {
return true
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
try {
@@ -119,18 +119,16 @@ class ImageViewerActivity : DatabaseLockActivity() {
resources.displayMetrics.heightPixels * 2
)
- database?.let { database ->
- BinaryDatabaseManager.loadBitmap(
- database,
- attachment.binaryData,
- mImagePreviewMaxWidth
- ) { bitmapLoaded ->
- if (bitmapLoaded == null) {
- finish()
- } else {
- progressView.visibility = View.GONE
- imageView.setImageBitmap(bitmapLoaded)
- }
+ BinaryDatabaseManager.loadBitmap(
+ database,
+ attachment.binaryData,
+ mImagePreviewMaxWidth
+ ) { bitmapLoaded ->
+ if (bitmapLoaded == null) {
+ finish()
+ } else {
+ progressView.visibility = View.GONE
+ imageView.setImageBitmap(bitmapLoaded)
}
}
} ?: finish()
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt
index e9d8cb02c..95ce84881 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt
@@ -36,7 +36,6 @@ import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
-import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.biometric.BiometricManager
@@ -49,16 +48,15 @@ import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
-import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
-import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
-import com.kunzisoft.keepass.autofill.AutofillComponent
-import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
import com.kunzisoft.keepass.biometric.deviceUnlockError
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
+import com.kunzisoft.keepass.credentialprovider.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
@@ -127,11 +125,6 @@ class MainCredentialActivity : DatabaseModeActivity() {
private var mReadOnly: Boolean = false
private var mForceReadOnly: Boolean = false
- private var mAutofillActivityResultLauncher: ActivityResultLauncher? =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
- AutofillHelper.buildActivityResultLauncher(this)
- else null
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -313,20 +306,18 @@ class MainCredentialActivity : DatabaseModeActivity() {
}
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
- if (database != null) {
- // Trying to load another database
- if (mDatabaseFileUri != null
- && database.fileUri != null
- && mDatabaseFileUri != database.fileUri) {
- Toast.makeText(this,
- R.string.warning_database_already_opened,
- Toast.LENGTH_LONG
- ).show()
- }
- launchGroupActivityIfLoaded(database)
+ // Trying to load another database
+ if (mDatabaseFileUri != null
+ && database.fileUri != null
+ && mDatabaseFileUri != database.fileUri) {
+ Toast.makeText(this,
+ R.string.warning_database_already_opened,
+ Toast.LENGTH_LONG
+ ).show()
}
+ launchGroupActivityIfLoaded(database)
}
override fun onDatabaseActionFinished(
@@ -433,7 +424,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
{ onValidateSpecialMode() },
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() },
- mAutofillActivityResultLauncher
+ mCredentialActivityResultLauncher
)
}
}
@@ -511,10 +502,11 @@ class MainCredentialActivity : DatabaseModeActivity() {
val password = intent.getStringExtra(KEY_PASSWORD)
// Consume the intent extra password
intent.removeExtra(KEY_PASSWORD)
- val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
if (password != null) {
mainCredentialView?.populatePasswordTextView(password)
}
+ val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
+ intent.removeExtra(KEY_LAUNCH_IMMEDIATELY)
if (launchImmediately) {
loadDatabase()
} else {
@@ -569,13 +561,10 @@ class MainCredentialActivity : DatabaseModeActivity() {
clearCredentialsViews()
}
- if (mReadOnly && (
- mSpecialMode == SpecialMode.SAVE
- || mSpecialMode == SpecialMode.REGISTRATION)
- ) {
- Log.e(TAG, getString(R.string.autofill_read_only_save))
+ if (mReadOnly && mSpecialMode == SpecialMode.REGISTRATION) {
+ Log.e(TAG, getString(R.string.error_save_read_only))
Snackbar.make(coordinatorLayout,
- R.string.autofill_read_only_save,
+ R.string.error_save_read_only,
Snackbar.LENGTH_LONG).asError().show()
} else {
databaseFileUri?.let { databaseUri ->
@@ -596,7 +585,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUUID: Boolean) {
- loadDatabase(
+ mDatabaseViewModel.loadDatabase(
databaseUri,
mainCredential,
readOnly,
@@ -749,11 +738,13 @@ class MainCredentialActivity : DatabaseModeActivity() {
private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
- private fun buildAndLaunchIntent(activity: Activity,
- databaseFile: Uri,
- keyFile: Uri?,
- hardwareKey: HardwareKey?,
- intentBuildLauncher: (Intent) -> Unit) {
+ private fun buildAndLaunchIntent(
+ activity: Activity,
+ databaseFile: Uri,
+ keyFile: Uri?,
+ hardwareKey: HardwareKey?,
+ intentBuildLauncher: (Intent) -> Unit
+ ) {
val intent = Intent(activity, MainCredentialActivity::class.java)
intent.putExtra(KEY_FILENAME, databaseFile)
if (keyFile != null)
@@ -770,10 +761,12 @@ class MainCredentialActivity : DatabaseModeActivity() {
*/
@Throws(FileNotFoundException::class)
- fun launch(activity: Activity,
- databaseFile: Uri,
- keyFile: Uri?,
- hardwareKey: HardwareKey?) {
+ fun launch(
+ activity: Activity,
+ databaseFile: Uri,
+ keyFile: Uri?,
+ hardwareKey: HardwareKey?
+ ) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
activity.startActivity(intent)
}
@@ -786,185 +779,73 @@ class MainCredentialActivity : DatabaseModeActivity() {
*/
@Throws(FileNotFoundException::class)
- fun launchForSearchResult(activity: Activity,
- databaseFile: Uri,
- keyFile: Uri?,
- hardwareKey: HardwareKey?,
- searchInfo: SearchInfo) {
+ fun launchForSearchResult(
+ activity: Activity,
+ databaseFile: Uri,
+ keyFile: Uri?,
+ hardwareKey: HardwareKey?,
+ searchInfo: SearchInfo
+ ) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSearchModeResult(
- activity,
- intent,
- searchInfo)
+ context = activity,
+ intent = intent,
+ searchInfo = searchInfo
+ )
}
}
/*
* -------------------------
- * Save Launch
+ * Selection Launch
* -------------------------
*/
@Throws(FileNotFoundException::class)
- fun launchForSaveResult(activity: Activity,
- databaseFile: Uri,
- keyFile: Uri?,
- hardwareKey: HardwareKey?,
- searchInfo: SearchInfo) {
+ fun launchForSelection(
+ activity: AppCompatActivity,
+ databaseFile: Uri,
+ keyFile: Uri?,
+ hardwareKey: HardwareKey?,
+ typeMode: TypeMode,
+ searchInfo: SearchInfo?,
+ activityResultLauncher: ActivityResultLauncher? = null,
+ ) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
- EntrySelectionHelper.startActivityForSaveModeResult(
- activity,
- intent,
- searchInfo)
+ EntrySelectionHelper.startActivityForSelectionModeResult(
+ context = activity,
+ intent = intent,
+ typeMode = typeMode,
+ searchInfo = searchInfo,
+ activityResultLauncher = activityResultLauncher
+ )
}
}
/*
* -------------------------
- * Keyboard Launch
+ * Registration Launch
* -------------------------
*/
@Throws(FileNotFoundException::class)
- fun launchForKeyboardResult(activity: Activity,
- databaseFile: Uri,
- keyFile: Uri?,
- hardwareKey: HardwareKey?,
- searchInfo: SearchInfo?) {
- buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
- EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
- activity,
- intent,
- searchInfo)
- }
- }
-
- /*
- * -------------------------
- * Autofill Launch
- * -------------------------
- */
-
- @RequiresApi(api = Build.VERSION_CODES.O)
- @Throws(FileNotFoundException::class)
- fun launchForAutofillResult(activity: AppCompatActivity,
- databaseFile: Uri,
- keyFile: Uri?,
- hardwareKey: HardwareKey?,
- activityResultLauncher: ActivityResultLauncher?,
- autofillComponent: AutofillComponent,
- searchInfo: SearchInfo?) {
- buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
- AutofillHelper.startActivityForAutofillResult(
- activity,
- intent,
- activityResultLauncher,
- autofillComponent,
- searchInfo)
- }
- }
-
- /*
- * -------------------------
- * Registration Launch
- * -------------------------
- */
- fun launchForRegistration(activity: Activity,
- databaseFile: Uri,
- keyFile: Uri?,
- hardwareKey: HardwareKey?,
- registerInfo: RegisterInfo?) {
+ fun launchForRegistration(
+ activity: Activity,
+ activityResultLauncher: ActivityResultLauncher?,
+ databaseFile: Uri,
+ keyFile: Uri?,
+ hardwareKey: HardwareKey?,
+ typeMode: TypeMode,
+ registerInfo: RegisterInfo?
+ ) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForRegistrationModeResult(
- activity,
- intent,
- registerInfo)
- }
- }
-
- /*
- * -------------------------
- * Global Launch
- * -------------------------
- */
- fun launch(activity: AppCompatActivity,
- databaseUri: Uri,
- keyFile: Uri?,
- hardwareKey: HardwareKey?,
- fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
- onCancelSpecialMode: () -> Unit,
- onLaunchActivitySpecialMode: () -> Unit,
- autofillActivityResultLauncher: ActivityResultLauncher?) {
-
- try {
- EntrySelectionHelper.doSpecialAction(activity.intent,
- {
- launch(
- activity,
- databaseUri,
- keyFile,
- hardwareKey
- )
- },
- { searchInfo -> // Search Action
- launchForSearchResult(
- activity,
- databaseUri,
- keyFile,
- hardwareKey,
- searchInfo
- )
- onLaunchActivitySpecialMode()
- },
- { searchInfo -> // Save Action
- launchForSaveResult(
- activity,
- databaseUri,
- keyFile,
- hardwareKey,
- searchInfo
- )
- onLaunchActivitySpecialMode()
- },
- { searchInfo -> // Keyboard Selection Action
- launchForKeyboardResult(
- activity,
- databaseUri,
- keyFile,
- hardwareKey,
- searchInfo
- )
- onLaunchActivitySpecialMode()
- },
- { searchInfo, autofillComponent -> // Autofill Selection Action
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- launchForAutofillResult(
- activity,
- databaseUri,
- keyFile,
- hardwareKey,
- autofillActivityResultLauncher,
- autofillComponent,
- searchInfo
- )
- onLaunchActivitySpecialMode()
- } else {
- onCancelSpecialMode()
- }
- },
- { registerInfo -> // Registration Action
- launchForRegistration(
- activity,
- databaseUri,
- keyFile,
- hardwareKey,
- registerInfo
- )
- onLaunchActivitySpecialMode()
- }
+ context = activity,
+ activityResultLauncher = activityResultLauncher,
+ intent = intent,
+ typeMode = typeMode,
+ registerInfo = registerInfo
)
- } catch (e: FileNotFoundException) {
- fileNoFoundAction(e)
}
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseChangedDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseChangedDialogFragment.kt
index 9318a6167..8b2bf24da 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseChangedDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseChangedDialogFragment.kt
@@ -67,7 +67,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
}
builder.setMessage(stringBuilder)
builder.setPositiveButton(android.R.string.ok) { _, _ ->
- actionDatabaseListener?.validateDatabaseChanged()
+ actionDatabaseListener?.onDatabaseChangeValidated()
}
return builder.create()
}
@@ -76,7 +76,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
}
interface ActionDatabaseChangedListener {
- fun validateDatabaseChanged()
+ fun onDatabaseChangeValidated()
}
companion object {
@@ -86,9 +86,10 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE"
- fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
- newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
- readOnly: Boolean
+ fun getInstance(
+ oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
+ newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
+ readOnly: Boolean
)
: DatabaseChangedDialogFragment {
val fragment = DatabaseChangedDialogFragment()
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseDialogFragment.kt
index a8d53f9ee..e143fef3e 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseDialogFragment.kt
@@ -5,6 +5,9 @@ import android.view.View
import android.view.WindowManager.LayoutParams.FLAG_SECURE
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.ContextualDatabase
@@ -12,23 +15,40 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
+import kotlinx.coroutines.launch
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
- private var mDatabase: ContextualDatabase? = null
+ private val mDatabase: ContextualDatabase?
+ get() = mDatabaseViewModel.database
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
-
- mDatabaseViewModel.database.observe(this) { database ->
- this.mDatabase = database
- resetAppTimeoutOnTouchOrFocus()
- onDatabaseRetrieved(database)
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ mDatabaseViewModel.actionState.collect { uiState ->
+ when (uiState) {
+ is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
+ onDatabaseActionFinished(
+ uiState.database,
+ uiState.actionTask,
+ uiState.result
+ )
+ }
+ else -> {}
+ }
+ }
+ }
}
-
- mDatabaseViewModel.actionFinished.observe(this) { result ->
- onDatabaseActionFinished(result.database, result.actionTask, result.result)
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ mDatabaseViewModel.databaseState.collect { database ->
+ database?.let {
+ onDatabaseRetrieved(database)
+ }
+ }
+ }
}
}
@@ -45,13 +65,14 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
}
@Suppress("DEPRECATION")
+ @Deprecated(message = "")
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
resetAppTimeoutOnTouchOrFocus()
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Can be overridden by a subclass
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupDialogFragment.kt
index 593a99d6f..e3af2a169 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupDialogFragment.kt
@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
-import com.kunzisoft.keepass.utils.UuidUtil
+import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.DateTimeFieldView
@@ -62,14 +62,14 @@ class GroupDialogFragment : DatabaseDialogFragment() {
private lateinit var uuidContainerView: ViewGroup
private lateinit var uuidReferenceView: TextView
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
- database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
+ database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
}
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
- if (database?.allowCustomSearchableGroup() == true) {
+ if (database.allowCustomSearchableGroup()) {
searchableLabelView.visibility = View.VISIBLE
searchableView.visibility = View.VISIBLE
} else {
@@ -155,7 +155,7 @@ class GroupDialogFragment : DatabaseDialogFragment() {
searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable)
autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType,
mGroupInfo.defaultAutoTypeSequence)
- val uuid = UuidUtil.toHexString(mGroupInfo.id)
+ val uuid = mGroupInfo.id?.asHexString()
if (uuid == null || uuid.isEmpty()) {
uuidContainerView.visibility = View.GONE
} else {
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt
index 9bbe577bd..ae09979d9 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt
@@ -112,32 +112,32 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
}
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
- database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
+ database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
}
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
- searchableContainerView.visibility = if (database?.allowCustomSearchableGroup() == true) {
+ searchableContainerView.visibility = if (database.allowCustomSearchableGroup()) {
View.VISIBLE
} else {
View.GONE
}
- if (database?.allowAutoType() == true) {
+ if (database.allowAutoType()) {
autoTypeContainerView.visibility = View.VISIBLE
} else {
autoTypeContainerView.visibility = View.GONE
}
- tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
+ tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
tagsCompletionView.apply {
threshold = 1
setAdapter(tagsAdapter)
}
- tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
+ tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconEditDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconEditDialogFragment.kt
index 84b881eac..e5d8ba542 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconEditDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconEditDialogFragment.kt
@@ -45,10 +45,10 @@ class IconEditDialogFragment : DatabaseDialogFragment() {
private var mCustomIcon: IconImageCustom? = null
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
- database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
+ database.iconDrawableFactory.assignDatabaseIcon(imageView, icon)
}
mCustomIcon?.let { customIcon ->
populateViewsWithCustomIcon(customIcon)
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetMainCredentialDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetMainCredentialDialogFragment.kt
index f8e0c7013..d9dfbb4db 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetMainCredentialDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetMainCredentialDialogFragment.kt
@@ -35,9 +35,9 @@ import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
+import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey
-import com.kunzisoft.keepass.hardware.HardwareKeyActivity
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.UriUtil.openUrl
@@ -258,8 +258,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
showEmptyPasswordConfirmationDialog()
} else if (!error
&& hardwareKey != null
- && !HardwareKeyActivity.isHardwareKeyAvailable(
- requireActivity(), hardwareKey, false)
+ && !HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)
) {
// show hardware driver dialog if required
error = true
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt
index 1185dd066..c1f9cb834 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt
@@ -29,7 +29,11 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
-import android.widget.*
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.EditText
+import android.widget.Spinner
+import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
@@ -40,15 +44,15 @@ import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_OTP_DIGITS
import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_TOTP_PERIOD
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_HOTP_COUNTER
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_DIGITS
-import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_SECRET
+import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
import com.kunzisoft.keepass.otp.OtpTokenType
import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.otp.TokenCalculator
-import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
+import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
import com.kunzisoft.keepass.utils.UriUtil.openUrl
import com.kunzisoft.keepass.utils.getParcelableCompat
-import java.util.*
+import java.util.Locale
class SetOTPDialogFragment : DatabaseDialogFragment() {
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/DatabaseFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/DatabaseFragment.kt
index e59b48871..a630afd26 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/DatabaseFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/DatabaseFragment.kt
@@ -4,36 +4,59 @@ import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.ContextualDatabase
-import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
+import kotlinx.coroutines.launch
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
- private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
- protected var mDatabase: ContextualDatabase? = null
+ protected val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
+ protected val mDatabase: ContextualDatabase?
+ get() = mDatabaseViewModel.database
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
- if (mDatabase == null || mDatabase != database) {
- this.mDatabase = database
- onDatabaseRetrieved(database)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ mDatabaseViewModel.actionState.collect { uiState ->
+ when (uiState) {
+ is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
+ onDatabaseActionFinished(
+ uiState.database,
+ uiState.actionTask,
+ uiState.result
+ )
+ }
+
+ else -> {}
+ }
+ }
}
}
-
- mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result ->
- onDatabaseActionFinished(result.database, result.actionTask, result.result)
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ mDatabaseViewModel.databaseState.collect { database ->
+ database?.let {
+ onDatabaseRetrieved(database)
+ }
+ }
+ }
}
}
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
context?.let {
- view?.resetAppTimeoutWhenViewTouchedOrFocused(it, mDatabase?.loaded)
+ view?.resetAppTimeoutWhenViewTouchedOrFocused(
+ context = it,
+ databaseLoaded = mDatabase?.loaded
+ )
}
}
@@ -44,8 +67,4 @@ abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
) {
// Can be overridden by a subclass
}
-
- protected fun buildNewBinaryAttachment(): BinaryData? {
- return mDatabase?.buildNewBinaryAttachment()
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt
index d796d048b..dd7885ff4 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt
@@ -230,7 +230,7 @@ class EntryEditFragment: DatabaseFragment() {
val attachmentToUploadUri = it.attachmentToUploadUri
val fileName = it.fileName
- buildNewBinaryAttachment()?.let { binaryAttachment ->
+ mDatabaseViewModel.buildNewAttachment()?.let { binaryAttachment ->
val entryAttachment = Attachment(fileName, binaryAttachment)
// Ask to replace the current attachment
if ((!mAllowMultipleAttachments
@@ -273,13 +273,13 @@ class EntryEditFragment: DatabaseFragment() {
}
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
templateView.populateIconMethod = { imageView, icon ->
- database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
+ database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
}
- mAllowMultipleAttachments = database?.allowMultipleAttachments == true
+ mAllowMultipleAttachments = database.allowMultipleAttachments == true
attachmentsAdapter?.database = database
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
@@ -290,12 +290,12 @@ class EntryEditFragment: DatabaseFragment() {
}
}
- tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
+ tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
tagsCompletionView.apply {
threshold = 1
setAdapter(tagsAdapter)
}
- tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
+ tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
}
private fun assignEntryInfo(entryInfo: EntryInfo?) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt
index a9de1526c..5b276a168 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt
@@ -24,7 +24,7 @@ import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
-import com.kunzisoft.keepass.utils.UuidUtil
+import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
import com.kunzisoft.keepass.view.TemplateView
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.showByFading
@@ -133,7 +133,7 @@ class EntryFragment: DatabaseFragment() {
}
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
context?.let { context ->
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
attachmentsAdapter?.database = database
@@ -184,7 +184,7 @@ class EntryFragment: DatabaseFragment() {
// customDataView.text = entryInfo?.customData?.toString()
// Assign special data
- uuidReferenceView.text = UuidUtil.toHexString(entryInfo?.id)
+ uuidReferenceView.text = entryInfo?.id?.asHexString()
}
private fun showClipboardDialog() {
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt
index f26cc3a70..242d87ab0 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt
@@ -36,9 +36,9 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
-import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
-import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.adapters.NodesAdapter
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
+import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum
@@ -154,46 +154,44 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
super.onDetach()
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
context?.let { context ->
- database?.let { database ->
- mAdapter = NodesAdapter(context, database).apply {
- setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
- override fun onNodeClick(database: ContextualDatabase, node: Node) {
- if (nodeActionSelectionMode) {
- if (listActionNodes.contains(node)) {
- // Remove selected item if already selected
- listActionNodes.remove(node)
- } else {
- // Add selected item if not already selected
- listActionNodes.add(node)
- }
- nodeClickListener?.onNodeSelected(database, listActionNodes)
- setActionNodes(listActionNodes)
- notifyNodeChanged(node)
+ mAdapter = NodesAdapter(context, database).apply {
+ setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
+ override fun onNodeClick(database: ContextualDatabase, node: Node) {
+ if (nodeActionSelectionMode) {
+ if (listActionNodes.contains(node)) {
+ // Remove selected item if already selected
+ listActionNodes.remove(node)
} else {
- nodeClickListener?.onNodeClick(database, node)
+ // Add selected item if not already selected
+ listActionNodes.add(node)
}
+ nodeClickListener?.onNodeSelected(database, listActionNodes)
+ setActionNodes(listActionNodes)
+ notifyNodeChanged(node)
+ } else {
+ nodeClickListener?.onNodeClick(database, node)
}
+ }
- override fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean {
- if (nodeActionPasteMode == PasteMode.UNDEFINED) {
- // Select the first item after a long click
- if (!listActionNodes.contains(node))
- listActionNodes.add(node)
+ override fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean {
+ if (nodeActionPasteMode == PasteMode.UNDEFINED) {
+ // Select the first item after a long click
+ if (!listActionNodes.contains(node))
+ listActionNodes.add(node)
- nodeClickListener?.onNodeSelected(database, listActionNodes)
+ nodeClickListener?.onNodeSelected(database, listActionNodes)
- setActionNodes(listActionNodes)
- notifyNodeChanged(node)
- activity?.hideKeyboard()
- }
- return true
+ setActionNodes(listActionNodes)
+ notifyNodeChanged(node)
+ activity?.hideKeyboard()
}
- })
- }
- mNodesRecyclerView?.adapter = mAdapter
+ return true
+ }
+ })
}
+ mNodesRecyclerView?.adapter = mAdapter
}
}
@@ -248,7 +246,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
activity?.intent?.let {
- specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
+ specialMode = it.retrieveSpecialMode()
}
}
@@ -299,9 +297,9 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
}
}
- private fun containsRecycleBin(nodes: List): Boolean {
- return mDatabase?.isRecycleBinEnabled == true
- && nodes.any { it == mDatabase?.recycleBin }
+ private fun containsRecycleBin(database: ContextualDatabase?, nodes: List): Boolean {
+ return database?.isRecycleBinEnabled == true
+ && nodes.any { it == database.recycleBin }
}
fun actionNodesCallback(database: ContextualDatabase,
@@ -328,7 +326,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
// Open and Edit for a single item
if (nodes.size == 1) {
// Edition
- if (database.isReadOnly || containsRecycleBin(nodes)) {
+ if (database.isReadOnly || containsRecycleBin(database, nodes)) {
menu?.removeItem(R.id.menu_edit)
}
} else {
@@ -348,7 +346,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
}
// Deletion
- if (database.isReadOnly || containsRecycleBin(nodes)) {
+ if (database.isReadOnly || containsRecycleBin(database, nodes)) {
menu?.removeItem(R.id.menu_delete)
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconFragment.kt
index 929015b30..3c5c6cd90 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconFragment.kt
@@ -71,8 +71,8 @@ abstract class IconFragment : DatabaseFragment(),
resetAppTimeoutWhenViewFocusedOrChanged(view)
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
+ iconPickerAdapter.iconDrawableFactory = database.iconDrawableFactory
CoroutineScope(Dispatchers.IO).launch {
val populateList = launch {
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconPickerFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconPickerFragment.kt
index f2c2451f1..cbf798117 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconPickerFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconPickerFragment.kt
@@ -48,9 +48,9 @@ class IconPickerFragment : DatabaseFragment() {
}
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
- if (database?.allowCustomIcons == true) 2 else 1)
+ if (database.allowCustomIcons) 2 else 1)
viewPager.adapter = iconPickerPagerAdapter
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = when (position) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/KeyGeneratorFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/KeyGeneratorFragment.kt
index d4abf2569..9b196b202 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/KeyGeneratorFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/KeyGeneratorFragment.kt
@@ -107,7 +107,7 @@ class KeyGeneratorFragment : DatabaseFragment() {
super.onDestroyView()
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Nothing here
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/PassphraseGeneratorFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/PassphraseGeneratorFragment.kt
index 6070ea8d2..de576bd10 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/PassphraseGeneratorFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/PassphraseGeneratorFragment.kt
@@ -244,7 +244,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
}
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Nothing here
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/PasswordGeneratorFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/PasswordGeneratorFragment.kt
index 7a7d9c067..c35f90aaf 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/PasswordGeneratorFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/PasswordGeneratorFragment.kt
@@ -293,20 +293,22 @@ class PasswordGeneratorFragment : DatabaseFragment() {
private fun generatePassword() {
var password = ""
try {
- password = PasswordGenerator(resources).generatePassword(getPasswordLength(),
- uppercaseCompound.isChecked,
- lowercaseCompound.isChecked,
- digitsCompound.isChecked,
- minusCompound.isChecked,
- underlineCompound.isChecked,
- spaceCompound.isChecked,
- specialsCompound.isChecked,
- bracketsCompound.isChecked,
- extendedCompound.isChecked,
- getConsiderChars(),
- getIgnoreChars(),
- atLeastOneCompound.isChecked,
- excludeAmbiguousCompound.isChecked)
+ password = PasswordGenerator(resources).generatePassword(
+ length = getPasswordLength(),
+ upperCase = uppercaseCompound.isChecked,
+ lowerCase = lowercaseCompound.isChecked,
+ digits = digitsCompound.isChecked,
+ minus = minusCompound.isChecked,
+ underline = underlineCompound.isChecked,
+ space = spaceCompound.isChecked,
+ specials = specialsCompound.isChecked,
+ brackets = bracketsCompound.isChecked,
+ extended = extendedCompound.isChecked,
+ considerChars = getConsiderChars(),
+ ignoreChars = getIgnoreChars(),
+ atLeastOneFromEach = atLeastOneCompound.isChecked,
+ excludeAmbiguousChar = excludeAmbiguousCompound.isChecked
+ )
} catch (e: Exception) {
Log.e(TAG, "Unable to generate a password", e)
}
@@ -318,7 +320,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
super.onDestroy()
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Nothing here
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt
deleted file mode 100644
index 31a988772..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt
+++ /dev/null
@@ -1,211 +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 .
- *
- */
-package com.kunzisoft.keepass.activities.helpers
-
-import android.content.Context
-import android.content.Intent
-import android.os.Build
-import com.kunzisoft.keepass.autofill.AutofillComponent
-import com.kunzisoft.keepass.autofill.AutofillHelper
-import com.kunzisoft.keepass.model.RegisterInfo
-import com.kunzisoft.keepass.model.SearchInfo
-import com.kunzisoft.keepass.utils.getParcelableExtraCompat
-import com.kunzisoft.keepass.utils.getEnumExtra
-import com.kunzisoft.keepass.utils.putEnumExtra
-
-object EntrySelectionHelper {
-
- private const val KEY_SPECIAL_MODE = "com.kunzisoft.keepass.extra.SPECIAL_MODE"
- private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
- private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
- private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
-
- fun startActivityForSearchModeResult(context: Context,
- intent: Intent,
- searchInfo: SearchInfo) {
- addSpecialModeInIntent(intent, SpecialMode.SEARCH)
- addSearchInfoInIntent(intent, searchInfo)
- intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
- context.startActivity(intent)
- }
-
- fun startActivityForSaveModeResult(context: Context,
- intent: Intent,
- searchInfo: SearchInfo) {
- addSpecialModeInIntent(intent, SpecialMode.SAVE)
- addTypeModeInIntent(intent, TypeMode.DEFAULT)
- addSearchInfoInIntent(intent, searchInfo)
- intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
- context.startActivity(intent)
- }
-
- fun startActivityForKeyboardSelectionModeResult(context: Context,
- intent: Intent,
- searchInfo: SearchInfo?) {
- addSpecialModeInIntent(intent, SpecialMode.SELECTION)
- addTypeModeInIntent(intent, TypeMode.MAGIKEYBOARD)
- addSearchInfoInIntent(intent, searchInfo)
- intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
- context.startActivity(intent)
- }
-
- fun startActivityForRegistrationModeResult(context: Context,
- intent: Intent,
- registerInfo: RegisterInfo?) {
- addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
- // At the moment, only autofill for registration
- addTypeModeInIntent(intent, TypeMode.AUTOFILL)
- addRegisterInfoInIntent(intent, registerInfo)
- intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
- context.startActivity(intent)
- }
-
- fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
- searchInfo?.let {
- intent.putExtra(KEY_SEARCH_INFO, it)
- }
- }
-
- fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? {
- return intent.getParcelableExtraCompat(KEY_SEARCH_INFO)
- }
-
- private fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
- registerInfo?.let {
- intent.putExtra(KEY_REGISTER_INFO, it)
- }
- }
-
- fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? {
- return intent.getParcelableExtraCompat(KEY_REGISTER_INFO)
- }
-
- fun removeInfoFromIntent(intent: Intent) {
- intent.removeExtra(KEY_SEARCH_INFO)
- intent.removeExtra(KEY_REGISTER_INFO)
- }
-
- fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
- intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
- }
-
- fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- if (AutofillHelper.retrieveAutofillComponent(intent) != null)
- return SpecialMode.SELECTION
- }
- return intent.getEnumExtra(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
- }
-
- private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
- intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
- }
-
- fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- if (AutofillHelper.retrieveAutofillComponent(intent) != null)
- return TypeMode.AUTOFILL
- }
- return intent.getEnumExtra(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
- }
-
- fun removeModesFromIntent(intent: Intent) {
- intent.removeExtra(KEY_SPECIAL_MODE)
- intent.removeExtra(KEY_TYPE_MODE)
- }
-
- fun doSpecialAction(intent: Intent,
- defaultAction: () -> Unit,
- searchAction: (searchInfo: SearchInfo) -> Unit,
- saveAction: (searchInfo: SearchInfo) -> Unit,
- keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
- autofillSelectionAction: (searchInfo: SearchInfo?,
- autofillComponent: AutofillComponent) -> Unit,
- autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
-
- when (retrieveSpecialModeFromIntent(intent)) {
- SpecialMode.DEFAULT -> {
- removeModesFromIntent(intent)
- removeInfoFromIntent(intent)
- defaultAction.invoke()
- }
- SpecialMode.SEARCH -> {
- val searchInfo = retrieveSearchInfoFromIntent(intent)
- removeModesFromIntent(intent)
- removeInfoFromIntent(intent)
- if (searchInfo != null)
- searchAction.invoke(searchInfo)
- else {
- defaultAction.invoke()
- }
- }
- SpecialMode.SAVE -> {
- val searchInfo = retrieveSearchInfoFromIntent(intent)
- removeModesFromIntent(intent)
- removeInfoFromIntent(intent)
- if (searchInfo != null)
- saveAction.invoke(searchInfo)
- else {
- defaultAction.invoke()
- }
- }
- SpecialMode.SELECTION -> {
- val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent)
- var autofillComponentInit = false
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- AutofillHelper.retrieveAutofillComponent(intent)?.let { autofillComponent ->
- autofillSelectionAction.invoke(searchInfo, autofillComponent)
- autofillComponentInit = true
- }
- }
- if (!autofillComponentInit) {
- if (intent.getEnumExtra(KEY_SPECIAL_MODE) != null) {
- when (retrieveTypeModeFromIntent(intent)) {
- TypeMode.DEFAULT -> {
- removeModesFromIntent(intent)
- if (searchInfo != null)
- searchAction.invoke(searchInfo)
- else
- defaultAction.invoke()
- }
- TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
- else -> {
- // In this case, error
- removeModesFromIntent(intent)
- removeInfoFromIntent(intent)
- }
- }
- } else {
- if (searchInfo != null)
- searchAction.invoke(searchInfo)
- else
- defaultAction.invoke()
- }
- }
- }
- SpecialMode.REGISTRATION -> {
- val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
- removeModesFromIntent(intent)
- removeInfoFromIntent(intent)
- autofillRegistrationAction.invoke(registerInfo)
- }
- }
- }
-}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/TypeMode.kt b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/TypeMode.kt
deleted file mode 100644
index 2269c0b01..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/TypeMode.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.kunzisoft.keepass.activities.helpers
-
-enum class TypeMode {
- DEFAULT, MAGIKEYBOARD, AUTOFILL
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt
index a38b173ed..f823220f6 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt
@@ -1,96 +1,240 @@
package com.kunzisoft.keepass.activities.legacy
-import android.net.Uri
+import android.Manifest
+import android.content.pm.PackageManager
+import android.os.Build
import android.os.Bundle
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
+import androidx.appcompat.app.AlertDialog
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
+import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.ContextualDatabase
-import com.kunzisoft.keepass.database.MainCredential
-import com.kunzisoft.keepass.database.DatabaseTaskProvider
-import com.kunzisoft.keepass.model.CipherEncryptDatabase
+import com.kunzisoft.keepass.database.DatabaseTaskProvider.Companion.startDatabaseService
+import com.kunzisoft.keepass.database.ProgressMessage
+import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.tasks.ActionRunnable
-import com.kunzisoft.keepass.utils.getBinaryDir
+import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
+import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
+import kotlinx.coroutines.launch
-abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
+abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
- protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
- protected var mDatabase: ContextualDatabase? = null
+ protected val mDatabase: ContextualDatabase?
+ get() = mDatabaseViewModel.database
+
+ private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
+ private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
+
+ private val mActionDatabaseListener =
+ object : DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
+ override fun onDatabaseChangeValidated() {
+ mDatabaseViewModel.onDatabaseChangeValidated()
+ }
+ }
+
+ private val tempServiceParameters = mutableListOf>()
+ private val requestPermissionLauncher = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { _ ->
+ // Whether or not the user has accepted, the service can be started,
+ // There just won't be any notification if it's not allowed.
+ tempServiceParameters.removeFirstOrNull()?.let {
+ startDatabaseService(it.first, it.second)
+ }
+ }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ mDatabaseViewModel.actionState.collect { uiState ->
+ when (uiState) {
+ is DatabaseViewModel.ActionState.Loading -> {}
+ is DatabaseViewModel.ActionState.OnDatabaseReloaded -> {
+ if (finishActivityIfReloadRequested()) {
+ finish()
+ }
+ }
- mDatabaseTaskProvider = DatabaseTaskProvider(this, showDatabaseDialog())
+ is DatabaseViewModel.ActionState.OnDatabaseInfoChanged -> {
+ if (manageDatabaseInfo()) {
+ showDatabaseChangedDialog(
+ uiState.previousDatabaseInfo,
+ uiState.newDatabaseInfo,
+ uiState.readOnlyDatabase
+ )
+ }
+ }
- mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
- val databaseWasReloaded = database?.wasReloaded == true
- if (databaseWasReloaded && finishActivityIfReloadRequested()) {
- finish()
- } else if (mDatabase == null || mDatabase != database || databaseWasReloaded) {
- database?.wasReloaded = false
- onDatabaseRetrieved(database)
+ is DatabaseViewModel.ActionState.OnDatabaseActionRequested -> {
+ startDatabasePermissionService(
+ uiState.bundle,
+ uiState.actionTask
+ )
+ }
+
+ is DatabaseViewModel.ActionState.OnDatabaseActionStarted -> {
+ if (showDatabaseDialog())
+ startDialog(uiState.progressMessage)
+ }
+
+ is DatabaseViewModel.ActionState.OnDatabaseActionUpdated -> {
+ if (showDatabaseDialog())
+ updateDialog(uiState.progressMessage)
+ }
+
+ is DatabaseViewModel.ActionState.OnDatabaseActionStopped -> {
+ // Remove the progress task
+ stopDialog()
+ }
+
+ is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
+ onDatabaseActionFinished(
+ uiState.database,
+ uiState.actionTask,
+ uiState.result
+ )
+ stopDialog()
+ }
+ }
+ }
}
}
- mDatabaseTaskProvider?.onActionFinish = { database, actionTask, result ->
- onDatabaseActionFinished(database, actionTask, result)
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ mDatabaseViewModel.databaseState.collect { database ->
+ // Nullable function
+ onUnknownDatabaseRetrieved(database)
+ database?.let {
+ onDatabaseRetrieved(database)
+ }
+ }
+ }
}
}
- protected open fun showDatabaseDialog(): Boolean {
- return true
- }
-
- override fun onDestroy() {
- mDatabaseTaskProvider?.destroy()
- mDatabaseTaskProvider = null
- mDatabase = null
- super.onDestroy()
- }
-
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- mDatabase = database
- mDatabaseViewModel.defineDatabase(database)
+ /**
+ * Nullable function to retrieve a database
+ */
+ open fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
// optional method implementation
}
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
+ // optional method implementation
+ }
+
+ open fun manageDatabaseInfo(): Boolean = true
+
override fun onDatabaseActionFinished(
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
- mDatabaseViewModel.onActionFinished(database, actionTask, result)
// optional method implementation
}
- fun createDatabase(
- databaseUri: Uri,
- mainCredential: MainCredential
+ private fun startDatabasePermissionService(bundle: Bundle?, actionTask: String) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
+ == PackageManager.PERMISSION_GRANTED
+ ) {
+ startDatabaseService(bundle, actionTask)
+ } else if (ActivityCompat.shouldShowRequestPermissionRationale(
+ this,
+ Manifest.permission.POST_NOTIFICATIONS
+ )
+ ) {
+ // it's not the first time, so the user deliberately chooses not to display the notification
+ startDatabaseService(bundle, actionTask)
+ } else {
+ AlertDialog.Builder(this)
+ .setMessage(R.string.warning_database_notification_permission)
+ .setNegativeButton(R.string.later) { _, _ ->
+ // Refuses the notification, so start the service
+ startDatabaseService(bundle, actionTask)
+ }
+ .setPositiveButton(R.string.ask) { _, _ ->
+ // Save the temp parameters to ask the permission
+ tempServiceParameters.add(Pair(bundle, actionTask))
+ requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ }.create().show()
+ }
+ } else {
+ startDatabaseService(bundle, actionTask)
+ }
+ }
+
+ private fun showDatabaseChangedDialog(
+ previousDatabaseInfo: SnapFileDatabaseInfo,
+ newDatabaseInfo: SnapFileDatabaseInfo,
+ readOnlyDatabase: Boolean
) {
- mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential)
+ lifecycleScope.launch {
+ if (databaseChangedDialogFragment == null) {
+ databaseChangedDialogFragment = supportFragmentManager
+ .findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
+ databaseChangedDialogFragment?.actionDatabaseListener =
+ mActionDatabaseListener
+ }
+ if (progressTaskDialogFragment == null) {
+ databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
+ previousDatabaseInfo,
+ newDatabaseInfo,
+ readOnlyDatabase
+ )
+ databaseChangedDialogFragment?.actionDatabaseListener =
+ mActionDatabaseListener
+ databaseChangedDialogFragment?.show(
+ supportFragmentManager,
+ DATABASE_CHANGED_DIALOG_TAG
+ )
+ }
+ }
}
- fun loadDatabase(
- databaseUri: Uri,
- mainCredential: MainCredential,
- readOnly: Boolean,
- cipherEncryptDatabase: CipherEncryptDatabase?,
- fixDuplicateUuid: Boolean
- ) {
- mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
+ private fun startDialog(progressMessage: ProgressMessage) {
+ lifecycleScope.launch {
+ if (progressTaskDialogFragment == null) {
+ progressTaskDialogFragment = supportFragmentManager
+ .findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
+ }
+ if (progressTaskDialogFragment == null) {
+ progressTaskDialogFragment = ProgressTaskDialogFragment()
+ progressTaskDialogFragment?.show(
+ supportFragmentManager,
+ PROGRESS_TASK_DIALOG_TAG
+ )
+ }
+ updateDialog(progressMessage)
+ }
}
- protected fun closeDatabase() {
- mDatabase?.clearAndClose(this.getBinaryDir())
+ private fun updateDialog(progressMessage: ProgressMessage) {
+ progressTaskDialogFragment?.apply {
+ updateTitle(progressMessage.titleId)
+ updateMessage(progressMessage.messageId)
+ updateWarning(progressMessage.warningId)
+ setCancellable(progressMessage.cancelable)
+ }
}
- override fun onResume() {
- super.onResume()
- mDatabaseTaskProvider?.registerProgressTask()
+ private fun stopDialog() {
+ progressTaskDialogFragment?.dismissAllowingStateLoss()
+ progressTaskDialogFragment = null
}
- override fun onPause() {
- mDatabaseTaskProvider?.unregisterProgressTask()
- super.onPause()
+ protected open fun showDatabaseDialog(): Boolean {
+ return true
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt
index 9f02b639b..dc0bf7f2e 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt
@@ -34,8 +34,8 @@ import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
-import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
-import com.kunzisoft.keepass.activities.helpers.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
+import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.element.Entry
@@ -87,128 +87,44 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
deleteDatabaseNodes(nodes)
}
- mDatabaseViewModel.saveDatabase.observe(this) { save ->
- mDatabaseTaskProvider?.startDatabaseSave(save)
- }
-
- mDatabaseViewModel.mergeDatabase.observe(this) { save ->
- mDatabaseTaskProvider?.startDatabaseMerge(save)
- }
-
- mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
- mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
- mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
- }
- }
-
- mDatabaseViewModel.saveName.observe(this) {
- mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
- }
-
- mDatabaseViewModel.saveDescription.observe(this) {
- mDatabaseTaskProvider?.startDatabaseSaveDescription(it.oldValue, it.newValue, it.save)
- }
-
- mDatabaseViewModel.saveDefaultUsername.observe(this) {
- mDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(it.oldValue, it.newValue, it.save)
- }
-
- mDatabaseViewModel.saveColor.observe(this) {
- mDatabaseTaskProvider?.startDatabaseSaveColor(it.oldValue, it.newValue, it.save)
- }
-
- mDatabaseViewModel.saveCompression.observe(this) {
- mDatabaseTaskProvider?.startDatabaseSaveCompression(it.oldValue, it.newValue, it.save)
- }
-
- mDatabaseViewModel.removeUnlinkData.observe(this) {
- mDatabaseTaskProvider?.startDatabaseRemoveUnlinkedData(it)
- }
-
- mDatabaseViewModel.saveRecycleBin.observe(this) {
- mDatabaseTaskProvider?.startDatabaseSaveRecycleBin(it.oldValue, it.newValue, it.save)
- }
-
- mDatabaseViewModel.saveTemplatesGroup.observe(this) {
- mDatabaseTaskProvider?.startDatabaseSaveTemplatesGroup(it.oldValue, it.newValue, it.save)
- }
-
- mDatabaseViewModel.saveMaxHistoryItems.observe(this) {
- mDatabaseTaskProvider?.startDatabaseSaveMaxHistoryItems(it.oldValue, it.newValue, it.save)
- }
-
- mDatabaseViewModel.saveMaxHistorySize.observe(this) {
- mDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(it.oldValue, it.newValue, it.save)
- }
-
- mDatabaseViewModel.saveEncryption.observe(this) {
- mDatabaseTaskProvider?.startDatabaseSaveEncryption(it.oldValue, it.newValue, it.save)
- }
-
- mDatabaseViewModel.saveKeyDerivation.observe(this) {
- mDatabaseTaskProvider?.startDatabaseSaveKeyDerivation(it.oldValue, it.newValue, it.save)
- }
-
- mDatabaseViewModel.saveIterations.observe(this) {
- mDatabaseTaskProvider?.startDatabaseSaveIterations(it.oldValue, it.newValue, it.save)
- }
-
- mDatabaseViewModel.saveMemoryUsage.observe(this) {
- mDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(it.oldValue, it.newValue, it.save)
- }
-
- mDatabaseViewModel.saveParallelism.observe(this) {
- mDatabaseTaskProvider?.startDatabaseSaveParallelism(it.oldValue, it.newValue, it.save)
- }
-
mExitLock = false
}
- open fun finishActivityIfDatabaseNotLoaded(): Boolean {
- return true
- }
-
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
-
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
// End activity if database not loaded
- if (finishActivityIfDatabaseNotLoaded() && (database == null || !database.loaded)) {
+ if (database.loaded.not())
finish()
- }
// Focus view to reinitialize timeout,
// view is not necessary loaded so retry later in resume
viewToInvalidateTimeout()
- ?.resetAppTimeoutWhenViewTouchedOrFocused(this, database?.loaded)
+ ?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded)
- database?.let {
- // check timeout
- if (mTimeoutEnable) {
- if (mLockReceiver == null) {
- mLockReceiver = LockReceiver {
- mDatabase = null
- closeDatabase(database)
- mExitLock = true
- closeOptionsMenu()
- finish()
- }
- registerLockReceiver(mLockReceiver)
+ // check timeout
+ if (mTimeoutEnable) {
+ if (mLockReceiver == null) {
+ mLockReceiver = LockReceiver {
+ closeDatabase(database)
+ mExitLock = true
+ closeOptionsMenu()
+ finish()
}
-
- // After the first creation
- // or If simply swipe with another application
- // If the time is out -> close the Activity
- TimeoutHelper.checkTimeAndLockIfTimeout(this)
- // If onCreate already record time
- if (!mExitLock)
- TimeoutHelper.recordTime(this, database.loaded)
+ registerLockReceiver(mLockReceiver)
}
- mDatabaseReadOnly = database.isReadOnly
- mMergeDataAllowed = database.isMergeDataAllowed()
-
- checkRegister()
+ // After the first creation
+ // or If simply swipe with another application
+ // If the time is out -> close the Activity
+ TimeoutHelper.checkTimeAndLockIfTimeout(this)
+ // If onCreate already record time
+ if (!mExitLock)
+ TimeoutHelper.recordTime(this, database.loaded)
}
+
+ mDatabaseReadOnly = database.isReadOnly
+ mMergeDataAllowed = database.isMergeDataAllowed()
+
+ checkRegister()
}
override fun finish() {
@@ -227,7 +143,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
actionTask: String,
result: ActionRunnable.Result
) {
- super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
@@ -249,24 +164,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
databaseUri: Uri?,
mainCredential: MainCredential
) {
- assignDatabasePassword(databaseUri, mainCredential)
+ mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
}
- private fun assignDatabasePassword(
- databaseUri: Uri?,
- mainCredential: MainCredential
- ) {
- if (databaseUri != null) {
- mDatabaseTaskProvider?.startDatabaseAssignCredential(databaseUri, mainCredential)
- }
- }
-
- fun assignPassword(mainCredential: MainCredential) {
+ fun assignMainCredential(mainCredential: MainCredential) {
mDatabase?.let { database ->
database.fileUri?.let { databaseUri ->
// Show the progress dialog now or after dialog confirmation
if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) {
- assignDatabasePassword(databaseUri, mainCredential)
+ mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
} else {
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
.show(supportFragmentManager, "passwordEncodingTag")
@@ -276,45 +182,51 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
fun saveDatabase() {
- mDatabaseTaskProvider?.startDatabaseSave(true)
+ mDatabaseViewModel.saveDatabase(save = true)
}
fun saveDatabaseTo(uri: Uri) {
- mDatabaseTaskProvider?.startDatabaseSave(true, uri)
+ mDatabaseViewModel.saveDatabase(save = true, saveToUri = uri)
}
fun mergeDatabase() {
- mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable)
+ mDatabaseViewModel.mergeDatabase(save = mAutoSaveEnable)
}
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
- mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable, uri, mainCredential)
+ mDatabaseViewModel.mergeDatabase(mAutoSaveEnable, uri, mainCredential)
}
fun reloadDatabase() {
- mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
- mDatabaseTaskProvider?.startDatabaseReload(false)
- }
+ mDatabaseViewModel.reloadDatabase(fixDuplicateUuid = false)
}
- fun createEntry(newEntry: Entry,
- parent: Group) {
- mDatabaseTaskProvider?.startDatabaseCreateEntry(newEntry, parent, mAutoSaveEnable)
+ fun createEntry(
+ newEntry: Entry,
+ parent: Group
+ ) {
+ mDatabaseViewModel.createEntry(newEntry, parent, mAutoSaveEnable)
}
- fun updateEntry(oldEntry: Entry,
- entryToUpdate: Entry) {
- mDatabaseTaskProvider?.startDatabaseUpdateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
+ fun updateEntry(
+ oldEntry: Entry,
+ entryToUpdate: Entry
+ ) {
+ mDatabaseViewModel.updateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
}
- fun copyNodes(nodesToCopy: List,
- newParent: Group) {
- mDatabaseTaskProvider?.startDatabaseCopyNodes(nodesToCopy, newParent, mAutoSaveEnable)
+ fun copyNodes(
+ nodesToCopy: List,
+ newParent: Group
+ ) {
+ mDatabaseViewModel.copyNodes(nodesToCopy, newParent, mAutoSaveEnable)
}
- fun moveNodes(nodesToMove: List,
- newParent: Group) {
- mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable)
+ fun moveNodes(
+ nodesToMove: List,
+ newParent: Group
+ ) {
+ mDatabaseViewModel.moveNodes(nodesToMove, newParent, mAutoSaveEnable)
}
private fun eachNodeRecyclable(database: ContextualDatabase, nodes: List): Boolean {
@@ -330,6 +242,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
fun deleteNodes(nodes: List, recycleBin: Boolean = false) {
+ // TODO Move in ViewModel
mDatabase?.let { database ->
// If recycle bin enabled, ensure it exists
if (database.isRecycleBinEnabled) {
@@ -350,11 +263,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
private fun deleteDatabaseNodes(nodes: List) {
- mDatabaseTaskProvider?.startDatabaseDeleteNodes(nodes, mAutoSaveEnable)
+ mDatabaseViewModel.deleteNodes(nodes, mAutoSaveEnable)
}
- fun createGroup(parent: Group,
- groupInfo: GroupInfo?) {
+ fun createGroup(
+ parent: Group,
+ groupInfo: GroupInfo?
+ ) {
+ // TODO Move in ViewModel
// Build the group
mDatabase?.createGroup()?.let { newGroup ->
groupInfo?.let { info ->
@@ -362,12 +278,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
// Not really needed here because added in runnable but safe
newGroup.parent = parent
- mDatabaseTaskProvider?.startDatabaseCreateGroup(newGroup, parent, mAutoSaveEnable)
+ mDatabaseViewModel.createGroup(newGroup, parent, mAutoSaveEnable)
}
}
- fun updateGroup(oldGroup: Group,
- groupInfo: GroupInfo) {
+ fun updateGroup(
+ oldGroup: Group,
+ groupInfo: GroupInfo
+ ) {
+ // TODO Move in ViewModel
// If group updated save it in the database
val updateGroup = Group(oldGroup).let { updateGroup ->
updateGroup.apply {
@@ -377,27 +296,28 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
this.setGroupInfo(groupInfo)
}
}
- mDatabaseTaskProvider?.startDatabaseUpdateGroup(oldGroup, updateGroup, mAutoSaveEnable)
+ mDatabaseViewModel.updateGroup(oldGroup, updateGroup, mAutoSaveEnable)
}
- fun restoreEntryHistory(mainEntryId: NodeId,
- entryHistoryPosition: Int) {
- mDatabaseTaskProvider
- ?.startDatabaseRestoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
+ fun restoreEntryHistory(
+ mainEntryId: NodeId,
+ entryHistoryPosition: Int
+ ) {
+ mDatabaseViewModel.restoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
}
- fun deleteEntryHistory(mainEntryId: NodeId,
- entryHistoryPosition: Int) {
- mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
+ fun deleteEntryHistory(
+ mainEntryId: NodeId,
+ entryHistoryPosition: Int
+ ) {
+ mDatabaseViewModel.deleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
}
private fun checkRegister() {
- // If in ave or registration mode, don't allow read only
- if ((mSpecialMode == SpecialMode.SAVE
- || mSpecialMode == SpecialMode.REGISTRATION)
- && mDatabaseReadOnly) {
+ // If in registration mode, don't allow read only
+ if (mSpecialMode == SpecialMode.REGISTRATION && mDatabaseReadOnly) {
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
- EntrySelectionHelper.removeModesFromIntent(intent)
+ intent.removeModes()
finish()
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt
index 640682278..4f9860753 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt
@@ -1,13 +1,24 @@
package com.kunzisoft.keepass.activities.legacy
+import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
import com.kunzisoft.keepass.R
-import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
-import com.kunzisoft.keepass.activities.helpers.SpecialMode
-import com.kunzisoft.keepass.activities.helpers.TypeMode
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveTypeMode
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
+import com.kunzisoft.keepass.credentialprovider.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.TypeMode
+import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.ToolbarSpecial
@@ -19,10 +30,25 @@ import com.kunzisoft.keepass.view.ToolbarSpecial
abstract class DatabaseModeActivity : DatabaseActivity() {
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
- private var mTypeMode: TypeMode = TypeMode.DEFAULT
+ protected var mTypeMode: TypeMode = TypeMode.DEFAULT
private var mToolbarSpecial: ToolbarSpecial? = null
+ /**
+ * Utility activity result launcher,
+ * Used recursively, close each activity with return data
+ */
+ protected open var mCredentialActivityResultLauncher: ActivityResultLauncher? =
+ registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) {
+ setActivityResult(
+ lockDatabase = false,
+ resultCode = it.resultCode,
+ data = it.data
+ )
+ }
+
open fun onDatabaseBackPressed() {
if (mSpecialMode != SpecialMode.DEFAULT)
onCancelSpecialMode()
@@ -42,20 +68,14 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
/**
* Intent sender uses special retains data in callback
*/
- private fun isIntentSender(): Boolean {
- return (mSpecialMode == SpecialMode.SELECTION
- && mTypeMode == TypeMode.AUTOFILL)
- /* TODO Registration callback #765
- || (mSpecialMode == SpecialMode.REGISTRATION
- && mTypeMode == TypeMode.AUTOFILL
- && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
- */
+ protected fun isIntentSender(): Boolean {
+ return isIntentSenderMode(mSpecialMode, mTypeMode)
}
fun onLaunchActivitySpecialMode() {
if (!isIntentSender()) {
- EntrySelectionHelper.removeModesFromIntent(intent)
- EntrySelectionHelper.removeInfoFromIntent(intent)
+ intent.removeModes()
+ intent.removeInfo()
finish()
}
}
@@ -64,8 +84,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
if (isIntentSender()) {
super.finish()
} else {
- EntrySelectionHelper.removeModesFromIntent(intent)
- EntrySelectionHelper.removeInfoFromIntent(intent)
+ intent.removeModes()
+ intent.removeInfo()
if (mSpecialMode != SpecialMode.DEFAULT) {
backToTheMainAppAndFinish()
}
@@ -77,8 +97,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
// To get the app caller, only for IntentSender
onRegularBackPressed()
} else {
- EntrySelectionHelper.removeModesFromIntent(intent)
- EntrySelectionHelper.removeInfoFromIntent(intent)
+ intent.removeModes()
+ intent.removeInfo()
if (mSpecialMode != SpecialMode.DEFAULT) {
backToTheMainAppAndFinish()
}
@@ -109,17 +129,18 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
}
})
- mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
- mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
+ mSpecialMode = intent.retrieveSpecialMode()
+ mTypeMode = intent.retrieveTypeMode()
}
override fun onResume() {
super.onResume()
- mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
- mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
- val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)?.searchInfo
- ?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
+ mSpecialMode = intent.retrieveSpecialMode()
+ mTypeMode = intent.retrieveTypeMode()
+ val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
+ val searchInfo: SearchInfo? = registerInfo?.searchInfo
+ ?: intent.retrieveSearchInfo()
// To show the selection mode
mToolbarSpecial = findViewById(R.id.special_mode_view)
@@ -128,26 +149,25 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
val selectionModeStringId = when (mSpecialMode) {
SpecialMode.DEFAULT, // Not important because hidden
SpecialMode.SEARCH -> R.string.search_mode
- SpecialMode.SAVE -> R.string.save_mode
SpecialMode.SELECTION -> R.string.selection_mode
- SpecialMode.REGISTRATION -> R.string.registration_mode
+ SpecialMode.REGISTRATION -> R.string.save_mode // Save is registration mode
}
val typeModeStringId = when (mTypeMode) {
TypeMode.DEFAULT, // Not important because hidden
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
TypeMode.AUTOFILL -> R.string.autofill
+ TypeMode.PASSKEY -> R.string.passkey
}
title = getString(selectionModeStringId)
if (mTypeMode != TypeMode.DEFAULT)
title = "$title (${getString(typeModeStringId)})"
// Populate subtitle
- subtitle = searchInfo?.getName(resources)
+ subtitle = registerInfo?.getName(resources) ?: searchInfo?.getName(resources)
// Show the toolbar or not
visible = when (mSpecialMode) {
SpecialMode.DEFAULT -> false
SpecialMode.SEARCH -> true
- SpecialMode.SAVE -> true
SpecialMode.SELECTION -> true
SpecialMode.REGISTRATION -> true
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseRetrieval.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseRetrieval.kt
index 8c9db97ac..4ac41db06 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseRetrieval.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseRetrieval.kt
@@ -4,8 +4,11 @@ import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.tasks.ActionRunnable
interface DatabaseRetrieval {
- fun onDatabaseRetrieved(database: ContextualDatabase?)
- fun onDatabaseActionFinished(database: ContextualDatabase,
- actionTask: String,
- result: ActionRunnable.Result)
+ fun onDatabaseRetrieved(database: ContextualDatabase)
+
+ fun onDatabaseActionFinished(
+ database: ContextualDatabase,
+ actionTask: String,
+ result: ActionRunnable.Result
+ )
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/NodesAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/NodesAdapter.kt
index b58005093..890e0d7b4 100644
--- a/app/src/main/java/com/kunzisoft/keepass/adapters/NodesAdapter.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/adapters/NodesAdapter.kt
@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.adapters
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
-import android.os.Build
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
@@ -418,6 +417,7 @@ class NodesAdapter (
}
}
+ // OTP
val otpElement = entry.getOtpElement()
holder.otpContainer?.removeCallbacks(holder.otpRunnable)
if (otpElement != null
@@ -438,7 +438,11 @@ class NodesAdapter (
holder.otpContainer?.visibility = View.GONE
}
holder.attachmentIcon?.visibility =
- if (entry.containsAttachment()) View.VISIBLE else View.GONE
+ if (entry.containsAttachment()) View.VISIBLE else View.GONE
+
+ // Passkey
+ holder.passkeyIcon?.visibility =
+ if (entry.getPasskey() != null) View.VISIBLE else View.GONE
// Assign colors
assignBackgroundColor(holder.container, entry)
@@ -451,6 +455,7 @@ class NodesAdapter (
holder.otpToken?.setTextColor(foregroundColor)
holder.otpProgress?.setIndicatorColor(foregroundColor)
holder.attachmentIcon?.setColorFilter(foregroundColor)
+ holder.passkeyIcon?.setColorFilter(foregroundColor)
holder.meta.setTextColor(foregroundColor)
iconColor = foregroundColor
} else {
@@ -459,6 +464,7 @@ class NodesAdapter (
holder.otpToken?.setTextColor(mTextColorSecondary)
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
+ holder.passkeyIcon?.setColorFilter(mTextColorSecondary)
holder.meta.setTextColor(mTextColor)
}
} else {
@@ -467,6 +473,7 @@ class NodesAdapter (
holder.otpToken?.setTextColor(mColorOnSecondary)
holder.otpProgress?.setIndicatorColor(mColorOnSecondary)
holder.attachmentIcon?.setColorFilter(mColorOnSecondary)
+ holder.passkeyIcon?.setColorFilter(mColorOnSecondary)
holder.meta.setTextColor(mColorOnSecondary)
}
@@ -611,6 +618,7 @@ class NodesAdapter (
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
+ var passkeyIcon: ImageView? = itemView.findViewById(R.id.node_passkey_icon)
}
companion object {
diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillComponent.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillComponent.kt
deleted file mode 100644
index bf8a2996f..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillComponent.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.kunzisoft.keepass.autofill
-
-import android.app.assist.AssistStructure
-
-data class AutofillComponent(val assistStructure: AssistStructure,
- val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/EntrySelectionHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/EntrySelectionHelper.kt
new file mode 100644
index 000000000..bf60df362
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/EntrySelectionHelper.kt
@@ -0,0 +1,374 @@
+/*
+ * 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 .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.os.Bundle
+import android.os.ParcelUuid
+import android.util.Log
+import android.widget.RemoteViews
+import androidx.activity.result.ActivityResultLauncher
+import androidx.annotation.RequiresApi
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.IconCompat
+import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.database.ContextualDatabase
+import com.kunzisoft.keepass.model.EntryInfo
+import com.kunzisoft.keepass.model.RegisterInfo
+import com.kunzisoft.keepass.model.SearchInfo
+import com.kunzisoft.keepass.utils.LOCK_ACTION
+import com.kunzisoft.keepass.utils.getEnumExtra
+import com.kunzisoft.keepass.utils.getParcelableExtraCompat
+import com.kunzisoft.keepass.utils.getParcelableList
+import com.kunzisoft.keepass.utils.putEnumExtra
+import com.kunzisoft.keepass.utils.putParcelableList
+import java.util.UUID
+
+object EntrySelectionHelper {
+
+ private const val KEY_SPECIAL_MODE = "com.kunzisoft.keepass.extra.SPECIAL_MODE"
+ private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
+ private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
+ private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
+ private const val EXTRA_NODES_IDS = "com.kunzisoft.keepass.extra.NODES_IDS"
+ private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.NODE_ID"
+
+ /**
+ * Finish the activity by passing the result code and by locking the database if necessary
+ */
+ fun Activity.setActivityResult(
+ lockDatabase: Boolean = false,
+ resultCode: Int,
+ data: Intent? = null
+ ) {
+ when (resultCode) {
+ Activity.RESULT_OK ->
+ this.setResult(resultCode, data)
+ Activity.RESULT_CANCELED ->
+ this.setResult(resultCode)
+ }
+ this.finish()
+
+ if (lockDatabase) {
+ // Close the database
+ this.sendBroadcast(Intent(LOCK_ACTION))
+ }
+ }
+
+ fun startActivityForSearchModeResult(
+ context: Context,
+ intent: Intent,
+ searchInfo: SearchInfo
+ ) {
+ intent.addSpecialMode(SpecialMode.SEARCH)
+ intent.addSearchInfo(searchInfo)
+ intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ context.startActivity(intent)
+ }
+
+ fun startActivityForSelectionModeResult(
+ context: Context,
+ intent: Intent,
+ typeMode: TypeMode,
+ searchInfo: SearchInfo?,
+ activityResultLauncher: ActivityResultLauncher? = null,
+ ) {
+ intent.addSpecialMode(SpecialMode.SELECTION)
+ intent.addTypeMode(typeMode)
+ intent.addSearchInfo(searchInfo)
+ if (activityResultLauncher == null) {
+ intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }
+ activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
+ }
+
+ fun startActivityForRegistrationModeResult(
+ context: Context,
+ activityResultLauncher: ActivityResultLauncher?,
+ intent: Intent,
+ registerInfo: RegisterInfo?,
+ typeMode: TypeMode
+ ) {
+ intent.addSpecialMode(SpecialMode.REGISTRATION)
+ intent.addTypeMode(typeMode)
+ intent.addRegisterInfo(registerInfo)
+ if (activityResultLauncher == null) {
+ intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }
+ activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
+ }
+
+ /**
+ * Build the special mode response for internal entry selection for one entry
+ */
+ fun Activity.buildSpecialModeResponseAndSetResult(
+ entryInfo: EntryInfo,
+ extras: Bundle? = null
+ ) {
+ this.buildSpecialModeResponseAndSetResult(listOf(entryInfo), extras)
+ }
+
+ /**
+ * Build the special mode response for internal entry selection for multiple entries
+ */
+ fun Activity.buildSpecialModeResponseAndSetResult(
+ entriesInfo: List,
+ extras: Bundle? = null
+ ) {
+ try {
+ val mReplyIntent = Intent()
+ Log.d(javaClass.name, "Success special mode manual selection")
+ mReplyIntent.addNodesIds(entriesInfo.map { it.id })
+ extras?.let {
+ mReplyIntent.putExtras(it)
+ }
+ setResult(Activity.RESULT_OK, mReplyIntent)
+ } catch (e: Exception) {
+ Log.e(javaClass.name, "Unable to add the result", e)
+ setResult(Activity.RESULT_CANCELED)
+ }
+ }
+
+ fun Intent.addSearchInfo(searchInfo: SearchInfo?): Intent {
+ searchInfo?.let {
+ putExtra(KEY_SEARCH_INFO, it)
+ }
+ return this
+ }
+
+ fun Intent.retrieveSearchInfo(): SearchInfo? {
+ return getParcelableExtraCompat(KEY_SEARCH_INFO)
+ }
+
+ fun Intent.addRegisterInfo(registerInfo: RegisterInfo?): Intent {
+ registerInfo?.let {
+ putExtra(KEY_REGISTER_INFO, it)
+ }
+ return this
+ }
+
+ fun Intent.retrieveRegisterInfo(): RegisterInfo? {
+ return getParcelableExtraCompat(KEY_REGISTER_INFO)
+ }
+
+ fun Intent.removeInfo() {
+ removeExtra(KEY_SEARCH_INFO)
+ removeExtra(KEY_REGISTER_INFO)
+ }
+
+ fun Intent.addSpecialMode(specialMode: SpecialMode): Intent {
+ this.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
+ return this
+ }
+
+ fun Intent.retrieveSpecialMode(): SpecialMode {
+ return getEnumExtra(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
+ }
+
+ fun Intent.addTypeMode(typeMode: TypeMode): Intent {
+ this.putEnumExtra(KEY_TYPE_MODE, typeMode)
+ return this
+ }
+
+ fun Intent.retrieveTypeMode(): TypeMode {
+ return getEnumExtra(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
+ }
+
+ fun Intent.removeModes() {
+ removeExtra(KEY_SPECIAL_MODE)
+ removeExtra(KEY_TYPE_MODE)
+ }
+
+ fun Intent.addNodesIds(nodesIds: List): Intent {
+ this.putParcelableList(EXTRA_NODES_IDS, nodesIds.map { ParcelUuid(it) })
+ return this
+ }
+
+ fun Intent.retrieveNodesIds(): List? {
+ return getParcelableList(EXTRA_NODES_IDS)?.map { it.uuid }
+ }
+
+ fun Intent.removeNodesIds() {
+ removeExtra(EXTRA_NODES_IDS)
+ }
+
+ /**
+ * Add the node id to the intent
+ */
+ fun Intent.addNodeId(nodeId: UUID?) {
+ nodeId?.let {
+ putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId))
+ }
+ }
+
+ /**
+ * Retrieve the node id from the intent
+ */
+ fun Intent.retrieveNodeId(): UUID? {
+ return getParcelableExtraCompat(EXTRA_NODE_ID)?.uuid
+ }
+
+ fun Intent.removeNodeId() {
+ removeExtra(EXTRA_NODE_ID)
+ }
+
+ /**
+ * Intent sender uses special retains data in callback
+ */
+ fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
+ return (specialMode == SpecialMode.SELECTION
+ && (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
+ || (specialMode == SpecialMode.REGISTRATION
+ && (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
+ }
+
+ fun doSpecialAction(
+ intent: Intent,
+ defaultAction: () -> Unit,
+ searchAction: (searchInfo: SearchInfo) -> Unit,
+ selectionAction: (
+ intentSenderMode: Boolean,
+ typeMode: TypeMode,
+ searchInfo: SearchInfo?
+ ) -> Unit,
+ registrationAction: (
+ intentSenderMode: Boolean,
+ typeMode: TypeMode,
+ registerInfo: RegisterInfo?
+ ) -> Unit
+ ) {
+ when (val specialMode = intent.retrieveSpecialMode()) {
+ SpecialMode.DEFAULT -> {
+ intent.removeModes()
+ intent.removeInfo()
+ defaultAction.invoke()
+ }
+ SpecialMode.SEARCH -> {
+ val searchInfo = intent.retrieveSearchInfo()
+ intent.removeModes()
+ intent.removeInfo()
+ if (searchInfo != null)
+ searchAction.invoke(searchInfo)
+ else {
+ defaultAction.invoke()
+ }
+ }
+ SpecialMode.SELECTION -> {
+ val searchInfo: SearchInfo? = intent.retrieveSearchInfo()
+ if (intent.getEnumExtra(KEY_SPECIAL_MODE) != null) {
+ when (val typeMode = intent.retrieveTypeMode()) {
+ TypeMode.DEFAULT -> {
+ intent.removeModes()
+ if (searchInfo != null)
+ searchAction.invoke(searchInfo)
+ else
+ defaultAction.invoke()
+ }
+ TypeMode.MAGIKEYBOARD -> selectionAction.invoke(
+ isIntentSenderMode(specialMode, typeMode),
+ typeMode,
+ searchInfo
+ )
+ TypeMode.PASSKEY ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ selectionAction.invoke(
+ isIntentSenderMode(specialMode, typeMode),
+ typeMode,
+ searchInfo
+ )
+ } else
+ defaultAction.invoke()
+ TypeMode.AUTOFILL -> {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ selectionAction.invoke(
+ isIntentSenderMode(specialMode, typeMode),
+ typeMode,
+ searchInfo
+ )
+ } else
+ defaultAction.invoke()
+ }
+ }
+ } else {
+ if (searchInfo != null)
+ searchAction.invoke(searchInfo)
+ else
+ defaultAction.invoke()
+ }
+ }
+ SpecialMode.REGISTRATION -> {
+ val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
+ val typeMode = intent.retrieveTypeMode()
+ val intentSenderMode = isIntentSenderMode(specialMode, typeMode)
+ if (!intentSenderMode) {
+ intent.removeModes()
+ intent.removeInfo()
+ }
+ if (registerInfo != null)
+ registrationAction.invoke(
+ intentSenderMode,
+ typeMode,
+ registerInfo
+ )
+ else {
+ defaultAction.invoke()
+ }
+ }
+ }
+ }
+
+ fun performSelection(items: List,
+ actionPopulateCredentialProvider: (entryInfo: EntryInfo) -> Unit,
+ actionEntrySelection: (autoSearch: Boolean) -> Unit) {
+ if (items.size == 1) {
+ val itemFound = items[0]
+ actionPopulateCredentialProvider.invoke(itemFound)
+ } else if (items.size > 1) {
+ // Select the one we want in the selection
+ actionEntrySelection.invoke(true)
+ } else {
+ // Select an arbitrary one
+ actionEntrySelection.invoke(false)
+ }
+ }
+
+ /**
+ * Method to assign a drawable to a new icon from a database icon
+ */
+ @RequiresApi(Build.VERSION_CODES.M)
+ fun EntryInfo.buildIcon(
+ context: Context,
+ database: ContextualDatabase
+ ): Icon? {
+ try {
+ database.iconDrawableFactory.getBitmapFromIcon(context,
+ this.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
+ return IconCompat.createWithBitmap(bitmap).toIcon(context)
+ }
+ } catch (e: Exception) {
+ Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
+ }
+ return null
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/SpecialMode.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/SpecialMode.kt
similarity index 59%
rename from app/src/main/java/com/kunzisoft/keepass/activities/helpers/SpecialMode.kt
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/SpecialMode.kt
index 7b1dbc2ef..d4694d50a 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/SpecialMode.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/SpecialMode.kt
@@ -1,9 +1,8 @@
-package com.kunzisoft.keepass.activities.helpers
+package com.kunzisoft.keepass.credentialprovider
enum class SpecialMode {
DEFAULT,
SEARCH,
- SAVE,
SELECTION,
REGISTRATION;
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/TypeMode.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/TypeMode.kt
new file mode 100644
index 000000000..55b28e31c
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/TypeMode.kt
@@ -0,0 +1,5 @@
+package com.kunzisoft.keepass.credentialprovider
+
+enum class TypeMode {
+ DEFAULT, MAGIKEYBOARD, PASSKEY, AUTOFILL
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/AutofillLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/AutofillLauncherActivity.kt
new file mode 100644
index 000000000..1fad3d25d
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/AutofillLauncherActivity.kt
@@ -0,0 +1,237 @@
+/*
+ * 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 .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.activity
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
+import androidx.annotation.RequiresApi
+import androidx.lifecycle.lifecycleScope
+import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
+import com.kunzisoft.keepass.activities.GroupActivity
+import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addRegisterInfo
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
+import com.kunzisoft.keepass.credentialprovider.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
+import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
+import com.kunzisoft.keepass.credentialprovider.viewmodel.AutofillLauncherViewModel
+import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
+import com.kunzisoft.keepass.database.ContextualDatabase
+import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
+import com.kunzisoft.keepass.model.RegisterInfo
+import com.kunzisoft.keepass.model.SearchInfo
+import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
+import com.kunzisoft.keepass.view.toastError
+import kotlinx.coroutines.launch
+
+@RequiresApi(api = Build.VERSION_CODES.O)
+class AutofillLauncherActivity : DatabaseModeActivity() {
+
+ private val autofillLauncherViewModel: AutofillLauncherViewModel by viewModels()
+
+ private var mAutofillSelectionActivityResultLauncher: ActivityResultLauncher? =
+ this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ autofillLauncherViewModel.manageSelectionResult(it)
+ }
+
+ private var mAutofillRegistrationActivityResultLauncher: ActivityResultLauncher? =
+ this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ autofillLauncherViewModel.manageRegistrationResult(it)
+ }
+
+ override fun applyCustomStyle(): Boolean {
+ return false
+ }
+
+ override fun finishActivityIfReloadRequested(): Boolean {
+ return true
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ autofillLauncherViewModel.initialize()
+ lifecycleScope.launch {
+ // Initialize the parameters
+ autofillLauncherViewModel.uiState.collect { uiState ->
+ when (uiState) {
+ AutofillLauncherViewModel.UIState.Loading -> {}
+ is AutofillLauncherViewModel.UIState.ShowBlockRestartMessage -> {
+ showBlockRestartMessage()
+ autofillLauncherViewModel.cancelResult()
+ }
+ is AutofillLauncherViewModel.UIState.ShowReadOnlyMessage -> {
+ showReadOnlySaveMessage()
+ autofillLauncherViewModel.cancelResult()
+ }
+ is AutofillLauncherViewModel.UIState.ShowAutofillSuggestionMessage -> {
+ showAutofillSuggestionMessage()
+ }
+ }
+ }
+ }
+ lifecycleScope.launch {
+ // Retrieve the UI
+ autofillLauncherViewModel.credentialUiState.collect { uiState ->
+ when (uiState) {
+ is CredentialLauncherViewModel.UIState.Loading -> {}
+ is CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection -> {
+ GroupActivity.launchForSelection(
+ context = this@AutofillLauncherActivity,
+ database = uiState.database,
+ searchInfo = uiState.searchInfo,
+ typeMode = uiState.typeMode,
+ activityResultLauncher = mAutofillSelectionActivityResultLauncher,
+ )
+ }
+ is CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> {
+ GroupActivity.launchForRegistration(
+ context = this@AutofillLauncherActivity,
+ database = uiState.database,
+ registerInfo = uiState.registerInfo,
+ typeMode = uiState.typeMode,
+ activityResultLauncher = mAutofillRegistrationActivityResultLauncher
+ )
+ }
+ is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> {
+ FileDatabaseSelectActivity.launchForSelection(
+ context = this@AutofillLauncherActivity,
+ searchInfo = uiState.searchInfo,
+ typeMode = uiState.typeMode,
+ activityResultLauncher = mAutofillSelectionActivityResultLauncher
+ )
+ }
+ is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> {
+ FileDatabaseSelectActivity.launchForRegistration(
+ context = this@AutofillLauncherActivity,
+ registerInfo = uiState.registerInfo,
+ typeMode = uiState.typeMode,
+ activityResultLauncher = mAutofillRegistrationActivityResultLauncher,
+ )
+ }
+ is CredentialLauncherViewModel.UIState.SetActivityResult -> {
+ setActivityResult(
+ lockDatabase = uiState.lockDatabase,
+ resultCode = uiState.resultCode,
+ data = uiState.data
+ )
+ }
+ is CredentialLauncherViewModel.UIState.ShowError -> {
+ toastError(uiState.error)
+ autofillLauncherViewModel.cancelResult()
+ }
+ }
+ }
+ }
+ }
+
+ override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
+ super.onUnknownDatabaseRetrieved(database)
+ autofillLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
+ }
+
+ private fun showBlockRestartMessage() {
+ // If item not allowed, show a toast
+ Toast.makeText(
+ applicationContext,
+ R.string.autofill_block_restart,
+ Toast.LENGTH_LONG
+ ).show()
+ }
+
+ private fun showAutofillSuggestionMessage() {
+ Toast.makeText(
+ applicationContext,
+ R.string.autofill_inline_suggestions_keyboard,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+
+ private fun showReadOnlySaveMessage() {
+ toastError(RegisterInReadOnlyDatabaseException())
+ }
+
+ companion object {
+
+ private val TAG = AutofillLauncherActivity::class.java.name
+
+ fun getPendingIntentForSelection(
+ context: Context,
+ searchInfo: SearchInfo? = null,
+ autofillComponent: AutofillComponent
+ ): PendingIntent? {
+ try {
+ return PendingIntent.getActivity(
+ context,
+ randomRequestCode(),
+ // Doesn't work with direct extra Parcelable (don't know why?)
+ // Wrap into a bundle to bypass the problem
+ Intent(context, AutofillLauncherActivity::class.java).apply {
+ addSpecialMode(SpecialMode.SELECTION)
+ addSearchInfo(searchInfo)
+ addAutofillComponent(autofillComponent)
+ },
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
+ } else {
+ PendingIntent.FLAG_CANCEL_CURRENT
+ }
+ )
+ } catch (e: RuntimeException) {
+ Log.e(TAG, "Unable to create pending intent for selection", e)
+ return null
+ }
+ }
+
+ fun getPendingIntentForRegistration(
+ context: Context,
+ registerInfo: RegisterInfo
+ ): PendingIntent? {
+ try {
+ return PendingIntent.getActivity(
+ context,
+ randomRequestCode(),
+ Intent(context, AutofillLauncherActivity::class.java).apply {
+ addSpecialMode(SpecialMode.REGISTRATION)
+ addRegisterInfo(registerInfo)
+ },
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
+ } else {
+ PendingIntent.FLAG_CANCEL_CURRENT
+ }
+ )
+ } catch (e: RuntimeException) {
+ Log.e(TAG, "Unable to create pending intent for registration", e)
+ return null
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntrySelectionLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/EntrySelectionLauncherActivity.kt
similarity index 50%
rename from app/src/main/java/com/kunzisoft/keepass/activities/EntrySelectionLauncherActivity.kt
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/EntrySelectionLauncherActivity.kt
index 679ab39ba..7bb3936e8 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/EntrySelectionLauncherActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/EntrySelectionLauncherActivity.kt
@@ -17,23 +17,25 @@
* along with KeePassDX. If not, see .
*
*/
-package com.kunzisoft.keepass.activities
+package com.kunzisoft.keepass.credentialprovider.activity
import android.content.Context
import android.content.Intent
-import android.net.Uri
import android.os.Bundle
-import android.widget.Toast
-import com.kunzisoft.keepass.R
+import androidx.core.net.toUri
+import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
+import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
+import com.kunzisoft.keepass.credentialprovider.TypeMode
+import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.database.ContextualDatabase
+import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper
-import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
import com.kunzisoft.keepass.utils.getParcelableCompat
-import com.kunzisoft.keepass.utils.WebDomain
+import com.kunzisoft.keepass.view.toastError
/**
* Activity to search or select entry in database,
@@ -49,8 +51,8 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
return false
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
+ override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
+ super.onUnknownDatabaseRetrieved(database)
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
if (keySelectionBundle != null) {
@@ -73,7 +75,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
else
- sharedWebDomain = Uri.parse(extra).host
+ sharedWebDomain = extra.toUri().host
}
}
launchSelection(database, sharedWebDomain, otpString)
@@ -84,7 +86,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
}
- launchSelection(database, sharedWebDomain, otpString)
+ launchSelection(database, null, otpString)
}
else -> {
if (database != null) {
@@ -106,11 +108,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
this.webDomain = sharedWebDomain
this.otpString = otpString
}
-
- WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
- searchInfo.webDomain = concreteWebDomain
- launch(database, searchInfo)
- }
+ launch(database, searchInfo)
}
private fun launch(database: ContextualDatabase?,
@@ -121,87 +119,106 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
// If database is open
val readOnly = database?.isReadOnly != false
- SearchHelper.checkAutoSearchInfo(this,
- database,
- searchInfo,
- { openedDatabase, items ->
- // Items found
- if (searchInfo.otpString != null) {
- if (!readOnly) {
- GroupActivity.launchForSaveResult(
- this,
- openedDatabase,
- searchInfo,
- false)
- } else {
- Toast.makeText(applicationContext,
- R.string.autofill_read_only_save,
- Toast.LENGTH_LONG)
- .show()
- }
- } else if (searchShareForMagikeyboard) {
- MagikeyboardService.performSelection(
- items,
- { entryInfo ->
- // Automatically populate keyboard
- MagikeyboardService.populateKeyboardAndMoveAppToBackground(
- this,
- entryInfo
- )
- },
- { autoSearch ->
- GroupActivity.launchForKeyboardSelectionResult(this,
- openedDatabase,
- searchInfo,
- autoSearch)
- }
+ SearchHelper.checkAutoSearchInfo(
+ context = this,
+ database = database,
+ searchInfo = searchInfo,
+ onItemsFound = { openedDatabase, items ->
+ // Items found
+ if (searchInfo.otpString != null) {
+ if (!readOnly) {
+ GroupActivity.launchForRegistration(
+ context = this,
+ activityResultLauncher = null,
+ database = openedDatabase,
+ registerInfo = searchInfo.toRegisterInfo(),
+ typeMode = TypeMode.DEFAULT
)
} else {
- GroupActivity.launchForSearchResult(this,
- openedDatabase,
- searchInfo,
- true)
+ toastError(RegisterInReadOnlyDatabaseException())
}
- },
- { openedDatabase ->
- // Show the database UI to select the entry
- if (searchInfo.otpString != null) {
- if (!readOnly) {
- GroupActivity.launchForSaveResult(this,
- openedDatabase,
- searchInfo,
- false)
- } else {
- Toast.makeText(applicationContext,
- R.string.autofill_read_only_save,
- Toast.LENGTH_LONG)
- .show()
+ } else if (searchShareForMagikeyboard) {
+ MagikeyboardService.performSelection(
+ items,
+ { entryInfo ->
+ // Automatically populate keyboard
+ MagikeyboardService.populateKeyboardAndMoveAppToBackground(
+ this,
+ entryInfo
+ )
+ },
+ { autoSearch ->
+ GroupActivity.launchForSelection(
+ context = this,
+ database = openedDatabase,
+ typeMode = TypeMode.MAGIKEYBOARD,
+ searchInfo = searchInfo,
+ autoSearch = autoSearch
+ )
}
- } else if (searchShareForMagikeyboard) {
- GroupActivity.launchForKeyboardSelectionResult(this,
- openedDatabase,
- searchInfo,
- false)
- } else {
- GroupActivity.launchForSearchResult(this,
- openedDatabase,
- searchInfo,
- false)
- }
- },
- {
- // If database not open
- if (searchInfo.otpString != null) {
- FileDatabaseSelectActivity.launchForSaveResult(this,
- searchInfo)
- } else if (searchShareForMagikeyboard) {
- FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
- searchInfo)
- } else {
- FileDatabaseSelectActivity.launchForSearchResult(this,
- searchInfo)
- }
+ )
+ } else {
+ GroupActivity.launchForSearchResult(
+ this,
+ openedDatabase,
+ searchInfo,
+ true
+ )
}
+ },
+ onItemNotFound = { openedDatabase ->
+ // Show the database UI to select the entry
+ if (searchInfo.otpString != null) {
+ if (!readOnly) {
+ GroupActivity.launchForRegistration(
+ context = this,
+ activityResultLauncher = null,
+ database = openedDatabase,
+ registerInfo = searchInfo.toRegisterInfo(),
+ typeMode = TypeMode.DEFAULT
+ )
+ } else {
+ toastError(RegisterInReadOnlyDatabaseException())
+ }
+ } else if (searchShareForMagikeyboard) {
+ GroupActivity.launchForSelection(
+ context = this,
+ database = openedDatabase,
+ typeMode = TypeMode.MAGIKEYBOARD,
+ searchInfo = searchInfo,
+ autoSearch = false
+ )
+ } else {
+ GroupActivity.launchForSearchResult(
+ this,
+ openedDatabase,
+ searchInfo,
+ false
+ )
+ }
+ },
+ onDatabaseClosed = {
+ // If database not open
+ if (searchInfo.otpString != null) {
+ FileDatabaseSelectActivity.launchForRegistration(
+ context = this,
+ activityResultLauncher = null,
+ registerInfo = searchInfo.toRegisterInfo(),
+ typeMode = TypeMode.DEFAULT
+ )
+ } else if (searchShareForMagikeyboard) {
+ FileDatabaseSelectActivity.launchForSelection(
+ context = this,
+ typeMode = TypeMode.MAGIKEYBOARD,
+ searchInfo = searchInfo
+ )
+ } else {
+ FileDatabaseSelectActivity.launchForSearchResult(
+ this,
+ searchInfo
+ )
+ }
+ }
)
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/HardwareKeyActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/HardwareKeyActivity.kt
new file mode 100644
index 000000000..8759cc86e
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/HardwareKeyActivity.kt
@@ -0,0 +1,170 @@
+package com.kunzisoft.keepass.credentialprovider.activity
+
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
+import androidx.appcompat.app.AlertDialog
+import androidx.lifecycle.lifecycleScope
+import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
+import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
+import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel
+import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.addHardwareKey
+import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.addSeed
+import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.buildHardwareKeyChallenge
+import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.isYubikeyDriverAvailable
+import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.UIState
+import com.kunzisoft.keepass.database.ContextualDatabase
+import com.kunzisoft.keepass.hardware.HardwareKey
+import com.kunzisoft.keepass.tasks.ActionRunnable
+import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
+import com.kunzisoft.keepass.view.toastError
+import kotlinx.coroutines.launch
+
+/**
+ * Special activity to deal with hardware key drivers,
+ * return the response to the database service once finished
+ */
+class HardwareKeyActivity: DatabaseModeActivity(){
+
+ private val mHardwareKeyLauncherViewModel: HardwareKeyLauncherViewModel by viewModels()
+
+ private var activityResultLauncher: ActivityResultLauncher =
+ this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ mHardwareKeyLauncherViewModel.manageSelectionResult(it)
+ }
+
+ override fun applyCustomStyle(): Boolean = false
+
+ override fun showDatabaseDialog(): Boolean = false
+
+ override fun manageDatabaseInfo(): Boolean = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ lifecycleScope.launch {
+ mHardwareKeyLauncherViewModel.uiState.collect { uiState ->
+ when (uiState) {
+ is UIState.Loading -> {}
+ is UIState.ShowHardwareKeyDriverNeeded -> {
+ showHardwareKeyDriverNeeded(
+ this@HardwareKeyActivity,
+ uiState.hardwareKey
+ ) {
+ mDatabaseViewModel.onChallengeResponded(null)
+ finish()
+ }
+ }
+ is UIState.LaunchChallengeActivityForResponse -> {
+ // Send to the driver
+ activityResultLauncher.launch(
+ buildHardwareKeyChallenge(uiState.challenge)
+ )
+ }
+ is UIState.OnChallengeResponded -> {
+ mDatabaseViewModel.onChallengeResponded(uiState.response)
+ }
+ }
+ }
+ }
+ lifecycleScope.launch {
+ mHardwareKeyLauncherViewModel.credentialUiState.collect { uiState ->
+ when (uiState) {
+ is CredentialLauncherViewModel.UIState.SetActivityResult -> {
+ setActivityResult(
+ lockDatabase = uiState.lockDatabase,
+ resultCode = uiState.resultCode,
+ data = uiState.data
+ )
+ }
+ is CredentialLauncherViewModel.UIState.ShowError -> {
+ toastError(uiState.error)
+ mHardwareKeyLauncherViewModel.cancelResult()
+ }
+ else -> {}
+ }
+ }
+ }
+ }
+
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
+ super.onDatabaseRetrieved(database)
+ mHardwareKeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
+ }
+
+ override fun onDatabaseActionFinished(
+ database: ContextualDatabase,
+ actionTask: String,
+ result: ActionRunnable.Result
+ ) {
+ super.onDatabaseActionFinished(database, actionTask, result)
+ finish()
+ }
+
+ private fun showHardwareKeyDriverNeeded(
+ context: Context,
+ hardwareKey: HardwareKey?,
+ onDialogDismissed: DialogInterface.OnDismissListener
+ ) {
+ val builder = AlertDialog.Builder(context)
+ builder
+ .setMessage(
+ context.getString(R.string.error_driver_required, hardwareKey.toString())
+ )
+ .setPositiveButton(R.string.download) { _, _ ->
+ context.openExternalApp(
+ context.getString(R.string.key_driver_app_id),
+ context.getString(R.string.key_driver_url)
+ )
+ }
+ .setNegativeButton(android.R.string.cancel) { _, _ -> }
+ .setOnDismissListener(onDialogDismissed)
+ builder.create().show()
+ }
+
+ companion object {
+ private val TAG = HardwareKeyActivity::class.java.simpleName
+
+ fun launchHardwareKeyActivity(
+ context: Context,
+ hardwareKey: HardwareKey,
+ seed: ByteArray?
+ ) {
+ context.startActivity(
+ Intent(
+ context,
+ HardwareKeyActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or
+ Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+ addHardwareKey(hardwareKey)
+ addSeed(seed)
+ })
+ }
+
+ fun isHardwareKeyAvailable(
+ context: Context,
+ hardwareKey: HardwareKey?
+ ): Boolean {
+ if (hardwareKey == null)
+ return false
+ return when (hardwareKey) {
+ /*
+ HardwareKey.FIDO2_SECRET -> {
+ // TODO FIDO2 under development
+ false
+ }
+ */
+ HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
+ // Check available intent
+ isYubikeyDriverAvailable(context)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt
new file mode 100644
index 000000000..1fc0c04c6
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.activity
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AlertDialog
+import androidx.lifecycle.lifecycleScope
+import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
+import com.kunzisoft.keepass.activities.GroupActivity
+import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
+import com.kunzisoft.keepass.credentialprovider.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.TypeMode
+import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
+import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
+import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
+import com.kunzisoft.keepass.database.ContextualDatabase
+import com.kunzisoft.keepass.model.AppOrigin
+import com.kunzisoft.keepass.model.SearchInfo
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
+import com.kunzisoft.keepass.tasks.ActionRunnable
+import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
+import com.kunzisoft.keepass.view.toastError
+import kotlinx.coroutines.launch
+import java.util.UUID
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class PasskeyLauncherActivity : DatabaseLockActivity() {
+
+ private val passkeyLauncherViewModel: PasskeyLauncherViewModel by viewModels()
+
+ private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher? =
+ this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ passkeyLauncherViewModel.manageSelectionResult(it)
+ }
+
+ private var mPasskeyRegistrationActivityResultLauncher: ActivityResultLauncher? =
+ this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ passkeyLauncherViewModel.manageRegistrationResult(it)
+ }
+
+ override fun applyCustomStyle(): Boolean {
+ return false
+ }
+
+ override fun finishActivityIfReloadRequested(): Boolean {
+ return false
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycleScope.launch {
+ // Initialize the parameters
+ passkeyLauncherViewModel.initialize()
+ // Retrieve the UI
+ passkeyLauncherViewModel.uiState.collect { uiState ->
+ when (uiState) {
+ is PasskeyLauncherViewModel.UIState.Loading -> {
+ // Nothing to do
+ }
+ is PasskeyLauncherViewModel.UIState.ShowAppPrivilegedDialog -> {
+ showAppPrivilegedDialog(
+ temptingApp = uiState.temptingApp
+ )
+ }
+ is PasskeyLauncherViewModel.UIState.ShowAppSignatureDialog -> {
+ showAppSignatureDialog(
+ temptingApp = uiState.temptingApp,
+ nodeId = uiState.nodeId
+ )
+ }
+ is PasskeyLauncherViewModel.UIState.UpdateEntry -> {
+ updateEntry(uiState.oldEntry, uiState.newEntry)
+ }
+ }
+ }
+ }
+ lifecycleScope.launch {
+ passkeyLauncherViewModel.credentialUiState.collect { uiState ->
+ when (uiState) {
+ is CredentialLauncherViewModel.UIState.Loading -> {}
+ is CredentialLauncherViewModel.UIState.SetActivityResult -> {
+ setActivityResult(
+ lockDatabase = uiState.lockDatabase,
+ resultCode = uiState.resultCode,
+ data = uiState.data
+ )
+ }
+ is CredentialLauncherViewModel.UIState.ShowError -> {
+ toastError(uiState.error)
+ passkeyLauncherViewModel.cancelResult()
+ }
+ is CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection -> {
+ GroupActivity.launchForSelection(
+ context = this@PasskeyLauncherActivity,
+ database = uiState.database,
+ typeMode = uiState.typeMode,
+ searchInfo = uiState.searchInfo,
+ activityResultLauncher = mPasskeySelectionActivityResultLauncher
+ )
+ }
+ is CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> {
+ GroupActivity.launchForRegistration(
+ context = this@PasskeyLauncherActivity,
+ database = uiState.database,
+ typeMode = uiState.typeMode,
+ registerInfo = uiState.registerInfo,
+ activityResultLauncher = mPasskeyRegistrationActivityResultLauncher
+ )
+ }
+ is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> {
+ FileDatabaseSelectActivity.launchForSelection(
+ context = this@PasskeyLauncherActivity,
+ typeMode = uiState.typeMode,
+ searchInfo = uiState.searchInfo,
+ activityResultLauncher = mPasskeySelectionActivityResultLauncher
+ )
+ }
+ is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> {
+ FileDatabaseSelectActivity.launchForRegistration(
+ context = this@PasskeyLauncherActivity,
+ typeMode = uiState.typeMode,
+ registerInfo = uiState.registerInfo,
+ activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
+ )
+ }
+ }
+ }
+ }
+ }
+
+ override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
+ super.onUnknownDatabaseRetrieved(database)
+ passkeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
+ }
+
+ override fun onDatabaseActionFinished(
+ database: ContextualDatabase,
+ actionTask: String,
+ result: ActionRunnable.Result
+ ) {
+ super.onDatabaseActionFinished(database, actionTask, result)
+ when (actionTask) {
+ ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
+ // TODO When auto save is enabled, WARNING filter by the calling activity
+ // passkeyLauncherViewModel.autoSelectPasskey(result, database)
+ }
+ }
+ }
+
+ override fun viewToInvalidateTimeout(): View? {
+ return null
+ }
+
+ /**
+ * Display a dialog that asks the user to add an app to the list of privileged apps.
+ */
+ private fun showAppPrivilegedDialog(
+ temptingApp: AndroidPrivilegedApp
+ ) {
+ Log.w(javaClass.simpleName, "No privileged apps file found")
+ AlertDialog.Builder(this@PasskeyLauncherActivity).apply {
+ setTitle(getString(R.string.passkeys_privileged_apps_ask_title))
+ setMessage(StringBuilder()
+ .append(
+ getString(
+ R.string.passkeys_privileged_apps_ask_message,
+ temptingApp.toString()
+ )
+ )
+ .append("\n\n")
+ .append(getString(R.string.passkeys_privileged_apps_explanation))
+ .toString()
+ )
+ setPositiveButton(android.R.string.ok) { _, _ ->
+ passkeyLauncherViewModel.saveCustomPrivilegedApp(
+ intent = intent,
+ specialMode = mSpecialMode,
+ database = mDatabase,
+ temptingApp = temptingApp
+ )
+ }
+ setNegativeButton(android.R.string.cancel) { _, _ ->
+ passkeyLauncherViewModel.cancelResult()
+ }
+ setOnCancelListener {
+ passkeyLauncherViewModel.cancelResult()
+ }
+ }.create().show()
+ }
+
+ /**
+ * Display a dialog that asks the user to add an app signature in an existing passkey
+ */
+ private fun showAppSignatureDialog(
+ temptingApp: AppOrigin,
+ nodeId: UUID
+ ) {
+ AlertDialog.Builder(this@PasskeyLauncherActivity).apply {
+ setTitle(getString(R.string.passkeys_missing_signature_app_ask_title))
+ setMessage(StringBuilder()
+ .append(
+ getString(
+ R.string.passkeys_missing_signature_app_ask_message,
+ temptingApp.toString()
+ )
+ )
+ .append("\n\n")
+ .append(getString(R.string.passkeys_missing_signature_app_ask_explanation))
+ .append("\n\n")
+ .append(getString(R.string.passkeys_missing_signature_app_ask_question))
+ .toString()
+ )
+ setPositiveButton(android.R.string.ok) { _, _ ->
+ passkeyLauncherViewModel.saveAppSignature(
+ database = mDatabase,
+ temptingApp = temptingApp,
+ nodeId = nodeId
+ )
+ }
+ setNegativeButton(android.R.string.cancel) { _, _ ->
+ passkeyLauncherViewModel.cancelResult()
+ }
+ setOnCancelListener {
+ passkeyLauncherViewModel.cancelResult()
+ }
+ }.create().show()
+ }
+
+ companion object {
+ private val TAG = PasskeyLauncherActivity::class.java.name
+
+ /**
+ * Get a pending intent to launch the passkey launcher activity
+ * [nodeId] can be :
+ * - null if manual selection is requested
+ * - null if manual registration is requested
+ * - an entry node id if direct selection is requested
+ * - a group node id if direct registration is requested in a default group
+ * - an entry node id if overwriting is requested in an existing entry
+ */
+ fun getPendingIntent(
+ context: Context,
+ specialMode: SpecialMode,
+ searchInfo: SearchInfo? = null,
+ appOrigin: AppOrigin? = null,
+ nodeId: UUID? = null
+ ): PendingIntent? {
+ return PendingIntent.getActivity(
+ context,
+ randomRequestCode(),
+ Intent(context, PasskeyLauncherActivity::class.java).apply {
+ addSpecialMode(specialMode)
+ addTypeMode(TypeMode.PASSKEY)
+ addSearchInfo(searchInfo)
+ addAppOrigin(appOrigin)
+ addNodeId(nodeId)
+ addAuthCode(nodeId)
+ },
+ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillComponent.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillComponent.kt
new file mode 100644
index 000000000..7fe7a5e37
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillComponent.kt
@@ -0,0 +1,8 @@
+package com.kunzisoft.keepass.credentialprovider.autofill
+
+import android.app.assist.AssistStructure
+
+data class AutofillComponent(
+ val assistStructure: AssistStructure,
+ val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillHelper.kt
similarity index 68%
rename from app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillHelper.kt
index b0af5bf53..3663db746 100644
--- a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillHelper.kt
@@ -17,10 +17,9 @@
* along with KeePassDX. If not, see .
*
*/
-package com.kunzisoft.keepass.autofill
+package com.kunzisoft.keepass.credentialprovider.autofill
import android.annotation.SuppressLint
-import android.app.Activity
import android.app.PendingIntent
import android.app.assist.AssistStructure
import android.content.Context
@@ -38,19 +37,14 @@ import android.view.autofill.AutofillId
import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue
import android.widget.RemoteViews
-import android.widget.Toast
import android.widget.inline.InlinePresentationSpec
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
-import androidx.appcompat.app.AppCompatActivity
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
-import com.kunzisoft.keepass.activities.AutofillLauncherActivity
-import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
-import com.kunzisoft.keepass.activities.helpers.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
+import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.template.TemplateField
@@ -58,22 +52,32 @@ import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
-import com.kunzisoft.keepass.utils.LOCK_ACTION
+import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
+import java.io.IOException
import kotlin.math.min
@RequiresApi(api = Build.VERSION_CODES.O)
object AutofillHelper {
- private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
+ private const val EXTRA_BASE_STRUCTURE = "com.kunzisoft.keepass.autofill.BASE_STRUCTURE"
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
- fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
- intent?.getParcelableExtraCompat(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
+ fun Intent.addAutofillComponent(autofillComponent: AutofillComponent) {
+ this.putExtra(EXTRA_BASE_STRUCTURE, autofillComponent.assistStructure)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ autofillComponent.compatInlineSuggestionsRequest?.let {
+ this.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
+ }
+ }
+ }
+
+ fun Intent.retrieveAutofillComponent(): AutofillComponent? {
+ getParcelableExtraCompat(EXTRA_BASE_STRUCTURE)?.let { assistStructure ->
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AutofillComponent(assistStructure,
- intent.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
+ getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
} else {
AutofillComponent(assistStructure, null)
}
@@ -132,11 +136,13 @@ object AutofillHelper {
return this
}
- private fun buildDatasetForEntry(context: Context,
- database: ContextualDatabase,
- entryInfo: EntryInfo,
- struct: StructureParser.Result,
- inlinePresentation: InlinePresentation?): Dataset {
+ private fun buildDatasetForEntry(
+ context: Context,
+ database: ContextualDatabase,
+ entryInfo: EntryInfo,
+ struct: StructureParser.Result,
+ inlinePresentation: InlinePresentation?
+ ): Dataset {
val remoteViews: RemoteViews = newRemoteViews(context, database, makeEntryTitle(entryInfo), entryInfo.icon)
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -263,7 +269,7 @@ object AutofillHelper {
}
}
}
- for (field in entryInfo.customFields) {
+ for (field in entryInfo.getCustomFieldsForFilling()) {
if (field.name == TemplateField.LABEL_HOLDER) {
struct.creditCardHolderId?.let { ccNameId ->
datasetBuilder.addValueToDatasetBuilder(
@@ -294,30 +300,15 @@ object AutofillHelper {
return dataset
}
- /**
- * Method to assign a drawable to a new icon from a database icon
- */
- private fun buildIconFromEntry(context: Context,
- database: ContextualDatabase,
- entryInfo: EntryInfo): Icon? {
- try {
- database.iconDrawableFactory.getBitmapFromIcon(context,
- entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
- return Icon.createWithBitmap(bitmap)
- }
- } catch (e: Exception) {
- Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
- }
- return null
- }
-
@SuppressLint("RestrictedApi")
@RequiresApi(Build.VERSION_CODES.R)
- private fun buildInlinePresentationForEntry(context: Context,
- database: ContextualDatabase,
- compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
- positionItem: Int,
- entryInfo: EntryInfo): InlinePresentation? {
+ private fun buildInlinePresentationForEntry(
+ context: Context,
+ database: ContextualDatabase,
+ compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
+ positionItem: Int,
+ entryInfo: EntryInfo
+ ): InlinePresentation? {
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
@@ -336,13 +327,9 @@ object AutofillHelper {
// Build the content for IME UI
val pendingIntent = PendingIntent.getActivity(
context,
- 0,
+ randomRequestCode(),
Intent(context, AutofillSettingsActivity::class.java),
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- PendingIntent.FLAG_IMMUTABLE
- } else {
- 0
- }
+ PendingIntent.FLAG_IMMUTABLE
)
return InlinePresentation(
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
@@ -353,7 +340,7 @@ object AutofillHelper {
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
setTintBlendMode(BlendMode.DST)
})
- buildIconFromEntry(context, database, entryInfo)?.let { icon ->
+ entryInfo.buildIcon(context, database)?.let { icon ->
setEndIcon(icon.apply {
setTintBlendMode(BlendMode.DST)
})
@@ -367,9 +354,11 @@ object AutofillHelper {
@RequiresApi(Build.VERSION_CODES.R)
@SuppressLint("RestrictedApi")
- private fun buildInlinePresentationForManualSelection(context: Context,
- inlinePresentationSpec: InlinePresentationSpec,
- pendingIntent: PendingIntent): InlinePresentation? {
+ private fun buildInlinePresentationForManualSelection(
+ context: Context,
+ inlinePresentationSpec: InlinePresentationSpec,
+ pendingIntent: PendingIntent
+ ): InlinePresentation? {
// 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))
@@ -386,11 +375,13 @@ object AutofillHelper {
}.build().slice, inlinePresentationSpec, false)
}
- fun buildResponse(context: Context,
- database: ContextualDatabase,
- entriesInfo: List,
- parseResult: StructureParser.Result,
- compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
+ fun buildResponse(
+ context: Context,
+ database: ContextualDatabase,
+ entriesInfo: List,
+ parseResult: StructureParser.Result,
+ autofillComponent: AutofillComponent
+ ): FillResponse? {
val responseBuilder = FillResponse.Builder()
// Add Header
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -411,7 +402,8 @@ object AutofillHelper {
// Add inline suggestion for new IME and dataset
var numberInlineSuggestions = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
+ autofillComponent.compatInlineSuggestionsRequest
+ ?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
@@ -427,21 +419,27 @@ object AutofillHelper {
var inlinePresentation: InlinePresentation? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& numberInlineSuggestions > 0
- && compatInlineSuggestionsRequest != null) {
+ && autofillComponent.compatInlineSuggestionsRequest != null) {
inlinePresentation = buildInlinePresentationForEntry(
context,
database,
- compatInlineSuggestionsRequest,
+ autofillComponent.compatInlineSuggestionsRequest,
numberInlineSuggestions--,
entry
)
}
// Create dataset for each entry
responseBuilder.addDataset(
- buildDatasetForEntry(context, database, entry, parseResult, inlinePresentation)
+ buildDatasetForEntry(
+ context = context,
+ database = database,
+ entryInfo = entry,
+ struct = parseResult,
+ inlinePresentation = inlinePresentation
+ )
)
} catch (e: Exception) {
- Log.e(TAG, "Unable to add dataset")
+ Log.e(TAG, "Unable to add dataset", e)
}
}
@@ -453,21 +451,28 @@ object AutofillHelper {
webScheme = parseResult.webScheme
manualSelection = true
}
- val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
- AutofillLauncherActivity.getPendingIntentForSelection(context,
- searchInfo, compatInlineSuggestionsRequest)?.let { pendingIntent ->
+ val manualSelectionView = RemoteViews(
+ context.packageName,
+ R.layout.item_autofill_select_entry
+ )
+ AutofillLauncherActivity.getPendingIntentForSelection(
+ context,
+ searchInfo,
+ autofillComponent
+ )?.let { pendingIntent ->
var inlinePresentation: InlinePresentation? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
- val inlinePresentationSpec =
- inlineSuggestionsRequest.inlinePresentationSpecs[0]
- inlinePresentation = buildInlinePresentationForManualSelection(
- context,
- inlinePresentationSpec,
- pendingIntent
- )
- }
+ autofillComponent.compatInlineSuggestionsRequest
+ ?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
+ val inlinePresentationSpec =
+ inlineSuggestionsRequest.inlinePresentationSpecs[0]
+ inlinePresentation = buildInlinePresentationForManualSelection(
+ context,
+ inlinePresentationSpec,
+ pendingIntent
+ )
+ }
}
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -512,92 +517,33 @@ object AutofillHelper {
}
/**
- * Build the Autofill response for one entry
+ * Build the Autofill response
*/
- fun buildResponseAndSetResult(activity: Activity,
- database: ContextualDatabase,
- entryInfo: EntryInfo) {
- buildResponseAndSetResult(activity, database, ArrayList().apply { add(entryInfo) })
- }
-
- /**
- * Build the Autofill response for many entry
- */
- fun buildResponseAndSetResult(activity: Activity,
- database: ContextualDatabase,
- entriesInfo: List) {
+ fun buildResponse(
+ context: Context,
+ autofillComponent: AutofillComponent,
+ database: ContextualDatabase,
+ entriesInfo: List,
+ onIntentCreated: (Intent) -> Unit
+ ) {
if (entriesInfo.isEmpty()) {
- activity.setResult(Activity.RESULT_CANCELED)
+ throw IOException("No entries found")
} else {
- var setResultOk = false
- activity.intent?.getParcelableExtraCompat(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
- StructureParser(structure).parse()?.let { result ->
- // New Response
- val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST)
- if (compatInlineSuggestionsRequest != null) {
- Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
- }
- buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest)
- } else {
- buildResponse(activity, database, entriesInfo, result, null)
- }
- val mReplyIntent = Intent()
- Log.d(activity.javaClass.name, "Success 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.")
- activity.setResult(Activity.RESULT_CANCELED)
- }
+ StructureParser(autofillComponent.assistStructure).parse()?.let { result ->
+ // New Response
+ onIntentCreated(Intent().putExtra(
+ AutofillManager.EXTRA_AUTHENTICATION_RESULT,
+ buildResponse(
+ context = context,
+ database = database,
+ entriesInfo = entriesInfo,
+ parseResult = result,
+ autofillComponent = autofillComponent
+ )
+ ))
+ } ?: throw IOException("Unable to parse the structure")
}
}
- fun buildActivityResultLauncher(activity: AppCompatActivity,
- lockDatabase: Boolean = false): ActivityResultLauncher {
- return activity.registerForActivityResult(
- ActivityResultContracts.StartActivityForResult()
- ) {
- // Utility method to loop and close each activity with return data
- if (it.resultCode == Activity.RESULT_OK) {
- activity.setResult(it.resultCode, it.data)
- }
- if (it.resultCode == Activity.RESULT_CANCELED) {
- activity.setResult(Activity.RESULT_CANCELED)
- }
- activity.finish()
-
- if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(activity)) {
- // Close the database
- activity.sendBroadcast(Intent(LOCK_ACTION))
- }
- }
- }
-
- /**
- * Utility method to start an activity with an Autofill for result
- */
- fun startActivityForAutofillResult(activity: AppCompatActivity,
- intent: Intent,
- activityResultLauncher: ActivityResultLauncher?,
- autofillComponent: AutofillComponent,
- searchInfo: SearchInfo?) {
- EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
- intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
- && PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
- autofillComponent.compatInlineSuggestionsRequest?.let {
- intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
- }
- }
- EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
- activityResultLauncher?.launch(intent)
- }
-
private val TAG = AutofillHelper::class.java.name
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/CompatInlineSuggestionsRequest.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/CompatInlineSuggestionsRequest.kt
similarity index 97%
rename from app/src/main/java/com/kunzisoft/keepass/autofill/CompatInlineSuggestionsRequest.kt
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/CompatInlineSuggestionsRequest.kt
index 5705682b5..8fba6845b 100644
--- a/app/src/main/java/com/kunzisoft/keepass/autofill/CompatInlineSuggestionsRequest.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/CompatInlineSuggestionsRequest.kt
@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see .
*
*/
-package com.kunzisoft.keepass.autofill
+package com.kunzisoft.keepass.credentialprovider.autofill
import android.annotation.TargetApi
import android.os.Build
diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/KeeAutofillService.kt
similarity index 81%
rename from app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/KeeAutofillService.kt
index 98979a9d9..6ac09d620 100644
--- a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/KeeAutofillService.kt
@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see .
*
*/
-package com.kunzisoft.keepass.autofill
+package com.kunzisoft.keepass.credentialprovider.autofill
import android.annotation.SuppressLint
import android.app.PendingIntent
@@ -43,8 +43,8 @@ import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi
import com.kunzisoft.keepass.R
-import com.kunzisoft.keepass.activities.AutofillLauncherActivity
-import com.kunzisoft.keepass.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
+import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
+import com.kunzisoft.keepass.credentialprovider.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.helper.SearchHelper
@@ -53,7 +53,7 @@ import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
-import com.kunzisoft.keepass.utils.WebDomain
+import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import org.joda.time.DateTime
@@ -92,10 +92,11 @@ class KeeAutofillService : AutofillService() {
autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this)
}
- override fun onFillRequest(request: FillRequest,
- cancellationSignal: CancellationSignal,
- callback: FillCallback) {
-
+ override fun onFillRequest(
+ request: FillRequest,
+ cancellationSignal: CancellationSignal,
+ callback: FillCallback
+ ) {
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
@@ -120,64 +121,64 @@ class KeeAutofillService : AutofillService() {
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
}
- WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
- searchInfo.webDomain = webDomainWithoutSubDomain
- val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
- && autofillInlineSuggestionsEnabled) {
- CompatInlineSuggestionsRequest(request)
- } else {
- null
- }
- launchSelection(mDatabase,
- searchInfo,
- parseResult,
- inlineSuggestionsRequest,
- callback)
+ val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
+ && autofillInlineSuggestionsEnabled) {
+ CompatInlineSuggestionsRequest(request)
+ } else {
+ null
}
+ val autofillComponent = AutofillComponent(
+ latestStructure,
+ inlineSuggestionsRequest
+ )
+ SearchHelper.checkAutoSearchInfo(
+ context = this,
+ database = mDatabase,
+ searchInfo = searchInfo,
+ onItemsFound = { openedDatabase, items ->
+ callback.onSuccess(
+ AutofillHelper.buildResponse(
+ context = this,
+ database = openedDatabase,
+ entriesInfo = items,
+ parseResult = parseResult,
+ autofillComponent = autofillComponent
+ )
+ )
+ },
+ onItemNotFound = { openedDatabase ->
+ // Show UI if no search result
+ showUIForEntrySelection(parseResult, openedDatabase,
+ searchInfo, autofillComponent, callback)
+ },
+ onDatabaseClosed = {
+ // Show UI if database not open
+ showUIForEntrySelection(parseResult, null,
+ searchInfo, autofillComponent, callback)
+ }
+ )
}
}
}
- private fun launchSelection(database: ContextualDatabase?,
- searchInfo: SearchInfo,
- parseResult: StructureParser.Result,
- inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
- callback: FillCallback) {
- SearchHelper.checkAutoSearchInfo(this,
- database,
- searchInfo,
- { openedDatabase, items ->
- callback.onSuccess(
- AutofillHelper.buildResponse(this, openedDatabase,
- items, parseResult, inlineSuggestionsRequest)
- )
- },
- { openedDatabase ->
- // Show UI if no search result
- showUIForEntrySelection(parseResult, openedDatabase,
- searchInfo, inlineSuggestionsRequest, callback)
- },
- {
- // Show UI if database not open
- showUIForEntrySelection(parseResult, null,
- searchInfo, inlineSuggestionsRequest, callback)
- }
- )
- }
-
@SuppressLint("RestrictedApi")
- private fun showUIForEntrySelection(parseResult: StructureParser.Result,
- database: ContextualDatabase?,
- searchInfo: SearchInfo,
- inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
- callback: FillCallback) {
+ private fun showUIForEntrySelection(
+ parseResult: StructureParser.Result,
+ database: ContextualDatabase?,
+ searchInfo: SearchInfo,
+ autofillComponent: AutofillComponent,
+ callback: FillCallback
+ ) {
var success = false
parseResult.allAutofillIds().let { autofillIds ->
if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response.
- AutofillLauncherActivity.getPendingIntentForSelection(this,
- searchInfo, inlineSuggestionsRequest)?.intentSender?.let { intentSender ->
+ AutofillLauncherActivity.getPendingIntentForSelection(
+ this,
+ searchInfo,
+ autofillComponent
+ )?.intentSender?.let { intentSender ->
val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (database == null) {
if (!parseResult.webDomain.isNullOrEmpty()) {
@@ -268,7 +269,8 @@ class KeeAutofillService : AutofillService() {
&& autofillInlineSuggestionsEnabled
) {
var inlinePresentation: InlinePresentation? = null
- inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
+ autofillComponent.compatInlineSuggestionsRequest
+ ?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs =
inlineSuggestionsRequest.inlinePresentationSpecs
if (inlineSuggestionsRequest.maxSuggestionCount > 0
@@ -286,7 +288,7 @@ class KeeAutofillService : AutofillService() {
InlineSuggestionUi.newContentBuilder(
PendingIntent.getActivity(
this,
- 0,
+ randomRequestCode(),
Intent(this, AutofillSettingsActivity::class.java),
PendingIntent.FLAG_IMMUTABLE
)
@@ -358,7 +360,7 @@ class KeeAutofillService : AutofillService() {
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
var success = false
- if (askToSaveData) {
+ if (askToSaveData && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val latestStructure = request.fillContexts.last().structure
StructureParser(latestStructure).parse(true)?.let { parseResult ->
@@ -384,30 +386,32 @@ class KeeAutofillService : AutofillService() {
}
// Show UI to save data
+ val searchInfo = SearchInfo().apply {
+ applicationId = parseResult.applicationId
+ webDomain = parseResult.webDomain
+ webScheme = parseResult.webScheme
+ }
val registerInfo = RegisterInfo(
- SearchInfo().apply {
- applicationId = parseResult.applicationId
- webDomain = parseResult.webDomain
- webScheme = parseResult.webScheme
- },
- parseResult.usernameValue?.textValue?.toString(),
- parseResult.passwordValue?.textValue?.toString(),
+ searchInfo = searchInfo,
+ username = parseResult.usernameValue?.textValue?.toString(),
+ password = parseResult.passwordValue?.textValue?.toString(),
+ creditCard = parseResult.creditCardNumber?.let { cardNumber ->
CreditCard(
parseResult.creditCardHolder,
- parseResult.creditCardNumber,
+ cardNumber,
expiration,
parseResult.cardVerificationValue
- ))
+ )
+ }
+ )
- // TODO Callback in each activity #765
- //if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- // callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
- // registerInfo))
- //} else {
- AutofillLauncherActivity.launchForRegistration(this, registerInfo)
- success = true
- callback.onSuccess()
- //}
+ AutofillLauncherActivity.getPendingIntentForRegistration(
+ this,
+ registerInfo
+ )?.intentSender?.let { intentSender ->
+ success = true
+ callback.onSuccess(intentSender)
+ }
}
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/StructureParser.kt
similarity index 99%
rename from app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/StructureParser.kt
index c4b7ad0c8..425181463 100644
--- a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/StructureParser.kt
@@ -16,7 +16,7 @@
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see .
*/
-package com.kunzisoft.keepass.autofill
+package com.kunzisoft.keepass.credentialprovider.autofill
import android.app.assist.AssistStructure
import android.os.Build
@@ -362,8 +362,8 @@ class StructureParser(private val structure: AssistStructure) {
if (result?.passwordId == null) {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
+ Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
}
- Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
}
inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
diff --git a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/Keyboard.java b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/Keyboard.java
similarity index 99%
rename from app/src/main/java/com/kunzisoft/keepass/magikeyboard/Keyboard.java
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/Keyboard.java
index 313fa465f..b385a897c 100644
--- a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/Keyboard.java
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/Keyboard.java
@@ -14,7 +14,7 @@
* the License.
*/
-package com.kunzisoft.keepass.magikeyboard;
+package com.kunzisoft.keepass.credentialprovider.magikeyboard;
import android.content.Context;
import android.content.res.Resources;
diff --git a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardView.java b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/KeyboardView.java
similarity index 98%
rename from app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardView.java
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/KeyboardView.java
index 1ee04d2ea..fc992247e 100644
--- a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardView.java
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/KeyboardView.java
@@ -14,14 +14,14 @@
* the License.
*/
-package com.kunzisoft.keepass.magikeyboard;
+package com.kunzisoft.keepass.credentialprovider.magikeyboard;
-import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
-import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
-import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY;
-import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
-import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP;
-import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
+import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
+import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
+import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY;
+import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
+import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP;
+import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
import android.content.Context;
import android.content.res.TypedArray;
@@ -52,7 +52,7 @@ import android.widget.TextView;
import androidx.annotation.RequiresApi;
import com.kunzisoft.keepass.R;
-import com.kunzisoft.keepass.magikeyboard.Keyboard.Key;
+import com.kunzisoft.keepass.credentialprovider.magikeyboard.Keyboard.Key;
import java.util.Arrays;
import java.util.HashMap;
diff --git a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikeyboardService.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/MagikeyboardService.kt
similarity index 93%
rename from app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikeyboardService.kt
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/MagikeyboardService.kt
index 34770e605..875609bd4 100644
--- a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikeyboardService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/MagikeyboardService.kt
@@ -18,7 +18,7 @@
*
*/
-package com.kunzisoft.keepass.magikeyboard
+package com.kunzisoft.keepass.credentialprovider.magikeyboard
import android.app.Activity
import android.content.Context
@@ -41,9 +41,10 @@ import androidx.core.graphics.BlendModeCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
-import com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity
-import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.adapters.FieldsAdapter
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
+import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Field
@@ -324,9 +325,9 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
actionGoAutomatically()
}
KEY_FIELDS -> {
- getEntryInfo()?.customFields?.let { customFields ->
+ getEntryInfo()?.getCustomFieldsForFilling()?.let { customFields ->
fieldsAdapter?.apply {
- setFields(customFields.filter { it.name != OTP_TOKEN_FIELD})
+ setFields(customFields)
notifyDataSetChanged()
}
}
@@ -341,10 +342,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
}
private fun actionKeyEntry(searchInfo: SearchInfo? = null) {
- SearchHelper.checkAutoSearchInfo(this,
- mDatabase,
- searchInfo,
- { _, items ->
+ SearchHelper.checkAutoSearchInfo(
+ context = this,
+ database = mDatabase,
+ searchInfo = searchInfo,
+ onItemsFound = { _, items ->
performSelection(
items,
{
@@ -361,11 +363,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
}
)
},
- {
+ onItemNotFound = {
// Select if not found
launchEntrySelection(searchInfo)
},
- {
+ onDatabaseClosed = {
// Select if database not opened
removeEntryInfo()
launchEntrySelection(searchInfo)
@@ -463,21 +465,18 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
fun performSelection(items: List,
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
- if (items.size == 1) {
- val itemFound = items[0]
- if (entryUUID != itemFound.id) {
- actionPopulateKeyboard.invoke(itemFound)
- } else {
- // Force selection if magikeyboard already populated
- actionEntrySelection.invoke(false)
- }
- } else if (items.size > 1) {
- // Select the one we want in the selection
- actionEntrySelection.invoke(true)
- } else {
- // Select an arbitrary one
- actionEntrySelection.invoke(false)
- }
+ EntrySelectionHelper.performSelection(
+ items = items,
+ actionPopulateCredentialProvider = { itemFound ->
+ if (entryUUID != itemFound.id) {
+ actionPopulateKeyboard.invoke(itemFound)
+ } else {
+ // Force selection if magikeyboard already populated
+ actionEntrySelection.invoke(false)
+ }
+ },
+ actionEntrySelection = actionEntrySelection
+ )
}
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
@@ -486,7 +485,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
// Populate Magikeyboard with entry
addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
// Consume the selection mode
- EntrySelectionHelper.removeModesFromIntent(activity.intent)
+ activity.intent.removeModes()
activity.moveTaskToBack(true)
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt
new file mode 100644
index 000000000..37ddd7e28
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt
@@ -0,0 +1,372 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey
+
+import android.graphics.BlendMode
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.os.CancellationSignal
+import android.os.OutcomeReceiver
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.credentials.exceptions.ClearCredentialException
+import androidx.credentials.exceptions.CreateCredentialException
+import androidx.credentials.exceptions.CreateCredentialUnknownException
+import androidx.credentials.exceptions.GetCredentialException
+import androidx.credentials.exceptions.GetCredentialUnknownException
+import androidx.credentials.provider.BeginCreateCredentialRequest
+import androidx.credentials.provider.BeginCreateCredentialResponse
+import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
+import androidx.credentials.provider.BeginGetCredentialRequest
+import androidx.credentials.provider.BeginGetCredentialResponse
+import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
+import androidx.credentials.provider.CreateEntry
+import androidx.credentials.provider.CredentialEntry
+import androidx.credentials.provider.CredentialProviderService
+import androidx.credentials.provider.ProviderClearCredentialStateRequest
+import androidx.credentials.provider.PublicKeyCredentialEntry
+import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
+import com.kunzisoft.keepass.credentialprovider.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity
+import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
+import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
+import com.kunzisoft.keepass.database.ContextualDatabase
+import com.kunzisoft.keepass.database.DatabaseTaskProvider
+import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
+import com.kunzisoft.keepass.database.helper.SearchHelper
+import com.kunzisoft.keepass.model.SearchInfo
+import com.kunzisoft.keepass.settings.PreferencesUtil.isPasskeyAutoSelectEnable
+import com.kunzisoft.keepass.view.toastError
+import java.io.IOException
+import java.time.Instant
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class PasskeyProviderService : CredentialProviderService() {
+
+ private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
+ private var mDatabase: ContextualDatabase? = null
+ private lateinit var defaultIcon: Icon
+ private var isAutoSelectAllowed: Boolean = false
+
+ override fun onCreate() {
+ super.onCreate()
+
+ mDatabaseTaskProvider = DatabaseTaskProvider(this)
+ mDatabaseTaskProvider?.registerProgressTask()
+ mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
+ this.mDatabase = database
+ }
+
+ defaultIcon = Icon.createWithResource(
+ this@PasskeyProviderService,
+ R.mipmap.ic_launcher_round
+ ).apply {
+ setTintBlendMode(BlendMode.DST)
+ }
+
+ isAutoSelectAllowed = isPasskeyAutoSelectEnable(this)
+ }
+
+ override fun onDestroy() {
+ mDatabaseTaskProvider?.unregisterProgressTask()
+ super.onDestroy()
+ }
+
+ private fun buildPasskeySearchInfo(relyingParty: String): SearchInfo {
+ return SearchInfo().apply {
+ this.relyingParty = relyingParty
+ }
+ }
+
+ override fun onBeginGetCredentialRequest(
+ request: BeginGetCredentialRequest,
+ cancellationSignal: CancellationSignal,
+ callback: OutcomeReceiver
+ ) {
+ Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called")
+ try {
+ processGetCredentialsRequest(request) { response ->
+ callback.onResult(response)
+ }
+ } catch (e: Exception) {
+ Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e)
+ callback.onError(GetCredentialUnknownException())
+ }
+ }
+
+ private fun processGetCredentialsRequest(
+ request: BeginGetCredentialRequest,
+ callback: (BeginGetCredentialResponse?) -> Unit
+ ) {
+ var knownOption = false
+ for (option in request.beginGetCredentialOptions) {
+ when (option) {
+ is BeginGetPublicKeyCredentialOption -> {
+ knownOption = true
+ populatePasskeyData(option) { listCredentials ->
+ callback(BeginGetCredentialResponse(listCredentials))
+ }
+ }
+ }
+ }
+ if (knownOption.not()) {
+ throw IOException("unknown type of beginGetCredentialOption")
+ }
+ }
+
+ private fun populatePasskeyData(
+ option: BeginGetPublicKeyCredentialOption,
+ callback: (List) -> Unit
+ ) {
+
+ val passkeyEntries: MutableList = mutableListOf()
+
+ val relyingPartyId = PublicKeyCredentialRequestOptions(option.requestJson).rpId
+ val searchInfo = buildPasskeySearchInfo(relyingPartyId)
+ Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
+ SearchHelper.checkAutoSearchInfo(
+ context = this,
+ database = mDatabase,
+ searchInfo = searchInfo,
+ onItemsFound = { database, items ->
+ Log.d(TAG, "Add pending intent for passkey selection with found items")
+ for (passkeyEntry in items) {
+ PasskeyLauncherActivity.getPendingIntent(
+ context = applicationContext,
+ specialMode = SpecialMode.SELECTION,
+ nodeId = passkeyEntry.id,
+ appOrigin = passkeyEntry.appOrigin
+ )?.let { usagePendingIntent ->
+ val passkey = passkeyEntry.passkey
+ passkeyEntries.add(
+ PublicKeyCredentialEntry(
+ context = applicationContext,
+ username = passkey?.username ?: "Unknown",
+ icon = passkeyEntry.buildIcon(this@PasskeyProviderService, database)?.apply {
+ setTintBlendMode(BlendMode.DST)
+ } ?: defaultIcon,
+ pendingIntent = usagePendingIntent,
+ beginGetPublicKeyCredentialOption = option,
+ displayName = passkeyEntry.getVisualTitle(),
+ isAutoSelectAllowed = isAutoSelectAllowed
+ )
+ )
+ }
+ }
+ callback(passkeyEntries)
+ },
+ onItemNotFound = { _ ->
+ Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
+ Log.d(TAG, "Add pending intent for passkey selection in opened database")
+ PasskeyLauncherActivity.getPendingIntent(
+ context = applicationContext,
+ specialMode = SpecialMode.SELECTION,
+ searchInfo = searchInfo
+ )?.let { pendingIntent ->
+ passkeyEntries.add(
+ PublicKeyCredentialEntry(
+ context = applicationContext,
+ username = getString(R.string.passkey_database_username),
+ displayName = getString(R.string.passkey_selection_description),
+ icon = defaultIcon,
+ pendingIntent = pendingIntent,
+ beginGetPublicKeyCredentialOption = option,
+ lastUsedTime = Instant.now(),
+ isAutoSelectAllowed = isAutoSelectAllowed
+ )
+ )
+ }
+ callback(passkeyEntries)
+ },
+ onDatabaseClosed = {
+ Log.d(TAG, "Add pending intent for passkey selection in closed database")
+ // Database is locked, a public key credential entry is shown to unlock it
+ PasskeyLauncherActivity.getPendingIntent(
+ context = applicationContext,
+ specialMode = SpecialMode.SELECTION,
+ searchInfo = searchInfo
+ )?.let { pendingIntent ->
+ passkeyEntries.add(
+ PublicKeyCredentialEntry(
+ context = applicationContext,
+ username = getString(R.string.passkey_database_username),
+ displayName = getString(R.string.passkey_locked_database_description),
+ icon = defaultIcon,
+ pendingIntent = pendingIntent,
+ beginGetPublicKeyCredentialOption = option,
+ lastUsedTime = Instant.now(),
+ isAutoSelectAllowed = isAutoSelectAllowed
+ )
+ )
+ }
+ callback(passkeyEntries)
+ }
+ )
+ }
+
+ override fun onBeginCreateCredentialRequest(
+ request: BeginCreateCredentialRequest,
+ cancellationSignal: CancellationSignal,
+ callback: OutcomeReceiver,
+ ) {
+ Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
+ try {
+ processCreateCredentialRequest(request) {
+ callback.onResult(BeginCreateCredentialResponse(it))
+ }
+ } catch (e: Exception) {
+ Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e)
+ toastError(e)
+ callback.onError(CreateCredentialUnknownException(e.localizedMessage))
+ }
+ }
+
+ private fun processCreateCredentialRequest(
+ request: BeginCreateCredentialRequest,
+ callback: (List) -> Unit
+ ) {
+ when (request) {
+ is BeginCreatePublicKeyCredentialRequest -> {
+ // Request is passkey type
+ handleCreatePasskeyQuery(request, callback)
+ }
+ else -> {
+ // request type not supported
+ throw IOException("unknown type of BeginCreateCredentialRequest")
+ }
+ }
+ }
+
+ private fun MutableList.addPendingIntentCreationNewEntry(
+ accountName: String,
+ searchInfo: SearchInfo?
+ ) {
+ Log.d(TAG, "Add pending intent for registration in opened database to create new item")
+ // TODO add a setting to directly store in a specific group
+ PasskeyLauncherActivity.getPendingIntent(
+ context = applicationContext,
+ specialMode = SpecialMode.REGISTRATION,
+ searchInfo = searchInfo
+ )?.let { pendingIntent ->
+ this.add(
+ CreateEntry(
+ accountName = accountName,
+ icon = defaultIcon,
+ pendingIntent = pendingIntent,
+ description = getString(R.string.passkey_creation_description)
+ )
+ )
+ }
+ }
+
+ private fun handleCreatePasskeyQuery(
+ request: BeginCreatePublicKeyCredentialRequest,
+ callback: (List) -> Unit
+ ) {
+ val databaseName = mDatabase?.name
+ val accountName =
+ if (databaseName?.isBlank() != false)
+ getString(R.string.passkey_database_username)
+ else databaseName
+ val createEntries: MutableList = mutableListOf()
+ val relyingPartyId = PublicKeyCredentialCreationOptions(
+ requestJson = request.requestJson,
+ clientDataHash = request.clientDataHash
+ ).relyingPartyEntity.id
+ val searchInfo = buildPasskeySearchInfo(relyingPartyId)
+ Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
+ SearchHelper.checkAutoSearchInfo(
+ context = this,
+ database = mDatabase,
+ searchInfo = searchInfo,
+ onItemsFound = { database, items ->
+ if (database.isReadOnly) {
+ throw RegisterInReadOnlyDatabaseException()
+ } else {
+ // To create a new entry
+ createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
+ /* TODO Overwrite
+ // To select an existing entry and permit an overwrite
+ Log.w(TAG, "Passkey already registered")
+ for (entryInfo in items) {
+ PasskeyHelper.getPendingIntent(
+ context = applicationContext,
+ specialMode = SpecialMode.REGISTRATION,
+ searchInfo = searchInfo,
+ passkeyEntryNodeId = entryInfo.id
+ )?.let { createPendingIntent ->
+ createEntries.add(
+ CreateEntry(
+ accountName = accountName,
+ pendingIntent = createPendingIntent,
+ description = getString(
+ R.string.passkey_update_description,
+ entryInfo.passkey?.displayName
+ )
+ )
+ )
+ }
+ }*/
+ }
+ callback(createEntries)
+ },
+ onItemNotFound = { database ->
+ // To create a new entry
+ if (database.isReadOnly) {
+ throw RegisterInReadOnlyDatabaseException()
+ } else {
+ createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
+ }
+ callback(createEntries)
+ },
+ onDatabaseClosed = {
+ // Launch the passkey launcher activity to open the database
+ Log.d(TAG, "Add pending intent for passkey registration in closed database")
+ PasskeyLauncherActivity.getPendingIntent(
+ context = applicationContext,
+ specialMode = SpecialMode.REGISTRATION
+ )?.let { pendingIntent ->
+ createEntries.add(
+ CreateEntry(
+ accountName = accountName,
+ icon = defaultIcon,
+ pendingIntent = pendingIntent,
+ description = getString(R.string.passkey_locked_database_description)
+ )
+ )
+ }
+ callback(createEntries)
+ }
+ )
+ }
+
+ override fun onClearCredentialStateRequest(
+ request: ProviderClearCredentialStateRequest,
+ cancellationSignal: CancellationSignal,
+ callback: OutcomeReceiver
+ ) {
+ // nothing to do
+ }
+
+ companion object {
+ private val TAG = PasskeyProviderService::class.java.simpleName
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AndroidPrivilegedApp.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AndroidPrivilegedApp.kt
new file mode 100644
index 000000000..848b2e3f6
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AndroidPrivilegedApp.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2025 AOSP modified by 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 .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+import android.os.Build
+import android.os.Parcelable
+import android.util.Log
+import kotlinx.parcelize.Parcelize
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+
+/**
+ * Represents an Android privileged app, based on AOSP code
+ */
+@Parcelize
+data class AndroidPrivilegedApp(
+ val packageName: String,
+ val fingerprints: Set
+): Parcelable {
+
+ override fun toString(): String {
+ return "$packageName ($fingerprints)"
+ }
+
+ companion object {
+ private const val PACKAGE_NAME_KEY = "package_name"
+ private const val SIGNATURES_KEY = "signatures"
+ private const val FINGERPRINT_KEY = "cert_fingerprint_sha256"
+ private const val BUILD_KEY = "build"
+ private const val USER_DEBUG_KEY = "userdebug"
+ private const val TYPE_KEY = "type"
+ private const val APP_INFO_KEY = "info"
+ private const val ANDROID_TYPE_KEY = "android"
+ private const val USER_BUILD_TYPE = "userdebug"
+ private const val APPS_KEY = "apps"
+
+ /**
+ * Extracts a list of AndroidPrivilegedApp objects from a JSONObject.
+ */
+ @JvmStatic
+ fun extractPrivilegedApps(jsonObject: JSONObject): List {
+ val apps = mutableListOf()
+ if (!jsonObject.has(APPS_KEY)) {
+ return apps
+ }
+ val appsJsonArray = jsonObject.getJSONArray(APPS_KEY)
+ for (i in 0 until appsJsonArray.length()) {
+ try {
+ val appJsonObject = appsJsonArray.getJSONObject(i)
+ if (appJsonObject.getString(TYPE_KEY) != ANDROID_TYPE_KEY) {
+ continue
+ }
+ if (!appJsonObject.has(APP_INFO_KEY)) {
+ continue
+ }
+ apps.add(
+ createFromJSONObject(
+ appJsonObject.getJSONObject(APP_INFO_KEY)
+ )
+ )
+ } catch (e: JSONException) {
+ Log.e(AndroidPrivilegedApp::class.simpleName, "Error parsing privileged app", e)
+ }
+ }
+ return apps
+ }
+
+ /**
+ * Creates an AndroidPrivilegedApp object from a JSONObject.
+ */
+ @JvmStatic
+ private fun createFromJSONObject(
+ appInfoJsonObject: JSONObject,
+ filterUserDebug: Boolean = true
+ ): AndroidPrivilegedApp {
+ val signaturesJson = appInfoJsonObject.getJSONArray(SIGNATURES_KEY)
+ val fingerprints = mutableSetOf()
+ for (j in 0 until signaturesJson.length()) {
+ if (filterUserDebug) {
+ if (USER_DEBUG_KEY == signaturesJson.getJSONObject(j)
+ .optString(BUILD_KEY) && USER_BUILD_TYPE != Build.TYPE
+ ) {
+ continue
+ }
+ }
+ fingerprints.add(signaturesJson.getJSONObject(j).getString(FINGERPRINT_KEY))
+ }
+ return AndroidPrivilegedApp(
+ packageName = appInfoJsonObject.getString(PACKAGE_NAME_KEY),
+ fingerprints = fingerprints
+ )
+ }
+
+ /**
+ * Creates a JSONObject from a list of AndroidPrivilegedApp objects.
+ * The structure will be similar to what `extractPrivilegedApps` expects.
+ *
+ * @param privilegedApps The list of AndroidPrivilegedApp objects.
+ * @return A JSONObject representing the list.
+ */
+ @JvmStatic
+ fun toJsonObject(privilegedApps: List): JSONObject {
+ val rootJsonObject = JSONObject()
+ val appsJsonArray = JSONArray()
+
+ for (app in privilegedApps) {
+ val appInfoObject = JSONObject()
+ appInfoObject.put(PACKAGE_NAME_KEY, app.packageName)
+
+ val signaturesArray = JSONArray()
+ for (fingerprint in app.fingerprints) {
+ val signatureObject = JSONObject()
+ signatureObject.put(FINGERPRINT_KEY, fingerprint)
+ // If needed: signatureObject.put(BUILD_KEY, "user")
+ signaturesArray.put(signatureObject)
+ }
+ appInfoObject.put(SIGNATURES_KEY, signaturesArray)
+
+ val appContainerObject = JSONObject()
+ appContainerObject.put(TYPE_KEY, ANDROID_TYPE_KEY)
+ appContainerObject.put(APP_INFO_KEY, appInfoObject)
+
+ appsJsonArray.put(appContainerObject)
+ }
+
+ rootJsonObject.put(APPS_KEY, appsJsonArray)
+ return rootJsonObject
+ }
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAssertionResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAssertionResponse.kt
new file mode 100644
index 000000000..88415d9b3
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAssertionResponse.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+import android.util.Log
+import androidx.credentials.exceptions.GetCredentialUnknownException
+import com.kunzisoft.encrypt.Signature
+import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
+import org.json.JSONObject
+
+class AuthenticatorAssertionResponse(
+ private val requestOptions: PublicKeyCredentialRequestOptions,
+ private val userPresent: Boolean,
+ private val userVerified: Boolean,
+ private val backupEligibility: Boolean,
+ private val backupState: Boolean,
+ private var userHandle: String,
+ privateKey: String,
+ private val clientDataResponse: ClientDataResponse,
+) : AuthenticatorResponse {
+
+ override var clientJson = JSONObject()
+ private var authenticatorData: ByteArray = AuthenticatorData.buildAuthenticatorData(
+ relyingPartyId = requestOptions.rpId.toByteArray(),
+ userPresent = userPresent,
+ userVerified = userVerified,
+ backupEligibility = backupEligibility,
+ backupState = backupState
+ )
+ private var signature: ByteArray = byteArrayOf()
+
+ init {
+ try {
+ signature = Signature.sign(privateKey, dataToSign())
+ } catch (e: Exception) {
+ Log.e(this::class.java.simpleName, "Unable to sign: ${e.message}")
+ throw GetCredentialUnknownException("Signing failed")
+ }
+ }
+
+ private fun dataToSign(): ByteArray {
+ return authenticatorData + clientDataResponse.hashData()
+ }
+
+ override fun json(): JSONObject {
+ // https://www.w3.org/TR/webauthn-3/#authdata-flags
+ return clientJson.apply {
+ put("clientDataJSON", clientDataResponse.buildResponse())
+ put("authenticatorData", b64Encode(authenticatorData))
+ put("signature", b64Encode(signature))
+ put("userHandle", userHandle)
+ }
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAttestationResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAttestationResponse.kt
new file mode 100644
index 000000000..07ac0ba45
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAttestationResponse.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
+import com.kunzisoft.keepass.utils.UUIDUtils.asBytes
+import org.json.JSONArray
+import org.json.JSONObject
+import java.util.UUID
+
+class AuthenticatorAttestationResponse(
+ private val requestOptions: PublicKeyCredentialCreationOptions,
+ private val credentialId: ByteArray,
+ private val credentialPublicKey: ByteArray,
+ private val userPresent: Boolean,
+ private val userVerified: Boolean,
+ private val backupEligibility: Boolean,
+ private val backupState: Boolean,
+ private val publicKeyTypeId: Long,
+ private val publicKeyCbor: ByteArray,
+ private val clientDataResponse: ClientDataResponse,
+) : AuthenticatorResponse {
+
+ override var clientJson = JSONObject()
+ var attestationObject: ByteArray
+
+ init {
+ attestationObject = defaultAttestationObject()
+ }
+
+ private fun buildAuthData(): ByteArray {
+ return AuthenticatorData.buildAuthenticatorData(
+ relyingPartyId = requestOptions.relyingPartyEntity.id.toByteArray(),
+ userPresent = userPresent,
+ userVerified = userVerified,
+ backupEligibility = backupEligibility,
+ backupState = backupState,
+ attestedCredentialData = true
+ ) + AAGUID +
+ //credIdLen
+ byteArrayOf((credentialId.size shr 8).toByte(), credentialId.size.toByte()) +
+ credentialId +
+ credentialPublicKey
+ }
+
+ internal fun defaultAttestationObject(): ByteArray {
+ // https://www.w3.org/TR/webauthn-3/#attestation-object
+ val ao = mutableMapOf()
+ ao.put("fmt", "none")
+ ao.put("attStmt", emptyMap())
+ ao.put("authData", buildAuthData())
+ return Cbor().encode(ao)
+ }
+
+ override fun json(): JSONObject {
+ // See AuthenticatorAttestationResponseJSON at
+ // https://w3c.github.io/webauthn/#ref-for-dom-publickeycredential-tojson
+ return clientJson.apply {
+ put("clientDataJSON", clientDataResponse.buildResponse())
+ put("authenticatorData", b64Encode(buildAuthData()))
+ put("transports", JSONArray(listOf("internal", "hybrid")))
+ put("publicKey", b64Encode(publicKeyCbor))
+ put("publicKeyAlgorithm", publicKeyTypeId)
+ put("attestationObject", b64Encode(attestationObject))
+ }
+ }
+
+ companion object {
+ // Authenticator Attestation Global Unique Identifier
+ private val AAGUID: ByteArray = UUID.fromString("eaecdef2-1c31-5634-8639-f1cbd9c00a08").asBytes()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorData.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorData.kt
new file mode 100644
index 000000000..6df2b10be
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorData.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+import com.kunzisoft.encrypt.HashManager
+
+class AuthenticatorData {
+
+ companion object {
+ fun buildAuthenticatorData(
+ relyingPartyId: ByteArray,
+ userPresent: Boolean,
+ userVerified: Boolean,
+ backupEligibility: Boolean,
+ backupState: Boolean,
+ attestedCredentialData: Boolean = false
+ ): ByteArray {
+ // https://www.w3.org/TR/webauthn-3/#table-authData
+ var flags = 0
+ if (userPresent)
+ flags = flags or 0x01
+ // bit at index 1 is reserved
+ if (userVerified)
+ flags = flags or 0x04
+ if (backupEligibility)
+ flags = flags or 0x08
+ if (backupState)
+ flags = flags or 0x10
+ // bit at index 5 is reserved
+ if (attestedCredentialData) {
+ flags = flags or 0x40
+ }
+ // bit at index 7: Extension data included == false
+
+ return HashManager.hashSha256(relyingPartyId) +
+ byteArrayOf(flags.toByte()) +
+ byteArrayOf(0, 0, 0, 0)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorResponse.kt
new file mode 100644
index 000000000..57d9ab2dc
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorResponse.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+import org.json.JSONObject
+
+interface AuthenticatorResponse {
+ var clientJson: JSONObject
+
+ fun json(): JSONObject
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/Cbor.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/Cbor.kt
new file mode 100644
index 000000000..8dbbb8657
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/Cbor.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+import androidx.annotation.RestrictTo
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class Cbor {
+ data class Item(val item: Any, val len: Int)
+
+ data class Arg(val arg: Long, val len: Int)
+
+ val TYPE_UNSIGNED_INT = 0x00
+ val TYPE_NEGATIVE_INT = 0x01
+ val TYPE_BYTE_STRING = 0x02
+ val TYPE_TEXT_STRING = 0x03
+ val TYPE_ARRAY = 0x04
+ val TYPE_MAP = 0x05
+ val TYPE_TAG = 0x06
+ val TYPE_FLOAT = 0x07
+
+ fun decode(data: ByteArray): Any {
+ val ret = parseItem(data, 0)
+ return ret.item
+ }
+
+ fun encode(data: Any): ByteArray {
+ if (data is Number) {
+ if (data is Double) {
+ throw IllegalArgumentException("Don't support doubles yet")
+ } else {
+ val value = data.toLong()
+ if (value >= 0) {
+ return createArg(TYPE_UNSIGNED_INT, value)
+ } else {
+ return createArg(TYPE_NEGATIVE_INT, -1 - value)
+ }
+ }
+ }
+ if (data is ByteArray) {
+ return createArg(TYPE_BYTE_STRING, data.size.toLong()) + data
+ }
+ if (data is String) {
+ return createArg(TYPE_TEXT_STRING, data.length.toLong()) + data.encodeToByteArray()
+ }
+ if (data is List<*>) {
+ var ret = createArg(TYPE_ARRAY, data.size.toLong())
+ for (i in data) {
+ ret += encode(i!!)
+ }
+ return ret
+ }
+ if (data is Map<*, *>) {
+ // See:
+ // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#ctap2-canonical-cbor-encoding-form
+ var ret = createArg(TYPE_MAP, data.size.toLong())
+ var byteMap: MutableMap = mutableMapOf()
+ for (i in data) {
+ // Convert to byte arrays so we can sort them.
+ byteMap.put(encode(i.key!!), encode(i.value!!))
+ }
+
+ var keysList = ArrayList(byteMap.keys)
+ keysList.sortedWith(
+ Comparator { a, b ->
+ // If two keys have different lengths, the shorter one sorts earlier;
+ // If two keys have the same length, the one with the lower value in (byte-wise)
+ // lexical order sorts earlier.
+ var aBytes = byteMap.get(a)!!
+ var bBytes = byteMap.get(b)!!
+ when {
+ a.size > b.size -> 1
+ a.size < b.size -> -1
+ aBytes.size > bBytes.size -> 1
+ aBytes.size < bBytes.size -> -1
+ else -> 0
+ }
+ }
+ )
+
+ for (key in keysList) {
+ ret += key
+ ret += byteMap.get(key)!!
+ }
+ return ret
+ }
+ throw IllegalArgumentException("Bad type")
+ }
+
+ private fun getType(data: ByteArray, offset: Int): Int {
+ val d = data[offset].toInt()
+ return (d and 0xFF) shr 5
+ }
+
+ private fun getArg(data: ByteArray, offset: Int): Arg {
+ val arg = data[offset].toLong() and 0x1F
+ if (arg < 24) {
+ return Arg(arg, 1)
+ }
+ if (arg == 24L) {
+ return Arg(data[offset + 1].toLong() and 0xFF, 2)
+ }
+ if (arg == 25L) {
+ var ret = (data[offset + 1].toLong() and 0xFF) shl 8
+ ret = ret or (data[offset + 2].toLong() and 0xFF)
+ return Arg(ret, 3)
+ }
+ if (arg == 26L) {
+ var ret = (data[offset + 1].toLong() and 0xFF) shl 24
+ ret = ret or ((data[offset + 2].toLong() and 0xFF) shl 16)
+ ret = ret or ((data[offset + 3].toLong() and 0xFF) shl 8)
+ ret = ret or (data[offset + 4].toLong() and 0xFF)
+ return Arg(ret, 5)
+ }
+ throw IllegalArgumentException("Bad arg")
+ }
+
+ private fun parseItem(data: ByteArray, offset: Int): Item {
+ val itemType = getType(data, offset)
+ val arg = getArg(data, offset)
+ println("Type $itemType ${arg.arg} ${arg.len}")
+
+ when (itemType) {
+ TYPE_UNSIGNED_INT -> {
+ return Item(arg.arg, arg.len)
+ }
+ TYPE_NEGATIVE_INT -> {
+ return Item(-1 - arg.arg, arg.len)
+ }
+ TYPE_BYTE_STRING -> {
+ val ret =
+ data.sliceArray(offset + arg.len.toInt() until offset + arg.len.toInt() + arg.arg.toInt())
+ return Item(ret, arg.len + arg.arg.toInt())
+ }
+ TYPE_TEXT_STRING -> {
+ val ret =
+ data.sliceArray(offset + arg.len.toInt() until offset + arg.len.toInt() + arg.arg.toInt())
+ return Item(ret.toString(Charsets.UTF_8), arg.len + arg.arg.toInt())
+ }
+ TYPE_ARRAY -> {
+ val ret = mutableListOf()
+ var consumed = arg.len
+ for (i in 0 until arg.arg.toInt()) {
+ val item = parseItem(data, offset + consumed)
+ ret.add(item.item)
+ consumed += item.len
+ }
+ return Item(ret.toList(), consumed)
+ }
+ TYPE_MAP -> {
+ val ret = mutableMapOf()
+ var consumed = arg.len
+ for (i in 0 until arg.arg.toInt()) {
+ val key = parseItem(data, offset + consumed)
+ consumed += key.len
+ val value = parseItem(data, offset + consumed)
+ consumed += value.len
+ ret[key.item] = value.item
+ }
+ return Item(ret.toMap(), consumed)
+ }
+ else -> {
+ throw IllegalArgumentException("Bad type")
+ }
+ }
+ }
+
+ private fun createArg(type: Int, arg: Long): ByteArray {
+ val t = type shl 5
+ val a = arg.toInt()
+ if (arg < 24) {
+ return byteArrayOf(((t or a) and 0xFF).toByte())
+ }
+ if (arg <= 0xFF) {
+ return byteArrayOf(((t or 24) and 0xFF).toByte(), (a and 0xFF).toByte())
+ }
+ if (arg <= 0xFFFF) {
+ return byteArrayOf(
+ ((t or 25) and 0xFF).toByte(),
+ ((a shr 8) and 0xFF).toByte(),
+ (a and 0xFF).toByte()
+ )
+ }
+ if (arg <= 0xFFFFFFFF) {
+ return byteArrayOf(
+ ((t or 26) and 0xFF).toByte(),
+ ((a shr 24) and 0xFF).toByte(),
+ ((a shr 16) and 0xFF).toByte(),
+ ((a shr 8) and 0xFF).toByte(),
+ (a and 0xFF).toByte()
+ )
+ }
+ throw IllegalArgumentException("bad Arg")
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataBuildResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataBuildResponse.kt
new file mode 100644
index 000000000..0e9454d19
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataBuildResponse.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+import com.kunzisoft.encrypt.HashManager
+import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
+import org.json.JSONObject
+
+open class ClientDataBuildResponse(
+ type: Type,
+ challenge: ByteArray,
+ origin: String,
+ crossOrigin: Boolean? = false,
+ topOrigin: String? = null,
+): AuthenticatorResponse, ClientDataResponse {
+ override var clientJson = JSONObject()
+
+ init {
+ // https://w3c.github.io/webauthn/#client-data
+ clientJson.put("type", type.value)
+ clientJson.put("challenge", b64Encode(challenge))
+ clientJson.put("origin", origin)
+ crossOrigin?.let {
+ clientJson.put("crossOrigin", it)
+ }
+ topOrigin?.let {
+ clientJson.put("topOrigin", it)
+ }
+ }
+
+ override fun json(): JSONObject {
+ return clientJson
+ }
+
+ enum class Type(val value: String) {
+ GET("webauthn.get"), CREATE("webauthn.create")
+ }
+
+ override fun buildResponse(): String {
+ return b64Encode(json().toString().toByteArray())
+ }
+
+ override fun hashData(): ByteArray {
+ return HashManager.hashSha256(json().toString().toByteArray())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataDefinedResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataDefinedResponse.kt
new file mode 100644
index 000000000..ae323b904
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataDefinedResponse.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+open class ClientDataDefinedResponse(
+ private val clientDataHash: ByteArray
+): ClientDataResponse {
+
+ override fun hashData(): ByteArray {
+ return clientDataHash
+ }
+
+ override fun buildResponse(): String {
+ return CLIENT_DATA_JSON_PRIVILEGED
+ }
+
+ companion object {
+ private const val CLIENT_DATA_JSON_PRIVILEGED = ""
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataResponse.kt
new file mode 100644
index 000000000..bdc5b3ec2
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataResponse.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+interface ClientDataResponse {
+ fun hashData(): ByteArray
+ fun buildResponse(): String
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/FidoDataTypes.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/FidoDataTypes.kt
new file mode 100644
index 000000000..62a70a485
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/FidoDataTypes.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+data class PublicKeyCredentialRpEntity(val name: String, val id: String)
+
+data class PublicKeyCredentialUserEntity(
+ val name: String,
+ val id: ByteArray,
+ val displayName: String
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as PublicKeyCredentialUserEntity
+
+ if (name != other.name) return false
+ if (!id.contentEquals(other.id)) return false
+ if (displayName != other.displayName) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = name.hashCode()
+ result = 31 * result + id.contentHashCode()
+ result = 31 * result + displayName.hashCode()
+ return result
+ }
+}
+
+data class PublicKeyCredentialParameters(val type: String, val alg: Long)
+
+data class PublicKeyCredentialDescriptor(
+ val type: String,
+ val id: ByteArray,
+ val transports: List
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as PublicKeyCredentialDescriptor
+
+ if (type != other.type) return false
+ if (!id.contentEquals(other.id)) return false
+ if (transports != other.transports) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = type.hashCode()
+ result = 31 * result + id.contentHashCode()
+ result = 31 * result + transports.hashCode()
+ return result
+ }
+}
+
+data class AuthenticatorSelectionCriteria(
+ val authenticatorAttachment: String,
+ val residentKey: String,
+ val requireResidentKey: Boolean = false,
+ val userVerification: String = "preferred"
+)
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/FidoPublicKeyCredential.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/FidoPublicKeyCredential.kt
new file mode 100644
index 000000000..9fa66a8a8
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/FidoPublicKeyCredential.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+import org.json.JSONObject
+
+class FidoPublicKeyCredential(
+ val id: String,
+ val response: AuthenticatorResponse,
+ val authenticatorAttachment: String
+) {
+
+ fun json(): String {
+ // see at https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension
+ val discoverableCredential = true
+ val rk = JSONObject()
+ rk.put("rk", discoverableCredential)
+ val credProps = JSONObject()
+ credProps.put("credProps", rk)
+
+ // See RegistrationResponseJSON at
+ // https://w3c.github.io/webauthn/#ref-for-dom-publickeycredential-tojson
+ val ret = JSONObject()
+ ret.put("id", id)
+ ret.put("rawId", id)
+ ret.put("type", "public-key")
+ ret.put("authenticatorAttachment", authenticatorAttachment)
+ ret.put("response", response.json())
+ ret.put("clientExtensionResults", JSONObject()) // TODO credProps
+
+ return ret.toString()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationOptions.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationOptions.kt
new file mode 100644
index 000000000..b64634f63
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationOptions.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+import com.kunzisoft.encrypt.Base64Helper
+import org.json.JSONObject
+
+class PublicKeyCredentialCreationOptions(
+ requestJson: String,
+ var clientDataHash: ByteArray?
+) {
+ val json: JSONObject = JSONObject(requestJson)
+
+ val relyingPartyEntity: PublicKeyCredentialRpEntity
+ val userEntity: PublicKeyCredentialUserEntity
+ val challenge: ByteArray
+ val pubKeyCredParams: List
+
+ var timeout: Long
+ var excludeCredentials: List
+ var authenticatorSelection: AuthenticatorSelectionCriteria
+ var attestation: String
+
+ init {
+ val rpJson = json.getJSONObject("rp")
+ relyingPartyEntity = PublicKeyCredentialRpEntity(rpJson.getString("name"), rpJson.getString("id"))
+ val rpUser = json.getJSONObject("user")
+ val userId = Base64Helper.b64Decode(rpUser.getString("id"))
+ userEntity =
+ PublicKeyCredentialUserEntity(
+ rpUser.getString("name"),
+ userId,
+ rpUser.getString("displayName")
+ )
+ challenge = Base64Helper.b64Decode(json.getString("challenge"))
+ val pubKeyCredParamsJson = json.getJSONArray("pubKeyCredParams")
+ val pubKeyCredParamsTmp: MutableList = mutableListOf()
+ for (i in 0 until pubKeyCredParamsJson.length()) {
+ val e = pubKeyCredParamsJson.getJSONObject(i)
+ pubKeyCredParamsTmp.add(
+ PublicKeyCredentialParameters(e.getString("type"), e.getLong("alg"))
+ )
+ }
+ pubKeyCredParams = pubKeyCredParamsTmp.toList()
+
+ timeout = json.optLong("timeout", 0)
+ // TODO: Fix excludeCredentials and authenticatorSelection
+ excludeCredentials = emptyList()
+ authenticatorSelection = AuthenticatorSelectionCriteria("platform", "required")
+ attestation = json.optString("attestation", "none")
+ }
+
+ companion object {
+ private val TAG = PublicKeyCredentialCreationOptions::class.simpleName
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationParameters.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationParameters.kt
new file mode 100644
index 000000000..542e15c0c
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationParameters.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+import java.security.KeyPair
+
+data class PublicKeyCredentialCreationParameters(
+ val publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions,
+ val credentialId: ByteArray,
+ val signatureKey: Pair,
+ val clientDataResponse: ClientDataResponse
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as PublicKeyCredentialCreationParameters
+
+ if (publicKeyCredentialCreationOptions != other.publicKeyCredentialCreationOptions) return false
+ if (!credentialId.contentEquals(other.credentialId)) return false
+ if (signatureKey != other.signatureKey) return false
+ if (clientDataResponse != other.clientDataResponse) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = publicKeyCredentialCreationOptions.hashCode()
+ result = 31 * result + credentialId.contentHashCode()
+ result = 31 * result + signatureKey.hashCode()
+ result = 31 * result + clientDataResponse.hashCode()
+ return result
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialRequestOptions.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialRequestOptions.kt
new file mode 100644
index 000000000..42b0d75de
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialRequestOptions.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+import com.kunzisoft.encrypt.Base64Helper
+import org.json.JSONObject
+
+class PublicKeyCredentialRequestOptions(requestJson: String) {
+ val json: JSONObject = JSONObject(requestJson)
+ val challenge: ByteArray = Base64Helper.b64Decode(json.getString("challenge"))
+ val timeout: Long = json.optLong("timeout", 0)
+ val rpId: String = json.optString("rpId", "")
+ val userVerification: String = json.optString("userVerification", "preferred")
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialUsageParameters.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialUsageParameters.kt
new file mode 100644
index 000000000..5d914bca8
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialUsageParameters.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+import com.kunzisoft.keepass.model.AppOrigin
+
+data class PublicKeyCredentialUsageParameters(
+ val publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions,
+ val clientDataResponse: ClientDataResponse,
+ var appOrigin: AppOrigin
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt
new file mode 100644
index 000000000..7af8e349b
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt
@@ -0,0 +1,604 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.util
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyProperties
+import android.util.Log
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.CreatePublicKeyCredentialResponse
+import androidx.credentials.GetPublicKeyCredentialOption
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.exceptions.CreateCredentialUnknownException
+import androidx.credentials.exceptions.GetCredentialUnknownException
+import androidx.credentials.provider.CallingAppInfo
+import androidx.credentials.provider.PendingIntentHandler
+import androidx.credentials.provider.ProviderCreateCredentialRequest
+import androidx.credentials.provider.ProviderGetCredentialRequest
+import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
+import com.kunzisoft.encrypt.Signature
+import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
+import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
+import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
+import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
+import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
+import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataBuildResponse
+import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataDefinedResponse
+import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataResponse
+import com.kunzisoft.keepass.credentialprovider.passkey.data.FidoPublicKeyCredential
+import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
+import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
+import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
+import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.getOriginFromPrivilegedAllowLists
+import com.kunzisoft.keepass.model.AndroidOrigin
+import com.kunzisoft.keepass.model.AppOrigin
+import com.kunzisoft.keepass.model.EntryInfo
+import com.kunzisoft.keepass.model.Passkey
+import com.kunzisoft.keepass.utils.AppUtil
+import com.kunzisoft.keepass.utils.StringUtil.toHexString
+import com.kunzisoft.keepass.utils.getParcelableExtraCompat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.io.IOException
+import java.security.KeyStore
+import java.security.MessageDigest
+import java.security.SecureRandom
+import java.time.Instant
+import java.util.UUID
+import javax.crypto.KeyGenerator
+import javax.crypto.Mac
+import javax.crypto.SecretKey
+
+/**
+ * Utility class to manage the passkey elements,
+ * allows to add and retrieve intent values with preconfigured keys,
+ * and makes it easy to create creation and usage requests
+ */
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+object PasskeyHelper {
+
+ private const val EXTRA_PASSKEY = "com.kunzisoft.keepass.passkey.extra.passkey"
+
+ private const val HMAC_TYPE = "HmacSHA256"
+
+ private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
+ private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
+ private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
+
+ private const val SEPARATOR = "_"
+
+ private const val NAME_OF_HMAC_KEY = "KeePassDXCredentialProviderHMACKey"
+
+ private const val KEYSTORE_TYPE = "AndroidKeyStore"
+
+ private val PLACEHOLDER_FOR_NEW_NODE_ID = "0".repeat(32)
+
+ private val REGEX_TIMESTAMP = "[0-9]{10}".toRegex()
+ private val REGEX_AUTHENTICATION_CODE = "[A-F0-9]{64}".toRegex() // 256 bits = 64 hex chars
+
+ private const val MAX_DIFF_IN_SECONDS = 60
+
+ private val internalSecureRandom: SecureRandom = SecureRandom()
+
+ /**
+ * Add an authentication code generated by an entry to the intent
+ */
+ fun Intent.addAuthCode(passkeyEntryNodeId: UUID? = null) {
+ putExtras(Bundle().apply {
+ val timestamp = Instant.now().epochSecond
+ putString(EXTRA_TIMESTAMP, timestamp.toString())
+ putString(
+ EXTRA_AUTHENTICATION_CODE,
+ generatedAuthenticationCode(
+ passkeyEntryNodeId, timestamp
+ ).toHexString()
+ )
+ })
+ }
+
+ /**
+ * Add the passkey to the intent
+ */
+ fun Intent.addPasskey(passkey: Passkey?) {
+ passkey?.let {
+ putExtra(EXTRA_PASSKEY, passkey)
+ }
+ }
+
+ /**
+ * Retrieve the passkey from the intent
+ */
+ fun Intent.retrievePasskey(): Passkey? {
+ return this.getParcelableExtraCompat(EXTRA_PASSKEY)
+ }
+
+ /**
+ * Remove the passkey from the intent
+ */
+ fun Intent.removePasskey() {
+ return this.removeExtra(EXTRA_PASSKEY)
+ }
+
+ /**
+ * Add the app origin to the intent
+ */
+ fun Intent.addAppOrigin(appOrigin: AppOrigin?) {
+ appOrigin?.let {
+ putExtra(EXTRA_APP_ORIGIN, appOrigin)
+ }
+ }
+
+ /**
+ * Retrieve the app origin from the intent
+ */
+ fun Intent.retrieveAppOrigin(): AppOrigin? {
+ return this.getParcelableExtraCompat(EXTRA_APP_ORIGIN)
+ }
+
+ /**
+ * Remove the app origin from the intent
+ */
+ fun Intent.removeAppOrigin() {
+ return this.removeExtra(EXTRA_APP_ORIGIN)
+ }
+
+ /**
+ * Build the Passkey response for one entry
+ */
+ fun Activity.buildPasskeyResponseAndSetResult(
+ entryInfo: EntryInfo,
+ extras: Bundle? = null
+ ) {
+ try {
+ entryInfo.passkey?.let { passkey ->
+ val mReplyIntent = Intent()
+ Log.d(javaClass.name, "Success Passkey manual selection")
+ mReplyIntent.addPasskey(passkey)
+ mReplyIntent.addAppOrigin(entryInfo.appOrigin)
+ mReplyIntent.addNodeId(entryInfo.id)
+ extras?.let {
+ mReplyIntent.putExtras(it)
+ }
+ setResult(Activity.RESULT_OK, mReplyIntent)
+ } ?: run {
+ throw IOException("No passkey found")
+ }
+ } catch (e: Exception) {
+ Log.e(javaClass.name, "Unable to add the passkey as result", e)
+ Toast.makeText(
+ this,
+ getString(R.string.error_passkey_result),
+ Toast.LENGTH_SHORT
+ ).show()
+ setResult(Activity.RESULT_CANCELED)
+ }
+ }
+
+ /**
+ * Check the timestamp and authentication code transmitted via PendingIntent
+ */
+ fun checkSecurity(intent: Intent, nodeId: UUID?) {
+ val timestampString = intent.getStringExtra(EXTRA_TIMESTAMP)
+ if (timestampString.isNullOrEmpty())
+ throw CreateCredentialUnknownException("Timestamp null")
+ if (timestampString.matches(REGEX_TIMESTAMP).not()) {
+ throw CreateCredentialUnknownException("Timestamp not valid")
+ }
+ val timestamp = timestampString.toLong()
+ val diff = Instant.now().epochSecond - timestamp
+ if (diff < 0 || diff > MAX_DIFF_IN_SECONDS) {
+ throw CreateCredentialUnknownException("Out of time")
+ }
+ verifyAuthenticationCode(
+ intent.getStringExtra(EXTRA_AUTHENTICATION_CODE),
+ generatedAuthenticationCode(nodeId, timestamp)
+ )
+ }
+
+ /**
+ * Verify the authentication code from the encrypted message received from the intent
+ */
+ private fun verifyAuthenticationCode(
+ valueToCheck: String?,
+ authenticationCode: ByteArray
+ ) {
+ if (valueToCheck.isNullOrEmpty())
+ throw CreateCredentialUnknownException("Authentication code empty")
+ if (valueToCheck.matches(REGEX_AUTHENTICATION_CODE).not())
+ throw CreateCredentialUnknownException("Authentication not valid")
+ if (MessageDigest.isEqual(authenticationCode, generateAuthenticationCode(valueToCheck)))
+ throw CreateCredentialUnknownException("Authentication code incorrect")
+ }
+
+ /**
+ * Generate the authentication code base on the entry [nodeId] and [timestamp]
+ */
+ private fun generatedAuthenticationCode(nodeId: UUID?, timestamp: Long): ByteArray {
+ return generateAuthenticationCode(
+ (nodeId?.toString() ?: PLACEHOLDER_FOR_NEW_NODE_ID) + SEPARATOR + timestamp.toString()
+ )
+ }
+
+ /**
+ * Generate the authentication code base on the entry [message]
+ */
+ private fun generateAuthenticationCode(message: String): ByteArray {
+ val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
+ keyStore.load(null)
+ val hmacKey = try {
+ keyStore.getKey(NAME_OF_HMAC_KEY, null) as SecretKey
+ } catch (_: Exception) {
+ // key not found
+ generateKey()
+ }
+
+ val mac = Mac.getInstance(HMAC_TYPE)
+ mac.init(hmacKey)
+ val authenticationCode = mac.doFinal(message.toByteArray())
+ return authenticationCode
+ }
+
+ /**
+ * Generate the HMAC key if cannot be found in the KeyStore
+ */
+ private fun generateKey(): SecretKey? {
+ val keyGenerator = KeyGenerator.getInstance(
+ KeyProperties.KEY_ALGORITHM_HMAC_SHA256, KEYSTORE_TYPE
+ )
+ val keySizeInBits = 128
+ keyGenerator.init(
+ KeyGenParameterSpec.Builder(NAME_OF_HMAC_KEY, KeyProperties.PURPOSE_SIGN)
+ .setKeySize(keySizeInBits)
+ .build()
+ )
+ val key = keyGenerator.generateKey()
+ return key
+ }
+
+ /**
+ * Retrieve the [PublicKeyCredentialCreationOptions] from the intent
+ */
+ fun ProviderCreateCredentialRequest.retrievePasskeyCreationComponent(): PublicKeyCredentialCreationOptions {
+ val request = this
+ if (request.callingRequest !is CreatePublicKeyCredentialRequest) {
+ throw CreateCredentialUnknownException("callingRequest is of wrong type: ${request.callingRequest.type}")
+ }
+ val createPublicKeyCredentialRequest = request.callingRequest as CreatePublicKeyCredentialRequest
+ return PublicKeyCredentialCreationOptions(
+ requestJson = createPublicKeyCredentialRequest.requestJson,
+ clientDataHash = createPublicKeyCredentialRequest.clientDataHash
+ )
+ }
+
+ /**
+ * Retrieve the [GetPublicKeyCredentialOption] from the intent
+ */
+ fun ProviderGetCredentialRequest.retrievePasskeyUsageComponent(): GetPublicKeyCredentialOption {
+ val request = this
+ if (request.credentialOptions.size != 1) {
+ throw GetCredentialUnknownException("not exact one credentialOption")
+ }
+ if (request.credentialOptions[0] !is GetPublicKeyCredentialOption) {
+ throw CreateCredentialUnknownException("credentialOptions is of wrong type: ${request.credentialOptions[0]}")
+ }
+ return request.credentialOptions[0] as GetPublicKeyCredentialOption
+ }
+
+ /**
+ * Utility method to retrieve the origin asynchronously,
+ * checks for the presence of the application in the privilege lists
+ *
+ * @param providedClientDataHash Client data hash precalculated by the system
+ * @param callingAppInfo CallingAppInfo to verify and retrieve the specific Origin
+ * @param context Context for file operations.
+ * call [onOriginRetrieved] if the origin is already calculated by the system and in the privileged list, return the clientDataHash
+ * call [onOriginNotRetrieved] if the origin is not retrieved from the system, return a new Android Origin
+ */
+ suspend fun getOrigin(
+ providedClientDataHash: ByteArray?,
+ callingAppInfo: CallingAppInfo?,
+ context: Context,
+ onOriginRetrieved: suspend (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit,
+ onOriginNotRetrieved: suspend (appOrigin: AppOrigin, androidOriginString: String) -> Unit
+ ) {
+ if (callingAppInfo == null) {
+ throw SecurityException("Calling app info cannot be retrieved")
+ }
+ withContext(Dispatchers.IO) {
+
+ // For trusted browsers like Chrome and Firefox
+ val callOrigin = try {
+ getOriginFromPrivilegedAllowLists(callingAppInfo, context)
+ } catch (e: Exception) {
+ // Throw the Privileged Exception only if it's a browser
+ if (e is PrivilegedAllowLists.PrivilegedException
+ && AppUtil.getInstalledBrowsersWithSignatures(context).any {
+ it.packageName == e.temptingApp.packageName
+ }
+ ) throw e
+ null
+ }
+
+ // Build the default Android origin
+ val androidOrigin = AndroidOrigin(
+ packageName = callingAppInfo.packageName,
+ fingerprint = callingAppInfo.signingInfo.getApplicationFingerprints()
+ )
+
+ // Check if the webDomain is validated by the system
+ withContext(Dispatchers.Main) {
+ if (callOrigin != null && providedClientDataHash != null) {
+ // Origin already defined by the system
+ Log.d(javaClass.simpleName, "Origin $callOrigin retrieved from callingAppInfo")
+ onOriginRetrieved(
+ AppOrigin.fromOrigin(callOrigin, androidOrigin, verified = true),
+ providedClientDataHash
+ )
+ } else {
+ // Add Android origin by default
+ onOriginNotRetrieved(
+ AppOrigin(verified = false).apply {
+ addAndroidOrigin(androidOrigin)
+ },
+ androidOrigin.toOriginValue()
+ )
+ }
+ }
+ }
+ }
+
+ /**
+ * Generate a credential id randomly
+ */
+ private fun generateCredentialId(): ByteArray {
+ // see https://w3c.github.io/webauthn/#credential-id
+ val size = 16
+ val credentialId = ByteArray(size)
+ internalSecureRandom.nextBytes(credentialId)
+ return credentialId
+ }
+
+ /**
+ * Utility method to create a passkey and the associated creation request parameters
+ * [intent] allows to retrieve the request
+ * [context] context to manage package verification files
+ * [defaultBackupEligibility] the default backup eligibility to add the the passkey entry
+ * [defaultBackupState] the default backup state to add the the passkey entry
+ * [passkeyCreated] is called asynchronously when the passkey has been created
+ */
+ suspend fun retrievePasskeyCreationRequestParameters(
+ intent: Intent,
+ context: Context,
+ defaultBackupEligibility: Boolean?,
+ defaultBackupState: Boolean?,
+ passkeyCreated: suspend (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
+ ) {
+ val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
+ if (createCredentialRequest == null)
+ throw CreateCredentialUnknownException("could not retrieve request from intent")
+ val callingAppInfo = createCredentialRequest.callingAppInfo
+ val creationOptions = createCredentialRequest.retrievePasskeyCreationComponent()
+
+ val relyingParty = creationOptions.relyingPartyEntity.id
+ val username = creationOptions.userEntity.name
+ val userHandle = creationOptions.userEntity.id
+ val pubKeyCredParams = creationOptions.pubKeyCredParams
+ val clientDataHash = creationOptions.clientDataHash
+
+ val credentialId = generateCredentialId()
+
+ val (keyPair, keyTypeId) = Signature.generateKeyPair(
+ pubKeyCredParams.map { params -> params.alg }
+ ) ?: throw CreateCredentialUnknownException("no known public key type found")
+ val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private)
+
+ // Create the passkey element
+ val passkey = Passkey(
+ username = username,
+ privateKeyPem = privateKeyPem,
+ credentialId = b64Encode(credentialId),
+ userHandle = b64Encode(userHandle),
+ relyingParty = relyingParty,
+ backupEligibility = defaultBackupEligibility,
+ backupState = defaultBackupState
+ )
+
+ // create new entry in database
+ getOrigin(
+ providedClientDataHash = clientDataHash,
+ callingAppInfo = callingAppInfo,
+ context = context,
+ onOriginRetrieved = { appInfoToStore, clientDataHash ->
+ passkeyCreated.invoke(
+ passkey,
+ appInfoToStore,
+ PublicKeyCredentialCreationParameters(
+ publicKeyCredentialCreationOptions = creationOptions,
+ credentialId = credentialId,
+ signatureKey = Pair(keyPair, keyTypeId),
+ clientDataResponse = ClientDataDefinedResponse(clientDataHash)
+ )
+ )
+ },
+ onOriginNotRetrieved = { appInfoToStore, origin ->
+ passkeyCreated.invoke(
+ passkey,
+ appInfoToStore,
+ PublicKeyCredentialCreationParameters(
+ publicKeyCredentialCreationOptions = creationOptions,
+ credentialId = credentialId,
+ signatureKey = Pair(keyPair, keyTypeId),
+ clientDataResponse = ClientDataBuildResponse(
+ type = ClientDataBuildResponse.Type.CREATE,
+ challenge = creationOptions.challenge,
+ origin = origin
+ )
+ )
+ )
+ }
+ )
+ }
+
+ /**
+ * Build the passkey public key credential response,
+ * by calling this method the user is always recognized as present and verified
+ */
+ fun buildCreatePublicKeyCredentialResponse(
+ publicKeyCredentialCreationParameters: PublicKeyCredentialCreationParameters,
+ backupEligibility: Boolean,
+ backupState: Boolean
+ ): CreatePublicKeyCredentialResponse {
+
+ val keyPair = publicKeyCredentialCreationParameters.signatureKey.first
+ val keyTypeId = publicKeyCredentialCreationParameters.signatureKey.second
+ val responseJson = FidoPublicKeyCredential(
+ id = b64Encode(publicKeyCredentialCreationParameters.credentialId),
+ response = AuthenticatorAttestationResponse(
+ requestOptions = publicKeyCredentialCreationParameters.publicKeyCredentialCreationOptions,
+ credentialId = publicKeyCredentialCreationParameters.credentialId,
+ credentialPublicKey = Cbor().encode(
+ Signature.convertPublicKeyToMap(
+ publicKeyIn = keyPair.public,
+ keyTypeId = keyTypeId
+ ) ?: mapOf()),
+ userPresent = true,
+ userVerified = true,
+ backupEligibility = backupEligibility,
+ backupState = backupState,
+ publicKeyTypeId = keyTypeId,
+ publicKeyCbor = Signature.convertPublicKey(keyPair.public, keyTypeId)!!,
+ clientDataResponse = publicKeyCredentialCreationParameters.clientDataResponse
+ ),
+ authenticatorAttachment = "platform"
+ ).json()
+ // log only the length to prevent logging sensitive information
+ Log.d(javaClass.simpleName, "Json response for key creation")
+ return CreatePublicKeyCredentialResponse(responseJson)
+ }
+
+ /**
+ * Utility method to use a passkey and create the associated usage request parameters
+ * [intent] allows to retrieve the request
+ * [context] context to manage package verification files
+ * [result] is called asynchronously after the creation of PublicKeyCredentialUsageParameters, the origin associated with it may or may not be verified
+ */
+ suspend fun retrievePasskeyUsageRequestParameters(
+ intent: Intent,
+ context: Context,
+ result: suspend (PublicKeyCredentialUsageParameters) -> Unit
+ ) {
+ val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
+ if (getCredentialRequest == null)
+ throw CreateCredentialUnknownException("could not retrieve request from intent")
+ val callingAppInfo = getCredentialRequest.callingAppInfo
+ val credentialOption = getCredentialRequest.retrievePasskeyUsageComponent()
+ val clientDataHash = credentialOption.clientDataHash
+
+ val requestOptions = PublicKeyCredentialRequestOptions(credentialOption.requestJson)
+
+ getOrigin(
+ providedClientDataHash = clientDataHash,
+ callingAppInfo = callingAppInfo,
+ context = context,
+ onOriginRetrieved = { appOrigin, clientDataHash ->
+ result.invoke(
+ PublicKeyCredentialUsageParameters(
+ publicKeyCredentialRequestOptions = requestOptions,
+ clientDataResponse = ClientDataDefinedResponse(clientDataHash),
+ appOrigin = appOrigin
+ )
+ )
+ },
+ onOriginNotRetrieved = { appOrigin, androidOriginString ->
+ // By default we crate an usage parameter with Android origin
+ result.invoke(
+ PublicKeyCredentialUsageParameters(
+ publicKeyCredentialRequestOptions = requestOptions,
+ clientDataResponse = ClientDataBuildResponse(
+ type = ClientDataBuildResponse.Type.GET,
+ challenge = requestOptions.challenge,
+ origin = androidOriginString
+ ),
+ appOrigin = appOrigin
+ )
+ )
+ }
+ )
+ }
+
+ /**
+ * Build the passkey public key credential response,
+ * by calling this method the user is always recognized as present and verified
+ */
+ fun buildPasskeyPublicKeyCredential(
+ requestOptions: PublicKeyCredentialRequestOptions,
+ clientDataResponse: ClientDataResponse,
+ passkey: Passkey,
+ defaultBackupEligibility: Boolean,
+ defaultBackupState: Boolean
+ ): PublicKeyCredential {
+ val getCredentialResponse = FidoPublicKeyCredential(
+ id = passkey.credentialId,
+ response = AuthenticatorAssertionResponse(
+ requestOptions = requestOptions,
+ userPresent = true,
+ userVerified = true,
+ backupEligibility = passkey.backupEligibility ?: defaultBackupEligibility,
+ backupState = passkey.backupState ?: defaultBackupState,
+ userHandle = passkey.userHandle,
+ privateKey = passkey.privateKeyPem,
+ clientDataResponse = clientDataResponse
+ ),
+ authenticatorAttachment = "platform"
+ ).json()
+ Log.d(javaClass.simpleName, "Json response for key usage")
+ return PublicKeyCredential(getCredentialResponse)
+ }
+
+
+ /**
+ * Verify that the application signature is contained in the [appOrigin]
+ */
+ fun getVerifiedGETClientDataResponse(
+ usageParameters: PublicKeyCredentialUsageParameters,
+ appOrigin: AppOrigin
+ ): ClientDataResponse {
+ val appToCheck = usageParameters.appOrigin
+ return if (appToCheck.verified) {
+ usageParameters.clientDataResponse
+ } else {
+ // Origin checked by Android app signature
+ ClientDataBuildResponse(
+ type = ClientDataBuildResponse.Type.GET,
+ challenge = usageParameters.publicKeyCredentialRequestOptions.challenge,
+ origin = appToCheck.checkAppOrigin(appOrigin)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PrivilegedAllowLists.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PrivilegedAllowLists.kt
new file mode 100644
index 000000000..df9b48b4a
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PrivilegedAllowLists.kt
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.util
+
+import android.content.Context
+import android.util.Log
+import androidx.credentials.provider.CallingAppInfo
+import com.kunzisoft.encrypt.Signature.getAllFingerprints
+import com.kunzisoft.keepass.BuildConfig
+import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
+import org.json.JSONObject
+import java.io.File
+import java.io.FileNotFoundException
+import java.io.InputStream
+
+object PrivilegedAllowLists {
+
+ private const val FILE_NAME_PRIVILEGED_APPS_CUSTOM = "passkeys_privileged_apps_custom.json"
+ private const val FILE_NAME_PRIVILEGED_APPS_COMMUNITY = "passkeys_privileged_apps_community.json"
+ private const val FILE_NAME_PRIVILEGED_APPS_GOOGLE = "passkeys_privileged_apps_google.json"
+
+ private fun retrieveContentFromStream(
+ inputStream: InputStream,
+ ): String {
+ return inputStream.use { fileInputStream ->
+ fileInputStream.bufferedReader(Charsets.UTF_8).readText()
+ }
+ }
+
+ /**
+ * Get the origin from a predefined privileged allow list
+ *
+ * @param callingAppInfo CallingAppInfo to verify and retrieve the specific Origin
+ * @param inputStream File input stream containing the origin list as JSON
+ */
+ private fun getOriginFromPrivilegedAllowListStream(
+ callingAppInfo: CallingAppInfo,
+ inputStream: InputStream
+ ): String? {
+ val privilegedAllowList = retrieveContentFromStream(inputStream)
+ return callingAppInfo.getOrigin(privilegedAllowList)?.removeSuffix("/")
+ }
+
+ /**
+ * Get the origin from the predefined privileged allow lists
+ *
+ * @param callingAppInfo CallingAppInfo to verify and retrieve the specific Origin
+ * @param context Context for file operations.
+ */
+ fun getOriginFromPrivilegedAllowLists(
+ callingAppInfo: CallingAppInfo,
+ context: Context
+ ): String? {
+ return try {
+ // Check the custom apps first
+ getOriginFromPrivilegedAllowListStream(
+ callingAppInfo = callingAppInfo,
+ File(context.filesDir, FILE_NAME_PRIVILEGED_APPS_CUSTOM)
+ .inputStream()
+ )
+ } catch (e: Exception) {
+ // Then the Google list if allowed
+ if (BuildConfig.CLOSED_STORE) {
+ try {
+ // Check the Google list if allowed
+ // http://www.gstatic.com/gpm-passkeys-privileged-apps/apps.json
+ getOriginFromPrivilegedAllowListStream(
+ callingAppInfo = callingAppInfo,
+ inputStream = context.assets.open(FILE_NAME_PRIVILEGED_APPS_GOOGLE)
+ )
+ } catch (_: Exception) {
+ // Then the community apps list
+ getOriginFromPrivilegedAllowListStream(
+ callingAppInfo = callingAppInfo,
+ inputStream = context.assets.open(FILE_NAME_PRIVILEGED_APPS_COMMUNITY)
+ )
+ }
+ } else {
+ when (e) {
+ is FileNotFoundException -> {
+ val attemptApp = AndroidPrivilegedApp(
+ packageName = callingAppInfo.packageName,
+ fingerprints = callingAppInfo.signingInfo
+ .getAllFingerprints() ?: emptySet()
+ )
+ throw PrivilegedException(
+ temptingApp = attemptApp,
+ message = "$attemptApp is not in the allow list"
+ )
+ }
+ else -> throw e
+ }
+ }
+ }
+ }
+
+ /**
+ * Retrieves a list of predefined AndroidPrivilegedApp objects from an asset JSON file.
+ *
+ * @param inputStream File input stream containing the origin list as JSON
+ */
+ private fun retrievePrivilegedApps(
+ inputStream: InputStream
+ ): List {
+ val jsonObject = JSONObject(retrieveContentFromStream(inputStream))
+ return AndroidPrivilegedApp.extractPrivilegedApps(jsonObject)
+ }
+
+ /**
+ * Retrieves a list of predefined AndroidPrivilegedApp objects from a context
+ *
+ * @param context Context for file operations.
+ */
+ fun retrievePredefinedPrivilegedApps(
+ context: Context
+ ): List {
+ return try {
+ val predefinedApps = mutableListOf()
+ predefinedApps.addAll(retrievePrivilegedApps(context.assets.open(FILE_NAME_PRIVILEGED_APPS_COMMUNITY)))
+ if (BuildConfig.CLOSED_STORE) {
+ predefinedApps.addAll(retrievePrivilegedApps(context.assets.open(FILE_NAME_PRIVILEGED_APPS_GOOGLE)))
+ }
+ predefinedApps
+ } catch (e: Exception) {
+ Log.e(PrivilegedAllowLists::class.simpleName, "Error retrieving privileged apps", e)
+ emptyList()
+ }
+ }
+
+ /**
+ * Retrieves a list of AndroidPrivilegedApp objects from the custom JSON file.
+ *
+ * @param context Context for file operations.
+ */
+ fun retrieveCustomPrivilegedApps(
+ context: Context
+ ): List {
+ return try {
+ retrievePrivilegedApps(File(context.filesDir, FILE_NAME_PRIVILEGED_APPS_CUSTOM).inputStream())
+ } catch (e: Exception) {
+ Log.i(PrivilegedAllowLists::class.simpleName, "No custom privileged apps", e)
+ emptyList()
+ }
+ }
+
+ /**
+ * Retrieves a list of all predefined and custom AndroidPrivilegedApp objects.
+ */
+ fun retrieveAllPrivilegedApps(
+ context: Context
+ ): List {
+ return retrievePredefinedPrivilegedApps(context) + retrieveCustomPrivilegedApps(context)
+ }
+
+ /**
+ * Saves a list of custom AndroidPrivilegedApp objects to a JSON file.
+ *
+ * @param context Context for file operations.
+ * @param privilegedApps The list of apps to save.
+ * @return True if saving was successful, false otherwise.
+ */
+ fun saveCustomPrivilegedApps(context: Context, privilegedApps: List): Boolean {
+ return try {
+ val jsonToSave = AndroidPrivilegedApp.toJsonObject(privilegedApps)
+ val file = File(context.filesDir, FILE_NAME_PRIVILEGED_APPS_CUSTOM)
+
+ // Delete existing file before writing to ensure atomicity if needed
+ if (file.exists()) {
+ file.delete()
+ }
+
+ file.outputStream().use { fileOutputStream ->
+ fileOutputStream.write(
+ jsonToSave
+ .toString(4) // toString(4) for pretty print
+ .toByteArray(Charsets.UTF_8)
+ )
+ }
+ true
+ } catch (e: Exception) {
+ Log.e(PrivilegedAllowLists::class.simpleName, "Error saving privileged apps", e)
+ false
+ }
+ }
+
+ /**
+ * Deletes the custom JSON file.
+ *
+ * @param context Context for file operations.
+ * @return True if deletion was successful or file didn't exist, false otherwise.
+ */
+ fun deletePrivilegedAppsFile(context: Context): Boolean {
+ return try {
+ val file = File(context.filesDir, FILE_NAME_PRIVILEGED_APPS_CUSTOM)
+ if (file.exists()) {
+ file.delete()
+ } else {
+ true // File didn't exist, so considered "successfully deleted"
+ }
+ } catch (e: SecurityException) {
+ Log.e(PrivilegedAllowLists::class.simpleName, "Error deleting privileged apps file", e)
+ false
+ }
+ }
+
+ class PrivilegedException(
+ val temptingApp: AndroidPrivilegedApp,
+ message: String
+ ) : Exception(message)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/AutofillLauncherViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/AutofillLauncherViewModel.kt
new file mode 100644
index 000000000..b16a552fa
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/AutofillLauncherViewModel.kt
@@ -0,0 +1,284 @@
+package com.kunzisoft.keepass.credentialprovider.viewmodel
+
+import android.app.Activity.RESULT_CANCELED
+import android.app.Activity.RESULT_OK
+import android.app.Application
+import android.content.Intent
+import android.os.Build
+import android.util.Log
+import androidx.activity.result.ActivityResult
+import androidx.annotation.RequiresApi
+import androidx.lifecycle.viewModelScope
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeNodesIds
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNodesIds
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
+import com.kunzisoft.keepass.credentialprovider.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.TypeMode
+import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
+import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
+import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.retrieveAutofillComponent
+import com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService
+import com.kunzisoft.keepass.database.ContextualDatabase
+import com.kunzisoft.keepass.database.element.node.NodeIdUUID
+import com.kunzisoft.keepass.database.helper.SearchHelper
+import com.kunzisoft.keepass.model.RegisterInfo
+import com.kunzisoft.keepass.model.SearchInfo
+import com.kunzisoft.keepass.settings.PreferencesUtil
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.IOException
+
+@RequiresApi(api = Build.VERSION_CODES.O)
+class AutofillLauncherViewModel(application: Application): CredentialLauncherViewModel(application) {
+
+ private var mAutofillComponent: AutofillComponent? = null
+
+ private var mLockDatabaseAfterSelection: Boolean = false
+
+ private val mUiState = MutableStateFlow(UIState.Loading)
+ val uiState: StateFlow = mUiState
+
+ fun initialize() {
+ mLockDatabaseAfterSelection = PreferencesUtil.isAutofillCloseDatabaseEnable(getApplication())
+ }
+
+ override fun onResult() {
+ super.onResult()
+ mAutofillComponent = null
+ }
+
+ override suspend fun launchAction(
+ intent: Intent,
+ specialMode: SpecialMode,
+ database: ContextualDatabase?
+ ) {
+ // Retrieve selection mode
+ when (intent.retrieveSpecialMode()) {
+ SpecialMode.SELECTION -> {
+ val searchInfo = intent.retrieveSearchInfo()
+ if (searchInfo == null)
+ throw IOException("Search info is null")
+ mAutofillComponent = intent.retrieveAutofillComponent()
+ // Build search param
+ launchSelection(database, mAutofillComponent, searchInfo)
+ }
+ SpecialMode.REGISTRATION -> {
+ // To register info
+ val registerInfo = intent.retrieveRegisterInfo()
+ if (registerInfo == null)
+ throw IOException("Register info is null")
+ launchRegistration(database, registerInfo)
+ }
+ else -> {
+ // Not an autofill call
+ cancelResult()
+ }
+ }
+ }
+
+ private suspend fun launchSelection(
+ database: ContextualDatabase?,
+ autofillComponent: AutofillComponent?,
+ searchInfo: SearchInfo
+ ) {
+ withContext(Dispatchers.IO) {
+ if (autofillComponent == null) {
+ throw IOException("Autofill component is null")
+ }
+ if (KeeAutofillService.autofillAllowedFor(
+ applicationId = searchInfo.applicationId,
+ webDomain = searchInfo.webDomain,
+ context = getApplication()
+ )
+ ) {
+ // If database is open
+ SearchHelper.checkAutoSearchInfo(
+ context = getApplication(),
+ database = database,
+ searchInfo = searchInfo,
+ onItemsFound = { openedDatabase, items ->
+ // Items found
+ if (autofillComponent.compatInlineSuggestionsRequest != null) {
+ mUiState.value = UIState.ShowAutofillSuggestionMessage
+ }
+ AutofillHelper.buildResponse(
+ context = getApplication(),
+ autofillComponent = autofillComponent,
+ database = openedDatabase,
+ entriesInfo = items
+ ) { intent ->
+ setResult(intent, lockDatabase = mLockDatabaseAfterSelection)
+ }
+ },
+ onItemNotFound = { openedDatabase ->
+ // Show the database UI to select the entry
+ mCredentialUiState.value =
+ CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection(
+ database = openedDatabase,
+ searchInfo = searchInfo,
+ typeMode = TypeMode.AUTOFILL
+ )
+ },
+ onDatabaseClosed = {
+ // If database not open
+ mCredentialUiState.value =
+ CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection(
+ searchInfo = searchInfo,
+ typeMode = TypeMode.AUTOFILL
+ )
+ }
+ )
+ } else {
+ mUiState.value = UIState.ShowBlockRestartMessage
+ }
+ }
+ }
+
+ override fun manageSelectionResult(
+ database: ContextualDatabase,
+ activityResult: ActivityResult
+ ) {
+ super.manageSelectionResult(database, activityResult)
+ val intent = activityResult.data
+ viewModelScope.launch(CoroutineExceptionHandler { _, e ->
+ Log.e(TAG, "Unable to create selection response for autofill", e)
+ showError(e)
+ }) {
+ when (activityResult.resultCode) {
+ RESULT_OK -> {
+ withContext(Dispatchers.IO) {
+ Log.d(TAG, "Autofill selection result")
+ if (intent == null)
+ throw IOException("Intent is null")
+ val nodesIds = intent.retrieveNodesIds()
+ ?: throw IOException("NodesIds is null")
+ intent.removeNodesIds()
+ val autofillComponent = mAutofillComponent
+ if (autofillComponent == null)
+ throw IOException("Autofill component is null")
+ val entries = nodesIds.mapNotNull { nodeId ->
+ database
+ .getEntryById(NodeIdUUID(nodeId))
+ ?.getEntryInfo(database)
+ }
+ withContext(Dispatchers.Main) {
+ AutofillHelper.buildResponse(
+ context = getApplication(),
+ autofillComponent = autofillComponent,
+ database = database,
+ entriesInfo = entries
+ ) { intent ->
+ setResult(intent, lockDatabase = mLockDatabaseAfterSelection)
+ }
+ }
+ }
+ }
+ RESULT_CANCELED -> {
+ withContext(Dispatchers.Main) {
+ cancelResult()
+ }
+ }
+ }
+ }
+ }
+
+ // -------------
+ // Registration
+ // -------------
+
+ private fun launchRegistration(
+ database: ContextualDatabase?,
+ registerInfo: RegisterInfo
+ ) {
+ val searchInfo = registerInfo.searchInfo
+ if (KeeAutofillService.autofillAllowedFor(
+ applicationId = searchInfo.applicationId,
+ webDomain = searchInfo.webDomain,
+ context = getApplication()
+ )) {
+ val readOnly = database?.isReadOnly != false
+ SearchHelper.checkAutoSearchInfo(
+ context = getApplication(),
+ database = database,
+ searchInfo = searchInfo,
+ onItemsFound = { openedDatabase, _ ->
+ if (!readOnly) {
+ // Show the database UI to select the entry
+ mCredentialUiState.value =
+ CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
+ database = openedDatabase,
+ registerInfo = registerInfo,
+ typeMode = TypeMode.AUTOFILL
+ )
+ } else {
+ mUiState.value = UIState.ShowReadOnlyMessage
+ }
+ },
+ onItemNotFound = { openedDatabase ->
+ if (!readOnly) {
+ // Show the database UI to select the entry
+ mCredentialUiState.value =
+ CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
+ database = openedDatabase,
+ registerInfo = registerInfo,
+ typeMode = TypeMode.AUTOFILL
+ )
+ } else {
+ mUiState.value = UIState.ShowReadOnlyMessage
+ }
+ },
+ onDatabaseClosed = {
+ // If database not open
+ mCredentialUiState.value =
+ CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration(
+ registerInfo = registerInfo,
+ typeMode = TypeMode.AUTOFILL
+ )
+ }
+ )
+ } else {
+ mUiState.value = UIState.ShowBlockRestartMessage
+ }
+ }
+
+ override fun manageRegistrationResult(activityResult: ActivityResult) {
+ isResultLauncherRegistered = false
+ viewModelScope.launch(CoroutineExceptionHandler { _, e ->
+ Log.e(TAG, "Unable to create registration response for autofill", e)
+ showError(e)
+ }) {
+ val responseIntent = Intent()
+ when (activityResult.resultCode) {
+ RESULT_OK -> {
+ Log.d(TAG, "Autofill registration result")
+ withContext(Dispatchers.Main) {
+ setResult(responseIntent)
+ }
+ }
+ RESULT_CANCELED -> {
+ withContext(Dispatchers.Main) {
+ cancelResult()
+ }
+ }
+ }
+ }
+
+ }
+
+ sealed class UIState {
+ object Loading: UIState()
+ object ShowBlockRestartMessage: UIState()
+ object ShowReadOnlyMessage: UIState()
+ object ShowAutofillSuggestionMessage: UIState()
+ }
+
+ companion object {
+ private val TAG = AutofillLauncherViewModel::class.java.name
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/CredentialLauncherViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/CredentialLauncherViewModel.kt
new file mode 100644
index 000000000..7eb876f2a
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/CredentialLauncherViewModel.kt
@@ -0,0 +1,151 @@
+package com.kunzisoft.keepass.credentialprovider.viewmodel
+
+import android.app.Activity.RESULT_CANCELED
+import android.app.Activity.RESULT_OK
+import android.app.Application
+import android.content.Intent
+import android.util.Log
+import androidx.activity.result.ActivityResult
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import com.kunzisoft.keepass.credentialprovider.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.TypeMode
+import com.kunzisoft.keepass.database.ContextualDatabase
+import com.kunzisoft.keepass.model.RegisterInfo
+import com.kunzisoft.keepass.model.SearchInfo
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+abstract class CredentialLauncherViewModel(application: Application): AndroidViewModel(application) {
+
+ protected var mDatabase: ContextualDatabase? = null
+
+ protected var isResultLauncherRegistered: Boolean = false
+ private var mSelectionResult: ActivityResult? = null
+
+ protected val mCredentialUiState = MutableStateFlow(UIState.Loading)
+ val credentialUiState: StateFlow = mCredentialUiState
+
+ fun showError(error: Throwable) {
+ Log.e(TAG, "Error on credential provider launch", error)
+ mCredentialUiState.value = UIState.ShowError(error)
+ }
+
+ open fun onResult() {
+ isResultLauncherRegistered = false
+ mSelectionResult = null
+ }
+
+ fun setResult(intent: Intent, lockDatabase: Boolean = false) {
+ // Remove the launcher register
+ onResult()
+ mCredentialUiState.value = UIState.SetActivityResult(
+ lockDatabase = lockDatabase,
+ resultCode = RESULT_OK,
+ data = intent
+ )
+ }
+
+ fun cancelResult(lockDatabase: Boolean = false) {
+ onResult()
+ mCredentialUiState.value = UIState.SetActivityResult(
+ lockDatabase = lockDatabase,
+ resultCode = RESULT_CANCELED
+ )
+ }
+
+ private fun onDatabaseRetrieved(database: ContextualDatabase) {
+ mDatabase = database
+ mSelectionResult?.let { selectionResult ->
+ manageSelectionResult(database, selectionResult)
+ }
+ }
+
+ fun manageSelectionResult(activityResult: ActivityResult) {
+ // Waiting for the database if needed
+ when (activityResult.resultCode) {
+ RESULT_OK -> {
+ mSelectionResult = activityResult
+ mDatabase?.let { database ->
+ manageSelectionResult(database, activityResult)
+ }
+ }
+ RESULT_CANCELED -> {
+ cancelResult()
+ }
+ }
+ }
+
+ open fun manageSelectionResult(database: ContextualDatabase, activityResult: ActivityResult) {
+ mSelectionResult = null
+ }
+
+ open fun manageRegistrationResult(activityResult: ActivityResult) {}
+
+ open fun onExceptionOccurred(e: Throwable) {
+ showError(e)
+ }
+
+ open fun launchActionIfNeeded(
+ intent: Intent,
+ specialMode: SpecialMode,
+ database: ContextualDatabase?
+ ) {
+ if (database != null) {
+ onDatabaseRetrieved(database)
+ }
+ if (isResultLauncherRegistered.not()) {
+ isResultLauncherRegistered = true
+ viewModelScope.launch(CoroutineExceptionHandler { _, e ->
+ onExceptionOccurred(e)
+ }) {
+ launchAction(intent, specialMode, database)
+ }
+ }
+ }
+
+ /**
+ * Launch the main action
+ */
+ protected abstract suspend fun launchAction(
+ intent: Intent,
+ specialMode: SpecialMode,
+ database: ContextualDatabase?
+ )
+
+ sealed class UIState {
+ object Loading : UIState()
+ data class LaunchGroupActivityForSelection(
+ val database: ContextualDatabase,
+ val searchInfo: SearchInfo?,
+ val typeMode: TypeMode
+ ): UIState()
+ data class LaunchGroupActivityForRegistration(
+ val database: ContextualDatabase,
+ val registerInfo: RegisterInfo?,
+ val typeMode: TypeMode
+ ): UIState()
+ data class LaunchFileDatabaseSelectActivityForSelection(
+ val searchInfo: SearchInfo?,
+ val typeMode: TypeMode
+ ): UIState()
+ data class LaunchFileDatabaseSelectActivityForRegistration(
+ val registerInfo: RegisterInfo?,
+ val typeMode: TypeMode
+ ): UIState()
+ data class SetActivityResult(
+ val lockDatabase: Boolean,
+ val resultCode: Int,
+ val data: Intent? = null
+ ): UIState()
+ data class ShowError(
+ val error: Throwable
+ ): UIState()
+ }
+
+ companion object {
+ private val TAG = CredentialLauncherViewModel::class.java.name
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/HardwareKeyLauncherViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/HardwareKeyLauncherViewModel.kt
new file mode 100644
index 000000000..2ae03f854
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/HardwareKeyLauncherViewModel.kt
@@ -0,0 +1,147 @@
+package com.kunzisoft.keepass.credentialprovider.viewmodel
+
+import android.app.Activity.RESULT_OK
+import android.app.Application
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import androidx.activity.result.ActivityResult
+import com.kunzisoft.keepass.credentialprovider.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity.Companion.isHardwareKeyAvailable
+import com.kunzisoft.keepass.database.ContextualDatabase
+import com.kunzisoft.keepass.hardware.HardwareKey
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class HardwareKeyLauncherViewModel(application: Application): CredentialLauncherViewModel(application) {
+
+ private val mUiState = MutableStateFlow(UIState.Loading)
+ val uiState: StateFlow = mUiState
+
+ override suspend fun launchAction(
+ intent: Intent,
+ specialMode: SpecialMode,
+ database: ContextualDatabase?
+ ) {
+ val hardwareKey = HardwareKey.Companion.getHardwareKeyFromString(
+ intent.getStringExtra(DATA_HARDWARE_KEY)
+ )
+ if (isHardwareKeyAvailable(getApplication(), hardwareKey)) {
+ when (hardwareKey) {
+ /*
+ HardwareKey.FIDO2_SECRET -> {
+ // TODO FIDO2 under development
+ throw Exception("FIDO2 not implemented")
+ }
+ */
+ HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
+ launchYubikeyChallengeForResponse(intent.getByteArrayExtra(DATA_SEED))
+ }
+ else -> {
+ UIState.OnChallengeResponded(null)
+ }
+ }
+ } else {
+ mUiState.value = UIState.ShowHardwareKeyDriverNeeded(hardwareKey)
+ }
+ }
+
+ private fun launchYubikeyChallengeForResponse(seed: ByteArray?) {
+ // Transform the seed before sending
+ var challenge: ByteArray? = null
+ if (seed != null) {
+ challenge = ByteArray(64)
+ seed.copyInto(challenge, 0, 0, 32)
+ challenge.fill(32, 32, 64)
+ }
+ mUiState.value = UIState.LaunchChallengeActivityForResponse(challenge)
+ Log.d(TAG, "Challenge sent")
+ }
+
+ override fun manageSelectionResult(
+ database: ContextualDatabase,
+ activityResult: ActivityResult
+ ) {
+ super.manageSelectionResult(database, activityResult)
+
+ if (activityResult.resultCode == RESULT_OK) {
+ val challengeResponse: ByteArray? =
+ activityResult.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
+ Log.d(TAG, "Response form challenge")
+ mUiState.value = UIState.OnChallengeResponded(challengeResponse)
+ } else {
+ Log.e(TAG, "Response from challenge error")
+ mUiState.value = UIState.OnChallengeResponded(null)
+ }
+ }
+
+ sealed class UIState {
+ object Loading : UIState()
+ data class ShowHardwareKeyDriverNeeded(
+ val hardwareKey: HardwareKey?
+ ): UIState()
+ data class LaunchChallengeActivityForResponse(
+ val challenge: ByteArray?,
+ ): UIState() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as LaunchChallengeActivityForResponse
+
+ return challenge.contentEquals(other.challenge)
+ }
+
+ override fun hashCode(): Int {
+ return challenge?.contentHashCode() ?: 0
+ }
+ }
+ data class OnChallengeResponded(
+ val response: ByteArray?
+ ): UIState() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as OnChallengeResponded
+
+ return response.contentEquals(other.response)
+ }
+
+ override fun hashCode(): Int {
+ return response?.contentHashCode() ?: 0
+ }
+ }
+ }
+
+ companion object {
+ private val TAG = HardwareKeyLauncherViewModel::class.java.name
+
+ private const val DATA_HARDWARE_KEY = "DATA_HARDWARE_KEY"
+ private const val DATA_SEED = "DATA_SEED"
+
+ // Driver call
+ private const val YUBIKEY_CHALLENGE_RESPONSE_INTENT = "android.yubikey.intent.action.CHALLENGE_RESPONSE"
+ private const val HARDWARE_KEY_CHALLENGE_KEY = "challenge"
+ private const val HARDWARE_KEY_RESPONSE_KEY = "response"
+
+ fun isYubikeyDriverAvailable(context: Context): Boolean {
+ return Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT)
+ .resolveActivity(context.packageManager) != null
+ }
+
+ fun buildHardwareKeyChallenge(challenge: ByteArray?): Intent {
+ return Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
+ putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
+ }
+ }
+
+ fun Intent.addHardwareKey(hardwareKey: HardwareKey) {
+ putExtra(DATA_HARDWARE_KEY, hardwareKey.value)
+ }
+
+ fun Intent.addSeed(seed: ByteArray?) {
+ putExtra(DATA_SEED, seed)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt
new file mode 100644
index 000000000..138cef430
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt
@@ -0,0 +1,550 @@
+package com.kunzisoft.keepass.credentialprovider.viewmodel
+
+import android.app.Activity.RESULT_CANCELED
+import android.app.Activity.RESULT_OK
+import android.app.Application
+import android.content.Intent
+import android.os.Build
+import android.util.Log
+import androidx.activity.result.ActivityResult
+import androidx.annotation.RequiresApi
+import androidx.credentials.GetCredentialResponse
+import androidx.credentials.exceptions.GetCredentialUnknownException
+import androidx.credentials.provider.PendingIntentHandler
+import androidx.lifecycle.viewModelScope
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeNodeId
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNodeId
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
+import com.kunzisoft.keepass.credentialprovider.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.TypeMode
+import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
+import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
+import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildCreatePublicKeyCredentialResponse
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyPublicKeyCredential
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.checkSecurity
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVerifiedGETClientDataResponse
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps
+import com.kunzisoft.keepass.database.ContextualDatabase
+import com.kunzisoft.keepass.database.element.Entry
+import com.kunzisoft.keepass.database.element.node.NodeIdUUID
+import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
+import com.kunzisoft.keepass.database.helper.SearchHelper
+import com.kunzisoft.keepass.model.AppOrigin
+import com.kunzisoft.keepass.model.Passkey
+import com.kunzisoft.keepass.model.RegisterInfo
+import com.kunzisoft.keepass.model.SearchInfo
+import com.kunzisoft.keepass.model.SignatureNotFoundException
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getNewEntry
+import com.kunzisoft.keepass.settings.PreferencesUtil
+import com.kunzisoft.keepass.tasks.ActionRunnable
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.IOException
+import java.io.InvalidObjectException
+import java.util.UUID
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class PasskeyLauncherViewModel(application: Application): CredentialLauncherViewModel(application) {
+
+ private var mUsageParameters: PublicKeyCredentialUsageParameters? = null
+ private var mCreationParameters: PublicKeyCredentialCreationParameters? = null
+ private var mPasskey: Passkey? = null
+
+ private var mLockDatabaseAfterSelection: Boolean = false
+ private var mBackupEligibility: Boolean = true
+ private var mBackupState: Boolean = false
+
+ private val mUiState = MutableStateFlow(UIState.Loading)
+ val uiState: StateFlow = mUiState
+
+ fun initialize() {
+ mLockDatabaseAfterSelection = PreferencesUtil.isPasskeyCloseDatabaseEnable(getApplication())
+ mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication())
+ mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication())
+ }
+
+ fun showAppPrivilegedDialog(
+ temptingApp: AndroidPrivilegedApp
+ ) {
+ mUiState.value = UIState.ShowAppPrivilegedDialog(temptingApp)
+ }
+
+ fun showAppSignatureDialog(
+ temptingApp: AppOrigin,
+ nodeId: UUID
+ ) {
+ mUiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId)
+ }
+
+ fun saveCustomPrivilegedApp(
+ intent: Intent,
+ specialMode: SpecialMode,
+ database: ContextualDatabase?,
+ temptingApp: AndroidPrivilegedApp
+ ) {
+ viewModelScope.launch(CoroutineExceptionHandler { _, e ->
+ showError(e)
+ }) {
+ saveCustomPrivilegedApps(
+ context = getApplication(),
+ privilegedApps = listOf(temptingApp)
+ )
+ launchAction(
+ intent = intent,
+ specialMode = specialMode,
+ database = database
+ )
+ }
+ }
+
+ fun saveAppSignature(
+ database: ContextualDatabase?,
+ temptingApp: AppOrigin,
+ nodeId: UUID
+ ) {
+ viewModelScope.launch(CoroutineExceptionHandler { _, e ->
+ showError(e)
+ }) {
+ // Update the entry with app signature
+ val entry = database
+ ?.getEntryById(NodeIdUUID(nodeId))
+ ?: throw GetCredentialUnknownException(
+ "No passkey with nodeId $nodeId found"
+ )
+ if (database.isReadOnly)
+ throw RegisterInReadOnlyDatabaseException()
+ val newEntry = Entry(entry)
+ val entryInfo = newEntry.getEntryInfo(
+ database,
+ raw = true,
+ removeTemplateConfiguration = false
+ )
+ entryInfo.saveAppOrigin(database, temptingApp)
+ newEntry.setEntryInfo(database, entryInfo)
+ mUiState.value = UIState.UpdateEntry(
+ oldEntry = entry,
+ newEntry = newEntry
+ )
+ }
+ }
+
+ override fun onExceptionOccurred(e: Throwable) {
+ if (e is PrivilegedAllowLists.PrivilegedException) {
+ showAppPrivilegedDialog(e.temptingApp)
+ } else {
+ super.onExceptionOccurred(e)
+ }
+ }
+
+ override fun launchActionIfNeeded(
+ intent: Intent,
+ specialMode: SpecialMode,
+ database: ContextualDatabase?
+ ) {
+ // Launch with database when a nodeId is present
+ if ((database != null && database.loaded) || intent.retrieveNodeId() == null) {
+ super.launchActionIfNeeded(intent, specialMode, database)
+ }
+ }
+
+ override suspend fun launchAction(
+ intent: Intent,
+ specialMode: SpecialMode,
+ database: ContextualDatabase?
+ ) {
+ val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
+ val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
+ val nodeId = intent.retrieveNodeId()
+ intent.removeInfo()
+ intent.removeAppOrigin()
+ intent.removeNodeId()
+ checkSecurity(intent, nodeId)
+ when (specialMode) {
+ SpecialMode.SELECTION -> {
+ launchSelection(
+ intent = intent,
+ database = database,
+ nodeId = nodeId,
+ searchInfo = searchInfo,
+ appOrigin = appOrigin
+ )
+ }
+ SpecialMode.REGISTRATION -> {
+ // TODO Registration in predefined group
+ // launchRegistration(database, nodeId, mSearchInfo)
+ launchRegistration(
+ intent = intent,
+ database = database,
+ nodeId = null,
+ searchInfo = searchInfo
+ )
+ }
+ else -> {
+ throw InvalidObjectException("Passkey launch mode not supported")
+ }
+ }
+ }
+
+ // -------------
+ // Selection
+ // -------------
+
+ private suspend fun launchSelection(
+ intent: Intent,
+ database: ContextualDatabase?,
+ nodeId: UUID?,
+ searchInfo: SearchInfo,
+ appOrigin: AppOrigin
+ ) {
+ withContext(Dispatchers.IO) {
+ Log.d(TAG, "Launch passkey selection")
+ retrievePasskeyUsageRequestParameters(
+ intent = intent,
+ context = getApplication()
+ ) { usageParameters ->
+ // Save the requested parameters
+ mUsageParameters = usageParameters
+ // Manage the passkey to use
+ nodeId?.let { nodeId ->
+ autoSelectPasskeyAndSetResult(database, nodeId, appOrigin)
+ } ?: run {
+ SearchHelper.checkAutoSearchInfo(
+ context = getApplication(),
+ database = database,
+ searchInfo = searchInfo,
+ onItemsFound = { _, _ ->
+ Log.w(
+ TAG, "Passkey found for auto selection, should not append," +
+ "use PasskeyProviderService instead"
+ )
+ cancelResult()
+ },
+ onItemNotFound = { openedDatabase ->
+ Log.d(
+ TAG, "No Passkey found for selection," +
+ "launch manual selection in opened database"
+ )
+ mCredentialUiState.value =
+ CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection(
+ database = openedDatabase,
+ searchInfo = searchInfo,
+ typeMode = TypeMode.PASSKEY
+ )
+ },
+ onDatabaseClosed = {
+ Log.d(TAG, "Manual passkey selection in closed database")
+ mCredentialUiState.value =
+ CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection(
+ searchInfo = searchInfo,
+ typeMode = TypeMode.PASSKEY
+ )
+ }
+ )
+ }
+ }
+ }
+ }
+
+ fun autoSelectPasskey(
+ result: ActionRunnable.Result,
+ database: ContextualDatabase
+ ) {
+ viewModelScope.launch(CoroutineExceptionHandler { _, e ->
+ showError(e)
+ }) {
+ if (result.isSuccess) {
+ val entry = result.data?.getNewEntry(database)
+ ?: throw IOException("No passkey entry found")
+ autoSelectPasskeyAndSetResult(
+ database = database,
+ nodeId = entry.nodeId.id,
+ appOrigin = entry.getAppOrigin()
+ ?: throw IOException("No App origin found")
+ )
+ } else throw result.exception
+ ?: IOException("Unable to auto select passkey")
+ }
+ }
+
+ private suspend fun autoSelectPasskeyAndSetResult(
+ database: ContextualDatabase?,
+ nodeId: UUID,
+ appOrigin: AppOrigin
+ ) {
+ withContext(Dispatchers.IO) {
+ mUsageParameters?.let { usageParameters ->
+ // To get the passkey from the database
+ val passkey = database
+ ?.getEntryById(NodeIdUUID(nodeId))
+ ?.getEntryInfo(database)
+ ?.passkey
+ ?: throw IOException(
+ "No passkey with nodeId $nodeId found"
+ )
+ // Build the response
+ val result = Intent()
+ try {
+ PendingIntentHandler.setGetCredentialResponse(
+ result,
+ GetCredentialResponse(
+ buildPasskeyPublicKeyCredential(
+ requestOptions = usageParameters.publicKeyCredentialRequestOptions,
+ clientDataResponse = getVerifiedGETClientDataResponse(
+ usageParameters = usageParameters,
+ appOrigin = appOrigin
+ ),
+ passkey = passkey,
+ defaultBackupEligibility = mBackupEligibility,
+ defaultBackupState = mBackupState
+ )
+ )
+ )
+ setResult(result, lockDatabase = mLockDatabaseAfterSelection)
+ } catch (e: SignatureNotFoundException) {
+ // Request the dialog if signature exception
+ showAppSignatureDialog(e.temptingApp, nodeId)
+ }
+ } ?: throw IOException("Usage parameters is null")
+ }
+ }
+
+ override fun manageSelectionResult(
+ database: ContextualDatabase,
+ activityResult: ActivityResult
+ ) {
+ super.manageSelectionResult(database, activityResult)
+ val intent = activityResult.data
+ viewModelScope.launch(CoroutineExceptionHandler { _, e ->
+ Log.e(TAG, "Unable to create selection response for passkey", e)
+ if (e is SignatureNotFoundException) {
+ intent?.retrieveNodeId()?.let { nodeId ->
+ showAppSignatureDialog(e.temptingApp, nodeId)
+ } ?: cancelResult()
+ } else {
+ showError(e)
+ }
+ }) {
+ // Build a new formatted response from the selection response
+ val responseIntent = Intent()
+ when (activityResult.resultCode) {
+ RESULT_OK -> {
+ withContext(Dispatchers.IO) {
+ Log.d(TAG, "Passkey selection result")
+ if (intent == null)
+ throw IOException("Intent is null")
+ val passkey = intent.retrievePasskey()
+ ?: throw IOException("Passkey is null")
+ val appOrigin = intent.retrieveAppOrigin()
+ ?: throw IOException("App origin is null")
+ intent.removePasskey()
+ intent.removeAppOrigin()
+ mUsageParameters?.let { usageParameters ->
+ // Check verified origin
+ PendingIntentHandler.setGetCredentialResponse(
+ responseIntent,
+ GetCredentialResponse(
+ buildPasskeyPublicKeyCredential(
+ requestOptions = usageParameters.publicKeyCredentialRequestOptions,
+ clientDataResponse = getVerifiedGETClientDataResponse(
+ usageParameters = usageParameters,
+ appOrigin = appOrigin
+ ),
+ passkey = passkey,
+ defaultBackupEligibility = mBackupEligibility,
+ defaultBackupState = mBackupState
+ )
+ )
+ )
+ } ?: run {
+ throw IOException("Usage parameters is null")
+ }
+ withContext(Dispatchers.Main) {
+ setResult(responseIntent, lockDatabase = mLockDatabaseAfterSelection)
+ }
+ }
+ }
+ RESULT_CANCELED -> {
+ withContext(Dispatchers.Main) {
+ cancelResult()
+ }
+ }
+ }
+ }
+ }
+
+ // -------------
+ // Registration
+ // -------------
+
+ private suspend fun launchRegistration(
+ intent: Intent,
+ database: ContextualDatabase?,
+ nodeId: UUID?,
+ searchInfo: SearchInfo
+ ) {
+ withContext(Dispatchers.IO) {
+ Log.d(TAG, "Launch passkey registration")
+ retrievePasskeyCreationRequestParameters(
+ intent = intent,
+ context = getApplication(),
+ defaultBackupEligibility = mBackupEligibility,
+ defaultBackupState = mBackupState,
+ passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters ->
+ // Save the requested parameters
+ mPasskey = passkey
+ mCreationParameters = publicKeyCredentialParameters
+ // Manage the passkey and create a register info
+ val registerInfo = RegisterInfo(
+ searchInfo = searchInfo,
+ passkey = passkey,
+ appOrigin = appInfoToStore
+ )
+ // If nodeId already provided
+ nodeId?.let { nodeId ->
+ autoRegisterPasskeyAndSetResult(database, nodeId, passkey)
+ } ?: run {
+ SearchHelper.checkAutoSearchInfo(
+ context = getApplication(),
+ database = database,
+ searchInfo = searchInfo,
+ onItemsFound = { openedDatabase, _ ->
+ Log.w(
+ TAG, "Passkey found for registration, " +
+ "but launch manual registration for a new entry"
+ )
+ mCredentialUiState.value =
+ CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
+ database = openedDatabase,
+ registerInfo = registerInfo,
+ typeMode = TypeMode.PASSKEY
+ )
+ },
+ onItemNotFound = { openedDatabase ->
+ Log.d(TAG, "Launch new manual registration in opened database")
+ mCredentialUiState.value =
+ CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
+ database = openedDatabase,
+ registerInfo = registerInfo,
+ typeMode = TypeMode.PASSKEY
+ )
+ },
+ onDatabaseClosed = {
+ Log.d(TAG, "Manual passkey registration in closed database")
+ mCredentialUiState.value =
+ CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration(
+ registerInfo = registerInfo,
+ typeMode = TypeMode.PASSKEY
+ )
+ }
+ )
+ }
+ }
+ )
+ }
+ }
+
+ private suspend fun autoRegisterPasskeyAndSetResult(
+ database: ContextualDatabase?,
+ nodeId: UUID,
+ passkey: Passkey
+ ) {
+ withContext(Dispatchers.IO) {
+ mCreationParameters?.let { creationParameters ->
+ // To set the passkey to the database
+ // TODO Overwrite and Register in a predefined group
+ withContext(Dispatchers.Main) {
+ setResult(Intent())
+ }
+ } ?: run {
+ withContext(Dispatchers.Main) {
+ Log.e(TAG, "Unable to auto select passkey, usage parameters are empty")
+ cancelResult()
+ }
+ }
+ }
+ }
+
+ override fun manageRegistrationResult(activityResult: ActivityResult) {
+ val intent = activityResult.data
+ viewModelScope.launch(CoroutineExceptionHandler { _, e ->
+ Log.e(TAG, "Unable to create registration response for passkey", e)
+ if (e is SignatureNotFoundException) {
+ intent?.retrieveNodeId()?.let { nodeId ->
+ showAppSignatureDialog(e.temptingApp, nodeId)
+ } ?: cancelResult()
+ } else {
+ showError(e)
+ }
+ }) {
+ // Build a new formatted response from the creation response
+ val responseIntent = Intent()
+ when (activityResult.resultCode) {
+ RESULT_OK -> {
+ withContext(Dispatchers.IO) {
+ Log.d(TAG, "Passkey registration result")
+ val passkey = intent?.retrievePasskey()
+ intent?.removePasskey()
+ intent?.removeAppOrigin()
+ // If registered passkey is the same as the one we want to validate,
+ if (mPasskey == passkey) {
+ mCreationParameters?.let {
+ PendingIntentHandler.setCreateCredentialResponse(
+ intent = responseIntent,
+ response = buildCreatePublicKeyCredentialResponse(
+ publicKeyCredentialCreationParameters = it,
+ backupEligibility = passkey?.backupEligibility
+ ?: mBackupEligibility,
+ backupState = passkey?.backupState
+ ?: mBackupState
+ )
+ )
+ }
+ } else {
+ throw SecurityException("Passkey was modified before registration")
+ }
+ withContext(Dispatchers.Main) {
+ setResult(responseIntent)
+ }
+ }
+ }
+ RESULT_CANCELED -> {
+ withContext(Dispatchers.Main) {
+ cancelResult()
+ }
+ }
+ }
+ }
+ }
+
+ sealed class UIState {
+ object Loading : UIState()
+ data class ShowAppPrivilegedDialog(
+ val temptingApp: AndroidPrivilegedApp
+ ): UIState()
+ data class ShowAppSignatureDialog(
+ val temptingApp: AppOrigin,
+ val nodeId: UUID
+ ): UIState()
+ data class UpdateEntry(
+ val oldEntry: Entry,
+ val newEntry: Entry
+ ): UIState()
+ }
+
+ companion object {
+ private val TAG = PasskeyLauncherViewModel::class.java.name
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/DatabaseTaskProvider.kt b/app/src/main/java/com/kunzisoft/keepass/database/DatabaseTaskProvider.kt
index 303a725a6..9b1c9faa2 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/DatabaseTaskProvider.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/DatabaseTaskProvider.kt
@@ -19,7 +19,6 @@
*/
package com.kunzisoft.keepass.database
-import android.Manifest
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
@@ -29,23 +28,15 @@ import android.content.Context.BIND_IMPORTANT
import android.content.Intent
import android.content.IntentFilter
import android.content.ServiceConnection
-import android.content.pm.PackageManager
import android.net.Uri
-import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.widget.Toast
-import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
-import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
-import androidx.fragment.app.FragmentActivity
-import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
-import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
-import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Entry
@@ -55,7 +46,6 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.model.CipherEncryptDatabase
-import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK
@@ -89,13 +79,9 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
-import com.kunzisoft.keepass.tasks.ActionRunnable
-import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
-import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.utils.putParcelableList
-import kotlinx.coroutines.launch
import java.util.UUID
/**
@@ -103,175 +89,59 @@ import java.util.UUID
* Useful to retrieve a database instance and sending tasks commands
*/
class DatabaseTaskProvider(
- private var context: Context,
- private var showDialog: Boolean = true
+ private var context: Context
) {
- // To show dialog only if context is an activity
- private var activity: FragmentActivity? = try { context as? FragmentActivity? }
- catch (_: Exception) { null }
-
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
- var onActionFinish: ((database: ContextualDatabase,
- actionTask: String,
- result: ActionRunnable.Result) -> Unit)? = null
-
- private var intentDatabaseTask: Intent = Intent(
- context.applicationContext,
- DatabaseTaskNotificationService::class.java
- )
+ var onStartActionRequested: ((bundle: Bundle?, actionTask: String) -> Unit)? = null
+ var actionTaskListener: DatabaseTaskNotificationService.ActionTaskListener? = null
+ var databaseInfoListener: DatabaseTaskNotificationService.DatabaseInfoListener? = null
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
private var serviceConnection: ServiceConnection? = null
- private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
- private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
-
fun destroy() {
- this.activity = null
this.onDatabaseRetrieved = null
- this.onActionFinish = null
this.databaseTaskBroadcastReceiver = null
this.mBinder = null
this.serviceConnection = null
- this.progressTaskDialogFragment = null
- this.databaseChangedDialogFragment = null
}
- private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
- override fun onActionStarted(
- database: ContextualDatabase,
- progressMessage: ProgressMessage
- ) {
- if (showDialog)
- startDialog(progressMessage)
- }
-
- override fun onActionUpdated(
- database: ContextualDatabase,
- progressMessage: ProgressMessage
- ) {
- if (showDialog)
- updateDialog(progressMessage)
- }
-
- override fun onActionStopped(
- database: ContextualDatabase
- ) {
- // Remove the progress task
- stopDialog()
- }
-
- override fun onActionFinished(
- database: ContextualDatabase,
- actionTask: String,
- result: ActionRunnable.Result
- ) {
- onActionFinish?.invoke(database, actionTask, result)
- onActionStopped(database)
- }
+ fun onDatabaseChangeValidated() {
+ mBinder?.getService()?.saveDatabaseInfo()
}
- 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,
- readOnlyDatabase: Boolean
- ) {
- activity?.let { activity ->
- activity.lifecycleScope.launch {
- if (databaseChangedDialogFragment == null) {
- databaseChangedDialogFragment = activity.supportFragmentManager
- .findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
- databaseChangedDialogFragment?.actionDatabaseListener =
- mActionDatabaseListener
- }
- if (progressTaskDialogFragment == null) {
- databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
- previousDatabaseInfo,
- newDatabaseInfo,
- readOnlyDatabase
- )
- databaseChangedDialogFragment?.actionDatabaseListener =
- mActionDatabaseListener
- databaseChangedDialogFragment?.show(
- activity.supportFragmentManager,
- DATABASE_CHANGED_DIALOG_TAG
- )
- }
- }
- }
- }
- }
-
- private var databaseListener = object: DatabaseTaskNotificationService.DatabaseListener {
+ private var databaseListener = object : DatabaseTaskNotificationService.DatabaseListener {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
onDatabaseRetrieved?.invoke(database)
}
}
- private fun startDialog(progressMessage: ProgressMessage) {
- activity?.let { activity ->
- activity.lifecycleScope.launch {
- if (progressTaskDialogFragment == null) {
- progressTaskDialogFragment = activity.supportFragmentManager
- .findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
- }
- if (progressTaskDialogFragment == null) {
- progressTaskDialogFragment = ProgressTaskDialogFragment()
- progressTaskDialogFragment?.show(
- activity.supportFragmentManager,
- PROGRESS_TASK_DIALOG_TAG
- )
- }
- updateDialog(progressMessage)
- }
- }
- }
-
- private fun updateDialog(progressMessage: ProgressMessage) {
- progressTaskDialogFragment?.apply {
- updateTitle(progressMessage.titleId)
- updateMessage(progressMessage.messageId)
- updateWarning(progressMessage.warningId)
- setCancellable(progressMessage.cancelable)
- }
- }
-
- private fun stopDialog() {
- progressTaskDialogFragment?.dismissAllowingStateLoss()
- progressTaskDialogFragment = null
- }
-
private fun initServiceConnection() {
- stopDialog()
+ actionTaskListener?.onActionStopped()
if (serviceConnection == null) {
serviceConnection = object : ServiceConnection {
override fun onBindingDied(name: ComponentName?) {
- stopDialog()
+ actionTaskListener?.onActionStopped()
+ onDatabaseRetrieved?.invoke(null)
}
override fun onNullBinding(name: ComponentName?) {
- stopDialog()
+ actionTaskListener?.onActionStopped()
+ onDatabaseRetrieved?.invoke(null)
}
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
- mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
- addServiceListeners(this)
- getService().checkDatabase()
- getService().checkDatabaseInfo()
- getService().checkAction()
- }
+ mBinder =
+ (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
+ addServiceListeners(this)
+ getService().checkDatabase()
+ getService().checkDatabaseInfo()
+ getService().checkAction()
+ }
}
override fun onServiceDisconnected(name: ComponentName?) {
@@ -284,20 +154,36 @@ class DatabaseTaskProvider(
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
service?.addDatabaseListener(databaseListener)
- service?.addDatabaseFileInfoListener(databaseInfoListener)
- service?.addActionTaskListener(actionTaskListener)
+ databaseInfoListener?.let { infoListener ->
+ service?.addDatabaseFileInfoListener(infoListener)
+ }
+ actionTaskListener?.let { taskListener ->
+ service?.addActionTaskListener(taskListener)
+ }
}
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
- service?.removeActionTaskListener(actionTaskListener)
- service?.removeDatabaseFileInfoListener(databaseInfoListener)
+ actionTaskListener?.let { taskListener ->
+ service?.removeActionTaskListener(taskListener)
+ }
+ databaseInfoListener?.let { infoListener ->
+ service?.removeDatabaseFileInfoListener(infoListener)
+ }
service?.removeDatabaseListener(databaseListener)
+ onDatabaseRetrieved?.invoke(null)
}
private fun bindService() {
initServiceConnection()
serviceConnection?.let {
- context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT)
+ context.bindService(
+ Intent(
+ context.applicationContext,
+ DatabaseTaskNotificationService::class.java
+ ),
+ it,
+ BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT
+ )
}
}
@@ -325,6 +211,7 @@ class DatabaseTaskProvider(
// Bind to the service when is starting
bindService()
}
+
DATABASE_STOP_TASK_ACTION -> {
// Remove the progress task
unBindService()
@@ -332,7 +219,8 @@ class DatabaseTaskProvider(
}
}
}
- ContextCompat.registerReceiver(context, databaseTaskBroadcastReceiver,
+ ContextCompat.registerReceiver(
+ context, databaseTaskBroadcastReceiver,
IntentFilter().apply {
addAction(DATABASE_START_TASK_ACTION)
addAction(DATABASE_STOP_TASK_ACTION)
@@ -356,58 +244,9 @@ class DatabaseTaskProvider(
}
}
- private val tempServiceParameters = mutableListOf>()
- private val requestPermissionLauncher = activity?.registerForActivityResult(
- ActivityResultContracts.RequestPermission()
- ) { _ ->
- // Whether or not the user has accepted, the service can be started,
- // There just won't be any notification if it's not allowed.
- tempServiceParameters.removeFirstOrNull()?.let {
- startService(it.first, it.second)
- }
- }
-
private fun start(bundle: Bundle? = null, actionTask: String) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- val contextActivity = activity
- if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
- == PackageManager.PERMISSION_GRANTED
- ) {
- startService(bundle, actionTask)
- } else if (contextActivity != null && shouldShowRequestPermissionRationale(
- contextActivity,
- Manifest.permission.POST_NOTIFICATIONS
- )
- ) {
- // it's not the first time, so the user deliberately chooses not to display the notification
- startService(bundle, actionTask)
- } else {
- AlertDialog.Builder(context)
- .setMessage(R.string.warning_database_notification_permission)
- .setNegativeButton(R.string.later) { _, _ ->
- // Refuses the notification, so start the service
- startService(bundle, actionTask)
- }
- .setPositiveButton(R.string.ask) { _, _ ->
- // Save the temp parameters to ask the permission
- tempServiceParameters.add(Pair(bundle, actionTask))
- requestPermissionLauncher?.launch(Manifest.permission.POST_NOTIFICATIONS)
- }.create().show()
- }
- } else {
- startService(bundle, actionTask)
- }
- }
-
- private fun startService(bundle: Bundle? = null, actionTask: String) {
- try {
- if (bundle != null)
- intentDatabaseTask.putExtras(bundle)
- intentDatabaseTask.action = actionTask
- context.startService(intentDatabaseTask)
- } catch (e: Exception) {
- Log.e(TAG, "Unable to perform database action", e)
- Toast.makeText(context, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
+ onStartActionRequested?.invoke(bundle, actionTask) ?: run {
+ context.startDatabaseService(bundle, actionTask)
}
}
@@ -417,47 +256,51 @@ class DatabaseTaskProvider(
----
*/
- fun startDatabaseCreate(databaseUri: Uri,
- mainCredential: MainCredential
+ fun startDatabaseCreate(
+ databaseUri: Uri,
+ mainCredential: MainCredential
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
- }
- , ACTION_DATABASE_CREATE_TASK)
+ }, ACTION_DATABASE_CREATE_TASK)
}
- fun startDatabaseLoad(databaseUri: Uri,
- mainCredential: MainCredential,
- readOnly: Boolean,
- cipherEncryptDatabase: CipherEncryptDatabase?,
- fixDuplicateUuid: Boolean) {
+ fun startDatabaseLoad(
+ databaseUri: Uri,
+ mainCredential: MainCredential,
+ readOnly: Boolean,
+ cipherEncryptDatabase: CipherEncryptDatabase?,
+ fixDuplicateUuid: Boolean
+ ) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
- putParcelable(DatabaseTaskNotificationService.CIPHER_DATABASE_KEY, cipherEncryptDatabase)
+ putParcelable(
+ DatabaseTaskNotificationService.CIPHER_DATABASE_KEY,
+ cipherEncryptDatabase
+ )
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
- }
- , ACTION_DATABASE_LOAD_TASK)
+ }, ACTION_DATABASE_LOAD_TASK)
}
- fun startDatabaseMerge(save: Boolean,
- fromDatabaseUri: Uri? = null,
- mainCredential: MainCredential? = null) {
+ fun startDatabaseMerge(
+ save: Boolean,
+ fromDatabaseUri: Uri? = null,
+ mainCredential: MainCredential? = null
+ ) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
- }
- , ACTION_DATABASE_MERGE_TASK)
+ }, ACTION_DATABASE_MERGE_TASK)
}
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
- }
- , ACTION_DATABASE_RELOAD_TASK)
+ }, ACTION_DATABASE_RELOAD_TASK)
}
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
@@ -473,15 +316,15 @@ class DatabaseTaskProvider(
}
}
- fun startDatabaseAssignCredential(databaseUri: Uri,
- mainCredential: MainCredential
+ fun startDatabaseAssignCredential(
+ databaseUri: Uri,
+ mainCredential: MainCredential
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
- }
- , ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK)
+ }, ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK)
}
/*
@@ -490,54 +333,60 @@ class DatabaseTaskProvider(
----
*/
- fun startDatabaseCreateGroup(newGroup: Group,
- parent: Group,
- save: Boolean) {
+ fun startDatabaseCreateGroup(
+ newGroup: Group,
+ parent: Group,
+ save: Boolean
+ ) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, newGroup)
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_CREATE_GROUP_TASK)
+ }, ACTION_DATABASE_CREATE_GROUP_TASK)
}
- fun startDatabaseUpdateGroup(oldGroup: Group,
- groupToUpdate: Group,
- save: Boolean) {
+ fun startDatabaseUpdateGroup(
+ oldGroup: Group,
+ groupToUpdate: Group,
+ save: Boolean
+ ) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.GROUP_ID_KEY, oldGroup.nodeId)
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_GROUP_TASK)
+ }, ACTION_DATABASE_UPDATE_GROUP_TASK)
}
- fun startDatabaseCreateEntry(newEntry: Entry,
- parent: Group,
- save: Boolean) {
+ fun startDatabaseCreateEntry(
+ newEntry: Entry,
+ parent: Group,
+ save: Boolean
+ ) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, newEntry)
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_CREATE_ENTRY_TASK)
+ }, ACTION_DATABASE_CREATE_ENTRY_TASK)
}
- fun startDatabaseUpdateEntry(oldEntry: Entry,
- entryToUpdate: Entry,
- save: Boolean) {
+ fun startDatabaseUpdateEntry(
+ oldEntry: Entry,
+ entryToUpdate: Entry,
+ save: Boolean
+ ) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, oldEntry.nodeId)
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_ENTRY_TASK)
+ }, ACTION_DATABASE_UPDATE_ENTRY_TASK)
}
- private fun startDatabaseActionListNodes(actionTask: String,
- nodesPaste: List,
- newParent: Group?,
- save: Boolean) {
+ private fun startDatabaseActionListNodes(
+ actionTask: String,
+ nodesPaste: List,
+ newParent: Group?,
+ save: Boolean
+ ) {
val groupsIdToCopy = ArrayList>()
val entriesIdToCopy = ArrayList>()
nodesPaste.forEach { nodeVersioned ->
@@ -545,6 +394,7 @@ class DatabaseTaskProvider(
Type.GROUP -> {
groupsIdToCopy.add((nodeVersioned as Group).nodeId)
}
+
Type.ENTRY -> {
entriesIdToCopy.add((nodeVersioned as Entry).nodeId)
}
@@ -559,24 +409,29 @@ class DatabaseTaskProvider(
if (newParentId != null)
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , actionTask)
+ }, actionTask)
}
- fun startDatabaseCopyNodes(nodesToCopy: List,
- newParent: Group,
- save: Boolean) {
+ fun startDatabaseCopyNodes(
+ nodesToCopy: List,
+ newParent: Group,
+ save: Boolean
+ ) {
startDatabaseActionListNodes(ACTION_DATABASE_COPY_NODES_TASK, nodesToCopy, newParent, save)
}
- fun startDatabaseMoveNodes(nodesToMove: List,
- newParent: Group,
- save: Boolean) {
+ fun startDatabaseMoveNodes(
+ nodesToMove: List,
+ newParent: Group,
+ save: Boolean
+ ) {
startDatabaseActionListNodes(ACTION_DATABASE_MOVE_NODES_TASK, nodesToMove, newParent, save)
}
- fun startDatabaseDeleteNodes(nodesToDelete: List,
- save: Boolean) {
+ fun startDatabaseDeleteNodes(
+ nodesToDelete: List,
+ save: Boolean
+ ) {
startDatabaseActionListNodes(ACTION_DATABASE_DELETE_NODES_TASK, nodesToDelete, null, save)
}
@@ -586,26 +441,28 @@ class DatabaseTaskProvider(
-----------------
*/
- fun startDatabaseRestoreEntryHistory(mainEntryId: NodeId,
- entryHistoryPosition: Int,
- save: Boolean) {
+ fun startDatabaseRestoreEntryHistory(
+ mainEntryId: NodeId,
+ entryHistoryPosition: Int,
+ save: Boolean
+ ) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
+ }, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
}
- fun startDatabaseDeleteEntryHistory(mainEntryId: NodeId,
- entryHistoryPosition: Int,
- save: Boolean) {
+ fun startDatabaseDeleteEntryHistory(
+ mainEntryId: NodeId,
+ entryHistoryPosition: Int,
+ save: Boolean
+ ) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_DELETE_ENTRY_HISTORY)
+ }, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
}
/*
@@ -614,110 +471,118 @@ class DatabaseTaskProvider(
-----------------
*/
- fun startDatabaseSaveName(oldName: String,
- newName: String,
- save: Boolean) {
+ fun startDatabaseSaveName(
+ oldName: String,
+ newName: String,
+ save: Boolean
+ ) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldName)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_NAME_TASK)
+ }, ACTION_DATABASE_UPDATE_NAME_TASK)
}
- fun startDatabaseSaveDescription(oldDescription: String,
- newDescription: String,
- save: Boolean) {
+ fun startDatabaseSaveDescription(
+ oldDescription: String,
+ newDescription: String,
+ save: Boolean
+ ) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDescription)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
+ }, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
}
- fun startDatabaseSaveDefaultUsername(oldDefaultUsername: String,
- newDefaultUsername: String,
- save: Boolean) {
+ fun startDatabaseSaveDefaultUsername(
+ oldDefaultUsername: String,
+ newDefaultUsername: String,
+ save: Boolean
+ ) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDefaultUsername)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
+ }, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
}
- fun startDatabaseSaveColor(oldColor: String,
- newColor: String,
- save: Boolean) {
+ fun startDatabaseSaveColor(
+ oldColor: String,
+ newColor: String,
+ save: Boolean
+ ) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldColor)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_COLOR_TASK)
+ }, ACTION_DATABASE_UPDATE_COLOR_TASK)
}
- fun startDatabaseSaveCompression(oldCompression: CompressionAlgorithm,
- newCompression: CompressionAlgorithm,
- save: Boolean) {
+ fun startDatabaseSaveCompression(
+ oldCompression: CompressionAlgorithm,
+ newCompression: CompressionAlgorithm,
+ save: Boolean
+ ) {
start(Bundle().apply {
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldCompression)
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
+ }, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
}
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
+ }, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
}
- fun startDatabaseSaveRecycleBin(oldRecycleBin: Group?,
- newRecycleBin: Group?,
- save: Boolean) {
+ fun startDatabaseSaveRecycleBin(
+ oldRecycleBin: Group?,
+ newRecycleBin: Group?,
+ save: Boolean
+ ) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldRecycleBin)
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newRecycleBin)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
+ }, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
}
- fun startDatabaseSaveTemplatesGroup(oldTemplatesGroup: Group?,
- newTemplatesGroup: Group?,
- save: Boolean) {
+ fun startDatabaseSaveTemplatesGroup(
+ oldTemplatesGroup: Group?,
+ newTemplatesGroup: Group?,
+ save: Boolean
+ ) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldTemplatesGroup)
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newTemplatesGroup)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
+ }, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
}
- fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
- newMaxHistoryItems: Int,
- save: Boolean) {
+ fun startDatabaseSaveMaxHistoryItems(
+ oldMaxHistoryItems: Int,
+ newMaxHistoryItems: Int,
+ save: Boolean
+ ) {
start(Bundle().apply {
putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistoryItems)
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
+ }, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
}
- fun startDatabaseSaveMaxHistorySize(oldMaxHistorySize: Long,
- newMaxHistorySize: Long,
- save: Boolean) {
+ fun startDatabaseSaveMaxHistorySize(
+ oldMaxHistorySize: Long,
+ newMaxHistorySize: Long,
+ save: Boolean
+ ) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistorySize)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
+ }, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
}
/*
@@ -726,59 +591,64 @@ class DatabaseTaskProvider(
-------------------
*/
- fun startDatabaseSaveEncryption(oldEncryption: EncryptionAlgorithm,
- newEncryption: EncryptionAlgorithm,
- save: Boolean) {
+ fun startDatabaseSaveEncryption(
+ oldEncryption: EncryptionAlgorithm,
+ newEncryption: EncryptionAlgorithm,
+ save: Boolean
+ ) {
start(Bundle().apply {
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldEncryption)
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
+ }, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
}
- fun startDatabaseSaveKeyDerivation(oldKeyDerivation: KdfEngine,
- newKeyDerivation: KdfEngine,
- save: Boolean) {
+ fun startDatabaseSaveKeyDerivation(
+ oldKeyDerivation: KdfEngine,
+ newKeyDerivation: KdfEngine,
+ save: Boolean
+ ) {
start(Bundle().apply {
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldKeyDerivation)
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
+ }, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
}
- fun startDatabaseSaveIterations(oldIterations: Long,
- newIterations: Long,
- save: Boolean) {
+ fun startDatabaseSaveIterations(
+ oldIterations: Long,
+ newIterations: Long,
+ save: Boolean
+ ) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldIterations)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
+ }, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
}
- fun startDatabaseSaveMemoryUsage(oldMemoryUsage: Long,
- newMemoryUsage: Long,
- save: Boolean) {
+ fun startDatabaseSaveMemoryUsage(
+ oldMemoryUsage: Long,
+ newMemoryUsage: Long,
+ save: Boolean
+ ) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMemoryUsage)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
+ }, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
}
- fun startDatabaseSaveParallelism(oldParallelism: Long,
- newParallelism: Long,
- save: Boolean) {
+ fun startDatabaseSaveParallelism(
+ oldParallelism: Long,
+ newParallelism: Long,
+ save: Boolean
+ ) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
+ }, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
}
/**
@@ -788,18 +658,32 @@ class DatabaseTaskProvider(
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
- }
- , ACTION_DATABASE_SAVE)
+ }, ACTION_DATABASE_SAVE)
}
fun startChallengeResponded(response: ByteArray?) {
start(Bundle().apply {
putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response)
- }
- , ACTION_CHALLENGE_RESPONDED)
+ }, ACTION_CHALLENGE_RESPONDED)
}
companion object {
private val TAG = DatabaseTaskProvider::class.java.name
+
+ fun Context.startDatabaseService(bundle: Bundle? = null, actionTask: String) {
+ try {
+ val intentDatabaseTask = Intent(
+ applicationContext,
+ DatabaseTaskNotificationService::class.java
+ )
+ if (bundle != null)
+ intentDatabaseTask.putExtras(bundle)
+ intentDatabaseTask.action = actionTask
+ startService(intentDatabaseTask)
+ } catch (e: Exception) {
+ Log.e(TAG, "Unable to perform database action", e)
+ Toast.makeText(this, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
+ }
+ }
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/helper/LocalizedHelper.kt b/app/src/main/java/com/kunzisoft/keepass/database/helper/LocalizedHelper.kt
index 7ebe3aa0f..82791b2a6 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/helper/LocalizedHelper.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/helper/LocalizedHelper.kt
@@ -24,14 +24,44 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.template.TemplateEngine
import com.kunzisoft.keepass.database.element.template.TemplateField
-import com.kunzisoft.keepass.database.exception.*
+import com.kunzisoft.keepass.database.exception.CopyEntryDatabaseException
+import com.kunzisoft.keepass.database.exception.CopyGroupDatabaseException
+import com.kunzisoft.keepass.database.exception.CorruptedDatabaseException
+import com.kunzisoft.keepass.database.exception.DatabaseInputException
+import com.kunzisoft.keepass.database.exception.DatabaseOutputException
+import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
+import com.kunzisoft.keepass.database.exception.EmptyKeyDatabaseException
+import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
+import com.kunzisoft.keepass.database.exception.HardwareKeyDatabaseException
+import com.kunzisoft.keepass.database.exception.InvalidAlgorithmDatabaseException
+import com.kunzisoft.keepass.database.exception.InvalidCredentialsDatabaseException
+import com.kunzisoft.keepass.database.exception.KDFMemoryDatabaseException
+import com.kunzisoft.keepass.database.exception.LocalizedException
+import com.kunzisoft.keepass.database.exception.MergeDatabaseKDBException
+import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException
+import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
+import com.kunzisoft.keepass.database.exception.NoMemoryDatabaseException
+import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
+import com.kunzisoft.keepass.database.exception.SignatureDatabaseException
+import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
+import com.kunzisoft.keepass.database.exception.VersionDatabaseException
+import com.kunzisoft.keepass.database.exception.XMLMalformedDatabaseException
+import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_CREDENTIAL_ID
+import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_FLAG_BE
+import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_FLAG_BS
+import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_PRIVATE_KEY
+import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_RELYING_PARTY
+import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USERNAME
+import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USER_HANDLE
+import com.kunzisoft.keepass.model.PasskeyEntryFields.PASSKEY_FIELD
-fun DatabaseException.getLocalizedMessage(resources: Resources): String? =
+fun LocalizedException.getLocalizedMessage(resources: Resources): String? =
when (this) {
is FileNotFoundDatabaseException -> resources.getString(R.string.file_not_found_content)
is CorruptedDatabaseException -> resources.getString(R.string.corrupted_file)
is InvalidAlgorithmDatabaseException -> resources.getString(R.string.invalid_algorithm)
is UnknownDatabaseLocationException -> resources.getString(R.string.error_location_unknown)
+ is RegisterInReadOnlyDatabaseException -> resources.getString(R.string.error_save_read_only)
is HardwareKeyDatabaseException -> resources.getString(R.string.error_hardware_key_unsupported)
is EmptyKeyDatabaseException -> resources.getString(R.string.error_empty_key)
is SignatureDatabaseException -> resources.getString(R.string.invalid_db_sig)
@@ -63,6 +93,11 @@ fun TemplateField.isStandardPasswordName(context: Context, name: String): Boolea
|| name == getLocalizedName(context, LABEL_PASSWORD)
}
+fun TemplateField.isPasskeyLabel(context: Context, name: String): Boolean {
+ return name.equals(PASSKEY_FIELD, true)
+ || name == getLocalizedName(context, PASSKEY_FIELD)
+}
+
fun TemplateField.getLocalizedName(context: Context?, name: String): String {
if (context == null
|| TemplateEngine.containsTemplateDecorator(name)
@@ -107,6 +142,15 @@ fun TemplateField.getLocalizedName(context: Context?, name: String): String {
LABEL_SECURE_NOTE.equals(name, true) -> context.getString(R.string.secure_note)
LABEL_MEMBERSHIP.equals(name, true) -> context.getString(R.string.membership)
+ PASSKEY_FIELD.equals(name, true) -> context.getString(R.string.passkey)
+ FIELD_USERNAME.equals(name, true) -> context.getString(R.string.passkey_username)
+ FIELD_PRIVATE_KEY.equals(name, true) -> context.getString(R.string.passkey_private_key)
+ FIELD_CREDENTIAL_ID.equals(name, true) -> context.getString(R.string.passkey_credential_id)
+ FIELD_USER_HANDLE.equals(name, true) -> context.getString(R.string.passkey_user_handle)
+ FIELD_RELYING_PARTY.equals(name, true) -> context.getString(R.string.passkey_relying_party)
+ FIELD_FLAG_BE.equals(name, true) -> context.getString(R.string.passkey_backup_eligibility)
+ FIELD_FLAG_BS.equals(name, true) -> context.getString(R.string.passkey_backup_state)
+
else -> name
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt b/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt
index 232b88e17..ade5ff023 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt
@@ -21,9 +21,16 @@ package com.kunzisoft.keepass.database.helper
import android.content.Context
import com.kunzisoft.keepass.database.ContextualDatabase
+import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
+import com.kunzisoft.keepass.settings.PreferencesUtil.searchSubDomains
import com.kunzisoft.keepass.timeout.TimeoutHelper
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.components.lib.publicsuffixlist.PublicSuffixList
object SearchHelper {
@@ -41,38 +48,112 @@ object SearchHelper {
}
/**
- * Utility method to perform actions if item is found or not after an auto search in [database]
+ * Get the concrete web domain AKA without sub domain if needed
*/
- fun checkAutoSearchInfo(context: Context,
- database: ContextualDatabase?,
- searchInfo: SearchInfo?,
- onItemsFound: (openedDatabase: ContextualDatabase,
- items: List) -> Unit,
- onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
- onDatabaseClosed: () -> Unit) {
- if (database == null || !database.loaded) {
- onDatabaseClosed.invoke()
- } else if (TimeoutHelper.checkTime(context)) {
- var searchWithoutUI = false
- if (searchInfo != null
- && !searchInfo.manualSelection
- && !searchInfo.containsOnlyNullValues()) {
- // If search provide results
- database.createVirtualGroupFromSearchInfo(
- searchInfoString = searchInfo.toString(),
- searchInfoByDomain = searchInfo.isASearchByDomain(),
- max = MAX_SEARCH_ENTRY
- )?.let { searchGroup ->
- if (searchGroup.numberOfChildEntries > 0) {
- searchWithoutUI = true
- onItemsFound.invoke(database,
- searchGroup.getChildEntriesInfo(database))
+ private fun getConcreteWebDomain(
+ context: Context,
+ webDomain: String?,
+ concreteWebDomain: (searchSubDomains: Boolean, concreteWebDomain: String?) -> Unit
+ ) {
+ val domain = webDomain
+ val searchSubDomains = searchSubDomains(context)
+ if (domain != null) {
+ // Warning, web domain can contains IP, don't crop in this case
+ if (searchSubDomains
+ || Regex(SearchInfo.WEB_IP_REGEX).matches(domain)) {
+ concreteWebDomain.invoke(searchSubDomains, webDomain)
+ } else {
+ CoroutineScope(Dispatchers.IO).launch {
+ val publicSuffixList = PublicSuffixList(context)
+ val publicSuffix = publicSuffixList
+ .getPublicSuffixPlusOne(domain).await()
+ withContext(Dispatchers.Main) {
+ concreteWebDomain.invoke(false, publicSuffix)
}
}
}
- if (!searchWithoutUI) {
+ } else {
+ concreteWebDomain.invoke(searchSubDomains, null)
+ }
+ }
+
+ /**
+ * Create search parameters asynchronously from [SearchInfo]
+ */
+ fun SearchInfo.getSearchParametersFromSearchInfo(
+ context: Context,
+ callback: (SearchParameters) -> Unit
+ ) {
+ getConcreteWebDomain(
+ context,
+ webDomain
+ ) { searchSubDomains, concreteDomain ->
+ var query = this.toString()
+ if (isDomainSearch && concreteDomain != null)
+ query = concreteDomain
+ callback.invoke(
+ SearchParameters().apply {
+ searchQuery = query
+ allowEmptyQuery = false
+ searchInTitles = false
+ searchInUsernames = false
+ searchInPasswords = false
+ searchInAppIds = isAppIdSearch
+ searchInUrls = isDomainSearch
+ searchByDomain = true
+ searchBySubDomain = searchSubDomains
+ searchInRelyingParty = isPasskeySearch
+ searchInNotes = false
+ searchInOTP = isOTPSearch
+ searchInOther = false
+ searchInUUIDs = false
+ searchInTags = isTagSearch
+ searchInCurrentGroup = false
+ searchInSearchableGroup = true
+ searchInRecycleBin = false
+ searchInTemplates = false
+ }
+ )
+ }
+ }
+
+ /**
+ * Utility method to perform actions if item is found or not after an auto search in [database]
+ */
+ fun checkAutoSearchInfo(
+ context: Context,
+ database: ContextualDatabase?,
+ searchInfo: SearchInfo?,
+ onItemsFound: (openedDatabase: ContextualDatabase,
+ items: List) -> Unit,
+ onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
+ onDatabaseClosed: () -> Unit
+ ) {
+ // Do not place coroutine at start, bug in Passkey implementation
+ if (database == null || !database.loaded) {
+ onDatabaseClosed.invoke()
+ } else if (TimeoutHelper.checkTime(context)) {
+ if (searchInfo != null
+ && !searchInfo.manualSelection
+ && !searchInfo.containsOnlyNullValues()
+ ) {
+ searchInfo.getSearchParametersFromSearchInfo(context) { searchParameters ->
+ // If search provide results
+ database.createVirtualGroupFromSearchInfo(
+ searchParameters = searchParameters,
+ max = MAX_SEARCH_ENTRY
+ )?.let { searchGroup ->
+ if (searchGroup.numberOfChildEntries > 0) {
+ onItemsFound.invoke(
+ database,
+ searchGroup.getChildEntriesInfo(database)
+ )
+ } else
+ onItemNotFound.invoke(database)
+ } ?: onItemNotFound.invoke(database)
+ }
+ } else
onItemNotFound.invoke(database)
- }
}
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyActivity.kt b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyActivity.kt
deleted file mode 100644
index 77d802025..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyActivity.kt
+++ /dev/null
@@ -1,170 +0,0 @@
-package com.kunzisoft.keepass.hardware
-
-import android.app.Activity
-import android.content.Context
-import android.content.DialogInterface
-import android.content.Intent
-import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
-import android.util.Log
-import androidx.activity.result.ActivityResult
-import androidx.activity.result.ActivityResultCallback
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AlertDialog
-import com.kunzisoft.keepass.R
-import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
-import com.kunzisoft.keepass.database.ContextualDatabase
-import com.kunzisoft.keepass.utils.UriUtil.openExternalApp
-
-/**
- * Special activity to deal with hardware key drivers,
- * return the response to the database service once finished
- */
-class HardwareKeyActivity: DatabaseModeActivity(){
-
- // To manage hardware key challenge response
- private val resultCallback = ActivityResultCallback { result ->
- if (result.resultCode == Activity.RESULT_OK) {
- val challengeResponse: ByteArray? = result.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
- Log.d(TAG, "Response form challenge")
- mDatabaseTaskProvider?.startChallengeResponded(challengeResponse ?: ByteArray(0))
- } else {
- Log.e(TAG, "Response from challenge error")
- mDatabaseTaskProvider?.startChallengeResponded(ByteArray(0))
- }
- finish()
- }
-
- private var activityResultLauncher: ActivityResultLauncher = registerForActivityResult(
- ActivityResultContracts.StartActivityForResult(),
- resultCallback
- )
-
- override fun applyCustomStyle(): Boolean {
- return false
- }
-
- override fun showDatabaseDialog(): Boolean {
- return false
- }
-
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
-
- val hardwareKey = HardwareKey.getHardwareKeyFromString(
- intent.getStringExtra(DATA_HARDWARE_KEY)
- )
- if (isHardwareKeyAvailable(this, hardwareKey, true) {
- mDatabaseTaskProvider?.startChallengeResponded(ByteArray(0))
- }) {
- when (hardwareKey) {
- /*
- HardwareKey.FIDO2_SECRET -> {
- // TODO FIDO2 under development
- throw Exception("FIDO2 not implemented")
- }
- */
- HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
- launchYubikeyChallengeForResponse(intent.getByteArrayExtra(DATA_SEED))
- }
- else -> {
- finish()
- }
- }
- }
- }
-
- private fun launchYubikeyChallengeForResponse(seed: ByteArray?) {
- // Transform the seed before sending
- var challenge: ByteArray? = null
- if (seed != null) {
- challenge = ByteArray(64)
- seed.copyInto(challenge, 0, 0, 32)
- challenge.fill(32, 32, 64)
- }
- // Send to the driver
- activityResultLauncher.launch(
- Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
- putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
- }
- )
- Log.d(TAG, "Challenge sent")
- }
-
- companion object {
- private val TAG = HardwareKeyActivity::class.java.simpleName
-
- private const val DATA_HARDWARE_KEY = "DATA_HARDWARE_KEY"
- private const val DATA_SEED = "DATA_SEED"
- private const val YUBIKEY_CHALLENGE_RESPONSE_INTENT = "android.yubikey.intent.action.CHALLENGE_RESPONSE"
- private const val HARDWARE_KEY_CHALLENGE_KEY = "challenge"
- private const val HARDWARE_KEY_RESPONSE_KEY = "response"
-
- fun launchHardwareKeyActivity(
- context: Context,
- hardwareKey: HardwareKey,
- seed: ByteArray?
- ) {
- context.startActivity(Intent(context, HardwareKeyActivity::class.java).apply {
- flags = FLAG_ACTIVITY_NEW_TASK
- putExtra(DATA_HARDWARE_KEY, hardwareKey.value)
- putExtra(DATA_SEED, seed)
- })
- }
-
- fun isHardwareKeyAvailable(
- context: Context,
- hardwareKey: HardwareKey?,
- showDialog: Boolean = true,
- onDialogDismissed: DialogInterface.OnDismissListener? = null
- ): Boolean {
- if (hardwareKey == null)
- return false
- return when (hardwareKey) {
- /*
- HardwareKey.FIDO2_SECRET -> {
- // TODO FIDO2 under development
- if (showDialog)
- UnderDevelopmentFeatureDialogFragment()
- .show(activity.supportFragmentManager, "underDevFeatureDialog")
- false
- }
- */
- HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
- // Check available intent
- val yubikeyDriverAvailable =
- Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT)
- .resolveActivity(context.packageManager) != null
- if (showDialog && !yubikeyDriverAvailable
- && context is Activity)
- showHardwareKeyDriverNeeded(context, hardwareKey) {
- onDialogDismissed?.onDismiss(it)
- context.finish()
- }
- yubikeyDriverAvailable
- }
- }
- }
-
- private fun showHardwareKeyDriverNeeded(
- context: Context,
- hardwareKey: HardwareKey,
- onDialogDismissed: DialogInterface.OnDismissListener
- ) {
- val builder = AlertDialog.Builder(context)
- builder
- .setMessage(
- context.getString(R.string.error_driver_required, hardwareKey.toString())
- )
- .setPositiveButton(R.string.download) { _, _ ->
- context.openExternalApp(
- context.getString(R.string.key_driver_app_id),
- context.getString(R.string.key_driver_url)
- )
- }
- .setNegativeButton(android.R.string.cancel) { _, _ -> }
- .setOnDismissListener(onDialogDismissed)
- builder.create().show()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt
deleted file mode 100644
index 5f00e2138..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt
+++ /dev/null
@@ -1,144 +0,0 @@
-package com.kunzisoft.keepass.hardware
-
-import android.app.Activity
-import android.content.Intent
-import android.os.Bundle
-import android.util.Log
-import androidx.activity.result.ActivityResult
-import androidx.activity.result.ActivityResultCallback
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AlertDialog
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.FragmentActivity
-import androidx.lifecycle.lifecycleScope
-import com.kunzisoft.keepass.R
-import com.kunzisoft.keepass.utils.UriUtil.openExternalApp
-import kotlinx.coroutines.launch
-
-class HardwareKeyResponseHelper {
-
- private var activity: FragmentActivity? = null
- private var fragment: Fragment? = null
-
- private var getChallengeResponseResultLauncher: ActivityResultLauncher? = null
-
- constructor(context: FragmentActivity) {
- this.activity = context
- this.fragment = null
- }
-
- constructor(context: Fragment) {
- this.activity = context.activity
- this.fragment = context
- }
-
- fun buildHardwareKeyResponse(onChallengeResponded: (challengeResponse: ByteArray?,
- extra: Bundle?) -> Unit) {
- val resultCallback = ActivityResultCallback { result ->
- if (result.resultCode == Activity.RESULT_OK) {
- val challengeResponse: ByteArray? = result.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
- Log.d(TAG, "Response form challenge")
- onChallengeResponded.invoke(challengeResponse,
- result.data?.getBundleExtra(EXTRA_BUNDLE_KEY))
- } else {
- Log.e(TAG, "Response from challenge error")
- onChallengeResponded.invoke(null,
- result.data?.getBundleExtra(EXTRA_BUNDLE_KEY))
- }
- }
-
- getChallengeResponseResultLauncher = if (fragment != null) {
- fragment?.registerForActivityResult(
- ActivityResultContracts.StartActivityForResult(),
- resultCallback
- )
- } else {
- activity?.registerForActivityResult(
- ActivityResultContracts.StartActivityForResult(),
- resultCallback
- )
- }
- }
-
- fun launchChallengeForResponse(hardwareKey: HardwareKey, seed: ByteArray?) {
- when (hardwareKey) {
- /*
- HardwareKey.FIDO2_SECRET -> {
- // TODO FIDO2 under development
- throw Exception("FIDO2 not implemented")
- }
- */
- HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
- // Transform the seed before sending
- var challenge: ByteArray? = null
- if (seed != null) {
- challenge = ByteArray(64)
- seed.copyInto(challenge, 0, 0, 32)
- challenge.fill(32, 32, 64)
- }
- // Send to the driver
- getChallengeResponseResultLauncher!!.launch(
- Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
- putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
- }
- )
- Log.d(TAG, "Challenge sent")
- }
- }
- }
-
- companion object {
- private val TAG = HardwareKeyResponseHelper::class.java.simpleName
-
- private const val YUBIKEY_CHALLENGE_RESPONSE_INTENT = "android.yubikey.intent.action.CHALLENGE_RESPONSE"
- private const val HARDWARE_KEY_CHALLENGE_KEY = "challenge"
- private const val HARDWARE_KEY_RESPONSE_KEY = "response"
- private const val EXTRA_BUNDLE_KEY = "EXTRA_BUNDLE_KEY"
-
- fun isHardwareKeyAvailable(
- activity: FragmentActivity,
- hardwareKey: HardwareKey,
- showDialog: Boolean = true
- ): Boolean {
- return when (hardwareKey) {
- /*
- HardwareKey.FIDO2_SECRET -> {
- // TODO FIDO2 under development
- if (showDialog)
- UnderDevelopmentFeatureDialogFragment()
- .show(activity.supportFragmentManager, "underDevFeatureDialog")
- false
- }
- */
- HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
- // Check available intent
- val yubikeyDriverAvailable =
- Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT)
- .resolveActivity(activity.packageManager) != null
- if (showDialog && !yubikeyDriverAvailable)
- showHardwareKeyDriverNeeded(activity, hardwareKey)
- yubikeyDriverAvailable
- }
- }
- }
-
- private fun showHardwareKeyDriverNeeded(
- activity: FragmentActivity,
- hardwareKey: HardwareKey
- ) {
- activity.lifecycleScope.launch {
- val builder = AlertDialog.Builder(activity)
- builder
- .setMessage(
- activity.getString(R.string.error_driver_required, hardwareKey.toString())
- )
- .setPositiveButton(R.string.download) { _, _ ->
- activity.openExternalApp(activity.getString(R.string.key_driver_app_id))
- }
- .setNegativeButton(android.R.string.cancel) { _, _ -> }
- builder.create().show()
- }
- }
- }
-}
diff --git a/app/src/main/java/com/kunzisoft/keepass/password/PasswordGenerator.kt b/app/src/main/java/com/kunzisoft/keepass/password/PasswordGenerator.kt
index 692f33557..3a1355768 100644
--- a/app/src/main/java/com/kunzisoft/keepass/password/PasswordGenerator.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/password/PasswordGenerator.kt
@@ -33,20 +33,22 @@ import java.util.*
class PasswordGenerator(private val resources: Resources) {
@Throws(IllegalArgumentException::class)
- fun generatePassword(length: Int,
- upperCase: Boolean,
- lowerCase: Boolean,
- digits: Boolean,
- minus: Boolean,
- underline: Boolean,
- space: Boolean,
- specials: Boolean,
- brackets: Boolean,
- extended: Boolean,
- considerChars: String,
- ignoreChars: String,
- atLeastOneFromEach: Boolean,
- excludeAmbiguousChar: Boolean): String {
+ fun generatePassword(
+ length: Int,
+ upperCase: Boolean,
+ lowerCase: Boolean,
+ digits: Boolean,
+ minus: Boolean,
+ underline: Boolean,
+ space: Boolean,
+ specials: Boolean,
+ brackets: Boolean,
+ extended: Boolean,
+ considerChars: String,
+ ignoreChars: String,
+ atLeastOneFromEach: Boolean,
+ excludeAmbiguousChar: Boolean
+ ): String {
// Desired password length is 0 or less
if (length <= 0) {
throw IllegalArgumentException(resources.getString(R.string.error_wrong_length))
@@ -228,7 +230,7 @@ class PasswordGenerator(private val resources: Resources) {
private const val MINUS_CHAR = "-"
private const val UNDERLINE_CHAR = "_"
private const val SPACE_CHAR = " "
- private const val SPECIAL_CHARS = "!\"#$%&'*+,./:;=?@\\^`"
+ private const val SPECIAL_CHARS = "&/,^@.#:%\\='$!?*`;+\"|~"
private const val BRACKET_CHARS = "[]{}()<>"
private const val AMBIGUOUS_CHARS = "iI|lLoO01"
diff --git a/app/src/main/java/com/kunzisoft/keepass/receivers/DexModeReceiver.kt b/app/src/main/java/com/kunzisoft/keepass/receivers/DexModeReceiver.kt
index 7844c4251..c2c4c5334 100644
--- a/app/src/main/java/com/kunzisoft/keepass/receivers/DexModeReceiver.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/receivers/DexModeReceiver.kt
@@ -1,13 +1,9 @@
package com.kunzisoft.keepass.receivers
import android.content.BroadcastReceiver
-import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.content.pm.PackageManager
import android.util.Log
-import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
-import com.kunzisoft.keepass.utils.DexUtil
import com.kunzisoft.keepass.utils.MagikeyboardUtil
class DexModeReceiver : BroadcastReceiver() {
diff --git a/app/src/main/java/com/kunzisoft/keepass/services/AttachmentFileNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/AttachmentFileNotificationService.kt
index 974db311c..00fa14a9e 100644
--- a/app/src/main/java/com/kunzisoft/keepass/services/AttachmentFileNotificationService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/services/AttachmentFileNotificationService.kt
@@ -36,6 +36,7 @@ import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
+import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import kotlinx.coroutines.CoroutineScope
@@ -194,7 +195,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
private fun newNotification(attachmentNotification: AttachmentNotification) {
val pendingContentIntent = PendingIntent.getActivity(this,
- 0,
+ randomRequestCode(),
Intent().apply {
action = Intent.ACTION_VIEW
setDataAndType(attachmentNotification.uri,
@@ -208,7 +209,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
)
val pendingDeleteIntent = PendingIntent.getService(this,
- 0,
+ randomRequestCode(),
Intent(this, AttachmentFileNotificationService::class.java).apply {
// No action to delete the service
putExtra(FILE_URI_KEY, attachmentNotification.uri)
diff --git a/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationService.kt
index b33213891..98cf199c6 100644
--- a/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationService.kt
@@ -274,10 +274,12 @@ class ClipboardEntryNotificationService : LockNotificationService() {
val containsPasswordToCopy = entry.password.isNotEmpty()
&& PreferencesUtil.allowCopyProtectedFields(context)
val containsOTPToCopy = entry.containsCustomField(OTP_TOKEN_FIELD)
- val containsExtraFieldToCopy = entry.customFields.isNotEmpty()
- && (entry.containsCustomFieldsNotProtected()
+ val customFields = entry.getCustomFieldsForFilling()
+ val containsExtraFieldToCopy = customFields.isNotEmpty()
+ && (customFields.any { !it.protectedValue.isProtected }
||
- (entry.containsCustomFieldsProtected() && PreferencesUtil.allowCopyProtectedFields(context))
+ (customFields.any { it.protectedValue.isProtected }
+ && PreferencesUtil.allowCopyProtectedFields(context))
)
var startService = false
@@ -320,7 +322,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
if (containsExtraFieldToCopy) {
try {
var anonymousFieldNumber = 0
- entry.customFields.forEach { field ->
+ entry.getCustomFieldsForFilling().forEach { field ->
//If value is not protected or allowed
if ((!field.protectedValue.isProtected
|| PreferencesUtil.allowCopyProtectedFields(context))
diff --git a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt
index 9d6c0baf3..b42b07f27 100644
--- a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt
@@ -61,13 +61,14 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.hardware.HardwareKey
-import com.kunzisoft.keepass.hardware.HardwareKeyActivity
+import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.timeout.TimeoutHelper
+import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.utils.LOCK_ACTION
@@ -175,7 +176,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
progressMessage: ProgressMessage
)
fun onActionStopped(
- database: ContextualDatabase
+ database: ContextualDatabase? = null
)
fun onActionFinished(
database: ContextualDatabase,
@@ -218,7 +219,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
Log.i(TAG, "Database file modified " +
"$previousDatabaseInfo != $lastFileDatabaseInfo ")
// Call listener to indicate a change in database info
- if (!mSaveState && previousDatabaseInfo != null) {
+ if (!mSaveState) {
mDatabaseInfoListeners.forEach { listener ->
listener.onDatabaseInfoChanged(
previousDatabaseInfo,
@@ -550,7 +551,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
// Build Intents for notification action
val pendingDatabaseIntent = PendingIntent.getActivity(
this,
- 0,
+ randomRequestCode(),
Intent(this, GroupActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
@@ -675,6 +676,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
override fun actionOnLock() {
if (!TimeoutHelper.temporarilyDisableLock) {
closeDatabase(mDatabase)
+ // Remove the database during the lock
+ // And notify each subscriber
+ mDatabase = null
+ mDatabaseListeners.forEach { listener ->
+ listener.onDatabaseRetrieved(null)
+ }
// Remove the lock timer (no more needed if it exists)
TimeoutHelper.cancelLockTimer(this)
// Service is stopped after receive the broadcast
@@ -709,9 +716,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
notifyProgressMessage()
HardwareKeyActivity
.launchHardwareKeyActivity(
- this@DatabaseTaskNotificationService,
- hardwareKey,
- seed
+ context = this@DatabaseTaskNotificationService,
+ hardwareKey = hardwareKey,
+ seed = seed
)
// Wait the response
mProgressMessage.apply {
@@ -1385,6 +1392,15 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
return nodesAction
}
+ fun Bundle.getNewEntry(database: ContextualDatabase): Entry? {
+ getBundle(NEW_NODES_KEY)
+ ?.getParcelableList>(ENTRIES_ID_KEY)
+ ?.get(0)?.let {
+ return database.getEntryById(it)
+ }
+ return null
+ }
+
fun getBundleFromListNodes(nodes: List): Bundle {
val groupsId = mutableListOf>()
val entriesId = mutableListOf>()
diff --git a/app/src/main/java/com/kunzisoft/keepass/services/KeyboardEntryNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/KeyboardEntryNotificationService.kt
index 9986b304a..557f00cd7 100644
--- a/app/src/main/java/com/kunzisoft/keepass/services/KeyboardEntryNotificationService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/services/KeyboardEntryNotificationService.kt
@@ -27,7 +27,7 @@ import android.util.Log
import android.widget.Toast
import androidx.preference.PreferenceManager
import com.kunzisoft.keepass.R
-import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
+import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsActivity.kt b/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsActivity.kt
index 048e06b7a..3d53116a3 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsActivity.kt
@@ -19,9 +19,12 @@
*/
package com.kunzisoft.keepass.settings
+import android.os.Build
+import androidx.annotation.RequiresApi
import androidx.preference.PreferenceFragmentCompat
import com.kunzisoft.keepass.R
+@RequiresApi(Build.VERSION_CODES.O)
class AutofillSettingsActivity : ExternalSettingsActivity() {
override fun retrieveTitle(): Int {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt
index abd8445e9..6e0beb5d8 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt
@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.settings
import android.os.Build
import android.os.Bundle
+import androidx.annotation.RequiresApi
import androidx.fragment.app.DialogFragment
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
@@ -29,6 +30,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistAppIdPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistWebDomainPreferenceDialogFragmentCompat
+@RequiresApi(Build.VERSION_CODES.O)
class AutofillSettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -39,11 +41,14 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
autofillInlineSuggestionsPreference?.isVisible = false
}
+
+ val autofillAskSaveDataPreference: TwoStatePreference? = findPreference(getString(R.string.autofill_ask_to_save_data_key))
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ autofillAskSaveDataPreference?.isVisible = false
+ }
}
override fun onDisplayPreferenceDialog(preference: Preference) {
- var otherDialogFragment = false
-
var dialogFragment: DialogFragment? = null
when (preference.key) {
@@ -53,7 +58,7 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
getString(R.string.autofill_web_domain_blocklist_key) -> {
dialogFragment = AutofillBlocklistWebDomainPreferenceDialogFragmentCompat.newInstance(preference.key)
}
- else -> otherDialogFragment = true
+ else -> {}
}
if (dialogFragment != null) {
@@ -62,7 +67,7 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
dialogFragment.show(parentFragmentManager, TAG_AUTOFILL_PREF_FRAGMENT)
}
// Could not be handled here. Try with the super method.
- else if (otherDialogFragment) {
+ else {
super.onDisplayPreferenceDialog(preference)
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.kt
index 9fa58d188..4f95e03ac 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.kt
@@ -21,20 +21,25 @@ package com.kunzisoft.keepass.settings
import android.content.Context
import android.os.Bundle
-import android.view.View
import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
+import kotlinx.coroutines.launch
class MainPreferenceFragment : PreferenceFragmentCompat() {
private var mCallback: Callback? = null
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
- private var mDatabaseLoaded: Boolean = false
+ private val mDatabase: ContextualDatabase?
+ get() = mDatabaseViewModel.database
override fun onAttach(context: Context) {
super.onAttach(context)
@@ -50,20 +55,24 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
mCallback = null
super.onDetach()
}
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
- mDatabaseLoaded = database?.loaded == true
- checkDatabaseLoaded()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ mDatabaseViewModel.databaseState.collect { database ->
+ checkDatabaseLoaded(database?.loaded == true)
+ }
+ }
}
- super.onViewCreated(view, savedInstanceState)
}
- private fun checkDatabaseLoaded() {
+ private fun checkDatabaseLoaded(isDatabaseLoaded: Boolean) {
findPreference(getString(R.string.settings_database_key))
- ?.isEnabled = mDatabaseLoaded
+ ?.isEnabled = isDatabaseLoaded
findPreference(getString(R.string.settings_database_category_key))
- ?.isVisible = mDatabaseLoaded
+ ?.isVisible = isDatabaseLoaded
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -119,7 +128,7 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
}
}
- checkDatabaseLoaded()
+ checkDatabaseLoaded(mDatabase?.loaded == true)
}
interface Callback {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt
index e22a6d042..7512a27d8 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt
@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.settings
import android.content.ActivityNotFoundException
import android.content.Intent
-import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
@@ -30,6 +29,7 @@ import android.view.autofill.AutofillManager
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
+import androidx.core.net.toUri
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import androidx.preference.ListPreference
@@ -47,7 +47,7 @@ import com.kunzisoft.keepass.icons.IconPackChooser
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
import com.kunzisoft.keepass.settings.preference.IconPackListPreference
import com.kunzisoft.keepass.settings.preferencedialogfragment.DurationDialogFragmentCompat
-import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
+import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
import com.kunzisoft.keepass.utils.UriUtil.openUrl
import com.kunzisoft.keepass.utils.UriUtil.releaseAllUnnecessaryPermissionUris
@@ -119,7 +119,16 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
activity?.let { activity ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val autoFillEnablePreference: TwoStatePreference? = findPreference(getString(R.string.settings_autofill_enable_key))
+
+ // Hide Passkeys settings if needed
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ findPreference(getString(R.string.passkeys_explanation_key))
+ ?.isVisible = false
+ findPreference(getString(R.string.settings_passkeys_key))
+ ?.isVisible = false
+ }
+
+ val autoFillEnablePreference: TwoStatePreference? = findPreference(getString(R.string.settings_credential_provider_enable_key))
activity.getSystemService(AutofillManager::class.java)?.let { autofillManager ->
if (autofillManager.hasEnabledAutofillServices())
autoFillEnablePreference?.isChecked = autofillManager.hasEnabledAutofillServices()
@@ -161,7 +170,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
val intent =
Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
intent.data =
- Uri.parse("package:com.kunzisoft.keepass.autofill.KeeAutofillService")
+ "package:com.kunzisoft.keepass.autofill.KeeAutofillService".toUri()
Log.d(javaClass.name, "Autofill enable service: intent=$intent")
startActivity(intent)
} else {
@@ -171,7 +180,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
}
} else {
- findPreference(getString(R.string.autofill_key))?.isVisible = false
+ findPreference(getString(R.string.credential_provider_key))?.isVisible = false
}
}
@@ -192,14 +201,28 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
false
}
- findPreference(getString(R.string.autofill_explanation_key))?.setOnPreferenceClickListener {
- context?.openUrl(R.string.autofill_explanation_url)
- false
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ findPreference(getString(R.string.passkeys_explanation_key))?.setOnPreferenceClickListener {
+ context?.openUrl(R.string.passkeys_explanation_url)
+ false
+ }
+
+ findPreference(getString(R.string.settings_passkeys_key))?.setOnPreferenceClickListener {
+ startActivity(Intent(context, PasskeysSettingsActivity::class.java))
+ false
+ }
}
- findPreference(getString(R.string.settings_autofill_key))?.setOnPreferenceClickListener {
- startActivity(Intent(context, AutofillSettingsActivity::class.java))
- false
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ findPreference(getString(R.string.autofill_explanation_key))?.setOnPreferenceClickListener {
+ context?.openUrl(R.string.autofill_explanation_url)
+ false
+ }
+
+ findPreference(getString(R.string.settings_autofill_key))?.setOnPreferenceClickListener {
+ startActivity(Intent(context, AutofillSettingsActivity::class.java))
+ false
+ }
}
findPreference(getString(R.string.clipboard_notifications_key))?.setOnPreferenceChangeListener { _, newValue ->
@@ -530,7 +553,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
super.onResume()
activity?.let { activity ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- findPreference(getString(R.string.settings_autofill_enable_key))?.let { autoFillEnablePreference ->
+ findPreference(getString(R.string.settings_credential_provider_enable_key))?.let { autoFillEnablePreference ->
val autofillManager = activity.getSystemService(AutofillManager::class.java)
autoFillEnablePreference.isChecked = autofillManager != null
&& autofillManager.hasEnabledAutofillServices()
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt
index 080ece82f..8cb5821e9 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt
@@ -19,13 +19,21 @@
*/
package com.kunzisoft.keepass.settings
-import android.graphics.Color
import android.os.Bundle
import android.util.Log
-import android.view.*
+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.core.graphics.toColorInt
import androidx.core.view.MenuProvider
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.TwoStatePreference
@@ -39,19 +47,40 @@ import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
-import com.kunzisoft.keepass.database.helper.*
+import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
-import com.kunzisoft.keepass.settings.preference.*
-import com.kunzisoft.keepass.settings.preferencedialogfragment.*
+import com.kunzisoft.keepass.settings.preference.DialogColorPreference
+import com.kunzisoft.keepass.settings.preference.DialogListExplanationPreference
+import com.kunzisoft.keepass.settings.preference.InputKdfNumberPreference
+import com.kunzisoft.keepass.settings.preference.InputKdfSizePreference
+import com.kunzisoft.keepass.settings.preference.InputNumberPreference
+import com.kunzisoft.keepass.settings.preference.InputTextPreference
+import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseColorPreferenceDialogFragmentCompat
+import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDataCompressionPreferenceDialogFragmentCompat
+import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDefaultUsernamePreferenceDialogFragmentCompat
+import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDescriptionPreferenceDialogFragmentCompat
+import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
+import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseKeyDerivationPreferenceDialogFragmentCompat
+import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat
+import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMaxHistorySizePreferenceDialogFragmentCompat
+import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMemoryUsagePreferenceDialogFragmentCompat
+import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseNamePreferenceDialogFragmentCompat
+import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseParallelismPreferenceDialogFragmentCompat
+import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRecycleBinGroupPreferenceDialogFragmentCompat
+import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat
+import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRoundsPreferenceDialogFragmentCompat
+import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseTemplatesGroupPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getSerializableCompat
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
+import kotlinx.coroutines.launch
class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
- private var mDatabase: ContextualDatabase? = null
+ private val mDatabase: ContextualDatabase?
+ get() = mDatabaseViewModel.database
private var mDatabaseReadOnly: Boolean = false
private var mMergeDataAllowed: Boolean = false
private var mDatabaseAutoSaveEnabled: Boolean = true
@@ -114,19 +143,46 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
}
}
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ mDatabaseViewModel.actionState.collect { uiState ->
+ when (uiState) {
+ is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
+ onDatabaseActionFinished(
+ uiState.database,
+ uiState.actionTask,
+ uiState.result
+ )
+ }
+
+ else -> {}
+ }
+ }
+ }
+ }
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ mDatabaseViewModel.databaseState.collect { database ->
+ database?.let {
+ onDatabaseRetrieved(database)
+ }
+ }
+ }
+ }
+ }
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
-
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
-
- mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
- mDatabase = database
- view.resetAppTimeoutWhenViewTouchedOrFocused(requireContext(), database?.loaded)
- onDatabaseRetrieved(database)
- }
-
- mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) {
- onDatabaseActionFinished(it.database, it.actionTask, it.result)
+ viewLifecycleOwner.lifecycleScope.launch {
+ mDatabaseViewModel.databaseState.collect { database ->
+ view.resetAppTimeoutWhenViewTouchedOrFocused(
+ context = requireContext(),
+ databaseLoaded = database?.loaded
+ )
+ }
}
}
@@ -167,29 +223,26 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
mDatabaseViewModel.reloadDatabase(false)
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- mDatabase = database
- mDatabaseReadOnly = database?.isReadOnly == true
- mMergeDataAllowed = database?.isMergeDataAllowed() == true
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
+ mDatabaseReadOnly = database.isReadOnly
+ mMergeDataAllowed = database.isMergeDataAllowed()
- mDatabase?.let {
- if (it.loaded) {
- when (mScreen) {
- Screen.DATABASE -> {
- onCreateDatabasePreference(it)
- }
- Screen.DATABASE_SECURITY -> {
- onCreateDatabaseSecurityPreference(it)
- }
- Screen.DATABASE_MASTER_KEY -> {
- onCreateDatabaseMasterKeyPreference(it)
- }
- else -> {
- }
+ if (database.loaded) {
+ when (mScreen) {
+ Screen.DATABASE -> {
+ onCreateDatabasePreference(database)
+ }
+ Screen.DATABASE_SECURITY -> {
+ onCreateDatabaseSecurityPreference(database)
+ }
+ Screen.DATABASE_MASTER_KEY -> {
+ onCreateDatabaseMasterKeyPreference(database)
+ }
+ else -> {
}
- } else {
- Log.e(javaClass.name, "Database isn't ready")
}
+ } else {
+ Log.e(javaClass.name, "Database isn't ready")
}
}
@@ -458,7 +511,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newDefaultUsername
} else {
- mDatabase?.defaultUsername = oldDefaultUsername
+ database.defaultUsername = oldDefaultUsername
oldDefaultUsername
}
dbDefaultUsernamePref?.summary = defaultUsernameToShow
@@ -471,7 +524,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newColor
} else {
- mDatabase?.customColor = Color.parseColor(oldColor)
+ database.customColor = oldColor.toColorInt()
oldColor
}
dbCustomColorPref?.summary = defaultColorToShow
@@ -483,7 +536,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newCompression
} else {
- mDatabase?.compressionAlgorithm = oldCompression
+ database.compressionAlgorithm = oldCompression
oldCompression
}
dbDataCompressionPref?.summary = algorithmToShow?.getLocalizedName(resources)
@@ -497,7 +550,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} else {
oldRecycleBin
}
- mDatabase?.setRecycleBin(recycleBinToShow)
+ database.setRecycleBin(recycleBinToShow)
refreshRecycleBinGroup(database)
}
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK -> {
@@ -509,7 +562,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} else {
oldTemplatesGroup
}
- mDatabase?.setTemplatesGroup(templatesGroupToShow)
+ database.setTemplatesGroup(templatesGroupToShow)
refreshTemplatesGroup(database)
}
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK -> {
@@ -519,7 +572,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newMaxHistoryItems
} else {
- mDatabase?.historyMaxItems = oldMaxHistoryItems
+ database.historyMaxItems = oldMaxHistoryItems
oldMaxHistoryItems
}
dbMaxHistoryItemsPref?.summary = maxHistoryItemsToShow.toString()
@@ -531,7 +584,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newMaxHistorySize
} else {
- mDatabase?.historyMaxSize = oldMaxHistorySize
+ database.historyMaxSize = oldMaxHistorySize
oldMaxHistorySize
}
dbMaxHistorySizePref?.summary = maxHistorySizeToShow.toString()
@@ -549,7 +602,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newEncryption
} else {
- mDatabase?.encryptionAlgorithm = oldEncryption
+ database.encryptionAlgorithm = oldEncryption
oldEncryption
}
mEncryptionAlgorithmPref?.summary = algorithmToShow.toString()
@@ -561,7 +614,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newKeyDerivationEngine
} else {
- mDatabase?.kdfEngine = oldKeyDerivationEngine
+ database.kdfEngine = oldKeyDerivationEngine
oldKeyDerivationEngine
}
mKeyDerivationPref?.summary = kdfEngineToShow.toString()
@@ -578,7 +631,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newIterations
} else {
- mDatabase?.numberKeyEncryptionRounds = oldIterations
+ database.numberKeyEncryptionRounds = oldIterations
oldIterations
}
mRoundPref?.summary = roundsToShow.toString()
@@ -590,7 +643,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newMemoryUsage
} else {
- mDatabase?.memoryUsage = oldMemoryUsage
+ database.memoryUsage = oldMemoryUsage
oldMemoryUsage
}
mMemoryPref?.summary = memoryToShow.toString()
@@ -602,7 +655,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newParallelism
} else {
- mDatabase?.parallelism = oldParallelism
+ database.parallelism = oldParallelism
oldParallelism
}
mParallelismPref?.summary = parallelismToShow.toString()
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/PasskeysSettingsActivity.kt b/app/src/main/java/com/kunzisoft/keepass/settings/PasskeysSettingsActivity.kt
new file mode 100644
index 000000000..3d7668a93
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/PasskeysSettingsActivity.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.settings
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.preference.PreferenceFragmentCompat
+import com.kunzisoft.keepass.R
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class PasskeysSettingsActivity : ExternalSettingsActivity() {
+
+ override fun retrieveTitle(): Int {
+ return R.string.passkeys_preference_title
+ }
+
+ override fun retrievePreferenceFragment(): PreferenceFragmentCompat {
+ return PasskeysSettingsFragment()
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/PasskeysSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/PasskeysSettingsFragment.kt
new file mode 100644
index 000000000..4a9a6dec8
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/PasskeysSettingsFragment.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.settings
+
+import android.os.Build
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import androidx.fragment.app.DialogFragment
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.settings.preferencedialogfragment.PasskeysPrivilegedAppsPreferenceDialogFragmentCompat
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class PasskeysSettingsFragment : PreferenceFragmentCompat() {
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ // Load the preferences from an XML resource
+ setPreferencesFromResource(R.xml.preferences_passkeys, rootKey)
+ }
+
+ @Suppress("DEPRECATION")
+ override fun onDisplayPreferenceDialog(preference: Preference) {
+ var dialogFragment: DialogFragment? = null
+
+ when (preference.key) {
+ getString(R.string.passkeys_privileged_apps_key) -> {
+ dialogFragment = PasskeysPrivilegedAppsPreferenceDialogFragmentCompat.newInstance(preference.key)
+ }
+ else -> {}
+ }
+
+ if (dialogFragment != null) {
+ dialogFragment.setTargetFragment(this, 0)
+ dialogFragment.show(parentFragmentManager, TAG_PASSKEYS_PREF_FRAGMENT)
+ } else {
+ super.onDisplayPreferenceDialog(preference)
+ }
+ }
+
+ companion object {
+
+ private const val TAG_PASSKEYS_PREF_FRAGMENT = "TAG_PASSKEYS_PREF_FRAGMENT"
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt
index 9bc6ae1c6..5604a4a6c 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt
@@ -35,8 +35,8 @@ import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.password.PassphraseGenerator
import com.kunzisoft.keepass.timeout.TimeoutHelper
+import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
-import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
import java.util.Properties
object PreferencesUtil {
@@ -108,7 +108,7 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.auto_focus_search_default))
}
- fun searchSubdomains(context: Context): Boolean {
+ fun searchSubDomains(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.subdomain_search_key),
context.resources.getBoolean(R.bool.subdomain_search_default))
@@ -352,6 +352,8 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.search_option_username_default))
searchInPasswords = prefs.getBoolean(context.getString(R.string.search_option_password_key),
context.resources.getBoolean(R.bool.search_option_password_default))
+ searchInAppIds = prefs.getBoolean(context.getString(R.string.search_option_application_id_key),
+ context.resources.getBoolean(R.bool.search_option_application_id_default))
searchInUrls = prefs.getBoolean(context.getString(R.string.search_option_url_key),
context.resources.getBoolean(R.bool.search_option_url_default))
searchInExpired = prefs.getBoolean(context.getString(R.string.search_option_expired_key),
@@ -389,6 +391,8 @@ object PreferencesUtil {
searchParameters.searchInUsernames)
putBoolean(context.getString(R.string.search_option_password_key),
searchParameters.searchInPasswords)
+ putBoolean(context.getString(R.string.search_option_application_id_key),
+ searchParameters.searchInAppIds)
putBoolean(context.getString(R.string.search_option_url_key),
searchParameters.searchInUrls)
putBoolean(context.getString(R.string.search_option_expired_key),
@@ -686,6 +690,32 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.keyboard_previous_lock_default))
}
+ fun isPasskeyCloseDatabaseEnable(context: Context): Boolean {
+ val prefs = PreferenceManager.getDefaultSharedPreferences(context)
+ return prefs.getBoolean(context.getString(R.string.passkeys_close_database_key),
+ context.resources.getBoolean(R.bool.passkeys_close_database_default))
+ }
+
+ fun isPasskeyBackupEligibilityEnable(context: Context): Boolean {
+ val prefs = PreferenceManager.getDefaultSharedPreferences(context)
+ return prefs.getBoolean(context.getString(R.string.passkeys_backup_eligibility_key),
+ context.resources.getBoolean(R.bool.passkeys_backup_eligibility_default))
+ }
+
+ fun isPasskeyAutoSelectEnable(context: Context): Boolean {
+ val prefs = PreferenceManager.getDefaultSharedPreferences(context)
+ return prefs.getBoolean(context.getString(R.string.passkeys_auto_select_key),
+ context.resources.getBoolean(R.bool.passkeys_auto_select_default))
+ }
+
+ fun isPasskeyBackupStateEnable(context: Context): Boolean {
+ if (!isPasskeyBackupEligibilityEnable(context))
+ return false
+ val prefs = PreferenceManager.getDefaultSharedPreferences(context)
+ return prefs.getBoolean(context.getString(R.string.passkeys_backup_state_key),
+ context.resources.getBoolean(R.bool.passkeys_backup_state_default))
+ }
+
fun isAutofillCloseDatabaseEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.autofill_close_database_key),
@@ -821,7 +851,7 @@ object PreferencesUtil {
context.getString(R.string.clipboard_notifications_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.clear_clipboard_notification_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.clipboard_timeout_key) -> editor.putString(name, value.toLong().toString())
- context.getString(R.string.settings_autofill_enable_key) -> editor.putBoolean(name, value.toBoolean())
+ context.getString(R.string.settings_credential_provider_enable_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_notification_entry_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_notification_entry_clear_close_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_entry_timeout_key) -> editor.putString(name, value.toLong().toString())
@@ -834,6 +864,10 @@ object PreferencesUtil {
context.getString(R.string.keyboard_previous_search_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_previous_fill_in_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_previous_lock_key) -> editor.putBoolean(name, value.toBoolean())
+ context.getString(R.string.passkeys_close_database_key) -> editor.putBoolean(name, value.toBoolean())
+ context.getString(R.string.passkeys_auto_select_key) -> editor.putBoolean(name, value.toBoolean())
+ context.getString(R.string.passkeys_backup_eligibility_key) -> editor.putBoolean(name, value.toBoolean())
+ context.getString(R.string.passkeys_backup_state_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean())
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt
index 7c996aac3..880a979ac 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt
@@ -159,10 +159,6 @@ open class SettingsActivity
return coordinatorLayout
}
- override fun finishActivityIfDatabaseNotLoaded(): Boolean {
- return false
- }
-
override fun onDatabaseActionFinished(
database: ContextualDatabase,
actionTask: String,
@@ -192,7 +188,7 @@ open class SettingsActivity
}
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
- assignPassword(mainCredential)
+ assignMainCredential(mainCredential)
}
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/DurationDialogPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/DurationDialogPreference.kt
index 46f7a4e92..dfaaeb2f0 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/DurationDialogPreference.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/DurationDialogPreference.kt
@@ -52,6 +52,7 @@ class DurationDialogPreference @JvmOverloads constructor(context: Context,
notifyChanged()
}
+ @Deprecated(message = "")
override fun onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any?) {
if (restorePersistedValue) {
mDuration = getPersistedString(mDuration.toString()).toLongOrNull() ?: mDuration
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseColorPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseColorPreferenceDialogFragmentCompat.kt
index 516fff33e..6d6904cdc 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseColorPreferenceDialogFragmentCompat.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseColorPreferenceDialogFragmentCompat.kt
@@ -95,20 +95,16 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
return dialog
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
-
- database?.let {
- var initColor = it.customColor
- if (initColor != null) {
- enableSwitchView.isChecked = true
- } else {
- enableSwitchView.isChecked = false
- initColor = DEFAULT_COLOR
- }
- chromaColorView.currentColor = initColor
- arguments?.putInt(ARG_INITIAL_COLOR, initColor)
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
+ var initColor = database.customColor
+ if (initColor != null) {
+ enableSwitchView.isChecked = true
+ } else {
+ enableSwitchView.isChecked = false
+ initColor = DEFAULT_COLOR
}
+ chromaColorView.currentColor = initColor
+ arguments?.putInt(ARG_INITIAL_COLOR, initColor)
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDataCompressionPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDataCompressionPreferenceDialogFragmentCompat.kt
index 98a34265b..a0511892d 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDataCompressionPreferenceDialogFragmentCompat.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDataCompressionPreferenceDialogFragmentCompat.kt
@@ -50,16 +50,14 @@ class DatabaseDataCompressionPreferenceDialogFragmentCompat
}
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
setExplanationText(R.string.database_data_compression_summary)
-
mRecyclerView?.adapter = mCompressionAdapter
-
- database?.let {
- compressionSelected = it.compressionAlgorithm
- mCompressionAdapter?.setItems(it.availableCompressionAlgorithms, compressionSelected)
- }
+ compressionSelected = database.compressionAlgorithm
+ mCompressionAdapter?.setItems(
+ items = database.availableCompressionAlgorithms,
+ itemUsed = compressionSelected
+ )
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDefaultUsernamePreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDefaultUsernamePreferenceDialogFragmentCompat.kt
index 9f0f6d116..94c13a467 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDefaultUsernamePreferenceDialogFragmentCompat.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDefaultUsernamePreferenceDialogFragmentCompat.kt
@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
class DatabaseDefaultUsernamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
- inputText = database?.defaultUsername?: ""
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
+ inputText = database.defaultUsername
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDescriptionPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDescriptionPreferenceDialogFragmentCompat.kt
index 496d14835..21337d600 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDescriptionPreferenceDialogFragmentCompat.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDescriptionPreferenceDialogFragmentCompat.kt
@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
class DatabaseDescriptionPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
- inputText = database?.description ?: ""
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
+ inputText = database.description
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.kt
index 2f76bb07b..a0e82b50f 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.kt
@@ -51,12 +51,9 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
}
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
- database?.let {
- algorithmSelected = database.encryptionAlgorithm
- mEncryptionAlgorithmAdapter?.setItems(database.availableEncryptionAlgorithms, algorithmSelected)
- }
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
+ algorithmSelected = database.encryptionAlgorithm
+ mEncryptionAlgorithmAdapter?.setItems(database.availableEncryptionAlgorithms, algorithmSelected)
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseKeyDerivationPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseKeyDerivationPreferenceDialogFragmentCompat.kt
index 12aac2292..0c7bbe7fc 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseKeyDerivationPreferenceDialogFragmentCompat.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseKeyDerivationPreferenceDialogFragmentCompat.kt
@@ -54,12 +54,12 @@ class DatabaseKeyDerivationPreferenceDialogFragmentCompat
}
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
- database?.let {
- kdfEngineSelected = database.kdfEngine
- mKdfAdapter?.setItems(database.availableKdfEngines, kdfEngineSelected)
- }
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
+ kdfEngineSelected = database.kdfEngine
+ mKdfAdapter?.setItems(
+ items = database.availableKdfEngines,
+ itemUsed = kdfEngineSelected
+ )
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat.kt
index d253cc6fd..025a109f8 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat.kt
@@ -31,19 +31,17 @@ class DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat : DatabaseSavePrefer
setExplanationText(R.string.max_history_items_summary)
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
- database?.historyMaxItems?.let { maxItemsDatabase ->
- inputText = maxItemsDatabase.toString()
- setSwitchAction({ isChecked ->
- inputText = if (!isChecked) {
- NONE_MAX_HISTORY_ITEMS.toString()
- } else {
- DEFAULT_MAX_HISTORY_ITEMS.toString()
- }
- showInputText(isChecked)
- }, maxItemsDatabase > NONE_MAX_HISTORY_ITEMS)
- }
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
+ val maxItemsDatabase = database.historyMaxItems
+ inputText = maxItemsDatabase.toString()
+ setSwitchAction({ isChecked ->
+ inputText = if (!isChecked) {
+ NONE_MAX_HISTORY_ITEMS.toString()
+ } else {
+ DEFAULT_MAX_HISTORY_ITEMS.toString()
+ }
+ showInputText(isChecked)
+ }, maxItemsDatabase > NONE_MAX_HISTORY_ITEMS)
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseMaxHistorySizePreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseMaxHistorySizePreferenceDialogFragmentCompat.kt
index 845d7c4b0..99a038a65 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseMaxHistorySizePreferenceDialogFragmentCompat.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseMaxHistorySizePreferenceDialogFragmentCompat.kt
@@ -34,31 +34,29 @@ class DatabaseMaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePrefere
setExplanationText(R.string.max_history_size_summary)
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
- database?.historyMaxSize?.let { maxItemsDatabase ->
- dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE)
- .toBetterByteFormat()
- inputText = dataByte.number.toString()
- if (dataByte.number >= 0) {
- setUnitText(dataByte.format.stringId)
- } else {
- unitText = null
- }
-
- setSwitchAction({ isChecked ->
- if (!isChecked) {
- dataByte = INFINITE_MAX_HISTORY_SIZE_DATA_BYTE
- inputText = INFINITE_MAX_HISTORY_SIZE.toString()
- unitText = null
- } else {
- dataByte = DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE
- inputText = dataByte.number.toString()
- setUnitText(dataByte.format.stringId)
- }
- showInputText(isChecked)
- }, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
+ val maxItemsDatabase = database.historyMaxSize
+ dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE)
+ .toBetterByteFormat()
+ inputText = dataByte.number.toString()
+ if (dataByte.number >= 0) {
+ setUnitText(dataByte.format.stringId)
+ } else {
+ unitText = null
}
+
+ setSwitchAction({ isChecked ->
+ if (!isChecked) {
+ dataByte = INFINITE_MAX_HISTORY_SIZE_DATA_BYTE
+ inputText = INFINITE_MAX_HISTORY_SIZE.toString()
+ unitText = null
+ } else {
+ dataByte = DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE
+ inputText = dataByte.number.toString()
+ setUnitText(dataByte.format.stringId)
+ }
+ showInputText(isChecked)
+ }, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseMemoryUsagePreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseMemoryUsagePreferenceDialogFragmentCompat.kt
index 53724b180..cf20b927e 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseMemoryUsagePreferenceDialogFragmentCompat.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseMemoryUsagePreferenceDialogFragmentCompat.kt
@@ -34,15 +34,12 @@ class DatabaseMemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreference
setExplanationText(R.string.memory_usage_explanation)
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
- database?.let {
- val memoryBytes = database.memoryUsage
- dataByte = DataByte(memoryBytes, DataByte.ByteFormat.BYTE)
- .toBetterByteFormat()
- inputText = dataByte.number.toString()
- setUnitText(dataByte.format.stringId)
- }
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
+ val memoryBytes = database.memoryUsage
+ dataByte = DataByte(memoryBytes, DataByte.ByteFormat.BYTE)
+ .toBetterByteFormat()
+ inputText = dataByte.number.toString()
+ setUnitText(dataByte.format.stringId)
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseNamePreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseNamePreferenceDialogFragmentCompat.kt
index 9ce8af0b7..be24c70ec 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseNamePreferenceDialogFragmentCompat.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseNamePreferenceDialogFragmentCompat.kt
@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
class DatabaseNamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
- inputText = database?.name ?: ""
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
+ inputText = database.name
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseParallelismPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseParallelismPreferenceDialogFragmentCompat.kt
index 2dfd7c8df..a70b5bdfe 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseParallelismPreferenceDialogFragmentCompat.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseParallelismPreferenceDialogFragmentCompat.kt
@@ -31,9 +31,8 @@ class DatabaseParallelismPreferenceDialogFragmentCompat : DatabaseSavePreference
setExplanationText(R.string.parallelism_explanation)
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
- inputText = database?.parallelism?.toString() ?: MIN_PARALLELISM.toString()
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
+ inputText = database.parallelism.toString()
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseRecycleBinGroupPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseRecycleBinGroupPreferenceDialogFragmentCompat.kt
index 8574dfc61..38659077b 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseRecycleBinGroupPreferenceDialogFragmentCompat.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseRecycleBinGroupPreferenceDialogFragmentCompat.kt
@@ -48,12 +48,9 @@ class DatabaseRecycleBinGroupPreferenceDialogFragmentCompat
}
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
- database?.let {
- mGroupRecycleBin = database.recycleBin
- mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupRecycleBin)
- }
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
+ mGroupRecycleBin = database.recycleBin
+ mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupRecycleBin)
}
override fun onItemSelected(item: Group) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat.kt
index 34862d853..4e7f19d68 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat.kt
@@ -46,6 +46,8 @@ class DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat : DatabaseSavePre
}
}
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {}
+
companion object {
fun newInstance(key: String): DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseRoundsPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseRoundsPreferenceDialogFragmentCompat.kt
index df16f8cd4..248b0374c 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseRoundsPreferenceDialogFragmentCompat.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseRoundsPreferenceDialogFragmentCompat.kt
@@ -32,9 +32,8 @@ class DatabaseRoundsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialo
explanationText = getString(R.string.rounds_explanation)
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
- inputText = database?.numberKeyEncryptionRounds?.toString() ?: MIN_ITERATIONS.toString()
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
+ inputText = database.numberKeyEncryptionRounds.toString()
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseSavePreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseSavePreferenceDialogFragmentCompat.kt
index 563c70d5c..2b6829b49 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseSavePreferenceDialogFragmentCompat.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseSavePreferenceDialogFragmentCompat.kt
@@ -22,6 +22,9 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.database.ContextualDatabase
@@ -32,13 +35,15 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
+import kotlinx.coroutines.launch
abstract class DatabaseSavePreferenceDialogFragmentCompat
: InputPreferenceDialogFragmentCompat(), DatabaseRetrieval {
private var mDatabaseAutoSaveEnable = true
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
- private var mDatabase: ContextualDatabase? = null
+ protected val mDatabase: ContextualDatabase?
+ get() = mDatabaseViewModel.database
override fun onAttach(context: Context) {
super.onAttach(context)
@@ -47,18 +52,32 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- mDatabaseViewModel.database.observe(this) { database ->
- onDatabaseRetrieved(database)
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ mDatabaseViewModel.actionState.collect { uiState ->
+ when (uiState) {
+ is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
+ onDatabaseActionFinished(
+ uiState.database,
+ uiState.actionTask,
+ uiState.result
+ )
+ }
+
+ else -> {}
+ }
+ }
+ }
+ }
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ mDatabaseViewModel.databaseState.collect { database ->
+ database?.let {
+ onDatabaseRetrieved(database)
+ }
+ }
+ }
}
- }
-
- override fun onResume() {
- super.onResume()
- onDatabaseRetrieved(mDatabase)
- }
-
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- this.mDatabase = database
}
override fun onDatabaseActionFinished(
@@ -77,8 +96,10 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
// To inherit to save element in database
}
- protected fun saveColor(oldColor: Int?,
- newColor: Int?) {
+ protected fun saveColor(
+ oldColor: Int?,
+ newColor: Int?
+ ) {
val oldColorString = if (oldColor != null)
ChromaUtil.getFormattedColorString(oldColor, false)
else
@@ -87,77 +108,158 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
ChromaUtil.getFormattedColorString(newColor, false)
else
""
- mDatabaseViewModel.saveColor(oldColorString, newColorString, mDatabaseAutoSaveEnable)
+ mDatabaseViewModel.saveColor(
+ oldColorString,
+ newColorString,
+ mDatabaseAutoSaveEnable
+ )
}
- protected fun saveCompression(oldCompression: CompressionAlgorithm,
- newCompression: CompressionAlgorithm
+ protected fun saveCompression(
+ oldCompression: CompressionAlgorithm,
+ newCompression: CompressionAlgorithm
) {
- mDatabaseViewModel.saveCompression(oldCompression, newCompression, mDatabaseAutoSaveEnable)
+ mDatabaseViewModel.saveCompression(
+ oldCompression,
+ newCompression,
+ mDatabaseAutoSaveEnable
+ )
}
- protected fun saveDefaultUsername(oldUsername: String,
- newUsername: String) {
- mDatabaseViewModel.saveDefaultUsername(oldUsername, newUsername, mDatabaseAutoSaveEnable)
+ protected fun saveDefaultUsername(
+ oldUsername: String,
+ newUsername: String
+ ) {
+ mDatabaseViewModel.saveDefaultUsername(
+ oldUsername,
+ newUsername,
+ mDatabaseAutoSaveEnable
+ )
}
- protected fun saveDescription(oldDescription: String,
- newDescription: String) {
- mDatabaseViewModel.saveDescription(oldDescription, newDescription, mDatabaseAutoSaveEnable)
+ protected fun saveDescription(
+ oldDescription: String,
+ newDescription: String
+ ) {
+ mDatabaseViewModel.saveDescription(
+ oldDescription,
+ newDescription,
+ mDatabaseAutoSaveEnable
+ )
}
- protected fun saveEncryption(oldEncryption: EncryptionAlgorithm,
- newEncryptionAlgorithm: EncryptionAlgorithm) {
- mDatabaseViewModel.saveEncryption(oldEncryption, newEncryptionAlgorithm, mDatabaseAutoSaveEnable)
+ protected fun saveEncryption(
+ oldEncryption: EncryptionAlgorithm,
+ newEncryptionAlgorithm: EncryptionAlgorithm
+ ) {
+ mDatabaseViewModel.saveEncryption(
+ oldEncryption,
+ newEncryptionAlgorithm,
+ mDatabaseAutoSaveEnable
+ )
}
- protected fun saveKeyDerivation(oldKeyDerivation: KdfEngine,
- newKeyDerivation: KdfEngine) {
- mDatabaseViewModel.saveKeyDerivation(oldKeyDerivation, newKeyDerivation, mDatabaseAutoSaveEnable)
+ protected fun saveKeyDerivation(
+ oldKeyDerivation: KdfEngine,
+ newKeyDerivation: KdfEngine
+ ) {
+ mDatabaseViewModel.saveKeyDerivation(
+ oldKeyDerivation,
+ newKeyDerivation,
+ mDatabaseAutoSaveEnable
+ )
}
- protected fun saveName(oldName: String,
- newName: String) {
- mDatabaseViewModel.saveName(oldName, newName, mDatabaseAutoSaveEnable)
+ protected fun saveName(
+ oldName: String,
+ newName: String
+ ) {
+ mDatabaseViewModel.saveName(
+ oldName,
+ newName,
+ mDatabaseAutoSaveEnable
+ )
}
- protected fun saveRecycleBin(oldGroup: Group?,
- newGroup: Group?) {
- mDatabaseViewModel.saveRecycleBin(oldGroup, newGroup, mDatabaseAutoSaveEnable)
+ protected fun saveRecycleBin(
+ oldGroup: Group?,
+ newGroup: Group?
+ ) {
+ mDatabaseViewModel.saveRecycleBin(
+ oldGroup,
+ newGroup,
+ mDatabaseAutoSaveEnable
+ )
}
protected fun removeUnlinkedData() {
mDatabaseViewModel.removeUnlinkedData(mDatabaseAutoSaveEnable)
}
- protected fun saveTemplatesGroup(oldGroup: Group?,
- newGroup: Group?) {
- mDatabaseViewModel.saveTemplatesGroup(oldGroup, newGroup, mDatabaseAutoSaveEnable)
+ protected fun saveTemplatesGroup(
+ oldGroup: Group?,
+ newGroup: Group?
+ ) {
+ mDatabaseViewModel.saveTemplatesGroup(
+ oldGroup,
+ newGroup,
+ mDatabaseAutoSaveEnable
+ )
}
- protected fun saveMaxHistoryItems(oldNumber: Int,
- newNumber: Int) {
- mDatabaseViewModel.saveMaxHistoryItems(oldNumber, newNumber, mDatabaseAutoSaveEnable)
+ protected fun saveMaxHistoryItems(
+ oldNumber: Int,
+ newNumber: Int
+ ) {
+ mDatabaseViewModel.saveMaxHistoryItems(
+ oldNumber,
+ newNumber,
+ mDatabaseAutoSaveEnable
+ )
}
- protected fun saveMaxHistorySize(oldNumber: Long,
- newNumber: Long) {
- mDatabaseViewModel.saveMaxHistorySize(oldNumber, newNumber, mDatabaseAutoSaveEnable)
+ protected fun saveMaxHistorySize(
+ oldNumber: Long,
+ newNumber: Long
+ ) {
+ mDatabaseViewModel.saveMaxHistorySize(
+ oldNumber,
+ newNumber,
+ mDatabaseAutoSaveEnable
+ )
}
- protected fun saveMemoryUsage(oldNumber: Long,
- newNumber: Long) {
- mDatabaseViewModel.saveMemoryUsage(oldNumber, newNumber, mDatabaseAutoSaveEnable)
+ protected fun saveMemoryUsage(
+ oldNumber: Long,
+ newNumber: Long
+ ) {
+ mDatabaseViewModel.saveMemoryUsage(
+ oldNumber,
+ newNumber,
+ mDatabaseAutoSaveEnable
+ )
}
- protected fun saveParallelism(oldNumber: Long,
- newNumber: Long) {
- mDatabaseViewModel.saveParallelism(oldNumber, newNumber, mDatabaseAutoSaveEnable)
+ protected fun saveParallelism(
+ oldNumber: Long,
+ newNumber: Long
+ ) {
+ mDatabaseViewModel.saveParallelism(
+ oldNumber,
+ newNumber,
+ mDatabaseAutoSaveEnable
+ )
}
- protected fun saveIterations(oldNumber: Long,
- newNumber: Long) {
- mDatabaseViewModel.saveIterations(oldNumber, newNumber, mDatabaseAutoSaveEnable)
+ protected fun saveIterations(
+ oldNumber: Long,
+ newNumber: Long
+ ) {
+ mDatabaseViewModel.saveIterations(
+ oldNumber,
+ newNumber,
+ mDatabaseAutoSaveEnable
+ )
}
companion object {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseTemplatesGroupPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseTemplatesGroupPreferenceDialogFragmentCompat.kt
index 1919420ef..ba4b6349b 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseTemplatesGroupPreferenceDialogFragmentCompat.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseTemplatesGroupPreferenceDialogFragmentCompat.kt
@@ -48,12 +48,9 @@ class DatabaseTemplatesGroupPreferenceDialogFragmentCompat
}
}
- override fun onDatabaseRetrieved(database: ContextualDatabase?) {
- super.onDatabaseRetrieved(database)
- database?.let {
- mGroupTemplates = database.templatesGroup
- mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupTemplates)
- }
+ override fun onDatabaseRetrieved(database: ContextualDatabase) {
+ mGroupTemplates = database.templatesGroup
+ mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupTemplates)
}
override fun onItemSelected(item: Group) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/PasskeysPrivilegedAppsPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/PasskeysPrivilegedAppsPreferenceDialogFragmentCompat.kt
new file mode 100644
index 000000000..ad13c3f0a
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/PasskeysPrivilegedAppsPreferenceDialogFragmentCompat.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.settings.preferencedialogfragment
+
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
+import com.kunzisoft.keepass.settings.preferencedialogfragment.adapter.ListSelectionItemAdapter
+import com.kunzisoft.keepass.settings.preferencedialogfragment.viewmodel.PasskeysPrivilegedAppsViewModel
+import kotlinx.coroutines.launch
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class PasskeysPrivilegedAppsPreferenceDialogFragmentCompat
+ : InputPreferenceDialogFragmentCompat() {
+
+ private var mAdapter = ListSelectionItemAdapter()
+ private val passkeysPrivilegedAppsViewModel : PasskeysPrivilegedAppsViewModel by activityViewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ lifecycleScope.launch {
+ lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ passkeysPrivilegedAppsViewModel.retrievePrivilegedAppsToSelect()
+
+ passkeysPrivilegedAppsViewModel.uiState.collect { uiState ->
+ when(uiState) {
+ is PasskeysPrivilegedAppsViewModel.UiState.Loading -> {}
+ is PasskeysPrivilegedAppsViewModel.UiState.OnPrivilegedAppsToSelectRetrieved -> {
+ mAdapter.apply {
+ setItems(uiState.privilegedApps)
+ selectedItems = uiState.selected.toMutableList()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun onBindDialogView(view: View) {
+ super.onBindDialogView(view)
+ setExplanationText(R.string.passkeys_privileged_apps_explanation)
+ view.findViewById(R.id.pref_dialog_list).apply {
+ layoutManager = LinearLayoutManager(context)
+ adapter = mAdapter
+ }
+ }
+
+ override fun onDialogClosed(positiveResult: Boolean) {
+ if (positiveResult) {
+ passkeysPrivilegedAppsViewModel.saveSelectedPrivilegedApp(mAdapter.selectedItems)
+ }
+ }
+
+ companion object {
+
+ fun newInstance(key: String): PasskeysPrivilegedAppsPreferenceDialogFragmentCompat {
+ val fragment = PasskeysPrivilegedAppsPreferenceDialogFragmentCompat()
+ val bundle = Bundle(1)
+ bundle.putString(ARG_KEY, key)
+ fragment.arguments = bundle
+
+ return fragment
+ }
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/adapter/ListSelectionItemAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/adapter/ListSelectionItemAdapter.kt
new file mode 100644
index 000000000..c20f632de
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/adapter/ListSelectionItemAdapter.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.settings.preferencedialogfragment.adapter
+
+import android.annotation.SuppressLint
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.kunzisoft.keepass.R
+
+class ListSelectionItemAdapter()
+ : RecyclerView.Adapter() {
+
+ private val itemList: MutableList = mutableListOf()
+ var selectedItems: MutableList = mutableListOf()
+ @SuppressLint("NotifyDataSetChanged")
+ set(value) {
+ field = value
+ notifyDataSetChanged()
+ }
+
+ var itemSelectedCallback: ItemSelectedCallback? = null
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SelectionViewHolder {
+ return SelectionViewHolder(LayoutInflater.from(parent.context)
+ .inflate(R.layout.pref_dialog_list_item, parent, false))
+ }
+
+ @SuppressLint("SetTextI18n", "NotifyDataSetChanged")
+ override fun onBindViewHolder(holder: SelectionViewHolder, position: Int) {
+ val item = itemList[position]
+
+ holder.container.apply {
+ isSelected = selectedItems.contains(item)
+ }
+ holder.textView.apply {
+ text = item.toString()
+ setOnClickListener {
+ if (selectedItems.contains(item))
+ selectedItems.remove(item)
+ else
+ selectedItems.add(item)
+ itemSelectedCallback?.onItemSelected(item)
+ notifyDataSetChanged()
+ }
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return itemList.size
+ }
+
+ fun setItems(items: List) {
+ this.itemList.clear()
+ this.itemList.addAll(items)
+ }
+
+ interface ItemSelectedCallback {
+ fun onItemSelected(item: T)
+ }
+
+ class SelectionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ var textView: TextView = itemView.findViewById(R.id.pref_dialog_list_text)
+ var container: ViewGroup = itemView.findViewById(R.id.pref_dialog_list_container)
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/viewmodel/PasskeysPrivilegedAppsViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/viewmodel/PasskeysPrivilegedAppsViewModel.kt
new file mode 100644
index 000000000..6179c668f
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/viewmodel/PasskeysPrivilegedAppsViewModel.kt
@@ -0,0 +1,61 @@
+package com.kunzisoft.keepass.settings.preferencedialogfragment.viewmodel
+
+import android.app.Application
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.deletePrivilegedAppsFile
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.retrieveCustomPrivilegedApps
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.retrievePredefinedPrivilegedApps
+import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps
+import com.kunzisoft.keepass.utils.AppUtil.getInstalledBrowsersWithSignatures
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class PasskeysPrivilegedAppsViewModel(application: Application): AndroidViewModel(application) {
+
+ private val _uiState = MutableStateFlow(UiState.Loading)
+ val uiState: StateFlow = _uiState
+
+ fun retrievePrivilegedAppsToSelect() {
+ viewModelScope.launch {
+ val predefinedPrivilegedApps = retrievePredefinedPrivilegedApps(getApplication())
+ val customPrivilegedApps = retrieveCustomPrivilegedApps(getApplication())
+ // Only retrieve browser apps that are not already in the predefined list
+ val browserApps = getInstalledBrowsersWithSignatures(getApplication()).filter {
+ predefinedPrivilegedApps.none { privilegedApp ->
+ privilegedApp.packageName == it.packageName
+ && privilegedApp.fingerprints.any {
+ fingerprint -> fingerprint in it.fingerprints
+ }
+ }
+ }
+ _uiState.value = UiState.OnPrivilegedAppsToSelectRetrieved(
+ privilegedApps = browserApps,
+ selected = customPrivilegedApps
+ )
+ }
+ }
+
+ fun saveSelectedPrivilegedApp(privilegedApps: List) {
+ viewModelScope.launch {
+ if (privilegedApps.isNotEmpty())
+ saveCustomPrivilegedApps(getApplication(), privilegedApps)
+ else
+ deletePrivilegedAppsFile(getApplication())
+ }
+ }
+
+ sealed class UiState {
+
+ object Loading : UiState()
+ data class OnPrivilegedAppsToSelectRetrieved(
+ val privilegedApps: List,
+ val selected: List
+ ) : UiState()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/AppUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/AppUtil.kt
new file mode 100644
index 000000000..88e3741ef
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/utils/AppUtil.kt
@@ -0,0 +1,122 @@
+package com.kunzisoft.keepass.utils
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.net.toUri
+import com.kunzisoft.encrypt.Signature.getAllFingerprints
+import com.kunzisoft.keepass.BuildConfig
+import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
+import com.kunzisoft.keepass.education.Education
+
+object AppUtil {
+
+ fun randomRequestCode(): Int {
+ return (Math.random() * Integer.MAX_VALUE).toInt()
+ }
+
+ fun Context.isExternalAppInstalled(packageName: String, showError: Boolean = true): Boolean {
+ try {
+ this.applicationContext.packageManager.getPackageInfoCompat(
+ packageName,
+ PackageManager.GET_ACTIVITIES
+ )
+ Education.setEducationScreenReclickedPerformed(this)
+ return true
+ } catch (e: Exception) {
+ if (showError)
+ Log.e(AppUtil::class.simpleName, "App not accessible", e)
+ }
+ return false
+ }
+
+ fun Context.openExternalApp(packageName: String, sourcesURL: String? = null) {
+ var launchIntent: Intent? = null
+ try {
+ launchIntent = this.packageManager.getLaunchIntentForPackage(packageName)?.apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ } catch (ignored: Exception) { }
+ try {
+ if (launchIntent == null) {
+ this.startActivity(
+ Intent(Intent.ACTION_VIEW)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .setData(
+ if (sourcesURL != null
+ && !BuildConfig.CLOSED_STORE
+ ) {
+ sourcesURL
+ } else {
+ this.getString(
+ if (BuildConfig.CLOSED_STORE)
+ R.string.play_store_url
+ else
+ R.string.f_droid_url,
+ packageName
+ )
+ }.toUri()
+ )
+ )
+ } else {
+ this.startActivity(launchIntent)
+ }
+ } catch (e: Exception) {
+ Log.e(AppUtil::class.simpleName, "App cannot be open", e)
+ }
+ }
+
+ fun Context.isContributingUser(): Boolean {
+ return (Education.isEducationScreenReclickedPerformed(this)
+ || isExternalAppInstalled(this.getString(R.string.keepro_app_id), false)
+ )
+ }
+
+ @RequiresApi(Build.VERSION_CODES.P)
+ fun getInstalledBrowsersWithSignatures(context: Context): List {
+ val packageManager = context.packageManager
+ val browserList = mutableListOf()
+
+ // Create a generic web intent
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.data = context.getString(R.string.homepage_url).toUri()
+
+ // Query for apps that can handle this intent
+ val resolveInfoList: List = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ packageManager.queryIntentActivities(
+ intent,
+ PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_ALL.toLong())
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ packageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL)
+ }
+
+ val processedPackageNames = mutableSetOf()
+
+ for (resolveInfo in resolveInfoList) {
+ val packageName = resolveInfo.activityInfo.packageName
+ if (packageName != null && !processedPackageNames.contains(packageName)) {
+ try {
+ val packageInfo = packageManager.getPackageInfo(
+ packageName,
+ PackageManager.GET_SIGNING_CERTIFICATES
+ )
+ val signatureFingerprints = packageInfo.signingInfo?.getAllFingerprints()
+ signatureFingerprints?.let {
+ browserList.add(AndroidPrivilegedApp(packageName, signatureFingerprints))
+ processedPackageNames.add(packageName)
+ }
+ } catch (e: Exception) {
+ Log.e(AppUtil::class.simpleName, "Error processing package: $packageName", e)
+ }
+ }
+ }
+ return browserList.distinctBy { it.packageName } // Ensure uniqueness just in case
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt b/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt
index 6244ecbda..8c4eb9e91 100644
--- a/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt
@@ -33,7 +33,7 @@ import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.AppLifecycleObserver
import com.kunzisoft.keepass.database.ContextualDatabase
-import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
+import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/IOActionTask.kt b/app/src/main/java/com/kunzisoft/keepass/utils/IOActionTask.kt
index 5d4ff9e41..194236457 100644
--- a/app/src/main/java/com/kunzisoft/keepass/utils/IOActionTask.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/utils/IOActionTask.kt
@@ -19,30 +19,39 @@
*/
package com.kunzisoft.keepass.utils
-import kotlinx.coroutines.*
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlin.coroutines.EmptyCoroutineContext
/**
* Class to invoke action in a separate IO thread
*/
class IOActionTask(
- private val action: () -> T ,
- private val afterActionListener: ((T?) -> Unit)? = null) {
-
- private val mainScope = CoroutineScope(Dispatchers.Main)
-
+ private val action: () -> T,
+ private val onActionComplete: ((T?) -> Unit)? = null,
+ private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main),
+ private val exceptionHandler: CoroutineExceptionHandler? = null
+) {
fun execute() {
- mainScope.launch {
+ scope.launch(exceptionHandler ?: EmptyCoroutineContext) {
withContext(Dispatchers.IO) {
val asyncResult: Deferred = async {
- try {
- action.invoke()
- } catch (e: Exception) {
- e.printStackTrace()
- null
- }
+ exceptionHandler?.let {
+ action.invoke()
+ } ?: try {
+ action.invoke()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
}
withContext(Dispatchers.Main) {
- afterActionListener?.invoke(asyncResult.await())
+ onActionComplete?.invoke(asyncResult.await())
}
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/MagikeyboardUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/MagikeyboardUtil.kt
index be095f648..e98c2831b 100644
--- a/app/src/main/java/com/kunzisoft/keepass/utils/MagikeyboardUtil.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/utils/MagikeyboardUtil.kt
@@ -4,7 +4,7 @@ import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.util.Log
-import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
+import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
object MagikeyboardUtil {
private val TAG = MagikeyboardUtil::class.java.name
diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt
index cf0cd289b..6df1c4561 100644
--- a/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt
@@ -28,7 +28,7 @@ import android.view.MenuItem
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AboutActivity
import com.kunzisoft.keepass.settings.SettingsActivity
-import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
+import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
import com.kunzisoft.keepass.utils.UriUtil.openUrl
object MenuUtil {
@@ -40,9 +40,6 @@ object MenuUtil {
menu.findItem(R.id.menu_contribute)?.isVisible = false
}
- /*
- * @param checkLock Check the time lock before launch settings in LockingActivity
- */
fun onDefaultMenuOptionsItemSelected(activity: Activity,
item: MenuItem,
timeoutEnable: Boolean = false) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt
index c53a29e7d..83a06d6ee 100644
--- a/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt
@@ -200,64 +200,5 @@ object UriUtil {
this.openUrl(this.getString(resId))
}
- fun Context.isContributingUser(): Boolean {
- return (Education.isEducationScreenReclickedPerformed(this)
- || isExternalAppInstalled(this.getString(R.string.keepro_app_id), false)
- )
- }
-
- fun Context.isExternalAppInstalled(packageName: String, showError: Boolean = true): Boolean {
- try {
- this.applicationContext.packageManager.getPackageInfoCompat(
- packageName,
- PackageManager.GET_ACTIVITIES
- )
- Education.setEducationScreenReclickedPerformed(this)
- return true
- } catch (e: Exception) {
- if (showError)
- Log.e(TAG, "App not accessible", e)
- }
- return false
- }
-
- fun Context.openExternalApp(packageName: String, sourcesURL: String? = null) {
- var launchIntent: Intent? = null
- try {
- launchIntent = this.packageManager.getLaunchIntentForPackage(packageName)?.apply {
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- }
- } catch (ignored: Exception) { }
- try {
- if (launchIntent == null) {
- this.startActivity(
- Intent(Intent.ACTION_VIEW)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .setData(
- Uri.parse(
- if (sourcesURL != null
- && !BuildConfig.CLOSED_STORE
- ) {
- sourcesURL
- } else {
- this.getString(
- if (BuildConfig.CLOSED_STORE)
- R.string.play_store_url
- else
- R.string.f_droid_url,
- packageName
- )
- }
- )
- )
- )
- } else {
- this.startActivity(launchIntent)
- }
- } catch (e: Exception) {
- Log.e(TAG, "App cannot be open", e)
- }
- }
-
private const val TAG = "UriUtil"
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/WebDomain.kt b/app/src/main/java/com/kunzisoft/keepass/utils/WebDomain.kt
deleted file mode 100644
index 429405ee0..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/utils/WebDomain.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.kunzisoft.keepass.utils
-
-import android.content.Context
-import com.kunzisoft.keepass.model.SearchInfo
-import com.kunzisoft.keepass.settings.PreferencesUtil
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import mozilla.components.lib.publicsuffixlist.PublicSuffixList
-
-object WebDomain {
-
- /**
- * Get the concrete web domain AKA without sub domain if needed
- */
- fun getConcreteWebDomain(context: Context,
- webDomain: String?,
- concreteWebDomain: (String?) -> Unit) {
- CoroutineScope(Dispatchers.Main).launch {
- if (webDomain != null) {
- // Warning, web domain can contains IP, don't crop in this case
- if (PreferencesUtil.searchSubdomains(context)
- || Regex(SearchInfo.WEB_IP_REGEX).matches(webDomain)) {
- concreteWebDomain.invoke(webDomain)
- } else {
- val publicSuffixList = PublicSuffixList(context)
- concreteWebDomain.invoke(publicSuffixList
- .getPublicSuffixPlusOne(webDomain).await())
- }
- } else {
- concreteWebDomain.invoke(null)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/view/PasskeyTextFieldView.kt b/app/src/main/java/com/kunzisoft/keepass/view/PasskeyTextFieldView.kt
new file mode 100644
index 000000000..c099f92f9
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/view/PasskeyTextFieldView.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2025 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.util.TypedValue
+import android.view.ContextThemeWrapper
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.DrawableCompat
+import androidx.core.view.ViewCompat
+import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.password.PasswordEntropy
+
+
+class PasskeyTextFieldView @JvmOverloads constructor(context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0)
+ : PasswordTextFieldView(context, attrs, defStyle) {
+
+ private var relyingPartyViewId = ViewCompat.generateViewId()
+ private var usernameViewId = ViewCompat.generateViewId()
+ private var passkeyImageId = ViewCompat.generateViewId()
+
+ private var passkeyImage = AppCompatImageView(
+ ContextThemeWrapper(context, R.style.KeepassDXStyle_ImageButton_Simple), null, 0).apply {
+ layoutParams = LayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT)
+ setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_passkey_white_24dp))
+ contentDescription = context.getString(R.string.passkey)
+ }
+
+ private val relyingPartyView = AppCompatTextView(context).apply {
+ setTextAppearance(context,
+ R.style.KeepassDXStyle_TextAppearance_TextNodePrimary)
+ layoutParams = LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.WRAP_CONTENT
+ ).also {
+ it.topMargin = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ 8f,
+ resources.displayMetrics
+ ).toInt()
+ it.leftMargin = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ 8f,
+ resources.displayMetrics
+ ).toInt()
+ it.marginStart = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ 8f,
+ resources.displayMetrics
+ ).toInt()
+ }
+ setTextIsSelectable(true)
+ }
+ private val usernameView = AppCompatTextView(context).apply {
+ setTextAppearance(context,
+ R.style.KeepassDXStyle_TextAppearance_TextNodeSecondary)
+ layoutParams = LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.WRAP_CONTENT).also {
+ it.topMargin = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ 4f,
+ resources.displayMetrics
+ ).toInt()
+ it.leftMargin = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ 8f,
+ resources.displayMetrics
+ ).toInt()
+ it.marginStart = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ 8f,
+ resources.displayMetrics
+ ).toInt()
+ }
+ setTextIsSelectable(true)
+ }
+
+ private fun buildViews() {
+ indicatorDrawable?.let {
+ DrawableCompat.setTint(it, PasswordEntropy.Strength.VERY_UNGUESSABLE.color)
+ }
+ passkeyImage.apply {
+ id = passkeyImageId
+ layoutParams = (layoutParams as LayoutParams?)?.also {
+ it.addRule(ALIGN_PARENT_RIGHT)
+ it.addRule(ALIGN_PARENT_END)
+ }
+ }
+ labelView.apply {
+ layoutParams = (layoutParams as LayoutParams?)?.also {
+ it.addRule(LEFT_OF, passkeyImageId)
+ it.addRule(START_OF, passkeyImageId)
+ }
+ }
+ relyingPartyView.apply {
+ id = relyingPartyViewId
+ layoutParams = (layoutParams as LayoutParams?)?.also {
+ it.addRule(LEFT_OF, passkeyImageId)
+ it.addRule(START_OF, passkeyImageId)
+ it.addRule(BELOW, labelViewId)
+ }
+ }
+ usernameView.apply {
+ id = usernameViewId
+ layoutParams = (layoutParams as LayoutParams?)?.also {
+ it.addRule(LEFT_OF, passkeyImageId)
+ it.addRule(START_OF, passkeyImageId)
+ it.addRule(BELOW, relyingPartyViewId)
+ }
+ }
+ }
+
+ init {
+ removeAllViews()
+ buildViews()
+ addView(passkeyImage)
+ addView(labelView)
+ addView(relyingPartyView)
+ addView(usernameView)
+ }
+
+ override var default: String = ""
+ override var isFieldVisible: Boolean = true
+
+ var relyingParty: String
+ get() {
+ return relyingPartyView.text.toString()
+ }
+ set(value) {
+ relyingPartyView.text = value
+ }
+
+ var username: String
+ get() {
+ return usernameView.text.toString()
+ }
+ set(value) {
+ usernameView.text = value
+ }
+
+ override fun getEntropyStrength(passwordText: String) {
+ // Do nothing
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/view/PasswordTextFieldView.kt b/app/src/main/java/com/kunzisoft/keepass/view/PasswordTextFieldView.kt
index c56454d10..12942811e 100644
--- a/app/src/main/java/com/kunzisoft/keepass/view/PasswordTextFieldView.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/view/PasswordTextFieldView.kt
@@ -34,9 +34,9 @@ import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
-class PasswordTextFieldView @JvmOverloads constructor(context: Context,
- attrs: AttributeSet? = null,
- defStyle: Int = 0)
+open class PasswordTextFieldView @JvmOverloads constructor(context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0)
: TextFieldView(context, attrs, defStyle) {
private var mPasswordEntropyCalculator: PasswordEntropy = PasswordEntropy {
@@ -45,7 +45,7 @@ class PasswordTextFieldView @JvmOverloads constructor(context: Context,
}
}
- private var indicatorDrawable = ContextCompat.getDrawable(
+ protected var indicatorDrawable = ContextCompat.getDrawable(
context,
R.drawable.ic_shield_white_24dp
)?.apply {
@@ -98,7 +98,7 @@ class PasswordTextFieldView @JvmOverloads constructor(context: Context,
value = resources.getString(valueId)
}
- private fun getEntropyStrength(passwordText: String) {
+ protected open fun getEntropyStrength(passwordText: String) {
mPasswordEntropyCalculator.getEntropyStrength(passwordText) { entropyStrength ->
labelView.apply {
post {
diff --git a/app/src/main/java/com/kunzisoft/keepass/view/SearchFiltersView.kt b/app/src/main/java/com/kunzisoft/keepass/view/SearchFiltersView.kt
index 8f957e6f9..5a501a1e2 100644
--- a/app/src/main/java/com/kunzisoft/keepass/view/SearchFiltersView.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/view/SearchFiltersView.kt
@@ -3,7 +3,6 @@ package com.kunzisoft.keepass.view
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
-import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.ImageView
@@ -30,8 +29,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
private var searchTitle: CompoundButton
private var searchUsername: CompoundButton
private var searchPassword: CompoundButton
+ private var searchApplicationId: CompoundButton
private var searchURL: CompoundButton
private var searchByURLDomain: Boolean = false
+ private var searchByURLSubDomain: Boolean = false
private var searchExpired: CompoundButton
private var searchNotes: CompoundButton
private var searchOther: CompoundButton
@@ -50,8 +51,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
this.searchInTitles = searchTitle.isChecked
this.searchInUsernames = searchUsername.isChecked
this.searchInPasswords = searchPassword.isChecked
+ this.searchInAppIds = searchApplicationId.isChecked
this.searchInUrls = searchURL.isChecked
this.searchByDomain = searchByURLDomain
+ this.searchBySubDomain = searchByURLSubDomain
this.searchInExpired = searchExpired.isChecked
this.searchInNotes = searchNotes.isChecked
this.searchInOther = searchOther.isChecked
@@ -71,8 +74,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchTitle.isChecked = value.searchInTitles
searchUsername.isChecked = value.searchInUsernames
searchPassword.isChecked = value.searchInPasswords
+ searchApplicationId.isChecked = value.searchInAppIds
searchURL.isChecked = value.searchInUrls
searchByURLDomain = value.searchByDomain
+ searchByURLSubDomain = value.searchBySubDomain
searchExpired.isChecked = value.searchInExpired
searchNotes.isChecked = value.searchInNotes
searchOther.isChecked = value.searchInOther
@@ -87,7 +92,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
var onParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = null
private var mOnParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = {
// To recalculate height
- if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) {
+ if (searchAdvanceFiltersContainer?.visibility == VISIBLE) {
searchAdvanceFiltersContainer?.expand(
false,
searchAdvanceFiltersContainer?.getFullHeight()
@@ -110,6 +115,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchTitle = findViewById(R.id.search_chip_title)
searchUsername = findViewById(R.id.search_chip_username)
searchPassword = findViewById(R.id.search_chip_password)
+ searchApplicationId = findViewById(R.id.search_chip_application_id)
searchURL = findViewById(R.id.search_chip_url)
searchExpired = findViewById(R.id.search_chip_expires)
searchNotes = findViewById(R.id.search_chip_note)
@@ -125,7 +131,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
// Expand menu with button
searchExpandButton.setOnClickListener {
- val isVisible = searchAdvanceFiltersContainer?.visibility == View.VISIBLE
+ val isVisible = searchAdvanceFiltersContainer?.visibility == VISIBLE
if (isVisible)
closeAdvancedFilters()
else
@@ -156,6 +162,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchParameters.searchInPasswords = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
+ searchApplicationId.setOnCheckedChangeListener { _, isChecked ->
+ searchParameters.searchInAppIds = isChecked
+ mOnParametersChangeListener?.invoke(searchParameters)
+ }
searchURL.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInUrls = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
@@ -200,10 +210,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchNumbers.text = SearchHelper.showNumberOfSearchResults(numbers)
}
- fun setCurrentGroupText(text: String) {
+ fun setCurrentGroupText(text: String?) {
val maxChars = 12
searchCurrentGroup.text = when {
- text.isEmpty() -> context.getString(R.string.current_group)
+ text.isNullOrEmpty() -> context.getString(R.string.current_group)
text.length > maxChars -> text.substring(0, maxChars) + "…"
else -> text
}
@@ -213,6 +223,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchOther.isVisible = available
}
+ fun availableApplicationIds(available: Boolean) {
+ searchApplicationId.isVisible = available
+ }
+
fun availableTags(available: Boolean) {
searchTag.isVisible = available
}
@@ -243,16 +257,20 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
)
}
+ fun showSearchExpandButton(show: Boolean) {
+ searchExpandButton.isVisible = show
+ }
+
override fun setVisibility(visibility: Int) {
when (visibility) {
- View.VISIBLE -> {
- searchAdvanceFiltersContainer?.visibility = View.GONE
+ VISIBLE -> {
+ searchAdvanceFiltersContainer?.visibility = GONE
searchContainer.showByFading()
}
else -> {
searchContainer.hideByFading()
- if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) {
- searchAdvanceFiltersContainer?.visibility = View.INVISIBLE
+ if (searchAdvanceFiltersContainer?.visibility == VISIBLE) {
+ searchAdvanceFiltersContainer?.visibility = INVISIBLE
searchAdvanceFiltersContainer?.collapse()
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt
index 0d1823ffe..4d5eec7e7 100644
--- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt
@@ -534,10 +534,15 @@ abstract class TemplateAbstractView<
}
protected fun getCustomField(fieldName: String): Field {
+ return getCustomFieldOrNull(fieldName)
+ ?: Field(fieldName, ProtectedString(false))
+ }
+
+ protected fun getCustomFieldOrNull(fieldName: String): Field? {
return getCustomField(fieldName,
templateFieldNotEmpty = false,
retrieveDefaultValues = false
- ) ?: Field(fieldName, ProtectedString(false))
+ )
}
private fun getCustomField(fieldName: String,
diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt
index 8963534ba..ee07349b3 100644
--- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt
@@ -20,6 +20,8 @@ import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
import com.kunzisoft.keepass.model.DataDate
import com.kunzisoft.keepass.model.DataTime
+import com.kunzisoft.keepass.model.AppOriginEntryField
+import com.kunzisoft.keepass.model.PasskeyEntryFields
import com.kunzisoft.keepass.otp.OtpEntryFields
@@ -256,9 +258,12 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
override fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean,
retrieveDefaultValues: Boolean) {
super.populateEntryInfoWithViews(templateFieldNotEmpty, retrieveDefaultValues)
- mEntryInfo?.otpModel = OtpEntryFields.parseFields { key ->
- getCustomField(key).protectedValue.toString()
- }?.otpModel
+ val getField: (id: String) -> String? = { key ->
+ getCustomFieldOrNull(key)?.protectedValue?.stringValue
+ }
+ mEntryInfo?.otpModel = OtpEntryFields.parseFields(getField)?.otpModel
+ mEntryInfo?.passkey = PasskeyEntryFields.parseFields(getField)
+ mEntryInfo?.appOrigin = AppOriginEntryField.parseFields(getField)
}
override fun onRestoreEntryInstanceState(state: SavedState) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt
index 92e4b0140..12b55ca34 100644
--- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt
@@ -1,7 +1,6 @@
package com.kunzisoft.keepass.view
import android.content.Context
-import android.os.Build
import android.util.AttributeSet
import android.view.View
import androidx.core.view.isVisible
@@ -11,8 +10,11 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.element.template.TemplateAttribute
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.getLocalizedName
+import com.kunzisoft.keepass.database.helper.isPasskeyLabel
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
import com.kunzisoft.keepass.model.OtpModel
+import com.kunzisoft.keepass.model.Passkey
+import com.kunzisoft.keepass.model.PasskeyEntryFields.PASSKEY_FIELD
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
@@ -52,6 +54,8 @@ class TemplateView @JvmOverloads constructor(context: Context,
return context?.let {
(if (TemplateField.isStandardPasswordName(context, templateAttribute.label))
PasswordTextFieldView(it)
+ else if (TemplateField.isPasskeyLabel(context, templateAttribute.label))
+ PasskeyTextFieldView(it)
else TextFieldView(it)).apply {
applyFontVisibility(mFontInVisibility)
setProtection(field.protectedValue.isProtected, mHideProtectedValue)
@@ -123,20 +127,20 @@ class TemplateView @JvmOverloads constructor(context: Context,
override fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List {
val emptyCustomFields = super.populateViewsWithEntryInfo(false)
-
// Hide empty custom fields
emptyCustomFields.forEach { customFieldId ->
customFieldId.view.isVisible = false
}
-
removeOtpRunnable()
mEntryInfo?.let { entryInfo ->
// Assign specific OTP dynamic view
entryInfo.otpModel?.let {
assignOtp(it)
}
+ entryInfo.passkey?.let {
+ assignPasskey(it)
+ }
}
-
return emptyCustomFields
}
@@ -196,6 +200,22 @@ class TemplateView @JvmOverloads constructor(context: Context,
}
}
+ private fun getPasskeyView(): PasskeyTextFieldView? {
+ getViewFieldByName(PASSKEY_FIELD)?.let { viewField ->
+ val view = viewField.view
+ if (view is PasskeyTextFieldView)
+ return view
+ }
+ return null
+ }
+
+ private fun assignPasskey(passkey: Passkey) {
+ getPasskeyView()?.apply {
+ relyingParty = passkey.relyingParty
+ username = passkey.username
+ }
+ }
+
private fun removeOtpRunnable() {
mLastOtpTokenView?.removeCallbacks(mOtpRunnable)
mLastOtpTokenView = null
diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt
index d9fbf49fa..89f45b5d4 100644
--- a/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt
@@ -20,14 +20,12 @@
package com.kunzisoft.keepass.view
import android.content.Context
-import android.os.Build
import android.text.InputFilter
import android.text.util.Linkify
import android.util.AttributeSet
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.View
-import android.view.View.OnClickListener
import android.widget.RelativeLayout
import androidx.annotation.StringRes
import androidx.appcompat.widget.AppCompatImageButton
@@ -37,8 +35,8 @@ import androidx.core.text.util.LinkifyCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import com.kunzisoft.keepass.R
-import com.kunzisoft.keepass.model.EntryInfo.Companion.APPLICATION_ID_FIELD_NAME
-import com.kunzisoft.keepass.utils.UriUtil.openExternalApp
+import com.kunzisoft.keepass.model.AppOriginEntryField.APPLICATION_ID_FIELD_NAME
+import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
open class TextFieldView @JvmOverloads constructor(context: Context,
@@ -46,7 +44,7 @@ open class TextFieldView @JvmOverloads constructor(context: Context,
defStyle: Int = 0)
: RelativeLayout(context, attrs, defStyle), GenericTextFieldView {
- private var labelViewId = ViewCompat.generateViewId()
+ protected var labelViewId = ViewCompat.generateViewId()
private var valueViewId = ViewCompat.generateViewId()
private var showButtonId = ViewCompat.generateViewId()
private var copyButtonId = ViewCompat.generateViewId()
diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt
index 2437e070d..a5a5a6f19 100644
--- a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt
@@ -61,6 +61,7 @@ import androidx.core.view.updatePaddingRelative
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.database.exception.LocalizedException
import com.kunzisoft.keepass.database.helper.getLocalizedMessage
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -237,6 +238,17 @@ fun View.updateLockPaddingStart() {
}
}
+fun Context.toastError(e: Throwable) {
+ Toast.makeText(
+ applicationContext,
+ if (e is LocalizedException)
+ e.getLocalizedMessage(resources)
+ else
+ e.localizedMessage,
+ Toast.LENGTH_LONG
+ ).show()
+}
+
fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) {
if (!result.isSuccess) {
result.exception?.getLocalizedMessage(resources)?.let { errorMessage ->
diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt
index c8dfe9814..200b88f7d 100644
--- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt
@@ -1,214 +1,500 @@
package com.kunzisoft.keepass.viewmodels
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
+import android.app.Application
+import android.net.Uri
+import android.os.Bundle
+import androidx.lifecycle.AndroidViewModel
import com.kunzisoft.keepass.database.ContextualDatabase
+import com.kunzisoft.keepass.database.DatabaseTaskProvider
+import com.kunzisoft.keepass.database.MainCredential
+import com.kunzisoft.keepass.database.ProgressMessage
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
+import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
+import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
+import com.kunzisoft.keepass.database.element.node.Node
+import com.kunzisoft.keepass.database.element.node.NodeId
+import com.kunzisoft.keepass.model.CipherEncryptDatabase
+import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.tasks.ActionRunnable
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import java.util.UUID
-class DatabaseViewModel: ViewModel() {
+class DatabaseViewModel(application: Application): AndroidViewModel(application) {
- val database : LiveData get() = _database
- private val _database = MutableLiveData()
+ private val mDatabaseState = MutableStateFlow(null)
+ val databaseState: StateFlow = mDatabaseState
- val actionFinished : LiveData get() = _actionFinished
- private val _actionFinished = SingleLiveEvent()
+ val database: ContextualDatabase?
+ get() = databaseState.value
- val saveDatabase : LiveData get() = _saveDatabase
- private val _saveDatabase = SingleLiveEvent()
+ private val mActionState = MutableStateFlow(ActionState.Loading)
+ val actionState: StateFlow = mActionState
- val mergeDatabase : LiveData get() = _mergeDatabase
- private val _mergeDatabase = SingleLiveEvent()
+ private var mDatabaseTaskProvider: DatabaseTaskProvider = DatabaseTaskProvider(
+ context = application
+ )
- val reloadDatabase : LiveData get() = _reloadDatabase
- private val _reloadDatabase = SingleLiveEvent()
+ init {
+ mDatabaseTaskProvider.onDatabaseRetrieved = { databaseRetrieved ->
+ val databaseWasReloaded = databaseRetrieved?.wasReloaded == true
+ if (databaseWasReloaded) {
+ mActionState.value = ActionState.OnDatabaseReloaded
+ }
+ if (database == null || database != databaseRetrieved || databaseWasReloaded) {
+ databaseRetrieved?.wasReloaded = false
+ mDatabaseState.value = databaseRetrieved
+ }
+ }
+ mDatabaseTaskProvider.onStartActionRequested = { bundle, actionTask ->
+ mActionState.value = ActionState.OnDatabaseActionRequested(bundle, actionTask)
+ }
+ mDatabaseTaskProvider.databaseInfoListener = object : DatabaseTaskNotificationService.DatabaseInfoListener {
+ override fun onDatabaseInfoChanged(
+ previousDatabaseInfo: SnapFileDatabaseInfo,
+ newDatabaseInfo: SnapFileDatabaseInfo,
+ readOnlyDatabase: Boolean
+ ) {
+ mActionState.value = ActionState.OnDatabaseInfoChanged(
+ previousDatabaseInfo,
+ newDatabaseInfo,
+ readOnlyDatabase
+ )
+ }
+ }
+ mDatabaseTaskProvider.actionTaskListener = object : DatabaseTaskNotificationService.ActionTaskListener {
+ override fun onActionStarted(
+ database: ContextualDatabase,
+ progressMessage: ProgressMessage
+ ) {
+ mActionState.value = ActionState.OnDatabaseActionStarted(database, progressMessage)
+ }
- val saveName : LiveData get() = _saveName
- private val _saveName = SingleLiveEvent()
+ override fun onActionUpdated(
+ database: ContextualDatabase,
+ progressMessage: ProgressMessage
+ ) {
+ mActionState.value = ActionState.OnDatabaseActionUpdated(database, progressMessage)
+ }
- val saveDescription : LiveData get() = _saveDescription
- private val _saveDescription = SingleLiveEvent()
+ override fun onActionStopped(database: ContextualDatabase?) {
+ mActionState.value = ActionState.OnDatabaseActionStopped(database)
+ }
- val saveDefaultUsername : LiveData get() = _saveDefaultUsername
- private val _saveDefaultUsername = SingleLiveEvent()
+ override fun onActionFinished(
+ database: ContextualDatabase,
+ actionTask: String,
+ result: ActionRunnable.Result
+ ) {
+ mActionState.value = ActionState.OnDatabaseActionFinished(database, actionTask, result)
+ }
+ }
- val saveColor : LiveData get() = _saveColor
- private val _saveColor = SingleLiveEvent()
-
- val saveCompression : LiveData get() = _saveCompression
- private val _saveCompression = SingleLiveEvent()
-
- val removeUnlinkData : LiveData get() = _removeUnlinkData
- private val _removeUnlinkData = SingleLiveEvent()
-
- val saveRecycleBin : LiveData get() = _saveRecycleBin
- private val _saveRecycleBin = SingleLiveEvent()
-
- val saveTemplatesGroup : LiveData get() = _saveTemplatesGroup
- private val _saveTemplatesGroup = SingleLiveEvent()
-
- val saveMaxHistoryItems : LiveData get() = _saveMaxHistoryItems
- private val _saveMaxHistoryItems = SingleLiveEvent()
-
- val saveMaxHistorySize : LiveData get() = _saveMaxHistorySize
- private val _saveMaxHistorySize = SingleLiveEvent()
-
- val saveEncryption : LiveData get() = _saveEncryption
- private val _saveEncryption = SingleLiveEvent()
-
- val saveKeyDerivation : LiveData get() = _saveKeyDerivation
- private val _saveKeyDerivation = SingleLiveEvent()
-
- val saveIterations : LiveData get() = _saveIterations
- private val _saveIterations = SingleLiveEvent()
-
- val saveMemoryUsage : LiveData get() = _saveMemoryUsage
- private val _saveMemoryUsage = SingleLiveEvent()
-
- val saveParallelism : LiveData get() = _saveParallelism
- private val _saveParallelism = SingleLiveEvent()
-
-
- fun defineDatabase(database: ContextualDatabase?) {
- this._database.value = database
+ mDatabaseTaskProvider.registerProgressTask()
}
- fun onActionFinished(database: ContextualDatabase,
- actionTask: String,
- result: ActionRunnable.Result) {
- this._actionFinished.value = ActionResult(database, actionTask, result)
+ /*
+ * Main database actions
+ */
+
+ fun loadDatabase(
+ databaseUri: Uri,
+ mainCredential: MainCredential,
+ readOnly: Boolean,
+ cipherEncryptDatabase: CipherEncryptDatabase?,
+ fixDuplicateUuid: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseLoad(
+ databaseUri,
+ mainCredential,
+ readOnly,
+ cipherEncryptDatabase,
+ fixDuplicateUuid
+ )
}
- fun saveDatabase(save: Boolean) {
- _saveDatabase.value = save
+ fun createDatabase(
+ databaseUri: Uri,
+ mainCredential: MainCredential
+ ) {
+ mDatabaseTaskProvider.startDatabaseCreate(databaseUri, mainCredential)
}
- fun mergeDatabase(save: Boolean) {
- _mergeDatabase.value = save
+ fun assignMainCredential(
+ databaseUri: Uri?,
+ mainCredential: MainCredential
+ ) {
+ if (databaseUri != null) {
+ mDatabaseTaskProvider.startDatabaseAssignCredential(databaseUri, mainCredential)
+ }
+ }
+
+ fun saveDatabase(save: Boolean, saveToUri: Uri? = null) {
+ mDatabaseTaskProvider.startDatabaseSave(save, saveToUri)
+ }
+
+ fun mergeDatabase(
+ save: Boolean,
+ fromDatabaseUri: Uri? = null,
+ mainCredential: MainCredential? = null
+ ) {
+ mDatabaseTaskProvider.startDatabaseMerge(save, fromDatabaseUri, mainCredential)
}
fun reloadDatabase(fixDuplicateUuid: Boolean) {
- _reloadDatabase.value = fixDuplicateUuid
+ mDatabaseTaskProvider.askToStartDatabaseReload(
+ conditionToAsk = database?.dataModifiedSinceLastLoading != false
+ ) {
+ mDatabaseTaskProvider.startDatabaseReload(fixDuplicateUuid)
+ }
}
- fun saveName(oldValue: String,
- newValue: String,
- save: Boolean) {
- _saveName.value = SuperString(oldValue, newValue, save)
+ fun onDatabaseChangeValidated() {
+ mDatabaseTaskProvider.onDatabaseChangeValidated()
}
- fun saveDescription(oldValue: String,
- newValue: String,
- save: Boolean) {
- _saveDescription.value = SuperString(oldValue, newValue, save)
+ /*
+ * Nodes actions
+ */
+
+ fun createEntry(
+ newEntry: Entry,
+ parent: Group,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseCreateEntry(
+ newEntry,
+ parent,
+ save
+ )
}
- fun saveDefaultUsername(oldValue: String,
- newValue: String,
- save: Boolean) {
- _saveDefaultUsername.value = SuperString(oldValue, newValue, save)
+ fun updateEntry(
+ oldEntry: Entry,
+ entryToUpdate: Entry,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseUpdateEntry(
+ oldEntry,
+ entryToUpdate,
+ save
+ )
}
- fun saveColor(oldValue: String,
- newValue: String,
- save: Boolean) {
- _saveColor.value = SuperString(oldValue, newValue, save)
+ fun restoreEntryHistory(
+ mainEntryId: NodeId,
+ entryHistoryPosition: Int,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseRestoreEntryHistory(
+ mainEntryId,
+ entryHistoryPosition,
+ save
+ )
}
- fun saveCompression(oldValue: CompressionAlgorithm,
- newValue: CompressionAlgorithm,
- save: Boolean) {
- _saveCompression.value = SuperCompression(oldValue, newValue, save)
+ fun deleteEntryHistory(
+ mainEntryId: NodeId,
+ entryHistoryPosition: Int,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseDeleteEntryHistory(
+ mainEntryId,
+ entryHistoryPosition,
+ save
+ )
+ }
+
+ fun createGroup(
+ newGroup: Group,
+ parent: Group,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseCreateGroup(
+ newGroup,
+ parent,
+ save
+ )
+ }
+
+ fun updateGroup(
+ oldGroup: Group,
+ groupToUpdate: Group,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseUpdateGroup(
+ oldGroup,
+ groupToUpdate,
+ save
+ )
+ }
+
+ fun copyNodes(
+ nodesToCopy: List,
+ newParent: Group,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseCopyNodes(
+ nodesToCopy,
+ newParent,
+ save
+ )
+ }
+
+ fun moveNodes(
+ nodesToMove: List,
+ newParent: Group,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseMoveNodes(
+ nodesToMove,
+ newParent,
+ save
+ )
+ }
+
+ fun deleteNodes(
+ nodes: List,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseDeleteNodes(
+ nodes,
+ save
+ )
+ }
+
+ /*
+ * Attributes
+ */
+
+ fun buildNewAttachment(): BinaryData? {
+ return database?.buildNewBinaryAttachment()
+ }
+
+ /*
+ * Settings actions
+ */
+
+ fun saveName(
+ oldValue: String,
+ newValue: String,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseSaveName(
+ oldValue,
+ newValue,
+ save
+ )
+ }
+
+ fun saveDescription(
+ oldValue: String,
+ newValue: String,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseSaveDescription(
+ oldValue,
+ newValue,
+ save
+ )
+ }
+
+ fun saveDefaultUsername(
+ oldValue: String,
+ newValue: String,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseSaveDefaultUsername(
+ oldValue,
+ newValue,
+ save
+ )
+ }
+
+ fun saveColor(
+ oldValue: String,
+ newValue: String,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseSaveColor(
+ oldValue,
+ newValue,
+ save
+ )
+ }
+
+ fun saveCompression(
+ oldValue: CompressionAlgorithm,
+ newValue: CompressionAlgorithm,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseSaveCompression(
+ oldValue,
+ newValue,
+ save
+ )
}
fun removeUnlinkedData(save: Boolean) {
- _removeUnlinkData.value = save
+ mDatabaseTaskProvider.startDatabaseRemoveUnlinkedData(save)
}
- fun saveRecycleBin(oldValue: Group?,
- newValue: Group?,
- save: Boolean) {
- _saveRecycleBin.value = SuperGroup(oldValue, newValue, save)
+ fun saveRecycleBin(
+ oldValue: Group?,
+ newValue: Group?,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseSaveRecycleBin(
+ oldValue,
+ newValue,
+ save
+ )
}
- fun saveTemplatesGroup(oldValue: Group?,
- newValue: Group?,
- save: Boolean) {
- _saveTemplatesGroup.value = SuperGroup(oldValue, newValue, save)
+ fun saveTemplatesGroup(
+ oldValue: Group?,
+ newValue: Group?,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseSaveTemplatesGroup(
+ oldValue,
+ newValue,
+ save
+ )
}
- fun saveMaxHistoryItems(oldValue: Int,
- newValue: Int,
- save: Boolean) {
- _saveMaxHistoryItems.value = SuperInt(oldValue, newValue, save)
+ fun saveMaxHistoryItems(
+ oldValue: Int,
+ newValue: Int,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseSaveMaxHistoryItems(
+ oldValue,
+ newValue,
+ save
+ )
}
- fun saveMaxHistorySize(oldValue: Long,
- newValue: Long,
- save: Boolean) {
- _saveMaxHistorySize.value = SuperLong(oldValue, newValue, save)
+ fun saveMaxHistorySize(
+ oldValue: Long,
+ newValue: Long,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseSaveMaxHistorySize(
+ oldValue,
+ newValue,
+ save
+ )
}
- fun saveEncryption(oldValue: EncryptionAlgorithm,
- newValue: EncryptionAlgorithm,
- save: Boolean) {
- _saveEncryption.value = SuperEncryption(oldValue, newValue, save)
+ fun saveEncryption(
+ oldValue: EncryptionAlgorithm,
+ newValue: EncryptionAlgorithm,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseSaveEncryption(
+ oldValue,
+ newValue,
+ save
+ )
}
- fun saveKeyDerivation(oldValue: KdfEngine,
- newValue: KdfEngine,
- save: Boolean) {
- _saveKeyDerivation.value = SuperKeyDerivation(oldValue, newValue, save)
+ fun saveKeyDerivation(
+ oldValue: KdfEngine,
+ newValue: KdfEngine,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseSaveKeyDerivation(
+ oldValue,
+ newValue,
+ save
+ )
}
- fun saveIterations(oldValue: Long,
- newValue: Long,
- save: Boolean) {
- _saveIterations.value = SuperLong(oldValue, newValue, save)
+ fun saveIterations(
+ oldValue: Long,
+ newValue: Long,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseSaveIterations(
+ oldValue,
+ newValue,
+ save
+ )
}
- fun saveMemoryUsage(oldValue: Long,
- newValue: Long,
- save: Boolean) {
- _saveMemoryUsage.value = SuperLong(oldValue, newValue, save)
+ fun saveMemoryUsage(
+ oldValue: Long,
+ newValue: Long,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseSaveMemoryUsage(
+ oldValue,
+ newValue,
+ save
+ )
}
- fun saveParallelism(oldValue: Long,
- newValue: Long,
- save: Boolean) {
- _saveParallelism.value = SuperLong(oldValue, newValue, save)
+ fun saveParallelism(
+ oldValue: Long,
+ newValue: Long,
+ save: Boolean
+ ) {
+ mDatabaseTaskProvider.startDatabaseSaveParallelism(
+ oldValue,
+ newValue,
+ save
+ )
}
- data class ActionResult(val database: ContextualDatabase,
- val actionTask: String,
- val result: ActionRunnable.Result)
- data class SuperString(val oldValue: String,
- val newValue: String,
- val save: Boolean)
- data class SuperInt(val oldValue: Int,
- val newValue: Int,
- val save: Boolean)
- data class SuperLong(val oldValue: Long,
- val newValue: Long,
- val save: Boolean)
- data class SuperMerge(val fixDuplicateUuid: Boolean,
- val save: Boolean)
- data class SuperCompression(val oldValue: CompressionAlgorithm,
- val newValue: CompressionAlgorithm,
- val save: Boolean)
- data class SuperEncryption(val oldValue: EncryptionAlgorithm,
- val newValue: EncryptionAlgorithm,
- val save: Boolean)
- data class SuperKeyDerivation(val oldValue: KdfEngine,
- val newValue: KdfEngine,
- val save: Boolean)
- data class SuperGroup(val oldValue: Group?,
- val newValue: Group?,
- val save: Boolean)
+ /*
+ * Hardware Key
+ */
+ fun onChallengeResponded(challengeResponse: ByteArray?) {
+ mDatabaseTaskProvider.startChallengeResponded(
+ challengeResponse ?: ByteArray(0)
+ )
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ mDatabaseTaskProvider.unregisterProgressTask()
+ mDatabaseTaskProvider.destroy()
+ }
+
+ sealed class ActionState {
+ object Loading: ActionState()
+ object OnDatabaseReloaded: ActionState()
+ data class OnDatabaseActionRequested(
+ val bundle: Bundle? = null,
+ val actionTask: String
+ ): ActionState()
+ data class OnDatabaseInfoChanged(
+ val previousDatabaseInfo: SnapFileDatabaseInfo,
+ val newDatabaseInfo: SnapFileDatabaseInfo,
+ val readOnlyDatabase: Boolean
+ ): ActionState()
+ data class OnDatabaseActionStarted(
+ var database: ContextualDatabase,
+ val progressMessage: ProgressMessage
+ ): ActionState()
+ data class OnDatabaseActionUpdated(
+ var database: ContextualDatabase,
+ val progressMessage: ProgressMessage
+ ): ActionState()
+ data class OnDatabaseActionStopped(
+ var database: ContextualDatabase?
+ ): ActionState()
+ data class OnDatabaseActionFinished(
+ var database: ContextualDatabase,
+ val actionTask: String,
+ val result: ActionRunnable.Result
+ ): ActionState()
+ }
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt
index b740d1fb0..6b4deed54 100644
--- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt
@@ -3,6 +3,7 @@ package com.kunzisoft.keepass.viewmodels
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Entry
@@ -16,10 +17,11 @@ import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.RegisterInfo
-import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.utils.IOActionTask
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import java.util.UUID
@@ -28,12 +30,18 @@ class EntryEditViewModel: NodeEditViewModel() {
private var mEntryId: NodeId? = null
private var mParentId: NodeId<*>? = null
private var mRegisterInfo: RegisterInfo? = null
- private var mSearchInfo: SearchInfo? = null
private var mParent: Group? = null
private var mEntry: Entry? = null
private var mIsTemplate: Boolean = false
private val mTempAttachments = mutableListOf()
+ // To show dialog only one time
+ var backPressedAlreadyApproved = false
+ var warningOverwriteDataAlreadyApproved = false
+
+ // Useful to not relaunch a current action
+ private var actionLocked: Boolean = false
+
val templatesEntry : LiveData get() = _templatesEntry
private val _templatesEntry = MutableLiveData()
@@ -73,24 +81,28 @@ class EntryEditViewModel: NodeEditViewModel() {
val onBinaryPreviewLoaded : LiveData get() = _onBinaryPreviewLoaded
private val _onBinaryPreviewLoaded = SingleLiveEvent()
- fun loadDatabase(database: ContextualDatabase?) {
- loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo, mSearchInfo)
+ private val mUiState = MutableStateFlow(UIState.Loading)
+ val uiState: StateFlow = mUiState
+
+ fun loadTemplateEntry(database: ContextualDatabase?) {
+ loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo)
}
- fun loadTemplateEntry(database: ContextualDatabase?,
- entryId: NodeId?,
- parentId: NodeId<*>?,
- registerInfo: RegisterInfo?,
- searchInfo: SearchInfo?) {
+ fun loadTemplateEntry(
+ database: ContextualDatabase?,
+ entryId: NodeId?,
+ parentId: NodeId<*>?,
+ registerInfo: RegisterInfo?
+ ) {
this.mEntryId = entryId
this.mParentId = parentId
this.mRegisterInfo = registerInfo
- this.mSearchInfo = searchInfo
database?.let {
mEntryId?.let {
IOActionTask(
- {
+ scope = viewModelScope,
+ action = {
// Create an Entry copy to modify from the database entry
mEntry = database.getEntryById(it)
// Retrieve the parent
@@ -105,21 +117,24 @@ class EntryEditViewModel: NodeEditViewModel() {
database,
entry,
mIsTemplate,
- registerInfo,
- searchInfo
+ registerInfo
)
}
},
- { templatesEntry ->
+ onActionComplete = { templatesEntry ->
mEntryId = null
_templatesEntry.value = templatesEntry
+ if (templatesEntry?.overwrittenData == true) {
+ mUiState.value = UIState.ShowOverwriteMessage
+ }
}
).execute()
}
mParentId?.let {
IOActionTask(
- {
+ scope = viewModelScope,
+ action = {
mParent = database.getGroupById(it)
mParent?.let { parentGroup ->
mEntry = database.createEntry()?.apply {
@@ -145,12 +160,11 @@ class EntryEditViewModel: NodeEditViewModel() {
database,
mEntry,
mIsTemplate,
- registerInfo,
- searchInfo
+ registerInfo
)
}
},
- { templatesEntry ->
+ onActionComplete = { templatesEntry ->
mParentId = null
_templatesEntry.value = templatesEntry
}
@@ -159,32 +173,37 @@ class EntryEditViewModel: NodeEditViewModel() {
}
}
- private fun decodeTemplateEntry(database: ContextualDatabase,
- entry: Entry?,
- isTemplate: Boolean,
- registerInfo: RegisterInfo?,
- searchInfo: SearchInfo?): TemplatesEntry {
+ private fun decodeTemplateEntry(
+ database: ContextualDatabase,
+ entry: Entry?,
+ isTemplate: Boolean,
+ registerInfo: RegisterInfo?
+ ): TemplatesEntry {
val templates = database.getTemplates(isTemplate)
val entryTemplate = entry?.let { database.getTemplate(it) }
?: Template.STANDARD
var entryInfo: EntryInfo? = null
+ var overwrittenData = false
// Decode the entry / load entry info
entry?.let {
database.decodeEntryWithTemplateConfiguration(it).let { entry ->
// Load entry info
entry.getEntryInfo(database, true).let { tempEntryInfo ->
// Retrieve data from registration
- (registerInfo?.searchInfo ?: searchInfo)?.let { tempSearchInfo ->
- tempEntryInfo.saveSearchInfo(database, tempSearchInfo)
- }
registerInfo?.let { regInfo ->
- tempEntryInfo.saveRegisterInfo(database, regInfo)
+ overwrittenData = tempEntryInfo.saveRegisterInfo(database, regInfo)
}
entryInfo = tempEntryInfo
}
}
}
- return TemplatesEntry(isTemplate, templates, entryTemplate, entryInfo)
+ return TemplatesEntry(
+ isTemplate,
+ templates,
+ entryTemplate,
+ entryInfo,
+ overwrittenData
+ )
}
fun changeTemplate(template: Template) {
@@ -197,44 +216,52 @@ class EntryEditViewModel: NodeEditViewModel() {
_requestEntryInfoUpdate.value = EntryUpdate(database, mEntry, mParent)
}
+ fun unlockAction() {
+ actionLocked = false
+ }
+
fun saveEntryInfo(database: ContextualDatabase?, entry: Entry?, parent: Group?, entryInfo: EntryInfo) {
- IOActionTask(
- {
- removeTempAttachmentsNotCompleted(entryInfo)
- entry?.let { oldEntry ->
- // Create a clone
- var newEntry = Entry(oldEntry)
+ if (actionLocked.not()) {
+ actionLocked = true
+ IOActionTask(
+ scope = viewModelScope,
+ action = {
+ removeTempAttachmentsNotCompleted(entryInfo)
+ entry?.let { oldEntry ->
+ // Create a clone
+ var newEntry = Entry(oldEntry)
- // Build info
- newEntry.setEntryInfo(database, entryInfo)
+ // Build info
+ newEntry.setEntryInfo(database, entryInfo)
- // Encode entry properties for template
- _onTemplateChanged.value?.let { template ->
- newEntry =
- database?.encodeEntryWithTemplateConfiguration(newEntry, template)
- ?: newEntry
- }
+ // Encode entry properties for template
+ _onTemplateChanged.value?.let { template ->
+ newEntry =
+ database?.encodeEntryWithTemplateConfiguration(newEntry, template)
+ ?: newEntry
+ }
- // Delete temp attachment if not used
- mTempAttachments.forEach { tempAttachmentState ->
- val tempAttachment = tempAttachmentState.attachment
- database?.attachmentPool?.let { binaryPool ->
- if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) {
- database.removeAttachmentIfNotUsed(tempAttachment)
+ // Delete temp attachment if not used
+ mTempAttachments.forEach { tempAttachmentState ->
+ val tempAttachment = tempAttachmentState.attachment
+ database?.attachmentPool?.let { binaryPool ->
+ if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) {
+ database.removeAttachmentIfNotUsed(tempAttachment)
+ }
}
}
- }
- // Return entry to save
- EntrySave(oldEntry, newEntry, parent)
+ // Return entry to save
+ EntrySave(oldEntry, newEntry, parent)
+ }
+ },
+ onActionComplete = { entrySave ->
+ entrySave?.let {
+ _onEntrySaved.value = it
+ }
}
- },
- { entrySave ->
- entrySave?.let {
- _onEntrySaved.value = it
- }
- }
- ).execute()
+ ).execute()
+ }
}
private fun removeTempAttachmentsNotCompleted(entryInfo: EntryInfo) {
@@ -321,10 +348,13 @@ class EntryEditViewModel: NodeEditViewModel() {
_onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition)
}
- data class TemplatesEntry(val isTemplate: Boolean,
- val templates: List,
- val defaultTemplate: Template,
- val entryInfo: EntryInfo?)
+ data class TemplatesEntry(
+ val isTemplate: Boolean,
+ val templates: List,
+ val defaultTemplate: Template,
+ val entryInfo: EntryInfo?,
+ val overwrittenData: Boolean = false
+ )
data class EntryUpdate(val database: ContextualDatabase?, val entry: Entry?, val parent: Group?)
data class EntrySave(val oldEntry: Entry, val newEntry: Entry, val parent: Group?)
data class FieldEdition(val oldField: Field?, val newField: Field?)
@@ -332,6 +362,11 @@ class EntryEditViewModel: NodeEditViewModel() {
data class AttachmentUpload(val attachmentToUploadUri: Uri, val attachment: Attachment)
data class AttachmentPosition(val entryAttachmentState: EntryAttachmentState, val viewPosition: Float)
+ sealed class UIState {
+ object Loading: UIState()
+ object ShowOverwriteMessage: UIState()
+ }
+
companion object {
private val TAG = EntryEditViewModel::class.java.name
}
diff --git a/app/src/main/res/drawable/ic_passkey_white_24dp.xml b/app/src/main/res/drawable/ic_passkey_white_24dp.xml
new file mode 100644
index 000000000..4d0fabc83
--- /dev/null
+++ b/app/src/main/res/drawable/ic_passkey_white_24dp.xml
@@ -0,0 +1,10 @@
+
+