Merge tag '4.2.0' into develop

4.2.0
This commit is contained in:
J-Jamet
2025-10-15 23:30:49 +02:00
258 changed files with 11416 additions and 4078 deletions

3
.gitignore vendored
View File

@@ -19,6 +19,9 @@ bin/
gen/ gen/
out/ out/
# Kotlin folder
.kotlin/
# Gradle files # Gradle files
.gradle/ .gradle/
build/ build/

View File

@@ -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) KeePassDX(4.1.9)
* Fix landscape UI #2198 #2200 (@chenxiaolong) * Fix landscape UI #2198 #2200 (@chenxiaolong)
* Fix start loop and flash screen #2201 * Fix start loop and flash screen #2201

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 35 targetSdkVersion 35
versionCode = 143 versionCode = 145
versionName = "4.1.9" versionName = "4.2.0"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"
@@ -35,6 +35,10 @@ android {
} }
} }
buildFeatures {
buildConfig true
}
dependenciesInfo { dependenciesInfo {
// Disables dependency metadata when building APKs. // Disables dependency metadata when building APKs.
includeInApk = false includeInApk = false
@@ -101,6 +105,11 @@ android {
buildFeatures { buildFeatures {
buildConfig true 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" def room_version = "2.5.1"
@@ -140,7 +149,10 @@ dependencies {
implementation 'commons-codec:commons-codec:1.15' implementation 'commons-codec:commons-codec:1.15'
// Password generator // Password generator
implementation 'me.gosimple:nbvcxz:1.5.0' implementation 'me.gosimple:nbvcxz:1.5.0'
// Credentials Provider
implementation "androidx.credentials:credentials:1.2.2"
// Modules import // Modules import
implementation project(path: ':database') implementation project(path: ':database')
implementation project(path: ':icon-pack') implementation project(path: ':icon-pack')

View File

@@ -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"
}
]
}
}
]
}

View File

@@ -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"
}
]
}
}
]
}

View File

@@ -45,7 +45,8 @@
android:resizeableActivity="true" android:resizeableActivity="true"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/KeepassDXStyle.Night" android:theme="@style/KeepassDXStyle.Night"
tools:targetApi="s"> tools:targetApi="s"
tools:ignore="CredentialDependency">
<meta-data <meta-data
android:name="com.google.android.backup.api_key" android:name="com.google.android.backup.api_key"
android:value="${googleAndroidBackupAPIKey}" /> android:value="${googleAndroidBackupAPIKey}" />
@@ -158,25 +159,41 @@
android:label="@string/about" /> android:label="@string/about" />
<activity <activity
android:name="com.kunzisoft.keepass.settings.SettingsActivity" /> android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
<activity
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden"
android:excludeFromRecents="true"/>
<activity <activity
android:name="com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity" /> android:name="com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity" />
<activity <activity
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" /> android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
android:label="@string/keyboard_setting_label"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>
<activity
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity"
tools:targetApi="26" />
<activity
android:name="com.kunzisoft.keepass.settings.PasskeysSettingsActivity"
tools:targetApi="34" />
<activity <activity
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" /> android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
<activity <activity
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity" android:name="com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity"
android:theme="@style/Theme.Transparent" /> android:theme="@style/Theme.Transparent"
android:exported="false"
android:excludeFromRecents="true" />
<activity <activity
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity" android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden"
android:exported="false"
android:excludeFromRecents="true" />
<activity
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
android:theme="@style/Theme.Transparent" android:theme="@style/Theme.Transparent"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:exported="true"> android:exported="true"
android:excludeFromRecents="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@@ -192,14 +209,12 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity" android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
android:label="@string/keyboard_setting_label" android:theme="@style/Theme.Transparent"
android:exported="true"> android:configChanges="keyboardHidden"
<intent-filter> android:exported="false"
<action android:name="android.intent.action.MAIN"/> android:excludeFromRecents="true"
</intent-filter> tools:targetApi="upside_down_cake" />
</activity>
<service <service
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService" android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
android:foregroundServiceType="dataSync" android:foregroundServiceType="dataSync"
@@ -227,8 +242,8 @@
android:exported="false" /> android:exported="false" />
<!-- Receiver for Autofill --> <!-- Receiver for Autofill -->
<service <service
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService" android:name="com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService"
android:label="@string/autofill_service_name" android:label="@string/app_name"
android:exported="true" android:exported="true"
android:permission="android.permission.BIND_AUTOFILL_SERVICE"> android:permission="android.permission.BIND_AUTOFILL_SERVICE">
<meta-data <meta-data
@@ -239,7 +254,7 @@
</intent-filter> </intent-filter>
</service> </service>
<service <service
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService" android:name="com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService"
android:label="@string/keyboard_label" android:label="@string/keyboard_label"
android:exported="true" android:exported="true"
android:permission="android.permission.BIND_INPUT_METHOD" > android:permission="android.permission.BIND_INPUT_METHOD" >
@@ -249,6 +264,22 @@
<action android:name="android.view.InputMethod" /> <action android:name="android.view.InputMethod" />
</intent-filter> </intent-filter>
</service> </service>
<service
android:name="com.kunzisoft.keepass.credentialprovider.passkey.PasskeyProviderService"
android:enabled="true"
android:exported="true"
android:label="@string/passkey_service_name"
android:icon="@mipmap/ic_launcher"
android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
tools:targetApi="upside_down_cake">
<intent-filter>
<action android:name="android.service.credentials.CredentialProviderService" />
</intent-filter>
<meta-data
android:name="android.credentials.provider"
android:resource="@xml/provider" />
</service>
<receiver <receiver
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver" android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
android:exported="true"> android:exported="true">

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.activities package com.kunzisoft.keepass.activities
import android.content.pm.PackageManager.NameNotFoundException import android.content.pm.PackageManager.NameNotFoundException
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.util.Log import android.util.Log
@@ -32,7 +31,7 @@ import androidx.core.text.HtmlCompat
import com.kunzisoft.keepass.BuildConfig import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishActivity 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 com.kunzisoft.keepass.utils.getPackageInfoCompat
import org.joda.time.DateTime import org.joda.time.DateTime

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<Intent>? =
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<SearchInfo>(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<RegisterInfo>(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)
}
}
}

View File

@@ -50,15 +50,15 @@ import com.google.android.material.tabs.TabLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.fragments.EntryFragment import com.kunzisoft.keepass.activities.fragments.EntryFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper 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.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TagsAdapter 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.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryActivityEducation import com.kunzisoft.keepass.education.EntryActivityEducation
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.otp.OtpType import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.services.AttachmentFileNotificationService 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.ActionRunnable
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper 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.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.view.WindowInsetPosition import com.kunzisoft.keepass.view.WindowInsetPosition
import com.kunzisoft.keepass.view.applyWindowInsets import com.kunzisoft.keepass.view.applyWindowInsets
@@ -264,7 +264,7 @@ class EntryActivity : DatabaseLockActivity() {
mIcon = entryInfo.icon mIcon = entryInfo.icon
// Assign title text // Assign title text
val entryTitle = val entryTitle =
if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id) entryInfo.title.ifEmpty { entryInfo.id.asHexString() }
collapsingToolbarLayout?.title = entryTitle collapsingToolbarLayout?.title = entryTitle
toolbar?.title = entryTitle toolbar?.title = entryTitle
// Assign tags // Assign tags
@@ -312,11 +312,11 @@ class EntryActivity : DatabaseLockActivity() {
mEntryViewModel.historySelected.observe(this) { historySelected -> mEntryViewModel.historySelected.observe(this) { historySelected ->
mDatabase?.let { database -> mDatabase?.let { database ->
launch( launch(
this, activity = this,
database, database = database,
historySelected.nodeId, entryId = historySelected.nodeId,
historySelected.historyPosition, historyPosition = historySelected.historyPosition,
mEntryActivityResultLauncher activityResultLauncher = mEntryActivityResultLauncher
) )
} }
} }
@@ -330,9 +330,8 @@ class EntryActivity : DatabaseLockActivity() {
return coordinatorLayout return coordinatorLayout
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
mEntryViewModel.loadDatabase(database) mEntryViewModel.loadDatabase(database)
} }
@@ -478,11 +477,12 @@ class EntryActivity : DatabaseLockActivity() {
R.id.menu_edit -> { R.id.menu_edit -> {
mDatabase?.let { database -> mDatabase?.let { database ->
mMainEntryId?.let { entryId -> mMainEntryId?.let { entryId ->
EntryEditActivity.launchToUpdate( EntryEditActivity.launch(
this, activity = this,
database, database = database,
entryId, registrationType = EntryEditActivity.RegistrationType.UPDATE,
mEntryActivityResultLauncher nodeId = entryId,
activityResultLauncher = mEntryActivityResultLauncher
) )
} }
} }
@@ -520,7 +520,7 @@ class EntryActivity : DatabaseLockActivity() {
// Transit data in previous Activity after an update // Transit data in previous Activity after an update
Intent().apply { Intent().apply {
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId) putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
setResult(Activity.RESULT_OK, this) setResult(RESULT_OK, this)
} }
super.finish() super.finish()
} }
@@ -534,34 +534,22 @@ class EntryActivity : DatabaseLockActivity() {
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG" const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
/** /**
* Open standard Entry activity * Open standard or history Entry activity
*/ */
fun launch(activity: Activity, fun launch(
database: ContextualDatabase, activity: Activity,
entryId: NodeId<UUID>, database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>) { entryId: NodeId<UUID>,
historyPosition: Int? = null,
activityResultLauncher: ActivityResultLauncher<Intent>
) {
if (database.loaded) { if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java) val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId) intent.putExtra(KEY_ENTRY, entryId)
activityResultLauncher.launch(intent) historyPosition?.let {
} intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
} }
}
/**
* Open history Entry activity
*/
fun launch(activity: Activity,
database: ContextualDatabase,
entryId: NodeId<UUID>,
historyPosition: Int,
activityResultLauncher: ActivityResultLauncher<Intent>) {
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)
activityResultLauncher.launch(intent) activityResultLauncher.launch(intent)
} }
} }

View File

@@ -36,14 +36,14 @@ import android.widget.Spinner
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity 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.datepicker.MaterialDatePicker
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.timepicker.MaterialTimePicker 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.ReplaceFileDialogFragment
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.autofill.AutofillHelper 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.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Field 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.node.NodeId
import com.kunzisoft.keepass.database.element.template.Template import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.education.EntryEditActivityEducation import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.AttachmentState import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.DataTime import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.model.EntryAttachmentState 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.otp.OtpElement
import com.kunzisoft.keepass.services.AttachmentFileNotificationService import com.kunzisoft.keepass.services.AttachmentFileNotificationService
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService 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_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_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.services.KeyboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable 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.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
import kotlinx.coroutines.launch
import java.util.EnumSet import java.util.EnumSet
import java.util.UUID 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_entry_edit) setContentView(R.layout.activity_entry_edit)
@@ -210,8 +210,8 @@ class EntryEditActivity : DatabaseLockActivity(),
mDatabase, mDatabase,
entryId, entryId,
parentId, parentId,
EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent), intent.retrieveRegisterInfo()
EntrySelectionHelper.retrieveSearchInfoFromIntent(intent) ?: intent.retrieveSearchInfo()?.toRegisterInfo()
) )
// To retrieve attachment // To retrieve attachment
@@ -378,23 +378,30 @@ class EntryEditActivity : DatabaseLockActivity(),
} ?: run { } ?: run {
updateEntry(entrySave.oldEntry, entrySave.newEntry) updateEntry(entrySave.oldEntry, entrySave.newEntry)
} }
}
// Don't wait for saving if it's to provide autofill lifecycleScope.launch {
mDatabase?.let { database -> repeatOnLifecycle(Lifecycle.State.STARTED) {
EntrySelectionHelper.doSpecialAction(intent, mEntryEditViewModel.uiState.collect { uiState ->
{}, when (uiState) {
{}, EntryEditViewModel.UIState.Loading -> {}
{}, EntryEditViewModel.UIState.ShowOverwriteMessage -> {
{ if (mEntryEditViewModel.warningOverwriteDataAlreadyApproved.not()) {
entryValidatedForKeyboardSelection(database, entrySave.newEntry) AlertDialog.Builder(this@EntryEditActivity)
}, .setTitle(R.string.warning_overwrite_data_title)
{ _, _ -> .setMessage(R.string.warning_overwrite_data_description)
entryValidatedForAutofillSelection(database, entrySave.newEntry) .setNegativeButton(android.R.string.cancel) { _, _ ->
}, mEntryEditViewModel.backPressedAlreadyApproved = true
{ onCancelSpecialMode()
entryValidatedForAutofillRegistration(entrySave.newEntry) }
.setPositiveButton(android.R.string.ok) { _, _ ->
mEntryEditViewModel.warningOverwriteDataAlreadyApproved = true
}
.create().show()
}
}
} }
) }
} }
} }
} }
@@ -407,13 +414,13 @@ class EntryEditActivity : DatabaseLockActivity(),
return true return true
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
mAllowCustomFields = database?.allowEntryCustomFields() == true mAllowCustomFields = database.allowEntryCustomFields() == true
mAllowOTP = database?.allowOTP == true mAllowOTP = database.allowOTP == true
mEntryEditViewModel.loadDatabase(database) mEntryEditViewModel.loadTemplateEntry(database)
mTemplatesSelectorAdapter?.apply { mTemplatesSelectorAdapter?.apply {
iconDrawableFactory = mDatabase?.iconDrawableFactory iconDrawableFactory = database.iconDrawableFactory
notifyDataSetChanged() notifyDataSetChanged()
} }
} }
@@ -424,39 +431,45 @@ class EntryEditActivity : DatabaseLockActivity(),
result: ActionRunnable.Result result: ActionRunnable.Result
) { ) {
super.onDatabaseActionFinished(database, actionTask, result) super.onDatabaseActionFinished(database, actionTask, result)
mEntryEditViewModel.unlockAction()
when (actionTask) { when (actionTask) {
ACTION_DATABASE_CREATE_ENTRY_TASK, ACTION_DATABASE_CREATE_ENTRY_TASK,
ACTION_DATABASE_UPDATE_ENTRY_TASK -> { ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
try { try {
if (result.isSuccess) { if (result.isSuccess) {
var newNodes: List<Node> = ArrayList() result.data?.getNewEntry(database)?.let { entry ->
result.data?.getBundle(DatabaseTaskNotificationService.NEW_NODES_KEY)?.let { newNodesBundle -> EntrySelectionHelper.doSpecialAction(
newNodes = DatabaseTaskNotificationService.getListNodesFromBundle(database, newNodesBundle) intent = intent,
} defaultAction = {
if (newNodes.size == 1) { // Finish naturally
(newNodes[0] as? Entry?)?.let { entry -> finishForEntryResult(entry)
EntrySelectionHelper.doSpecialAction(intent, },
{ searchAction = {
// Finish naturally // Nothing when search retrieved
finishForEntryResult(entry) },
}, selectionAction = { intentSender, typeMode, searchInfo ->
{ when(typeMode) {
// Nothing when search retrieved TypeMode.DEFAULT -> {}
}, TypeMode.MAGIKEYBOARD ->
{ entryValidatedForKeyboardSelection(database, entry)
entryValidatedForSave(entry) TypeMode.PASSKEY ->
}, entryValidatedForPasskey(database, entry)
{ TypeMode.AUTOFILL ->
entryValidatedForKeyboardSelection(database, entry) entryValidatedForAutofill(database, entry)
},
{ _, _ ->
entryValidatedForAutofillSelection(database, entry)
},
{
entryValidatedForAutofillRegistration(entry)
} }
) },
} registrationAction = { _, typeMode, _ ->
when(typeMode) {
TypeMode.DEFAULT ->
entryValidatedForSave(entry)
TypeMode.MAGIKEYBOARD -> {}
TypeMode.PASSKEY ->
entryValidatedForPasskey(database, entry)
TypeMode.AUTOFILL ->
entryValidatedForAutofill(database, entry)
}
}
)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -483,19 +496,25 @@ class EntryEditActivity : DatabaseLockActivity(),
finishForEntryResult(entry) finishForEntryResult(entry)
} }
private fun entryValidatedForAutofillSelection(database: ContextualDatabase, entry: Entry) { private fun entryValidatedForAutofill(database: ContextualDatabase, entry: Entry) {
// Build Autofill response with the entry selected // Build Autofill response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity, this.buildSpecialModeResponseAndSetResult(
database, entryInfo = entry.getEntryInfo(database),
entry.getEntryInfo(database)) extras = buildEntryResult(entry)
)
} }
onValidateSpecialMode() 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() onValidateSpecialMode()
finishForEntryResult(entry)
} }
override fun onResume() { override fun onResume() {
@@ -729,13 +748,13 @@ class EntryEditActivity : DatabaseLockActivity(),
} }
private fun onApprovedBackPressed(approved: () -> Unit) { private fun onApprovedBackPressed(approved: () -> Unit) {
if (!backPressedAlreadyApproved) { if (mEntryEditViewModel.backPressedAlreadyApproved.not()) {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setMessage(R.string.discard_changes) .setMessage(R.string.discard_changes)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.discard) { _, _ -> .setPositiveButton(R.string.discard) { _, _ ->
mAttachmentFileBinderManager?.stopUploadAllAttachments() mAttachmentFileBinderManager?.stopUploadAllAttachments()
backPressedAlreadyApproved = true mEntryEditViewModel.backPressedAlreadyApproved = true
approved.invoke() approved.invoke()
}.create().show() }.create().show()
} else { } 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) { private fun finishForEntryResult(entry: Entry) {
// Assign entry callback as a result // Assign entry callback as a result
try { try {
val bundle = Bundle() val bundle = buildEntryResult(entry)
val intentEntry = Intent() val intentEntry = Intent()
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
intentEntry.putExtras(bundle) intentEntry.putExtras(bundle)
setResult(Activity.RESULT_OK, intentEntry) setResult(RESULT_OK, intentEntry)
super.finish() super.finish()
} catch (e: Exception) { } catch (e: Exception) {
// Exception when parcelable can't be done // Exception when parcelable can't be done
@@ -758,6 +782,10 @@ class EntryEditActivity : DatabaseLockActivity(),
} }
} }
enum class RegistrationType {
UPDATE, CREATE
}
companion object { companion object {
private val TAG = EntryEditActivity::class.java.name private val TAG = EntryEditActivity::class.java.name
@@ -767,23 +795,12 @@ class EntryEditActivity : DatabaseLockActivity(),
const val KEY_PARENT = "parent" const val KEY_PARENT = "parent"
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY" const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
fun registerForEntryResult(fragment: Fragment, fun registerForEntryResult(
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> { activity: FragmentActivity,
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit
if (result.resultCode == Activity.RESULT_OK) { ): ActivityResultLauncher<Intent> {
entryAddedOrUpdatedListener.invoke(
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
)
} else {
entryAddedOrUpdatedListener.invoke(null)
}
}
}
fun registerForEntryResult(activity: FragmentActivity,
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == RESULT_OK) {
entryAddedOrUpdatedListener.invoke( entryAddedOrUpdatedListener.invoke(
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY) 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, fun launch(
database: ContextualDatabase, activity: Activity,
entryId: NodeId<UUID>, database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>) { registrationType: RegistrationType,
nodeId: NodeId<*>,
activityResultLauncher: ActivityResultLauncher<Intent>
) {
if (database.loaded && !database.isReadOnly) { if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java) 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) 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, fun launchForSelection(
database: ContextualDatabase, context: Context,
groupId: NodeId<*>, database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>) { typeMode: TypeMode,
if (database.loaded && !database.isReadOnly) { groupId: NodeId<*>,
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { searchInfo: SearchInfo? = null,
val intent = Intent(activity, EntryEditActivity::class.java) activityResultLauncher: ActivityResultLauncher<Intent>? = null,
intent.putExtra(KEY_PARENT, groupId) ) {
activityResultLauncher.launch(intent)
}
}
}
fun launchToUpdateForSave(context: Context,
database: ContextualDatabase,
entryId: NodeId<UUID>,
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) {
if (database.loaded && !database.isReadOnly) { if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) { if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java) val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId) intent.putExtra(KEY_PARENT, groupId)
EntrySelectionHelper.startActivityForSaveModeResult( EntrySelectionHelper.startActivityForSelectionModeResult(
context, context = context,
intent, intent = intent,
searchInfo 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, fun launchForRegistration(
database: ContextualDatabase, context: Context,
groupId: NodeId<*>, database: ContextualDatabase,
searchInfo: SearchInfo? = null) { nodeId: NodeId<*>,
registerInfo: RegisterInfo? = null,
typeMode: TypeMode,
registrationType: RegistrationType,
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
) {
if (database.loaded && !database.isReadOnly) { if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) { if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java) val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId) when (registrationType) {
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult( RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
}
EntrySelectionHelper.startActivityForRegistrationModeResult(
context, 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<Intent>?,
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, activityResultLauncher,
autofillComponent,
searchInfo
)
}
}
}
/**
* Launch EntryEditActivity to register an updated entry (from autofill)
*/
fun launchToUpdateForRegistration(context: Context,
database: ContextualDatabase,
entryId: NodeId<UUID>,
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, intent,
registerInfo registerInfo,
) typeMode
}
}
}
/**
* 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
) )
} }
} }

View File

@@ -19,7 +19,6 @@
*/ */
package com.kunzisoft.keepass.activities package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@@ -33,8 +32,6 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
@@ -44,15 +41,14 @@ import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment 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.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation 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.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
import com.kunzisoft.keepass.utils.DexUtil import com.kunzisoft.keepass.utils.DexUtil
import com.kunzisoft.keepass.utils.MagikeyboardUtil import com.kunzisoft.keepass.utils.MagikeyboardUtil
import com.kunzisoft.keepass.utils.MenuUtil 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.UriUtil.openUrl
import com.kunzisoft.keepass.utils.getParcelableCompat import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
@@ -98,11 +94,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this)
else null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -132,7 +123,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri -> mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let { uri?.let {
launchPasswordActivityWithPath(uri) launchMainCredentialActivityWithPath(uri)
} }
} }
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri -> mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
@@ -161,7 +152,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
} }
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen -> mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri -> fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
launchPasswordActivity( launchMainCredentialActivity(
databaseFileUri, databaseFileUri,
fileDatabaseHistoryEntityToOpen.keyFileUri, fileDatabaseHistoryEntityToOpen.keyFileUri,
fileDatabaseHistoryEntityToOpen.hardwareKey fileDatabaseHistoryEntityToOpen.hardwareKey
@@ -180,7 +171,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
// Load default database the first time // Load default database the first time
databaseFilesViewModel.doForDefaultDatabase { databaseFileUri -> databaseFilesViewModel.doForDefaultDatabase { databaseFileUri ->
launchPasswordActivityWithPath(databaseFileUri) launchMainCredentialActivityWithPath(databaseFileUri)
} }
// Retrieve the database URI provided by file manager after an orientation change // Retrieve the database URI provided by file manager after an orientation change
@@ -225,11 +216,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) launchGroupActivityIfLoaded(database)
if (database != null) {
launchGroupActivityIfLoaded(database)
}
} }
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
@@ -237,8 +225,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
actionTask: String, actionTask: String,
result: ActionRunnable.Result result: ActionRunnable.Result
) { ) {
super.onDatabaseActionFinished(database, actionTask, result)
if (result.isSuccess) { if (result.isSuccess) {
// Update list // Update list
when (actionTask) { when (actionTask) {
@@ -288,17 +274,58 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show() Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
} }
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) { private fun launchMainCredentialActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
MainCredentialActivity.launch(this, try {
databaseUri, EntrySelectionHelper.doSpecialAction(
keyFile, intent = this.intent,
hardwareKey, defaultAction = {
{ exception -> MainCredentialActivity.launch(
fileNoFoundAction(exception) activity = this,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey
)
}, },
{ onCancelSpecialMode() }, searchAction = { searchInfo ->
{ onLaunchActivitySpecialMode() }, MainCredentialActivity.launchForSearchResult(
mAutofillActivityResultLauncher) 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) { private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
@@ -308,12 +335,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
{ onValidateSpecialMode() }, { onValidateSpecialMode() },
{ onCancelSpecialMode() }, { onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() }, { onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher) mCredentialActivityResultLauncher
)
} }
} }
private fun launchPasswordActivityWithPath(databaseUri: Uri) { private fun launchMainCredentialActivityWithPath(databaseUri: Uri) {
launchPasswordActivity(databaseUri, null, null) launchMainCredentialActivity(databaseUri, null, null)
// Delete flickering for kitkat <= // Delete flickering for kitkat <=
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) 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 // Show recent files if allowed
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) { if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
databaseFilesViewModel.loadListOfDatabases() databaseFilesViewModel.loadListOfDatabases()
@@ -359,7 +383,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
try { try {
mDatabaseFileUri?.let { databaseUri -> mDatabaseFileUri?.let { databaseUri ->
// Create the new database // Create the new database
createDatabase(databaseUri, mainCredential) mDatabaseViewModel.createDatabase(databaseUri, mainCredential)
} }
} catch (e: Exception) { } catch (e: Exception) {
val error = getString(R.string.error_create_database_file) val error = getString(R.string.error_create_database_file)
@@ -443,55 +467,36 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
* ------------------------- * -------------------------
*/ */
fun launchForSearchResult(context: Context, fun launchForSearchResult(
searchInfo: SearchInfo) { context: Context,
EntrySelectionHelper.startActivityForSearchModeResult(context, searchInfo: SearchInfo
Intent(context, FileDatabaseSelectActivity::class.java), ) {
searchInfo) EntrySelectionHelper.startActivityForSearchModeResult(
context = context,
intent = Intent(context, FileDatabaseSelectActivity::class.java),
searchInfo = searchInfo
)
} }
/* /*
* ------------------------- * -------------------------
* Save Launch * Selection Launch
* ------------------------- * -------------------------
*/ */
fun launchForSaveResult(context: Context, fun launchForSelection(
searchInfo: SearchInfo) { context: Context,
EntrySelectionHelper.startActivityForSaveModeResult(context, typeMode: TypeMode,
Intent(context, FileDatabaseSelectActivity::class.java), searchInfo: SearchInfo? = null,
searchInfo) activityResultLauncher: ActivityResultLauncher<Intent>? = null,
} ) {
EntrySelectionHelper.startActivityForSelectionModeResult(
/* context = context,
* ------------------------- intent = Intent(context, FileDatabaseSelectActivity::class.java),
* Keyboard Launch searchInfo = searchInfo,
* ------------------------- typeMode = typeMode,
*/ activityResultLauncher = activityResultLauncher
)
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<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null) {
AutofillHelper.startActivityForAutofillResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
activityResultLauncher,
autofillComponent,
searchInfo)
} }
/* /*
@@ -499,11 +504,19 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
* Registration Launch * Registration Launch
* ------------------------- * -------------------------
*/ */
fun launchForRegistration(context: Context, fun launchForRegistration(
registerInfo: RegisterInfo? = null) { context: Context,
EntrySelectionHelper.startActivityForRegistrationModeResult(context, typeMode: TypeMode,
Intent(context, FileDatabaseSelectActivity::class.java), registerInfo: RegisterInfo? = null,
registerInfo) activityResultLauncher: ActivityResultLauncher<Intent>?,
) {
EntrySelectionHelper.startActivityForRegistrationModeResult(
context = context,
activityResultLauncher = activityResultLauncher,
intent = Intent(context, FileDatabaseSelectActivity::class.java),
registerInfo = registerInfo,
typeMode = typeMode
)
} }
} }
} }

View File

@@ -174,10 +174,10 @@ class IconPickerActivity : DatabaseLockActivity() {
return true return true
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
if (database?.allowCustomIcons == true) { if (database.allowCustomIcons) {
uploadButton.setOpenDocumentClickListener(mExternalFileHelper) uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
} else { } else {
uploadButton.visibility = View.GONE uploadButton.visibility = View.GONE

View File

@@ -101,7 +101,7 @@ class ImageViewerActivity : DatabaseLockActivity() {
return true return true
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
try { try {
@@ -119,18 +119,16 @@ class ImageViewerActivity : DatabaseLockActivity() {
resources.displayMetrics.heightPixels * 2 resources.displayMetrics.heightPixels * 2
) )
database?.let { database -> BinaryDatabaseManager.loadBitmap(
BinaryDatabaseManager.loadBitmap( database,
database, attachment.binaryData,
attachment.binaryData, mImagePreviewMaxWidth
mImagePreviewMaxWidth ) { bitmapLoaded ->
) { bitmapLoaded -> if (bitmapLoaded == null) {
if (bitmapLoaded == null) { finish()
finish() } else {
} else { progressView.visibility = View.GONE
progressView.visibility = View.GONE imageView.setImageBitmap(bitmapLoaded)
imageView.setImageBitmap(bitmapLoaded)
}
} }
} }
} ?: finish() } ?: finish()

View File

@@ -36,7 +36,6 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
@@ -49,16 +48,15 @@ import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper 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.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.DeviceUnlockFragment import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
import com.kunzisoft.keepass.biometric.DeviceUnlockManager import com.kunzisoft.keepass.biometric.DeviceUnlockManager
import com.kunzisoft.keepass.biometric.deviceUnlockError 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.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
@@ -127,11 +125,6 @@ class MainCredentialActivity : DatabaseModeActivity() {
private var mReadOnly: Boolean = false private var mReadOnly: Boolean = false
private var mForceReadOnly: Boolean = false private var mForceReadOnly: Boolean = false
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this)
else null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -313,20 +306,18 @@ class MainCredentialActivity : DatabaseModeActivity() {
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
if (database != null) { // Trying to load another database
// Trying to load another database if (mDatabaseFileUri != null
if (mDatabaseFileUri != null && database.fileUri != null
&& database.fileUri != null && mDatabaseFileUri != database.fileUri) {
&& mDatabaseFileUri != database.fileUri) { Toast.makeText(this,
Toast.makeText(this, R.string.warning_database_already_opened,
R.string.warning_database_already_opened, Toast.LENGTH_LONG
Toast.LENGTH_LONG ).show()
).show()
}
launchGroupActivityIfLoaded(database)
} }
launchGroupActivityIfLoaded(database)
} }
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
@@ -433,7 +424,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
{ onValidateSpecialMode() }, { onValidateSpecialMode() },
{ onCancelSpecialMode() }, { onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() }, { onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher mCredentialActivityResultLauncher
) )
} }
} }
@@ -511,10 +502,11 @@ class MainCredentialActivity : DatabaseModeActivity() {
val password = intent.getStringExtra(KEY_PASSWORD) val password = intent.getStringExtra(KEY_PASSWORD)
// Consume the intent extra password // Consume the intent extra password
intent.removeExtra(KEY_PASSWORD) intent.removeExtra(KEY_PASSWORD)
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
if (password != null) { if (password != null) {
mainCredentialView?.populatePasswordTextView(password) mainCredentialView?.populatePasswordTextView(password)
} }
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
intent.removeExtra(KEY_LAUNCH_IMMEDIATELY)
if (launchImmediately) { if (launchImmediately) {
loadDatabase() loadDatabase()
} else { } else {
@@ -569,13 +561,10 @@ class MainCredentialActivity : DatabaseModeActivity() {
clearCredentialsViews() clearCredentialsViews()
} }
if (mReadOnly && ( if (mReadOnly && mSpecialMode == SpecialMode.REGISTRATION) {
mSpecialMode == SpecialMode.SAVE Log.e(TAG, getString(R.string.error_save_read_only))
|| mSpecialMode == SpecialMode.REGISTRATION)
) {
Log.e(TAG, getString(R.string.autofill_read_only_save))
Snackbar.make(coordinatorLayout, Snackbar.make(coordinatorLayout,
R.string.autofill_read_only_save, R.string.error_save_read_only,
Snackbar.LENGTH_LONG).asError().show() Snackbar.LENGTH_LONG).asError().show()
} else { } else {
databaseFileUri?.let { databaseUri -> databaseFileUri?.let { databaseUri ->
@@ -596,7 +585,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
readOnly: Boolean, readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?, cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUUID: Boolean) { fixDuplicateUUID: Boolean) {
loadDatabase( mDatabaseViewModel.loadDatabase(
databaseUri, databaseUri,
mainCredential, mainCredential,
readOnly, readOnly,
@@ -749,11 +738,13 @@ class MainCredentialActivity : DatabaseModeActivity() {
private const val KEY_PASSWORD = "password" private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately" private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private fun buildAndLaunchIntent(activity: Activity, private fun buildAndLaunchIntent(
databaseFile: Uri, activity: Activity,
keyFile: Uri?, databaseFile: Uri,
hardwareKey: HardwareKey?, keyFile: Uri?,
intentBuildLauncher: (Intent) -> Unit) { hardwareKey: HardwareKey?,
intentBuildLauncher: (Intent) -> Unit
) {
val intent = Intent(activity, MainCredentialActivity::class.java) val intent = Intent(activity, MainCredentialActivity::class.java)
intent.putExtra(KEY_FILENAME, databaseFile) intent.putExtra(KEY_FILENAME, databaseFile)
if (keyFile != null) if (keyFile != null)
@@ -770,10 +761,12 @@ class MainCredentialActivity : DatabaseModeActivity() {
*/ */
@Throws(FileNotFoundException::class) @Throws(FileNotFoundException::class)
fun launch(activity: Activity, fun launch(
databaseFile: Uri, activity: Activity,
keyFile: Uri?, databaseFile: Uri,
hardwareKey: HardwareKey?) { keyFile: Uri?,
hardwareKey: HardwareKey?
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
activity.startActivity(intent) activity.startActivity(intent)
} }
@@ -786,185 +779,73 @@ class MainCredentialActivity : DatabaseModeActivity() {
*/ */
@Throws(FileNotFoundException::class) @Throws(FileNotFoundException::class)
fun launchForSearchResult(activity: Activity, fun launchForSearchResult(
databaseFile: Uri, activity: Activity,
keyFile: Uri?, databaseFile: Uri,
hardwareKey: HardwareKey?, keyFile: Uri?,
searchInfo: SearchInfo) { hardwareKey: HardwareKey?,
searchInfo: SearchInfo
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSearchModeResult( EntrySelectionHelper.startActivityForSearchModeResult(
activity, context = activity,
intent, intent = intent,
searchInfo) searchInfo = searchInfo
)
} }
} }
/* /*
* ------------------------- * -------------------------
* Save Launch * Selection Launch
* ------------------------- * -------------------------
*/ */
@Throws(FileNotFoundException::class) @Throws(FileNotFoundException::class)
fun launchForSaveResult(activity: Activity, fun launchForSelection(
databaseFile: Uri, activity: AppCompatActivity,
keyFile: Uri?, databaseFile: Uri,
hardwareKey: HardwareKey?, keyFile: Uri?,
searchInfo: SearchInfo) { hardwareKey: HardwareKey?,
typeMode: TypeMode,
searchInfo: SearchInfo?,
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSaveModeResult( EntrySelectionHelper.startActivityForSelectionModeResult(
activity, context = activity,
intent, intent = intent,
searchInfo) typeMode = typeMode,
searchInfo = searchInfo,
activityResultLauncher = activityResultLauncher
)
} }
} }
/* /*
* ------------------------- * -------------------------
* Keyboard Launch * Registration Launch
* ------------------------- * -------------------------
*/ */
@Throws(FileNotFoundException::class) @Throws(FileNotFoundException::class)
fun launchForKeyboardResult(activity: Activity, fun launchForRegistration(
databaseFile: Uri, activity: Activity,
keyFile: Uri?, activityResultLauncher: ActivityResultLauncher<Intent>?,
hardwareKey: HardwareKey?, databaseFile: Uri,
searchInfo: SearchInfo?) { keyFile: Uri?,
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> hardwareKey: HardwareKey?,
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult( typeMode: TypeMode,
activity, registerInfo: RegisterInfo?
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<Intent>?,
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?) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForRegistrationModeResult( EntrySelectionHelper.startActivityForRegistrationModeResult(
activity, context = activity,
intent, activityResultLauncher = activityResultLauncher,
registerInfo) intent = intent,
} typeMode = typeMode,
} registerInfo = registerInfo
/*
* -------------------------
* Global Launch
* -------------------------
*/
fun launch(activity: AppCompatActivity,
databaseUri: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit,
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
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()
}
) )
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
} }
} }
} }

View File

@@ -67,7 +67,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
} }
builder.setMessage(stringBuilder) builder.setMessage(stringBuilder)
builder.setPositiveButton(android.R.string.ok) { _, _ -> builder.setPositiveButton(android.R.string.ok) { _, _ ->
actionDatabaseListener?.validateDatabaseChanged() actionDatabaseListener?.onDatabaseChangeValidated()
} }
return builder.create() return builder.create()
} }
@@ -76,7 +76,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
} }
interface ActionDatabaseChangedListener { interface ActionDatabaseChangedListener {
fun validateDatabaseChanged() fun onDatabaseChangeValidated()
} }
companion object { companion object {
@@ -86,9 +86,10 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO" private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE" private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE"
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo, fun getInstance(
newSnapFileDatabaseInfo: SnapFileDatabaseInfo, oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
readOnly: Boolean newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
readOnly: Boolean
) )
: DatabaseChangedDialogFragment { : DatabaseChangedDialogFragment {
val fragment = DatabaseChangedDialogFragment() val fragment = DatabaseChangedDialogFragment()

View File

@@ -5,6 +5,9 @@ import android.view.View
import android.view.WindowManager.LayoutParams.FLAG_SECURE import android.view.WindowManager.LayoutParams.FLAG_SECURE
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels 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.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.ContextualDatabase 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.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval { abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels() private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private var mDatabase: ContextualDatabase? = null private val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
lifecycleScope.launch {
mDatabaseViewModel.database.observe(this) { database -> repeatOnLifecycle(Lifecycle.State.STARTED) {
this.mDatabase = database mDatabaseViewModel.actionState.collect { uiState ->
resetAppTimeoutOnTouchOrFocus() when (uiState) {
onDatabaseRetrieved(database) is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished(
uiState.database,
uiState.actionTask,
uiState.result
)
}
else -> {}
}
}
}
} }
lifecycleScope.launch {
mDatabaseViewModel.actionFinished.observe(this) { result -> repeatOnLifecycle(Lifecycle.State.RESUMED) {
onDatabaseActionFinished(result.database, result.actionTask, result.result) mDatabaseViewModel.databaseState.collect { database ->
database?.let {
onDatabaseRetrieved(database)
}
}
}
} }
} }
@@ -45,13 +65,14 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@Deprecated(message = "")
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
resetAppTimeoutOnTouchOrFocus() resetAppTimeoutOnTouchOrFocus()
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Can be overridden by a subclass // Can be overridden by a subclass
} }

View File

@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.model.GroupInfo import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString 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.utils.getParcelableCompat
import com.kunzisoft.keepass.view.DateTimeFieldView import com.kunzisoft.keepass.view.DateTimeFieldView
@@ -62,14 +62,14 @@ class GroupDialogFragment : DatabaseDialogFragment() {
private lateinit var uuidContainerView: ViewGroup private lateinit var uuidContainerView: ViewGroup
private lateinit var uuidReferenceView: TextView private lateinit var uuidReferenceView: TextView
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon -> mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor) database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
} }
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon) mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
if (database?.allowCustomSearchableGroup() == true) { if (database.allowCustomSearchableGroup()) {
searchableLabelView.visibility = View.VISIBLE searchableLabelView.visibility = View.VISIBLE
searchableView.visibility = View.VISIBLE searchableView.visibility = View.VISIBLE
} else { } else {
@@ -155,7 +155,7 @@ class GroupDialogFragment : DatabaseDialogFragment() {
searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable) searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable)
autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType, autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType,
mGroupInfo.defaultAutoTypeSequence) mGroupInfo.defaultAutoTypeSequence)
val uuid = UuidUtil.toHexString(mGroupInfo.id) val uuid = mGroupInfo.id?.asHexString()
if (uuid == null || uuid.isEmpty()) { if (uuid == null || uuid.isEmpty()) {
uuidContainerView.visibility = View.GONE uuidContainerView.visibility = View.GONE
} else { } else {

View File

@@ -112,32 +112,32 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon -> mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor) database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
} }
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon) mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
searchableContainerView.visibility = if (database?.allowCustomSearchableGroup() == true) { searchableContainerView.visibility = if (database.allowCustomSearchableGroup()) {
View.VISIBLE View.VISIBLE
} else { } else {
View.GONE View.GONE
} }
if (database?.allowAutoType() == true) { if (database.allowAutoType()) {
autoTypeContainerView.visibility = View.VISIBLE autoTypeContainerView.visibility = View.VISIBLE
} else { } else {
autoTypeContainerView.visibility = View.GONE autoTypeContainerView.visibility = View.GONE
} }
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool) tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
tagsCompletionView.apply { tagsCompletionView.apply {
threshold = 1 threshold = 1
setAdapter(tagsAdapter) 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 { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

View File

@@ -45,10 +45,10 @@ class IconEditDialogFragment : DatabaseDialogFragment() {
private var mCustomIcon: IconImageCustom? = null private var mCustomIcon: IconImageCustom? = null
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon -> mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon) database.iconDrawableFactory.assignDatabaseIcon(imageView, icon)
} }
mCustomIcon?.let { customIcon -> mCustomIcon?.let { customIcon ->
populateViewsWithCustomIcon(customIcon) populateViewsWithCustomIcon(customIcon)

View File

@@ -35,9 +35,9 @@ import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
import com.kunzisoft.keepass.password.PasswordEntropy import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.UriUtil.openUrl import com.kunzisoft.keepass.utils.UriUtil.openUrl
@@ -258,8 +258,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
showEmptyPasswordConfirmationDialog() showEmptyPasswordConfirmationDialog()
} else if (!error } else if (!error
&& hardwareKey != null && hardwareKey != null
&& !HardwareKeyActivity.isHardwareKeyAvailable( && !HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)
requireActivity(), hardwareKey, false)
) { ) {
// show hardware driver dialog if required // show hardware driver dialog if required
error = true error = true

View File

@@ -29,7 +29,11 @@ import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.* 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 androidx.appcompat.app.AlertDialog
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R 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.MAX_TOTP_PERIOD
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_HOTP_COUNTER 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_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_OTP_SECRET
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
import com.kunzisoft.keepass.otp.OtpTokenType import com.kunzisoft.keepass.otp.OtpTokenType
import com.kunzisoft.keepass.otp.OtpType import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.otp.TokenCalculator 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.UriUtil.openUrl
import com.kunzisoft.keepass.utils.getParcelableCompat import com.kunzisoft.keepass.utils.getParcelableCompat
import java.util.* import java.util.Locale
class SetOTPDialogFragment : DatabaseDialogFragment() { class SetOTPDialogFragment : DatabaseDialogFragment() {

View File

@@ -4,36 +4,59 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels 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.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval { abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels() protected val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
protected var mDatabase: ContextualDatabase? = null protected val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database -> override fun onCreate(savedInstanceState: Bundle?) {
if (mDatabase == null || mDatabase != database) { super.onCreate(savedInstanceState)
this.mDatabase = database lifecycleScope.launch {
onDatabaseRetrieved(database) repeatOnLifecycle(Lifecycle.State.STARTED) {
mDatabaseViewModel.actionState.collect { uiState ->
when (uiState) {
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished(
uiState.database,
uiState.actionTask,
uiState.result
)
}
else -> {}
}
}
} }
} }
lifecycleScope.launch {
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result -> repeatOnLifecycle(Lifecycle.State.RESUMED) {
onDatabaseActionFinished(result.database, result.actionTask, result.result) mDatabaseViewModel.databaseState.collect { database ->
database?.let {
onDatabaseRetrieved(database)
}
}
}
} }
} }
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) { protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
context?.let { 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 // Can be overridden by a subclass
} }
protected fun buildNewBinaryAttachment(): BinaryData? {
return mDatabase?.buildNewBinaryAttachment()
}
} }

View File

@@ -230,7 +230,7 @@ class EntryEditFragment: DatabaseFragment() {
val attachmentToUploadUri = it.attachmentToUploadUri val attachmentToUploadUri = it.attachmentToUploadUri
val fileName = it.fileName val fileName = it.fileName
buildNewBinaryAttachment()?.let { binaryAttachment -> mDatabaseViewModel.buildNewAttachment()?.let { binaryAttachment ->
val entryAttachment = Attachment(fileName, binaryAttachment) val entryAttachment = Attachment(fileName, binaryAttachment)
// Ask to replace the current attachment // Ask to replace the current attachment
if ((!mAllowMultipleAttachments if ((!mAllowMultipleAttachments
@@ -273,13 +273,13 @@ class EntryEditFragment: DatabaseFragment() {
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
templateView.populateIconMethod = { imageView, icon -> 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?.database = database
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize -> attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
@@ -290,12 +290,12 @@ class EntryEditFragment: DatabaseFragment() {
} }
} }
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool) tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
tagsCompletionView.apply { tagsCompletionView.apply {
threshold = 1 threshold = 1
setAdapter(tagsAdapter) 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?) { private fun assignEntryInfo(entryInfo: EntryInfo?) {

View File

@@ -24,7 +24,7 @@ import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString 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.TemplateView
import com.kunzisoft.keepass.view.hideByFading import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.showByFading 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 -> context?.let { context ->
attachmentsAdapter = EntryAttachmentsItemsAdapter(context) attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
attachmentsAdapter?.database = database attachmentsAdapter?.database = database
@@ -184,7 +184,7 @@ class EntryFragment: DatabaseFragment() {
// customDataView.text = entryInfo?.customData?.toString() // customDataView.text = entryInfo?.customData?.toString()
// Assign special data // Assign special data
uuidReferenceView.text = UuidUtil.toHexString(entryInfo?.id) uuidReferenceView.text = entryInfo?.id?.asHexString()
} }
private fun showClipboardDialog() { private fun showClipboardDialog() {

View File

@@ -36,9 +36,9 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.adapters.NodesAdapter 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.ContextualDatabase
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum import com.kunzisoft.keepass.database.element.SortNodeEnum
@@ -154,46 +154,44 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
super.onDetach() super.onDetach()
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
context?.let { context -> context?.let { context ->
database?.let { database -> mAdapter = NodesAdapter(context, database).apply {
mAdapter = NodesAdapter(context, database).apply { setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback { override fun onNodeClick(database: ContextualDatabase, node: Node) {
override fun onNodeClick(database: ContextualDatabase, node: Node) { if (nodeActionSelectionMode) {
if (nodeActionSelectionMode) { if (listActionNodes.contains(node)) {
if (listActionNodes.contains(node)) { // Remove selected item if already selected
// Remove selected item if already selected listActionNodes.remove(node)
listActionNodes.remove(node)
} else {
// Add selected item if not already selected
listActionNodes.add(node)
}
nodeClickListener?.onNodeSelected(database, listActionNodes)
setActionNodes(listActionNodes)
notifyNodeChanged(node)
} else { } 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 { override fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean {
if (nodeActionPasteMode == PasteMode.UNDEFINED) { if (nodeActionPasteMode == PasteMode.UNDEFINED) {
// Select the first item after a long click // Select the first item after a long click
if (!listActionNodes.contains(node)) if (!listActionNodes.contains(node))
listActionNodes.add(node) listActionNodes.add(node)
nodeClickListener?.onNodeSelected(database, listActionNodes) nodeClickListener?.onNodeSelected(database, listActionNodes)
setActionNodes(listActionNodes) setActionNodes(listActionNodes)
notifyNodeChanged(node) notifyNodeChanged(node)
activity?.hideKeyboard() activity?.hideKeyboard()
}
return true
} }
}) return true
} }
mNodesRecyclerView?.adapter = mAdapter })
} }
mNodesRecyclerView?.adapter = mAdapter
} }
} }
@@ -248,7 +246,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener) mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
activity?.intent?.let { activity?.intent?.let {
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it) specialMode = it.retrieveSpecialMode()
} }
} }
@@ -299,9 +297,9 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
} }
} }
private fun containsRecycleBin(nodes: List<Node>): Boolean { private fun containsRecycleBin(database: ContextualDatabase?, nodes: List<Node>): Boolean {
return mDatabase?.isRecycleBinEnabled == true return database?.isRecycleBinEnabled == true
&& nodes.any { it == mDatabase?.recycleBin } && nodes.any { it == database.recycleBin }
} }
fun actionNodesCallback(database: ContextualDatabase, fun actionNodesCallback(database: ContextualDatabase,
@@ -328,7 +326,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
// Open and Edit for a single item // Open and Edit for a single item
if (nodes.size == 1) { if (nodes.size == 1) {
// Edition // Edition
if (database.isReadOnly || containsRecycleBin(nodes)) { if (database.isReadOnly || containsRecycleBin(database, nodes)) {
menu?.removeItem(R.id.menu_edit) menu?.removeItem(R.id.menu_edit)
} }
} else { } else {
@@ -348,7 +346,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
} }
// Deletion // Deletion
if (database.isReadOnly || containsRecycleBin(nodes)) { if (database.isReadOnly || containsRecycleBin(database, nodes)) {
menu?.removeItem(R.id.menu_delete) menu?.removeItem(R.id.menu_delete)
} }
} }

View File

@@ -71,8 +71,8 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
resetAppTimeoutWhenViewFocusedOrChanged(view) resetAppTimeoutWhenViewFocusedOrChanged(view)
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory iconPickerAdapter.iconDrawableFactory = database.iconDrawableFactory
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val populateList = launch { val populateList = launch {

View File

@@ -48,9 +48,9 @@ class IconPickerFragment : DatabaseFragment() {
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
iconPickerPagerAdapter = IconPickerPagerAdapter(this, iconPickerPagerAdapter = IconPickerPagerAdapter(this,
if (database?.allowCustomIcons == true) 2 else 1) if (database.allowCustomIcons) 2 else 1)
viewPager.adapter = iconPickerPagerAdapter viewPager.adapter = iconPickerPagerAdapter
TabLayoutMediator(tabLayout, viewPager) { tab, position -> TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = when (position) { tab.text = when (position) {

View File

@@ -107,7 +107,7 @@ class KeyGeneratorFragment : DatabaseFragment() {
super.onDestroyView() super.onDestroyView()
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Nothing here // Nothing here
} }

View File

@@ -244,7 +244,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Nothing here // Nothing here
} }

View File

@@ -293,20 +293,22 @@ class PasswordGeneratorFragment : DatabaseFragment() {
private fun generatePassword() { private fun generatePassword() {
var password = "" var password = ""
try { try {
password = PasswordGenerator(resources).generatePassword(getPasswordLength(), password = PasswordGenerator(resources).generatePassword(
uppercaseCompound.isChecked, length = getPasswordLength(),
lowercaseCompound.isChecked, upperCase = uppercaseCompound.isChecked,
digitsCompound.isChecked, lowerCase = lowercaseCompound.isChecked,
minusCompound.isChecked, digits = digitsCompound.isChecked,
underlineCompound.isChecked, minus = minusCompound.isChecked,
spaceCompound.isChecked, underline = underlineCompound.isChecked,
specialsCompound.isChecked, space = spaceCompound.isChecked,
bracketsCompound.isChecked, specials = specialsCompound.isChecked,
extendedCompound.isChecked, brackets = bracketsCompound.isChecked,
getConsiderChars(), extended = extendedCompound.isChecked,
getIgnoreChars(), considerChars = getConsiderChars(),
atLeastOneCompound.isChecked, ignoreChars = getIgnoreChars(),
excludeAmbiguousCompound.isChecked) atLeastOneFromEach = atLeastOneCompound.isChecked,
excludeAmbiguousChar = excludeAmbiguousCompound.isChecked
)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to generate a password", e) Log.e(TAG, "Unable to generate a password", e)
} }
@@ -318,7 +320,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
super.onDestroy() super.onDestroy()
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Nothing here // Nothing here
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<SpecialMode>(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<TypeMode>(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<SpecialMode>(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)
}
}
}
}

View File

@@ -1,5 +0,0 @@
package com.kunzisoft.keepass.activities.helpers
enum class TypeMode {
DEFAULT, MAGIKEYBOARD, AUTOFILL
}

View File

@@ -1,96 +1,240 @@
package com.kunzisoft.keepass.activities.legacy 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 android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels 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.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.DatabaseTaskProvider.Companion.startDatabaseService
import com.kunzisoft.keepass.database.DatabaseTaskProvider import com.kunzisoft.keepass.database.ProgressMessage
import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.tasks.ActionRunnable 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 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 val mDatabaseViewModel: DatabaseViewModel by viewModels()
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null protected val mDatabase: ContextualDatabase?
protected var mDatabase: ContextualDatabase? = null 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<Pair<Bundle?, String>>()
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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) 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 -> is DatabaseViewModel.ActionState.OnDatabaseActionRequested -> {
val databaseWasReloaded = database?.wasReloaded == true startDatabasePermissionService(
if (databaseWasReloaded && finishActivityIfReloadRequested()) { uiState.bundle,
finish() uiState.actionTask
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) { )
database?.wasReloaded = false }
onDatabaseRetrieved(database)
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 -> lifecycleScope.launch {
onDatabaseActionFinished(database, actionTask, result) repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database ->
// Nullable function
onUnknownDatabaseRetrieved(database)
database?.let {
onDatabaseRetrieved(database)
}
}
}
} }
} }
protected open fun showDatabaseDialog(): Boolean { /**
return true * Nullable function to retrieve a database
} */
open fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDestroy() {
mDatabaseTaskProvider?.destroy()
mDatabaseTaskProvider = null
mDatabase = null
super.onDestroy()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
mDatabase = database
mDatabaseViewModel.defineDatabase(database)
// optional method implementation // optional method implementation
} }
override fun onDatabaseRetrieved(database: ContextualDatabase) {
// optional method implementation
}
open fun manageDatabaseInfo(): Boolean = true
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
database: ContextualDatabase, database: ContextualDatabase,
actionTask: String, actionTask: String,
result: ActionRunnable.Result result: ActionRunnable.Result
) { ) {
mDatabaseViewModel.onActionFinished(database, actionTask, result)
// optional method implementation // optional method implementation
} }
fun createDatabase( private fun startDatabasePermissionService(bundle: Bundle?, actionTask: String) {
databaseUri: Uri, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
mainCredential: MainCredential 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( private fun startDialog(progressMessage: ProgressMessage) {
databaseUri: Uri, lifecycleScope.launch {
mainCredential: MainCredential, if (progressTaskDialogFragment == null) {
readOnly: Boolean, progressTaskDialogFragment = supportFragmentManager
cipherEncryptDatabase: CipherEncryptDatabase?, .findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
fixDuplicateUuid: Boolean }
) { if (progressTaskDialogFragment == null) {
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid) progressTaskDialogFragment = ProgressTaskDialogFragment()
progressTaskDialogFragment?.show(
supportFragmentManager,
PROGRESS_TASK_DIALOG_TAG
)
}
updateDialog(progressMessage)
}
} }
protected fun closeDatabase() { private fun updateDialog(progressMessage: ProgressMessage) {
mDatabase?.clearAndClose(this.getBinaryDir()) progressTaskDialogFragment?.apply {
updateTitle(progressMessage.titleId)
updateMessage(progressMessage.messageId)
updateWarning(progressMessage.warningId)
setCancellable(progressMessage.cancelable)
}
} }
override fun onResume() { private fun stopDialog() {
super.onResume() progressTaskDialogFragment?.dismissAllowingStateLoss()
mDatabaseTaskProvider?.registerProgressTask() progressTaskDialogFragment = null
} }
override fun onPause() { protected open fun showDatabaseDialog(): Boolean {
mDatabaseTaskProvider?.unregisterProgressTask() return true
super.onPause()
} }
} }

View File

@@ -34,8 +34,8 @@ import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
@@ -87,128 +87,44 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
deleteDatabaseNodes(nodes) 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 mExitLock = false
} }
open fun finishActivityIfDatabaseNotLoaded(): Boolean { override fun onDatabaseRetrieved(database: ContextualDatabase) {
return true
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
// End activity if database not loaded // End activity if database not loaded
if (finishActivityIfDatabaseNotLoaded() && (database == null || !database.loaded)) { if (database.loaded.not())
finish() finish()
}
// Focus view to reinitialize timeout, // Focus view to reinitialize timeout,
// view is not necessary loaded so retry later in resume // view is not necessary loaded so retry later in resume
viewToInvalidateTimeout() viewToInvalidateTimeout()
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database?.loaded) ?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded)
database?.let { // check timeout
// check timeout if (mTimeoutEnable) {
if (mTimeoutEnable) { if (mLockReceiver == null) {
if (mLockReceiver == null) { mLockReceiver = LockReceiver {
mLockReceiver = LockReceiver { closeDatabase(database)
mDatabase = null mExitLock = true
closeDatabase(database) closeOptionsMenu()
mExitLock = true finish()
closeOptionsMenu()
finish()
}
registerLockReceiver(mLockReceiver)
} }
registerLockReceiver(mLockReceiver)
// 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 // After the first creation
mMergeDataAllowed = database.isMergeDataAllowed() // or If simply swipe with another application
// If the time is out -> close the Activity
checkRegister() 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() { override fun finish() {
@@ -227,7 +143,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
actionTask: String, actionTask: String,
result: ActionRunnable.Result result: ActionRunnable.Result
) { ) {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) { when (actionTask) {
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK, DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> { DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
@@ -249,24 +164,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
databaseUri: Uri?, databaseUri: Uri?,
mainCredential: MainCredential mainCredential: MainCredential
) { ) {
assignDatabasePassword(databaseUri, mainCredential) mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
} }
private fun assignDatabasePassword( fun assignMainCredential(mainCredential: MainCredential) {
databaseUri: Uri?,
mainCredential: MainCredential
) {
if (databaseUri != null) {
mDatabaseTaskProvider?.startDatabaseAssignCredential(databaseUri, mainCredential)
}
}
fun assignPassword(mainCredential: MainCredential) {
mDatabase?.let { database -> mDatabase?.let { database ->
database.fileUri?.let { databaseUri -> database.fileUri?.let { databaseUri ->
// Show the progress dialog now or after dialog confirmation // Show the progress dialog now or after dialog confirmation
if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) { if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) {
assignDatabasePassword(databaseUri, mainCredential) mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
} else { } else {
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential) PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
.show(supportFragmentManager, "passwordEncodingTag") .show(supportFragmentManager, "passwordEncodingTag")
@@ -276,45 +182,51 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
} }
fun saveDatabase() { fun saveDatabase() {
mDatabaseTaskProvider?.startDatabaseSave(true) mDatabaseViewModel.saveDatabase(save = true)
} }
fun saveDatabaseTo(uri: Uri) { fun saveDatabaseTo(uri: Uri) {
mDatabaseTaskProvider?.startDatabaseSave(true, uri) mDatabaseViewModel.saveDatabase(save = true, saveToUri = uri)
} }
fun mergeDatabase() { fun mergeDatabase() {
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable) mDatabaseViewModel.mergeDatabase(save = mAutoSaveEnable)
} }
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) { fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable, uri, mainCredential) mDatabaseViewModel.mergeDatabase(mAutoSaveEnable, uri, mainCredential)
} }
fun reloadDatabase() { fun reloadDatabase() {
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) { mDatabaseViewModel.reloadDatabase(fixDuplicateUuid = false)
mDatabaseTaskProvider?.startDatabaseReload(false)
}
} }
fun createEntry(newEntry: Entry, fun createEntry(
parent: Group) { newEntry: Entry,
mDatabaseTaskProvider?.startDatabaseCreateEntry(newEntry, parent, mAutoSaveEnable) parent: Group
) {
mDatabaseViewModel.createEntry(newEntry, parent, mAutoSaveEnable)
} }
fun updateEntry(oldEntry: Entry, fun updateEntry(
entryToUpdate: Entry) { oldEntry: Entry,
mDatabaseTaskProvider?.startDatabaseUpdateEntry(oldEntry, entryToUpdate, mAutoSaveEnable) entryToUpdate: Entry
) {
mDatabaseViewModel.updateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
} }
fun copyNodes(nodesToCopy: List<Node>, fun copyNodes(
newParent: Group) { nodesToCopy: List<Node>,
mDatabaseTaskProvider?.startDatabaseCopyNodes(nodesToCopy, newParent, mAutoSaveEnable) newParent: Group
) {
mDatabaseViewModel.copyNodes(nodesToCopy, newParent, mAutoSaveEnable)
} }
fun moveNodes(nodesToMove: List<Node>, fun moveNodes(
newParent: Group) { nodesToMove: List<Node>,
mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable) newParent: Group
) {
mDatabaseViewModel.moveNodes(nodesToMove, newParent, mAutoSaveEnable)
} }
private fun eachNodeRecyclable(database: ContextualDatabase, nodes: List<Node>): Boolean { private fun eachNodeRecyclable(database: ContextualDatabase, nodes: List<Node>): Boolean {
@@ -330,6 +242,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
} }
fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) { fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) {
// TODO Move in ViewModel
mDatabase?.let { database -> mDatabase?.let { database ->
// If recycle bin enabled, ensure it exists // If recycle bin enabled, ensure it exists
if (database.isRecycleBinEnabled) { if (database.isRecycleBinEnabled) {
@@ -350,11 +263,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
} }
private fun deleteDatabaseNodes(nodes: List<Node>) { private fun deleteDatabaseNodes(nodes: List<Node>) {
mDatabaseTaskProvider?.startDatabaseDeleteNodes(nodes, mAutoSaveEnable) mDatabaseViewModel.deleteNodes(nodes, mAutoSaveEnable)
} }
fun createGroup(parent: Group, fun createGroup(
groupInfo: GroupInfo?) { parent: Group,
groupInfo: GroupInfo?
) {
// TODO Move in ViewModel
// Build the group // Build the group
mDatabase?.createGroup()?.let { newGroup -> mDatabase?.createGroup()?.let { newGroup ->
groupInfo?.let { info -> groupInfo?.let { info ->
@@ -362,12 +278,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
} }
// Not really needed here because added in runnable but safe // Not really needed here because added in runnable but safe
newGroup.parent = parent newGroup.parent = parent
mDatabaseTaskProvider?.startDatabaseCreateGroup(newGroup, parent, mAutoSaveEnable) mDatabaseViewModel.createGroup(newGroup, parent, mAutoSaveEnable)
} }
} }
fun updateGroup(oldGroup: Group, fun updateGroup(
groupInfo: GroupInfo) { oldGroup: Group,
groupInfo: GroupInfo
) {
// TODO Move in ViewModel
// If group updated save it in the database // If group updated save it in the database
val updateGroup = Group(oldGroup).let { updateGroup -> val updateGroup = Group(oldGroup).let { updateGroup ->
updateGroup.apply { updateGroup.apply {
@@ -377,27 +296,28 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
this.setGroupInfo(groupInfo) this.setGroupInfo(groupInfo)
} }
} }
mDatabaseTaskProvider?.startDatabaseUpdateGroup(oldGroup, updateGroup, mAutoSaveEnable) mDatabaseViewModel.updateGroup(oldGroup, updateGroup, mAutoSaveEnable)
} }
fun restoreEntryHistory(mainEntryId: NodeId<UUID>, fun restoreEntryHistory(
entryHistoryPosition: Int) { mainEntryId: NodeId<UUID>,
mDatabaseTaskProvider entryHistoryPosition: Int
?.startDatabaseRestoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable) ) {
mDatabaseViewModel.restoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
} }
fun deleteEntryHistory(mainEntryId: NodeId<UUID>, fun deleteEntryHistory(
entryHistoryPosition: Int) { mainEntryId: NodeId<UUID>,
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable) entryHistoryPosition: Int
) {
mDatabaseViewModel.deleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
} }
private fun checkRegister() { private fun checkRegister() {
// If in ave or registration mode, don't allow read only // If in registration mode, don't allow read only
if ((mSpecialMode == SpecialMode.SAVE if (mSpecialMode == SpecialMode.REGISTRATION && mDatabaseReadOnly) {
|| mSpecialMode == SpecialMode.REGISTRATION)
&& mDatabaseReadOnly) {
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show() Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
EntrySelectionHelper.removeModesFromIntent(intent) intent.removeModes()
finish() finish()
} }
} }

View File

@@ -1,13 +1,24 @@
package com.kunzisoft.keepass.activities.legacy package com.kunzisoft.keepass.activities.legacy
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
import com.kunzisoft.keepass.activities.helpers.TypeMode 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.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.ToolbarSpecial import com.kunzisoft.keepass.view.ToolbarSpecial
@@ -19,10 +30,25 @@ import com.kunzisoft.keepass.view.ToolbarSpecial
abstract class DatabaseModeActivity : DatabaseActivity() { abstract class DatabaseModeActivity : DatabaseActivity() {
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
private var mTypeMode: TypeMode = TypeMode.DEFAULT protected var mTypeMode: TypeMode = TypeMode.DEFAULT
private var mToolbarSpecial: ToolbarSpecial? = null private var mToolbarSpecial: ToolbarSpecial? = null
/**
* Utility activity result launcher,
* Used recursively, close each activity with return data
*/
protected open var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
setActivityResult(
lockDatabase = false,
resultCode = it.resultCode,
data = it.data
)
}
open fun onDatabaseBackPressed() { open fun onDatabaseBackPressed() {
if (mSpecialMode != SpecialMode.DEFAULT) if (mSpecialMode != SpecialMode.DEFAULT)
onCancelSpecialMode() onCancelSpecialMode()
@@ -42,20 +68,14 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
/** /**
* Intent sender uses special retains data in callback * Intent sender uses special retains data in callback
*/ */
private fun isIntentSender(): Boolean { protected fun isIntentSender(): Boolean {
return (mSpecialMode == SpecialMode.SELECTION return isIntentSenderMode(mSpecialMode, mTypeMode)
&& mTypeMode == TypeMode.AUTOFILL)
/* TODO Registration callback #765
|| (mSpecialMode == SpecialMode.REGISTRATION
&& mTypeMode == TypeMode.AUTOFILL
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
*/
} }
fun onLaunchActivitySpecialMode() { fun onLaunchActivitySpecialMode() {
if (!isIntentSender()) { if (!isIntentSender()) {
EntrySelectionHelper.removeModesFromIntent(intent) intent.removeModes()
EntrySelectionHelper.removeInfoFromIntent(intent) intent.removeInfo()
finish() finish()
} }
} }
@@ -64,8 +84,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
if (isIntentSender()) { if (isIntentSender()) {
super.finish() super.finish()
} else { } else {
EntrySelectionHelper.removeModesFromIntent(intent) intent.removeModes()
EntrySelectionHelper.removeInfoFromIntent(intent) intent.removeInfo()
if (mSpecialMode != SpecialMode.DEFAULT) { if (mSpecialMode != SpecialMode.DEFAULT) {
backToTheMainAppAndFinish() backToTheMainAppAndFinish()
} }
@@ -77,8 +97,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
// To get the app caller, only for IntentSender // To get the app caller, only for IntentSender
onRegularBackPressed() onRegularBackPressed()
} else { } else {
EntrySelectionHelper.removeModesFromIntent(intent) intent.removeModes()
EntrySelectionHelper.removeInfoFromIntent(intent) intent.removeInfo()
if (mSpecialMode != SpecialMode.DEFAULT) { if (mSpecialMode != SpecialMode.DEFAULT) {
backToTheMainAppAndFinish() backToTheMainAppAndFinish()
} }
@@ -109,17 +129,18 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
} }
}) })
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent) mSpecialMode = intent.retrieveSpecialMode()
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent) mTypeMode = intent.retrieveTypeMode()
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent) mSpecialMode = intent.retrieveSpecialMode()
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent) mTypeMode = intent.retrieveTypeMode()
val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)?.searchInfo val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent) val searchInfo: SearchInfo? = registerInfo?.searchInfo
?: intent.retrieveSearchInfo()
// To show the selection mode // To show the selection mode
mToolbarSpecial = findViewById(R.id.special_mode_view) mToolbarSpecial = findViewById(R.id.special_mode_view)
@@ -128,26 +149,25 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
val selectionModeStringId = when (mSpecialMode) { val selectionModeStringId = when (mSpecialMode) {
SpecialMode.DEFAULT, // Not important because hidden SpecialMode.DEFAULT, // Not important because hidden
SpecialMode.SEARCH -> R.string.search_mode SpecialMode.SEARCH -> R.string.search_mode
SpecialMode.SAVE -> R.string.save_mode
SpecialMode.SELECTION -> R.string.selection_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) { val typeModeStringId = when (mTypeMode) {
TypeMode.DEFAULT, // Not important because hidden TypeMode.DEFAULT, // Not important because hidden
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
TypeMode.AUTOFILL -> R.string.autofill TypeMode.AUTOFILL -> R.string.autofill
TypeMode.PASSKEY -> R.string.passkey
} }
title = getString(selectionModeStringId) title = getString(selectionModeStringId)
if (mTypeMode != TypeMode.DEFAULT) if (mTypeMode != TypeMode.DEFAULT)
title = "$title (${getString(typeModeStringId)})" title = "$title (${getString(typeModeStringId)})"
// Populate subtitle // Populate subtitle
subtitle = searchInfo?.getName(resources) subtitle = registerInfo?.getName(resources) ?: searchInfo?.getName(resources)
// Show the toolbar or not // Show the toolbar or not
visible = when (mSpecialMode) { visible = when (mSpecialMode) {
SpecialMode.DEFAULT -> false SpecialMode.DEFAULT -> false
SpecialMode.SEARCH -> true SpecialMode.SEARCH -> true
SpecialMode.SAVE -> true
SpecialMode.SELECTION -> true SpecialMode.SELECTION -> true
SpecialMode.REGISTRATION -> true SpecialMode.REGISTRATION -> true
} }

View File

@@ -4,8 +4,11 @@ import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
interface DatabaseRetrieval { interface DatabaseRetrieval {
fun onDatabaseRetrieved(database: ContextualDatabase?) fun onDatabaseRetrieved(database: ContextualDatabase)
fun onDatabaseActionFinished(database: ContextualDatabase,
actionTask: String, fun onDatabaseActionFinished(
result: ActionRunnable.Result) database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
)
} }

View File

@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.adapters
import android.content.Context import android.content.Context
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Color import android.graphics.Color
import android.os.Build
import android.util.Log import android.util.Log
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -418,6 +417,7 @@ class NodesAdapter (
} }
} }
// OTP
val otpElement = entry.getOtpElement() val otpElement = entry.getOtpElement()
holder.otpContainer?.removeCallbacks(holder.otpRunnable) holder.otpContainer?.removeCallbacks(holder.otpRunnable)
if (otpElement != null if (otpElement != null
@@ -438,7 +438,11 @@ class NodesAdapter (
holder.otpContainer?.visibility = View.GONE holder.otpContainer?.visibility = View.GONE
} }
holder.attachmentIcon?.visibility = 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 // Assign colors
assignBackgroundColor(holder.container, entry) assignBackgroundColor(holder.container, entry)
@@ -451,6 +455,7 @@ class NodesAdapter (
holder.otpToken?.setTextColor(foregroundColor) holder.otpToken?.setTextColor(foregroundColor)
holder.otpProgress?.setIndicatorColor(foregroundColor) holder.otpProgress?.setIndicatorColor(foregroundColor)
holder.attachmentIcon?.setColorFilter(foregroundColor) holder.attachmentIcon?.setColorFilter(foregroundColor)
holder.passkeyIcon?.setColorFilter(foregroundColor)
holder.meta.setTextColor(foregroundColor) holder.meta.setTextColor(foregroundColor)
iconColor = foregroundColor iconColor = foregroundColor
} else { } else {
@@ -459,6 +464,7 @@ class NodesAdapter (
holder.otpToken?.setTextColor(mTextColorSecondary) holder.otpToken?.setTextColor(mTextColorSecondary)
holder.otpProgress?.setIndicatorColor(mTextColorSecondary) holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
holder.attachmentIcon?.setColorFilter(mTextColorSecondary) holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
holder.passkeyIcon?.setColorFilter(mTextColorSecondary)
holder.meta.setTextColor(mTextColor) holder.meta.setTextColor(mTextColor)
} }
} else { } else {
@@ -467,6 +473,7 @@ class NodesAdapter (
holder.otpToken?.setTextColor(mColorOnSecondary) holder.otpToken?.setTextColor(mColorOnSecondary)
holder.otpProgress?.setIndicatorColor(mColorOnSecondary) holder.otpProgress?.setIndicatorColor(mColorOnSecondary)
holder.attachmentIcon?.setColorFilter(mColorOnSecondary) holder.attachmentIcon?.setColorFilter(mColorOnSecondary)
holder.passkeyIcon?.setColorFilter(mColorOnSecondary)
holder.meta.setTextColor(mColorOnSecondary) holder.meta.setTextColor(mColorOnSecondary)
} }
@@ -611,6 +618,7 @@ class NodesAdapter (
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer) var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers) var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon) var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
var passkeyIcon: ImageView? = itemView.findViewById(R.id.node_passkey_icon)
} }
companion object { companion object {

View File

@@ -1,6 +0,0 @@
package com.kunzisoft.keepass.autofill
import android.app.assist.AssistStructure
data class AutofillComponent(val assistStructure: AssistStructure,
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<Intent>? = 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: 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<EntryInfo>,
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<SpecialMode>(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<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
}
fun Intent.removeModes() {
removeExtra(KEY_SPECIAL_MODE)
removeExtra(KEY_TYPE_MODE)
}
fun Intent.addNodesIds(nodesIds: List<UUID>): Intent {
this.putParcelableList(EXTRA_NODES_IDS, nodesIds.map { ParcelUuid(it) })
return this
}
fun Intent.retrieveNodesIds(): List<UUID>? {
return getParcelableList<ParcelUuid>(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<ParcelUuid>(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<SpecialMode>(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<EntryInfo>,
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
}
}

View File

@@ -1,9 +1,8 @@
package com.kunzisoft.keepass.activities.helpers package com.kunzisoft.keepass.credentialprovider
enum class SpecialMode { enum class SpecialMode {
DEFAULT, DEFAULT,
SEARCH, SEARCH,
SAVE,
SELECTION, SELECTION,
REGISTRATION; REGISTRATION;
} }

View File

@@ -0,0 +1,5 @@
package com.kunzisoft.keepass.credentialprovider
enum class TypeMode {
DEFAULT, MAGIKEYBOARD, PASSKEY, AUTOFILL
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
autofillLauncherViewModel.manageSelectionResult(it)
}
private var mAutofillRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
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
}
}
}
}

View File

@@ -17,23 +17,25 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.activities package com.kunzisoft.keepass.credentialprovider.activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import androidx.core.net.toUri
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.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.ContextualDatabase
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
import com.kunzisoft.keepass.utils.getParcelableCompat 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, * Activity to search or select entry in database,
@@ -49,8 +51,8 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
return false return false
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database) super.onUnknownDatabaseRetrieved(database)
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE) val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
if (keySelectionBundle != null) { if (keySelectionBundle != null) {
@@ -73,7 +75,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
if (OtpEntryFields.isOTPUri(extra)) if (OtpEntryFields.isOTPUri(extra))
otpString = extra otpString = extra
else else
sharedWebDomain = Uri.parse(extra).host sharedWebDomain = extra.toUri().host
} }
} }
launchSelection(database, sharedWebDomain, otpString) launchSelection(database, sharedWebDomain, otpString)
@@ -84,7 +86,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
if (OtpEntryFields.isOTPUri(extra)) if (OtpEntryFields.isOTPUri(extra))
otpString = extra otpString = extra
} }
launchSelection(database, sharedWebDomain, otpString) launchSelection(database, null, otpString)
} }
else -> { else -> {
if (database != null) { if (database != null) {
@@ -106,11 +108,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
this.webDomain = sharedWebDomain this.webDomain = sharedWebDomain
this.otpString = otpString this.otpString = otpString
} }
launch(database, searchInfo)
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launch(database, searchInfo)
}
} }
private fun launch(database: ContextualDatabase?, private fun launch(database: ContextualDatabase?,
@@ -121,87 +119,106 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
// If database is open // If database is open
val readOnly = database?.isReadOnly != false val readOnly = database?.isReadOnly != false
SearchHelper.checkAutoSearchInfo(this, SearchHelper.checkAutoSearchInfo(
database, context = this,
searchInfo, database = database,
{ openedDatabase, items -> searchInfo = searchInfo,
// Items found onItemsFound = { openedDatabase, items ->
if (searchInfo.otpString != null) { // Items found
if (!readOnly) { if (searchInfo.otpString != null) {
GroupActivity.launchForSaveResult( if (!readOnly) {
this, GroupActivity.launchForRegistration(
openedDatabase, context = this,
searchInfo, activityResultLauncher = null,
false) database = openedDatabase,
} else { registerInfo = searchInfo.toRegisterInfo(),
Toast.makeText(applicationContext, typeMode = TypeMode.DEFAULT
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)
}
) )
} else { } else {
GroupActivity.launchForSearchResult(this, toastError(RegisterInReadOnlyDatabaseException())
openedDatabase,
searchInfo,
true)
} }
}, } else if (searchShareForMagikeyboard) {
{ openedDatabase -> MagikeyboardService.performSelection(
// Show the database UI to select the entry items,
if (searchInfo.otpString != null) { { entryInfo ->
if (!readOnly) { // Automatically populate keyboard
GroupActivity.launchForSaveResult(this, MagikeyboardService.populateKeyboardAndMoveAppToBackground(
openedDatabase, this,
searchInfo, entryInfo
false) )
} else { },
Toast.makeText(applicationContext, { autoSearch ->
R.string.autofill_read_only_save, GroupActivity.launchForSelection(
Toast.LENGTH_LONG) context = this,
.show() database = openedDatabase,
typeMode = TypeMode.MAGIKEYBOARD,
searchInfo = searchInfo,
autoSearch = autoSearch
)
} }
} else if (searchShareForMagikeyboard) { )
GroupActivity.launchForKeyboardSelectionResult(this, } else {
openedDatabase, GroupActivity.launchForSearchResult(
searchInfo, this,
false) openedDatabase,
} else { searchInfo,
GroupActivity.launchForSearchResult(this, true
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)
}
} }
},
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
)
}
}
) )
} }

View File

@@ -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<Intent> =
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)
}
}
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
passkeyLauncherViewModel.manageSelectionResult(it)
}
private var mPasskeyRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
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
)
}
}
}

View File

@@ -0,0 +1,8 @@
package com.kunzisoft.keepass.credentialprovider.autofill
import android.app.assist.AssistStructure
data class AutofillComponent(
val assistStructure: AssistStructure,
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?
)

View File

@@ -17,10 +17,9 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.autofill package com.kunzisoft.keepass.credentialprovider.autofill
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.app.PendingIntent import android.app.PendingIntent
import android.app.assist.AssistStructure import android.app.assist.AssistStructure
import android.content.Context import android.content.Context
@@ -38,19 +37,14 @@ import android.view.autofill.AutofillId
import android.view.autofill.AutofillManager import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue import android.view.autofill.AutofillValue
import android.widget.RemoteViews import android.widget.RemoteViews
import android.widget.Toast
import android.widget.inline.InlinePresentationSpec import android.widget.inline.InlinePresentationSpec
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.autofill.inline.UiVersions import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi import androidx.autofill.inline.v1.InlineSuggestionUi
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AutofillLauncherActivity import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.template.TemplateField 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.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil 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 com.kunzisoft.keepass.utils.getParcelableExtraCompat
import java.io.IOException
import kotlin.math.min import kotlin.math.min
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
object AutofillHelper { 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" private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? { fun Intent.addAutofillComponent(autofillComponent: AutofillComponent) {
intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure -> 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<AssistStructure>(EXTRA_BASE_STRUCTURE)?.let { assistStructure ->
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AutofillComponent(assistStructure, AutofillComponent(assistStructure,
intent.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST)) getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
} else { } else {
AutofillComponent(assistStructure, null) AutofillComponent(assistStructure, null)
} }
@@ -132,11 +136,13 @@ object AutofillHelper {
return this return this
} }
private fun buildDatasetForEntry(context: Context, private fun buildDatasetForEntry(
database: ContextualDatabase, context: Context,
entryInfo: EntryInfo, database: ContextualDatabase,
struct: StructureParser.Result, entryInfo: EntryInfo,
inlinePresentation: InlinePresentation?): Dataset { struct: StructureParser.Result,
inlinePresentation: InlinePresentation?
): Dataset {
val remoteViews: RemoteViews = newRemoteViews(context, database, makeEntryTitle(entryInfo), entryInfo.icon) val remoteViews: RemoteViews = newRemoteViews(context, database, makeEntryTitle(entryInfo), entryInfo.icon)
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 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) { if (field.name == TemplateField.LABEL_HOLDER) {
struct.creditCardHolderId?.let { ccNameId -> struct.creditCardHolderId?.let { ccNameId ->
datasetBuilder.addValueToDatasetBuilder( datasetBuilder.addValueToDatasetBuilder(
@@ -294,30 +300,15 @@ object AutofillHelper {
return dataset 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") @SuppressLint("RestrictedApi")
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
private fun buildInlinePresentationForEntry(context: Context, private fun buildInlinePresentationForEntry(
database: ContextualDatabase, context: Context,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest, database: ContextualDatabase,
positionItem: Int, compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
entryInfo: EntryInfo): InlinePresentation? { positionItem: Int,
entryInfo: EntryInfo
): InlinePresentation? {
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
@@ -336,13 +327,9 @@ object AutofillHelper {
// Build the content for IME UI // Build the content for IME UI
val pendingIntent = PendingIntent.getActivity( val pendingIntent = PendingIntent.getActivity(
context, context,
0, randomRequestCode(),
Intent(context, AutofillSettingsActivity::class.java), Intent(context, AutofillSettingsActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.FLAG_IMMUTABLE
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
) )
return InlinePresentation( return InlinePresentation(
InlineSuggestionUi.newContentBuilder(pendingIntent).apply { InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
@@ -353,7 +340,7 @@ object AutofillHelper {
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply { Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
setTintBlendMode(BlendMode.DST) setTintBlendMode(BlendMode.DST)
}) })
buildIconFromEntry(context, database, entryInfo)?.let { icon -> entryInfo.buildIcon(context, database)?.let { icon ->
setEndIcon(icon.apply { setEndIcon(icon.apply {
setTintBlendMode(BlendMode.DST) setTintBlendMode(BlendMode.DST)
}) })
@@ -367,9 +354,11 @@ object AutofillHelper {
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private fun buildInlinePresentationForManualSelection(context: Context, private fun buildInlinePresentationForManualSelection(
inlinePresentationSpec: InlinePresentationSpec, context: Context,
pendingIntent: PendingIntent): InlinePresentation? { inlinePresentationSpec: InlinePresentationSpec,
pendingIntent: PendingIntent
): InlinePresentation? {
// Make sure that the IME spec claims support for v1 UI template. // Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style val imeStyle = inlinePresentationSpec.style
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1)) if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
@@ -386,11 +375,13 @@ object AutofillHelper {
}.build().slice, inlinePresentationSpec, false) }.build().slice, inlinePresentationSpec, false)
} }
fun buildResponse(context: Context, fun buildResponse(
database: ContextualDatabase, context: Context,
entriesInfo: List<EntryInfo>, database: ContextualDatabase,
parseResult: StructureParser.Result, entriesInfo: List<EntryInfo>,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? { parseResult: StructureParser.Result,
autofillComponent: AutofillComponent
): FillResponse? {
val responseBuilder = FillResponse.Builder() val responseBuilder = FillResponse.Builder()
// Add Header // Add Header
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -411,7 +402,8 @@ object AutofillHelper {
// Add inline suggestion for new IME and dataset // Add inline suggestion for new IME and dataset
var numberInlineSuggestions = 0 var numberInlineSuggestions = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> autofillComponent.compatInlineSuggestionsRequest
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size) numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) { if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) { if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
@@ -427,21 +419,27 @@ object AutofillHelper {
var inlinePresentation: InlinePresentation? = null var inlinePresentation: InlinePresentation? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& numberInlineSuggestions > 0 && numberInlineSuggestions > 0
&& compatInlineSuggestionsRequest != null) { && autofillComponent.compatInlineSuggestionsRequest != null) {
inlinePresentation = buildInlinePresentationForEntry( inlinePresentation = buildInlinePresentationForEntry(
context, context,
database, database,
compatInlineSuggestionsRequest, autofillComponent.compatInlineSuggestionsRequest,
numberInlineSuggestions--, numberInlineSuggestions--,
entry entry
) )
} }
// Create dataset for each entry // Create dataset for each entry
responseBuilder.addDataset( responseBuilder.addDataset(
buildDatasetForEntry(context, database, entry, parseResult, inlinePresentation) buildDatasetForEntry(
context = context,
database = database,
entryInfo = entry,
struct = parseResult,
inlinePresentation = inlinePresentation
)
) )
} catch (e: Exception) { } 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 webScheme = parseResult.webScheme
manualSelection = true manualSelection = true
} }
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry) val manualSelectionView = RemoteViews(
AutofillLauncherActivity.getPendingIntentForSelection(context, context.packageName,
searchInfo, compatInlineSuggestionsRequest)?.let { pendingIntent -> R.layout.item_autofill_select_entry
)
AutofillLauncherActivity.getPendingIntentForSelection(
context,
searchInfo,
autofillComponent
)?.let { pendingIntent ->
var inlinePresentation: InlinePresentation? = null var inlinePresentation: InlinePresentation? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> autofillComponent.compatInlineSuggestionsRequest
val inlinePresentationSpec = ?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
inlineSuggestionsRequest.inlinePresentationSpecs[0] val inlinePresentationSpec =
inlinePresentation = buildInlinePresentationForManualSelection( inlineSuggestionsRequest.inlinePresentationSpecs[0]
context, inlinePresentation = buildInlinePresentationForManualSelection(
inlinePresentationSpec, context,
pendingIntent inlinePresentationSpec,
) pendingIntent
} )
}
} }
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 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, fun buildResponse(
database: ContextualDatabase, context: Context,
entryInfo: EntryInfo) { autofillComponent: AutofillComponent,
buildResponseAndSetResult(activity, database, ArrayList<EntryInfo>().apply { add(entryInfo) }) database: ContextualDatabase,
} entriesInfo: List<EntryInfo>,
onIntentCreated: (Intent) -> Unit
/** ) {
* Build the Autofill response for many entry
*/
fun buildResponseAndSetResult(activity: Activity,
database: ContextualDatabase,
entriesInfo: List<EntryInfo>) {
if (entriesInfo.isEmpty()) { if (entriesInfo.isEmpty()) {
activity.setResult(Activity.RESULT_CANCELED) throw IOException("No entries found")
} else { } else {
var setResultOk = false StructureParser(autofillComponent.assistStructure).parse()?.let { result ->
activity.intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure -> // New Response
StructureParser(structure).parse()?.let { result -> onIntentCreated(Intent().putExtra(
// New Response AutofillManager.EXTRA_AUTHENTICATION_RESULT,
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { buildResponse(
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat<CompatInlineSuggestionsRequest>(EXTRA_INLINE_SUGGESTIONS_REQUEST) context = context,
if (compatInlineSuggestionsRequest != null) { database = database,
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show() entriesInfo = entriesInfo,
} parseResult = result,
buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest) autofillComponent = autofillComponent
} else { )
buildResponse(activity, database, entriesInfo, result, null) ))
} } ?: throw IOException("Unable to parse the structure")
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)
}
} }
} }
fun buildActivityResultLauncher(activity: AppCompatActivity,
lockDatabase: Boolean = false): ActivityResultLauncher<Intent> {
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<Intent>?,
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 private val TAG = AutofillHelper::class.java.name
} }

View File

@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.autofill package com.kunzisoft.keepass.credentialprovider.autofill
import android.annotation.TargetApi import android.annotation.TargetApi
import android.os.Build import android.os.Build

View File

@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.autofill package com.kunzisoft.keepass.credentialprovider.autofill
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
@@ -43,8 +43,8 @@ import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi import androidx.autofill.inline.v1.InlineSuggestionUi
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AutofillLauncherActivity import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
import com.kunzisoft.keepass.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW import com.kunzisoft.keepass.credentialprovider.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.helper.SearchHelper 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.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.WebDomain import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import org.joda.time.DateTime import org.joda.time.DateTime
@@ -92,10 +92,11 @@ class KeeAutofillService : AutofillService() {
autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this) autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this)
} }
override fun onFillRequest(request: FillRequest, override fun onFillRequest(
cancellationSignal: CancellationSignal, request: FillRequest,
callback: FillCallback) { cancellationSignal: CancellationSignal,
callback: FillCallback
) {
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") } cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) { if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
@@ -120,64 +121,64 @@ class KeeAutofillService : AutofillService() {
webDomain = parseResult.webDomain webDomain = parseResult.webDomain
webScheme = parseResult.webScheme webScheme = parseResult.webScheme
} }
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain -> val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
searchInfo.webDomain = webDomainWithoutSubDomain && autofillInlineSuggestionsEnabled) {
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R CompatInlineSuggestionsRequest(request)
&& autofillInlineSuggestionsEnabled) { } else {
CompatInlineSuggestionsRequest(request) null
} else {
null
}
launchSelection(mDatabase,
searchInfo,
parseResult,
inlineSuggestionsRequest,
callback)
} }
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") @SuppressLint("RestrictedApi")
private fun showUIForEntrySelection(parseResult: StructureParser.Result, private fun showUIForEntrySelection(
database: ContextualDatabase?, parseResult: StructureParser.Result,
searchInfo: SearchInfo, database: ContextualDatabase?,
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?, searchInfo: SearchInfo,
callback: FillCallback) { autofillComponent: AutofillComponent,
callback: FillCallback
) {
var success = false var success = false
parseResult.allAutofillIds().let { autofillIds -> parseResult.allAutofillIds().let { autofillIds ->
if (autofillIds.isNotEmpty()) { if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used // If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response. // to generate Response.
AutofillLauncherActivity.getPendingIntentForSelection(this, AutofillLauncherActivity.getPendingIntentForSelection(
searchInfo, inlineSuggestionsRequest)?.intentSender?.let { intentSender -> this,
searchInfo,
autofillComponent
)?.intentSender?.let { intentSender ->
val responseBuilder = FillResponse.Builder() val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (database == null) { val remoteViewsUnlock: RemoteViews = if (database == null) {
if (!parseResult.webDomain.isNullOrEmpty()) { if (!parseResult.webDomain.isNullOrEmpty()) {
@@ -268,7 +269,8 @@ class KeeAutofillService : AutofillService() {
&& autofillInlineSuggestionsEnabled && autofillInlineSuggestionsEnabled
) { ) {
var inlinePresentation: InlinePresentation? = null var inlinePresentation: InlinePresentation? = null
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> autofillComponent.compatInlineSuggestionsRequest
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs = val inlinePresentationSpecs =
inlineSuggestionsRequest.inlinePresentationSpecs inlineSuggestionsRequest.inlinePresentationSpecs
if (inlineSuggestionsRequest.maxSuggestionCount > 0 if (inlineSuggestionsRequest.maxSuggestionCount > 0
@@ -286,7 +288,7 @@ class KeeAutofillService : AutofillService() {
InlineSuggestionUi.newContentBuilder( InlineSuggestionUi.newContentBuilder(
PendingIntent.getActivity( PendingIntent.getActivity(
this, this,
0, randomRequestCode(),
Intent(this, AutofillSettingsActivity::class.java), Intent(this, AutofillSettingsActivity::class.java),
PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_IMMUTABLE
) )
@@ -358,7 +360,7 @@ class KeeAutofillService : AutofillService() {
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) { override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
var success = false var success = false
if (askToSaveData) { if (askToSaveData && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val latestStructure = request.fillContexts.last().structure val latestStructure = request.fillContexts.last().structure
StructureParser(latestStructure).parse(true)?.let { parseResult -> StructureParser(latestStructure).parse(true)?.let { parseResult ->
@@ -384,30 +386,32 @@ class KeeAutofillService : AutofillService() {
} }
// Show UI to save data // Show UI to save data
val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
}
val registerInfo = RegisterInfo( val registerInfo = RegisterInfo(
SearchInfo().apply { searchInfo = searchInfo,
applicationId = parseResult.applicationId username = parseResult.usernameValue?.textValue?.toString(),
webDomain = parseResult.webDomain password = parseResult.passwordValue?.textValue?.toString(),
webScheme = parseResult.webScheme creditCard = parseResult.creditCardNumber?.let { cardNumber ->
},
parseResult.usernameValue?.textValue?.toString(),
parseResult.passwordValue?.textValue?.toString(),
CreditCard( CreditCard(
parseResult.creditCardHolder, parseResult.creditCardHolder,
parseResult.creditCardNumber, cardNumber,
expiration, expiration,
parseResult.cardVerificationValue parseResult.cardVerificationValue
)) )
}
)
// TODO Callback in each activity #765 AutofillLauncherActivity.getPendingIntentForRegistration(
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { this,
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this, registerInfo
// registerInfo)) )?.intentSender?.let { intentSender ->
//} else { success = true
AutofillLauncherActivity.launchForRegistration(this, registerInfo) callback.onSuccess(intentSender)
success = true }
callback.onSuccess()
//}
} }
} }
} }

View File

@@ -16,7 +16,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*/ */
package com.kunzisoft.keepass.autofill package com.kunzisoft.keepass.credentialprovider.autofill
import android.app.assist.AssistStructure import android.app.assist.AssistStructure
import android.os.Build import android.os.Build
@@ -362,8 +362,8 @@ class StructureParser(private val structure: AssistStructure) {
if (result?.passwordId == null) { if (result?.passwordId == null) {
usernameIdCandidate = autofillId usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
} }
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
} }
inputIsVariationType(inputType, inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> { InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {

View File

@@ -14,7 +14,7 @@
* the License. * the License.
*/ */
package com.kunzisoft.keepass.magikeyboard; package com.kunzisoft.keepass.credentialprovider.magikeyboard;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;

View File

@@ -14,14 +14,14 @@
* the License. * 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.credentialprovider.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD; import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY; import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY;
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT; import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP; import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP;
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP_ALT; import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray; import android.content.res.TypedArray;
@@ -52,7 +52,7 @@ import android.widget.TextView;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import com.kunzisoft.keepass.R; 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.Arrays;
import java.util.HashMap; import java.util.HashMap;

View File

@@ -18,7 +18,7 @@
* *
*/ */
package com.kunzisoft.keepass.magikeyboard package com.kunzisoft.keepass.credentialprovider.magikeyboard
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
@@ -41,9 +41,10 @@ import androidx.core.graphics.BlendModeCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R 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.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.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Field import com.kunzisoft.keepass.database.element.Field
@@ -324,9 +325,9 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
actionGoAutomatically() actionGoAutomatically()
} }
KEY_FIELDS -> { KEY_FIELDS -> {
getEntryInfo()?.customFields?.let { customFields -> getEntryInfo()?.getCustomFieldsForFilling()?.let { customFields ->
fieldsAdapter?.apply { fieldsAdapter?.apply {
setFields(customFields.filter { it.name != OTP_TOKEN_FIELD}) setFields(customFields)
notifyDataSetChanged() notifyDataSetChanged()
} }
} }
@@ -341,10 +342,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
} }
private fun actionKeyEntry(searchInfo: SearchInfo? = null) { private fun actionKeyEntry(searchInfo: SearchInfo? = null) {
SearchHelper.checkAutoSearchInfo(this, SearchHelper.checkAutoSearchInfo(
mDatabase, context = this,
searchInfo, database = mDatabase,
{ _, items -> searchInfo = searchInfo,
onItemsFound = { _, items ->
performSelection( performSelection(
items, items,
{ {
@@ -361,11 +363,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
} }
) )
}, },
{ onItemNotFound = {
// Select if not found // Select if not found
launchEntrySelection(searchInfo) launchEntrySelection(searchInfo)
}, },
{ onDatabaseClosed = {
// Select if database not opened // Select if database not opened
removeEntryInfo() removeEntryInfo()
launchEntrySelection(searchInfo) launchEntrySelection(searchInfo)
@@ -463,21 +465,18 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
fun performSelection(items: List<EntryInfo>, fun performSelection(items: List<EntryInfo>,
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit, actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
actionEntrySelection: (autoSearch: Boolean) -> Unit) { actionEntrySelection: (autoSearch: Boolean) -> Unit) {
if (items.size == 1) { EntrySelectionHelper.performSelection(
val itemFound = items[0] items = items,
if (entryUUID != itemFound.id) { actionPopulateCredentialProvider = { itemFound ->
actionPopulateKeyboard.invoke(itemFound) if (entryUUID != itemFound.id) {
} else { actionPopulateKeyboard.invoke(itemFound)
// Force selection if magikeyboard already populated } else {
actionEntrySelection.invoke(false) // 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) actionEntrySelection = actionEntrySelection
} else { )
// Select an arbitrary one
actionEntrySelection.invoke(false)
}
} }
fun populateKeyboardAndMoveAppToBackground(activity: Activity, fun populateKeyboardAndMoveAppToBackground(activity: Activity,
@@ -486,7 +485,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
// Populate Magikeyboard with entry // Populate Magikeyboard with entry
addEntryAndLaunchNotificationIfAllowed(activity, entry, toast) addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
// Consume the selection mode // Consume the selection mode
EntrySelectionHelper.removeModesFromIntent(activity.intent) activity.intent.removeModes()
activity.moveTaskToBack(true) activity.moveTaskToBack(true)
} }
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<BeginGetCredentialResponse, GetCredentialException>
) {
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<CredentialEntry>) -> Unit
) {
val passkeyEntries: MutableList<CredentialEntry> = 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<BeginCreateCredentialResponse, CreateCredentialException>,
) {
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<CreateEntry>) -> 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<CreateEntry>.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<CreateEntry>) -> Unit
) {
val databaseName = mDatabase?.name
val accountName =
if (databaseName?.isBlank() != false)
getString(R.string.passkey_database_username)
else databaseName
val createEntries: MutableList<CreateEntry> = 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<Void?, ClearCredentialException>
) {
// nothing to do
}
companion object {
private val TAG = PasskeyProviderService::class.java.simpleName
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<String>
): 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<AndroidPrivilegedApp> {
val apps = mutableListOf<AndroidPrivilegedApp>()
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<String>()
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<AndroidPrivilegedApp>): 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
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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)
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<String, Any>()
ao.put("fmt", "none")
ao.put("attStmt", emptyMap<Any, Any>())
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()
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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)
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import org.json.JSONObject
interface AuthenticatorResponse {
var clientJson: JSONObject
fun json(): JSONObject
}

View File

@@ -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<ByteArray, ByteArray> = 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<ByteArray>(byteMap.keys)
keysList.sortedWith(
Comparator<ByteArray> { 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<Any>()
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<Any, Any>()
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")
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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())
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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 = "<placeholder>"
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
interface ClientDataResponse {
fun hashData(): ByteArray
fun buildResponse(): String
}

View File

@@ -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<String>
) {
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"
)

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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()
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<PublicKeyCredentialParameters>
var timeout: Long
var excludeCredentials: List<PublicKeyCredentialDescriptor>
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<PublicKeyCredentialParameters> = 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
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import java.security.KeyPair
data class PublicKeyCredentialCreationParameters(
val publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions,
val credentialId: ByteArray,
val signatureKey: Pair<KeyPair, Long>,
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
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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")
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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
)

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<Int, Any>()),
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)
)
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<AndroidPrivilegedApp> {
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<AndroidPrivilegedApp> {
return try {
val predefinedApps = mutableListOf<AndroidPrivilegedApp>()
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<AndroidPrivilegedApp> {
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<AndroidPrivilegedApp> {
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<AndroidPrivilegedApp>): 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)
}

View File

@@ -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>(UIState.Loading)
val uiState: StateFlow<UIState> = 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
}
}

View File

@@ -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>(UIState.Loading)
val credentialUiState: StateFlow<UIState> = 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
}
}

View File

@@ -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>(UIState.Loading)
val uiState: StateFlow<UIState> = 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)
}
}
}

View File

@@ -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>(UIState.Loading)
val uiState: StateFlow<UIState> = 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
}
}

View File

@@ -19,7 +19,6 @@
*/ */
package com.kunzisoft.keepass.database package com.kunzisoft.keepass.database
import android.Manifest
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
@@ -29,23 +28,15 @@ import android.content.Context.BIND_IMPORTANT
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.ServiceConnection import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED 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.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.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Entry 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.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK 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_RECYCLE_BIN_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_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.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_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.utils.putParcelableList import com.kunzisoft.keepass.utils.putParcelableList
import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
/** /**
@@ -103,175 +89,59 @@ import java.util.UUID
* Useful to retrieve a database instance and sending tasks commands * Useful to retrieve a database instance and sending tasks commands
*/ */
class DatabaseTaskProvider( class DatabaseTaskProvider(
private var context: Context, private var context: Context
private var showDialog: Boolean = true
) { ) {
// 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 onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
var onActionFinish: ((database: ContextualDatabase, var onStartActionRequested: ((bundle: Bundle?, actionTask: String) -> Unit)? = null
actionTask: String, var actionTaskListener: DatabaseTaskNotificationService.ActionTaskListener? = null
result: ActionRunnable.Result) -> Unit)? = null var databaseInfoListener: DatabaseTaskNotificationService.DatabaseInfoListener? = null
private var intentDatabaseTask: Intent = Intent(
context.applicationContext,
DatabaseTaskNotificationService::class.java
)
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
private var serviceConnection: ServiceConnection? = null private var serviceConnection: ServiceConnection? = null
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
fun destroy() { fun destroy() {
this.activity = null
this.onDatabaseRetrieved = null this.onDatabaseRetrieved = null
this.onActionFinish = null
this.databaseTaskBroadcastReceiver = null this.databaseTaskBroadcastReceiver = null
this.mBinder = null this.mBinder = null
this.serviceConnection = null this.serviceConnection = null
this.progressTaskDialogFragment = null
this.databaseChangedDialogFragment = null
} }
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener { fun onDatabaseChangeValidated() {
override fun onActionStarted( mBinder?.getService()?.saveDatabaseInfo()
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)
}
} }
private val mActionDatabaseListener = object: DatabaseChangedDialogFragment.ActionDatabaseChangedListener { private var databaseListener = object : DatabaseTaskNotificationService.DatabaseListener {
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 {
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase?) {
onDatabaseRetrieved?.invoke(database) 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() { private fun initServiceConnection() {
stopDialog() actionTaskListener?.onActionStopped()
if (serviceConnection == null) { if (serviceConnection == null) {
serviceConnection = object : ServiceConnection { serviceConnection = object : ServiceConnection {
override fun onBindingDied(name: ComponentName?) { override fun onBindingDied(name: ComponentName?) {
stopDialog() actionTaskListener?.onActionStopped()
onDatabaseRetrieved?.invoke(null)
} }
override fun onNullBinding(name: ComponentName?) { override fun onNullBinding(name: ComponentName?) {
stopDialog() actionTaskListener?.onActionStopped()
onDatabaseRetrieved?.invoke(null)
} }
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) { override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply { mBinder =
addServiceListeners(this) (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
getService().checkDatabase() addServiceListeners(this)
getService().checkDatabaseInfo() getService().checkDatabase()
getService().checkAction() getService().checkDatabaseInfo()
} getService().checkAction()
}
} }
override fun onServiceDisconnected(name: ComponentName?) { override fun onServiceDisconnected(name: ComponentName?) {
@@ -284,20 +154,36 @@ class DatabaseTaskProvider(
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) { private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
service?.addDatabaseListener(databaseListener) service?.addDatabaseListener(databaseListener)
service?.addDatabaseFileInfoListener(databaseInfoListener) databaseInfoListener?.let { infoListener ->
service?.addActionTaskListener(actionTaskListener) service?.addDatabaseFileInfoListener(infoListener)
}
actionTaskListener?.let { taskListener ->
service?.addActionTaskListener(taskListener)
}
} }
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) { private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
service?.removeActionTaskListener(actionTaskListener) actionTaskListener?.let { taskListener ->
service?.removeDatabaseFileInfoListener(databaseInfoListener) service?.removeActionTaskListener(taskListener)
}
databaseInfoListener?.let { infoListener ->
service?.removeDatabaseFileInfoListener(infoListener)
}
service?.removeDatabaseListener(databaseListener) service?.removeDatabaseListener(databaseListener)
onDatabaseRetrieved?.invoke(null)
} }
private fun bindService() { private fun bindService() {
initServiceConnection() initServiceConnection()
serviceConnection?.let { 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 // Bind to the service when is starting
bindService() bindService()
} }
DATABASE_STOP_TASK_ACTION -> { DATABASE_STOP_TASK_ACTION -> {
// Remove the progress task // Remove the progress task
unBindService() unBindService()
@@ -332,7 +219,8 @@ class DatabaseTaskProvider(
} }
} }
} }
ContextCompat.registerReceiver(context, databaseTaskBroadcastReceiver, ContextCompat.registerReceiver(
context, databaseTaskBroadcastReceiver,
IntentFilter().apply { IntentFilter().apply {
addAction(DATABASE_START_TASK_ACTION) addAction(DATABASE_START_TASK_ACTION)
addAction(DATABASE_STOP_TASK_ACTION) addAction(DATABASE_STOP_TASK_ACTION)
@@ -356,58 +244,9 @@ class DatabaseTaskProvider(
} }
} }
private val tempServiceParameters = mutableListOf<Pair<Bundle?, String>>()
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) { private fun start(bundle: Bundle? = null, actionTask: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { onStartActionRequested?.invoke(bundle, actionTask) ?: run {
val contextActivity = activity context.startDatabaseService(bundle, actionTask)
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()
} }
} }
@@ -417,47 +256,51 @@ class DatabaseTaskProvider(
---- ----
*/ */
fun startDatabaseCreate(databaseUri: Uri, fun startDatabaseCreate(
mainCredential: MainCredential databaseUri: Uri,
mainCredential: MainCredential
) { ) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
} }, ACTION_DATABASE_CREATE_TASK)
, ACTION_DATABASE_CREATE_TASK)
} }
fun startDatabaseLoad(databaseUri: Uri, fun startDatabaseLoad(
mainCredential: MainCredential, databaseUri: Uri,
readOnly: Boolean, mainCredential: MainCredential,
cipherEncryptDatabase: CipherEncryptDatabase?, readOnly: Boolean,
fixDuplicateUuid: Boolean) { cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly) 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) putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
} }, ACTION_DATABASE_LOAD_TASK)
, ACTION_DATABASE_LOAD_TASK)
} }
fun startDatabaseMerge(save: Boolean, fun startDatabaseMerge(
fromDatabaseUri: Uri? = null, save: Boolean,
mainCredential: MainCredential? = null) { fromDatabaseUri: Uri? = null,
mainCredential: MainCredential? = null
) {
start(Bundle().apply { start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri) putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
} }, ACTION_DATABASE_MERGE_TASK)
, ACTION_DATABASE_MERGE_TASK)
} }
fun startDatabaseReload(fixDuplicateUuid: Boolean) { fun startDatabaseReload(fixDuplicateUuid: Boolean) {
start(Bundle().apply { start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid) putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
} }, ACTION_DATABASE_RELOAD_TASK)
, ACTION_DATABASE_RELOAD_TASK)
} }
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) { fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
@@ -473,15 +316,15 @@ class DatabaseTaskProvider(
} }
} }
fun startDatabaseAssignCredential(databaseUri: Uri, fun startDatabaseAssignCredential(
mainCredential: MainCredential databaseUri: Uri,
mainCredential: MainCredential
) { ) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) 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, fun startDatabaseCreateGroup(
parent: Group, newGroup: Group,
save: Boolean) { parent: Group,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, newGroup) putParcelable(DatabaseTaskNotificationService.GROUP_KEY, newGroup)
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId) putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, ACTION_DATABASE_CREATE_GROUP_TASK)
, ACTION_DATABASE_CREATE_GROUP_TASK)
} }
fun startDatabaseUpdateGroup(oldGroup: Group, fun startDatabaseUpdateGroup(
groupToUpdate: Group, oldGroup: Group,
save: Boolean) { groupToUpdate: Group,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.GROUP_ID_KEY, oldGroup.nodeId) putParcelable(DatabaseTaskNotificationService.GROUP_ID_KEY, oldGroup.nodeId)
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate) putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, ACTION_DATABASE_UPDATE_GROUP_TASK)
, ACTION_DATABASE_UPDATE_GROUP_TASK)
} }
fun startDatabaseCreateEntry(newEntry: Entry, fun startDatabaseCreateEntry(
parent: Group, newEntry: Entry,
save: Boolean) { parent: Group,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, newEntry) putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, newEntry)
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId) putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, ACTION_DATABASE_CREATE_ENTRY_TASK)
, ACTION_DATABASE_CREATE_ENTRY_TASK)
} }
fun startDatabaseUpdateEntry(oldEntry: Entry, fun startDatabaseUpdateEntry(
entryToUpdate: Entry, oldEntry: Entry,
save: Boolean) { entryToUpdate: Entry,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, oldEntry.nodeId) putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, oldEntry.nodeId)
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate) putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, ACTION_DATABASE_UPDATE_ENTRY_TASK)
, ACTION_DATABASE_UPDATE_ENTRY_TASK)
} }
private fun startDatabaseActionListNodes(actionTask: String, private fun startDatabaseActionListNodes(
nodesPaste: List<Node>, actionTask: String,
newParent: Group?, nodesPaste: List<Node>,
save: Boolean) { newParent: Group?,
save: Boolean
) {
val groupsIdToCopy = ArrayList<NodeId<*>>() val groupsIdToCopy = ArrayList<NodeId<*>>()
val entriesIdToCopy = ArrayList<NodeId<UUID>>() val entriesIdToCopy = ArrayList<NodeId<UUID>>()
nodesPaste.forEach { nodeVersioned -> nodesPaste.forEach { nodeVersioned ->
@@ -545,6 +394,7 @@ class DatabaseTaskProvider(
Type.GROUP -> { Type.GROUP -> {
groupsIdToCopy.add((nodeVersioned as Group).nodeId) groupsIdToCopy.add((nodeVersioned as Group).nodeId)
} }
Type.ENTRY -> { Type.ENTRY -> {
entriesIdToCopy.add((nodeVersioned as Entry).nodeId) entriesIdToCopy.add((nodeVersioned as Entry).nodeId)
} }
@@ -559,24 +409,29 @@ class DatabaseTaskProvider(
if (newParentId != null) if (newParentId != null)
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId) putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, actionTask)
, actionTask)
} }
fun startDatabaseCopyNodes(nodesToCopy: List<Node>, fun startDatabaseCopyNodes(
newParent: Group, nodesToCopy: List<Node>,
save: Boolean) { newParent: Group,
save: Boolean
) {
startDatabaseActionListNodes(ACTION_DATABASE_COPY_NODES_TASK, nodesToCopy, newParent, save) startDatabaseActionListNodes(ACTION_DATABASE_COPY_NODES_TASK, nodesToCopy, newParent, save)
} }
fun startDatabaseMoveNodes(nodesToMove: List<Node>, fun startDatabaseMoveNodes(
newParent: Group, nodesToMove: List<Node>,
save: Boolean) { newParent: Group,
save: Boolean
) {
startDatabaseActionListNodes(ACTION_DATABASE_MOVE_NODES_TASK, nodesToMove, newParent, save) startDatabaseActionListNodes(ACTION_DATABASE_MOVE_NODES_TASK, nodesToMove, newParent, save)
} }
fun startDatabaseDeleteNodes(nodesToDelete: List<Node>, fun startDatabaseDeleteNodes(
save: Boolean) { nodesToDelete: List<Node>,
save: Boolean
) {
startDatabaseActionListNodes(ACTION_DATABASE_DELETE_NODES_TASK, nodesToDelete, null, save) startDatabaseActionListNodes(ACTION_DATABASE_DELETE_NODES_TASK, nodesToDelete, null, save)
} }
@@ -586,26 +441,28 @@ class DatabaseTaskProvider(
----------------- -----------------
*/ */
fun startDatabaseRestoreEntryHistory(mainEntryId: NodeId<UUID>, fun startDatabaseRestoreEntryHistory(
entryHistoryPosition: Int, mainEntryId: NodeId<UUID>,
save: Boolean) { entryHistoryPosition: Int,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId) putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition) putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
} }
fun startDatabaseDeleteEntryHistory(mainEntryId: NodeId<UUID>, fun startDatabaseDeleteEntryHistory(
entryHistoryPosition: Int, mainEntryId: NodeId<UUID>,
save: Boolean) { entryHistoryPosition: Int,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId) putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition) putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) 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, fun startDatabaseSaveName(
newName: String, oldName: String,
save: Boolean) { newName: String,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldName) putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldName)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName) putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, ACTION_DATABASE_UPDATE_NAME_TASK)
, ACTION_DATABASE_UPDATE_NAME_TASK)
} }
fun startDatabaseSaveDescription(oldDescription: String, fun startDatabaseSaveDescription(
newDescription: String, oldDescription: String,
save: Boolean) { newDescription: String,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDescription) putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDescription)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription) putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
} }
fun startDatabaseSaveDefaultUsername(oldDefaultUsername: String, fun startDatabaseSaveDefaultUsername(
newDefaultUsername: String, oldDefaultUsername: String,
save: Boolean) { newDefaultUsername: String,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDefaultUsername) putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDefaultUsername)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername) putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
} }
fun startDatabaseSaveColor(oldColor: String, fun startDatabaseSaveColor(
newColor: String, oldColor: String,
save: Boolean) { newColor: String,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldColor) putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldColor)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor) putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, ACTION_DATABASE_UPDATE_COLOR_TASK)
, ACTION_DATABASE_UPDATE_COLOR_TASK)
} }
fun startDatabaseSaveCompression(oldCompression: CompressionAlgorithm, fun startDatabaseSaveCompression(
newCompression: CompressionAlgorithm, oldCompression: CompressionAlgorithm,
save: Boolean) { newCompression: CompressionAlgorithm,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldCompression) putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldCompression)
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression) putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
} }
fun startDatabaseRemoveUnlinkedData(save: Boolean) { fun startDatabaseRemoveUnlinkedData(save: Boolean) {
start(Bundle().apply { start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
} }
fun startDatabaseSaveRecycleBin(oldRecycleBin: Group?, fun startDatabaseSaveRecycleBin(
newRecycleBin: Group?, oldRecycleBin: Group?,
save: Boolean) { newRecycleBin: Group?,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldRecycleBin) putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldRecycleBin)
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newRecycleBin) putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newRecycleBin)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
} }
fun startDatabaseSaveTemplatesGroup(oldTemplatesGroup: Group?, fun startDatabaseSaveTemplatesGroup(
newTemplatesGroup: Group?, oldTemplatesGroup: Group?,
save: Boolean) { newTemplatesGroup: Group?,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldTemplatesGroup) putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldTemplatesGroup)
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newTemplatesGroup) putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newTemplatesGroup)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
} }
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int, fun startDatabaseSaveMaxHistoryItems(
newMaxHistoryItems: Int, oldMaxHistoryItems: Int,
save: Boolean) { newMaxHistoryItems: Int,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistoryItems) putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistoryItems)
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems) putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
} }
fun startDatabaseSaveMaxHistorySize(oldMaxHistorySize: Long, fun startDatabaseSaveMaxHistorySize(
newMaxHistorySize: Long, oldMaxHistorySize: Long,
save: Boolean) { newMaxHistorySize: Long,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistorySize) putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistorySize)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize) putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) 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, fun startDatabaseSaveEncryption(
newEncryption: EncryptionAlgorithm, oldEncryption: EncryptionAlgorithm,
save: Boolean) { newEncryption: EncryptionAlgorithm,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldEncryption) putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldEncryption)
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption) putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
} }
fun startDatabaseSaveKeyDerivation(oldKeyDerivation: KdfEngine, fun startDatabaseSaveKeyDerivation(
newKeyDerivation: KdfEngine, oldKeyDerivation: KdfEngine,
save: Boolean) { newKeyDerivation: KdfEngine,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldKeyDerivation) putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldKeyDerivation)
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation) putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
} }
fun startDatabaseSaveIterations(oldIterations: Long, fun startDatabaseSaveIterations(
newIterations: Long, oldIterations: Long,
save: Boolean) { newIterations: Long,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldIterations) putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldIterations)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations) putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
} }
fun startDatabaseSaveMemoryUsage(oldMemoryUsage: Long, fun startDatabaseSaveMemoryUsage(
newMemoryUsage: Long, oldMemoryUsage: Long,
save: Boolean) { newMemoryUsage: Long,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMemoryUsage) putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMemoryUsage)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage) putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
} }
fun startDatabaseSaveParallelism(oldParallelism: Long, fun startDatabaseSaveParallelism(
newParallelism: Long, oldParallelism: Long,
save: Boolean) { newParallelism: Long,
save: Boolean
) {
start(Bundle().apply { start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism) putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism) putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) 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 { start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri) putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
} }, ACTION_DATABASE_SAVE)
, ACTION_DATABASE_SAVE)
} }
fun startChallengeResponded(response: ByteArray?) { fun startChallengeResponded(response: ByteArray?) {
start(Bundle().apply { start(Bundle().apply {
putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response) putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response)
} }, ACTION_CHALLENGE_RESPONDED)
, ACTION_CHALLENGE_RESPONDED)
} }
companion object { companion object {
private val TAG = DatabaseTaskProvider::class.java.name 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()
}
}
} }
} }

View File

@@ -24,14 +24,44 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.template.TemplateEngine import com.kunzisoft.keepass.database.element.template.TemplateEngine
import com.kunzisoft.keepass.database.element.template.TemplateField 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) { when (this) {
is FileNotFoundDatabaseException -> resources.getString(R.string.file_not_found_content) is FileNotFoundDatabaseException -> resources.getString(R.string.file_not_found_content)
is CorruptedDatabaseException -> resources.getString(R.string.corrupted_file) is CorruptedDatabaseException -> resources.getString(R.string.corrupted_file)
is InvalidAlgorithmDatabaseException -> resources.getString(R.string.invalid_algorithm) is InvalidAlgorithmDatabaseException -> resources.getString(R.string.invalid_algorithm)
is UnknownDatabaseLocationException -> resources.getString(R.string.error_location_unknown) 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 HardwareKeyDatabaseException -> resources.getString(R.string.error_hardware_key_unsupported)
is EmptyKeyDatabaseException -> resources.getString(R.string.error_empty_key) is EmptyKeyDatabaseException -> resources.getString(R.string.error_empty_key)
is SignatureDatabaseException -> resources.getString(R.string.invalid_db_sig) 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) || 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 { fun TemplateField.getLocalizedName(context: Context?, name: String): String {
if (context == null if (context == null
|| TemplateEngine.containsTemplateDecorator(name) || 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_SECURE_NOTE.equals(name, true) -> context.getString(R.string.secure_note)
LABEL_MEMBERSHIP.equals(name, true) -> context.getString(R.string.membership) 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 else -> name
} }
} }

View File

@@ -21,9 +21,16 @@ package com.kunzisoft.keepass.database.helper
import android.content.Context import android.content.Context
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil.searchSubDomains
import com.kunzisoft.keepass.timeout.TimeoutHelper 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 { 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, private fun getConcreteWebDomain(
database: ContextualDatabase?, context: Context,
searchInfo: SearchInfo?, webDomain: String?,
onItemsFound: (openedDatabase: ContextualDatabase, concreteWebDomain: (searchSubDomains: Boolean, concreteWebDomain: String?) -> Unit
items: List<EntryInfo>) -> Unit, ) {
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit, val domain = webDomain
onDatabaseClosed: () -> Unit) { val searchSubDomains = searchSubDomains(context)
if (database == null || !database.loaded) { if (domain != null) {
onDatabaseClosed.invoke() // Warning, web domain can contains IP, don't crop in this case
} else if (TimeoutHelper.checkTime(context)) { if (searchSubDomains
var searchWithoutUI = false || Regex(SearchInfo.WEB_IP_REGEX).matches(domain)) {
if (searchInfo != null concreteWebDomain.invoke(searchSubDomains, webDomain)
&& !searchInfo.manualSelection } else {
&& !searchInfo.containsOnlyNullValues()) { CoroutineScope(Dispatchers.IO).launch {
// If search provide results val publicSuffixList = PublicSuffixList(context)
database.createVirtualGroupFromSearchInfo( val publicSuffix = publicSuffixList
searchInfoString = searchInfo.toString(), .getPublicSuffixPlusOne(domain).await()
searchInfoByDomain = searchInfo.isASearchByDomain(), withContext(Dispatchers.Main) {
max = MAX_SEARCH_ENTRY concreteWebDomain.invoke(false, publicSuffix)
)?.let { searchGroup ->
if (searchGroup.numberOfChildEntries > 0) {
searchWithoutUI = true
onItemsFound.invoke(database,
searchGroup.getChildEntriesInfo(database))
} }
} }
} }
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<EntryInfo>) -> 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) onItemNotFound.invoke(database)
}
} }
} }
} }

View File

@@ -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<ActivityResult> { 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<Intent> = 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()
}
}
}

View File

@@ -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<Intent>? = 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<ActivityResult> { 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()
}
}
}
}

View File

@@ -33,20 +33,22 @@ import java.util.*
class PasswordGenerator(private val resources: Resources) { class PasswordGenerator(private val resources: Resources) {
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
fun generatePassword(length: Int, fun generatePassword(
upperCase: Boolean, length: Int,
lowerCase: Boolean, upperCase: Boolean,
digits: Boolean, lowerCase: Boolean,
minus: Boolean, digits: Boolean,
underline: Boolean, minus: Boolean,
space: Boolean, underline: Boolean,
specials: Boolean, space: Boolean,
brackets: Boolean, specials: Boolean,
extended: Boolean, brackets: Boolean,
considerChars: String, extended: Boolean,
ignoreChars: String, considerChars: String,
atLeastOneFromEach: Boolean, ignoreChars: String,
excludeAmbiguousChar: Boolean): String { atLeastOneFromEach: Boolean,
excludeAmbiguousChar: Boolean
): String {
// Desired password length is 0 or less // Desired password length is 0 or less
if (length <= 0) { if (length <= 0) {
throw IllegalArgumentException(resources.getString(R.string.error_wrong_length)) 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 MINUS_CHAR = "-"
private const val UNDERLINE_CHAR = "_" private const val UNDERLINE_CHAR = "_"
private const val SPACE_CHAR = " " private const val SPACE_CHAR = " "
private const val SPECIAL_CHARS = "!\"#$%&'*+,./:;=?@\\^`" private const val SPECIAL_CHARS = "&/,^@.#:%\\='$!?*`;+\"|~"
private const val BRACKET_CHARS = "[]{}()<>" private const val BRACKET_CHARS = "[]{}()<>"
private const val AMBIGUOUS_CHARS = "iI|lLoO01" private const val AMBIGUOUS_CHARS = "iI|lLoO01"

View File

@@ -1,13 +1,9 @@
package com.kunzisoft.keepass.receivers package com.kunzisoft.keepass.receivers
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.utils.DexUtil
import com.kunzisoft.keepass.utils.MagikeyboardUtil import com.kunzisoft.keepass.utils.MagikeyboardUtil
class DexModeReceiver : BroadcastReceiver() { class DexModeReceiver : BroadcastReceiver() {

View File

@@ -36,6 +36,7 @@ import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.getParcelableExtraCompat import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -194,7 +195,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
private fun newNotification(attachmentNotification: AttachmentNotification) { private fun newNotification(attachmentNotification: AttachmentNotification) {
val pendingContentIntent = PendingIntent.getActivity(this, val pendingContentIntent = PendingIntent.getActivity(this,
0, randomRequestCode(),
Intent().apply { Intent().apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
setDataAndType(attachmentNotification.uri, setDataAndType(attachmentNotification.uri,
@@ -208,7 +209,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
) )
val pendingDeleteIntent = PendingIntent.getService(this, val pendingDeleteIntent = PendingIntent.getService(this,
0, randomRequestCode(),
Intent(this, AttachmentFileNotificationService::class.java).apply { Intent(this, AttachmentFileNotificationService::class.java).apply {
// No action to delete the service // No action to delete the service
putExtra(FILE_URI_KEY, attachmentNotification.uri) putExtra(FILE_URI_KEY, attachmentNotification.uri)

View File

@@ -274,10 +274,12 @@ class ClipboardEntryNotificationService : LockNotificationService() {
val containsPasswordToCopy = entry.password.isNotEmpty() val containsPasswordToCopy = entry.password.isNotEmpty()
&& PreferencesUtil.allowCopyProtectedFields(context) && PreferencesUtil.allowCopyProtectedFields(context)
val containsOTPToCopy = entry.containsCustomField(OTP_TOKEN_FIELD) val containsOTPToCopy = entry.containsCustomField(OTP_TOKEN_FIELD)
val containsExtraFieldToCopy = entry.customFields.isNotEmpty() val customFields = entry.getCustomFieldsForFilling()
&& (entry.containsCustomFieldsNotProtected() 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 var startService = false
@@ -320,7 +322,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
if (containsExtraFieldToCopy) { if (containsExtraFieldToCopy) {
try { try {
var anonymousFieldNumber = 0 var anonymousFieldNumber = 0
entry.customFields.forEach { field -> entry.getCustomFieldsForFilling().forEach { field ->
//If value is not protected or allowed //If value is not protected or allowed
if ((!field.protectedValue.isProtected if ((!field.protectedValue.isProtected
|| PreferencesUtil.allowCopyProtectedFields(context)) || PreferencesUtil.allowCopyProtectedFields(context))

View File

@@ -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.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.hardware.HardwareKey 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.CipherEncryptDatabase
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.timeout.TimeoutHelper 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_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.LOCK_ACTION
@@ -175,7 +176,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
progressMessage: ProgressMessage progressMessage: ProgressMessage
) )
fun onActionStopped( fun onActionStopped(
database: ContextualDatabase database: ContextualDatabase? = null
) )
fun onActionFinished( fun onActionFinished(
database: ContextualDatabase, database: ContextualDatabase,
@@ -218,7 +219,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
Log.i(TAG, "Database file modified " + Log.i(TAG, "Database file modified " +
"$previousDatabaseInfo != $lastFileDatabaseInfo ") "$previousDatabaseInfo != $lastFileDatabaseInfo ")
// Call listener to indicate a change in database info // Call listener to indicate a change in database info
if (!mSaveState && previousDatabaseInfo != null) { if (!mSaveState) {
mDatabaseInfoListeners.forEach { listener -> mDatabaseInfoListeners.forEach { listener ->
listener.onDatabaseInfoChanged( listener.onDatabaseInfoChanged(
previousDatabaseInfo, previousDatabaseInfo,
@@ -550,7 +551,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
// Build Intents for notification action // Build Intents for notification action
val pendingDatabaseIntent = PendingIntent.getActivity( val pendingDatabaseIntent = PendingIntent.getActivity(
this, this,
0, randomRequestCode(),
Intent(this, GroupActivity::class.java), Intent(this, GroupActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
@@ -675,6 +676,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
override fun actionOnLock() { override fun actionOnLock() {
if (!TimeoutHelper.temporarilyDisableLock) { if (!TimeoutHelper.temporarilyDisableLock) {
closeDatabase(mDatabase) 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) // Remove the lock timer (no more needed if it exists)
TimeoutHelper.cancelLockTimer(this) TimeoutHelper.cancelLockTimer(this)
// Service is stopped after receive the broadcast // Service is stopped after receive the broadcast
@@ -709,9 +716,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
notifyProgressMessage() notifyProgressMessage()
HardwareKeyActivity HardwareKeyActivity
.launchHardwareKeyActivity( .launchHardwareKeyActivity(
this@DatabaseTaskNotificationService, context = this@DatabaseTaskNotificationService,
hardwareKey, hardwareKey = hardwareKey,
seed seed = seed
) )
// Wait the response // Wait the response
mProgressMessage.apply { mProgressMessage.apply {
@@ -1385,6 +1392,15 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
return nodesAction return nodesAction
} }
fun Bundle.getNewEntry(database: ContextualDatabase): Entry? {
getBundle(NEW_NODES_KEY)
?.getParcelableList<NodeId<UUID>>(ENTRIES_ID_KEY)
?.get(0)?.let {
return database.getEntryById(it)
}
return null
}
fun getBundleFromListNodes(nodes: List<Node>): Bundle { fun getBundleFromListNodes(nodes: List<Node>): Bundle {
val groupsId = mutableListOf<NodeId<*>>() val groupsId = mutableListOf<NodeId<*>>()
val entriesId = mutableListOf<NodeId<UUID>>() val entriesId = mutableListOf<NodeId<UUID>>()

View File

@@ -27,7 +27,7 @@ import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.kunzisoft.keepass.R 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.model.EntryInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper

View File

@@ -19,9 +19,12 @@
*/ */
package com.kunzisoft.keepass.settings package com.kunzisoft.keepass.settings
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
@RequiresApi(Build.VERSION_CODES.O)
class AutofillSettingsActivity : ExternalSettingsActivity() { class AutofillSettingsActivity : ExternalSettingsActivity() {
override fun retrieveTitle(): Int { override fun retrieveTitle(): Int {

View File

@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.settings
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.annotation.RequiresApi
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
@@ -29,6 +30,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistAppIdPreferenceDialogFragmentCompat import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistAppIdPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistWebDomainPreferenceDialogFragmentCompat import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistWebDomainPreferenceDialogFragmentCompat
@RequiresApi(Build.VERSION_CODES.O)
class AutofillSettingsFragment : PreferenceFragmentCompat() { class AutofillSettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -39,11 +41,14 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
autofillInlineSuggestionsPreference?.isVisible = false 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) { override fun onDisplayPreferenceDialog(preference: Preference) {
var otherDialogFragment = false
var dialogFragment: DialogFragment? = null var dialogFragment: DialogFragment? = null
when (preference.key) { when (preference.key) {
@@ -53,7 +58,7 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
getString(R.string.autofill_web_domain_blocklist_key) -> { getString(R.string.autofill_web_domain_blocklist_key) -> {
dialogFragment = AutofillBlocklistWebDomainPreferenceDialogFragmentCompat.newInstance(preference.key) dialogFragment = AutofillBlocklistWebDomainPreferenceDialogFragmentCompat.newInstance(preference.key)
} }
else -> otherDialogFragment = true else -> {}
} }
if (dialogFragment != null) { if (dialogFragment != null) {
@@ -62,7 +67,7 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
dialogFragment.show(parentFragmentManager, TAG_AUTOFILL_PREF_FRAGMENT) dialogFragment.show(parentFragmentManager, TAG_AUTOFILL_PREF_FRAGMENT)
} }
// Could not be handled here. Try with the super method. // Could not be handled here. Try with the super method.
else if (otherDialogFragment) { else {
super.onDisplayPreferenceDialog(preference) super.onDisplayPreferenceDialog(preference)
} }
} }

View File

@@ -21,20 +21,25 @@ package com.kunzisoft.keepass.settings
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
class MainPreferenceFragment : PreferenceFragmentCompat() { class MainPreferenceFragment : PreferenceFragmentCompat() {
private var mCallback: Callback? = null private var mCallback: Callback? = null
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels() private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private var mDatabaseLoaded: Boolean = false private val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
@@ -50,20 +55,24 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
mCallback = null mCallback = null
super.onDetach() super.onDetach()
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database -> override fun onCreate(savedInstanceState: Bundle?) {
mDatabaseLoaded = database?.loaded == true super.onCreate(savedInstanceState)
checkDatabaseLoaded() 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<Preference>(getString(R.string.settings_database_key)) findPreference<Preference>(getString(R.string.settings_database_key))
?.isEnabled = mDatabaseLoaded ?.isEnabled = isDatabaseLoaded
findPreference<PreferenceCategory>(getString(R.string.settings_database_category_key)) findPreference<PreferenceCategory>(getString(R.string.settings_database_category_key))
?.isVisible = mDatabaseLoaded ?.isVisible = isDatabaseLoaded
} }
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -119,7 +128,7 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
} }
} }
checkDatabaseLoaded() checkDatabaseLoaded(mDatabase?.loaded == true)
} }
interface Callback { interface Callback {

View File

@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.settings
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
@@ -30,6 +29,7 @@ import android.view.autofill.AutofillManager
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.net.toUri
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.preference.ListPreference import androidx.preference.ListPreference
@@ -47,7 +47,7 @@ import com.kunzisoft.keepass.icons.IconPackChooser
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
import com.kunzisoft.keepass.settings.preference.IconPackListPreference import com.kunzisoft.keepass.settings.preference.IconPackListPreference
import com.kunzisoft.keepass.settings.preferencedialogfragment.DurationDialogFragmentCompat import com.kunzisoft.keepass.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.openUrl
import com.kunzisoft.keepass.utils.UriUtil.releaseAllUnnecessaryPermissionUris import com.kunzisoft.keepass.utils.UriUtil.releaseAllUnnecessaryPermissionUris
@@ -119,7 +119,16 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
activity?.let { activity -> activity?.let { activity ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 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<Preference>(getString(R.string.passkeys_explanation_key))
?.isVisible = false
findPreference<Preference>(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 -> activity.getSystemService(AutofillManager::class.java)?.let { autofillManager ->
if (autofillManager.hasEnabledAutofillServices()) if (autofillManager.hasEnabledAutofillServices())
autoFillEnablePreference?.isChecked = autofillManager.hasEnabledAutofillServices() autoFillEnablePreference?.isChecked = autofillManager.hasEnabledAutofillServices()
@@ -161,7 +170,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
val intent = val intent =
Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE) Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
intent.data = 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") Log.d(javaClass.name, "Autofill enable service: intent=$intent")
startActivity(intent) startActivity(intent)
} else { } else {
@@ -171,7 +180,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
} }
} }
} else { } else {
findPreference<Preference>(getString(R.string.autofill_key))?.isVisible = false findPreference<Preference>(getString(R.string.credential_provider_key))?.isVisible = false
} }
} }
@@ -192,14 +201,28 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
false false
} }
findPreference<Preference>(getString(R.string.autofill_explanation_key))?.setOnPreferenceClickListener { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
context?.openUrl(R.string.autofill_explanation_url) findPreference<Preference>(getString(R.string.passkeys_explanation_key))?.setOnPreferenceClickListener {
false context?.openUrl(R.string.passkeys_explanation_url)
false
}
findPreference<Preference>(getString(R.string.settings_passkeys_key))?.setOnPreferenceClickListener {
startActivity(Intent(context, PasskeysSettingsActivity::class.java))
false
}
} }
findPreference<Preference>(getString(R.string.settings_autofill_key))?.setOnPreferenceClickListener { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startActivity(Intent(context, AutofillSettingsActivity::class.java)) findPreference<Preference>(getString(R.string.autofill_explanation_key))?.setOnPreferenceClickListener {
false context?.openUrl(R.string.autofill_explanation_url)
false
}
findPreference<Preference>(getString(R.string.settings_autofill_key))?.setOnPreferenceClickListener {
startActivity(Intent(context, AutofillSettingsActivity::class.java))
false
}
} }
findPreference<Preference>(getString(R.string.clipboard_notifications_key))?.setOnPreferenceChangeListener { _, newValue -> findPreference<Preference>(getString(R.string.clipboard_notifications_key))?.setOnPreferenceChangeListener { _, newValue ->
@@ -530,7 +553,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
super.onResume() super.onResume()
activity?.let { activity -> activity?.let { activity ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
findPreference<TwoStatePreference?>(getString(R.string.settings_autofill_enable_key))?.let { autoFillEnablePreference -> findPreference<TwoStatePreference?>(getString(R.string.settings_credential_provider_enable_key))?.let { autoFillEnablePreference ->
val autofillManager = activity.getSystemService(AutofillManager::class.java) val autofillManager = activity.getSystemService(AutofillManager::class.java)
autoFillEnablePreference.isChecked = autofillManager != null autoFillEnablePreference.isChecked = autofillManager != null
&& autofillManager.hasEnabledAutofillServices() && autofillManager.hasEnabledAutofillServices()

View File

@@ -19,13 +19,21 @@
*/ */
package com.kunzisoft.keepass.settings package com.kunzisoft.keepass.settings
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.util.Log 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.core.view.MenuProvider
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import androidx.preference.TwoStatePreference 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.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm 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.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.settings.preference.* import com.kunzisoft.keepass.settings.preference.DialogColorPreference
import com.kunzisoft.keepass.settings.preferencedialogfragment.* 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.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.getParcelableCompat import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getSerializableCompat import com.kunzisoft.keepass.utils.getSerializableCompat
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval { class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels() 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 mDatabaseReadOnly: Boolean = false
private var mMergeDataAllowed: Boolean = false private var mMergeDataAllowed: Boolean = false
private var mDatabaseAutoSaveEnabled: Boolean = true 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
activity?.addMenuProvider(menuProvider, viewLifecycleOwner) activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
viewLifecycleOwner.lifecycleScope.launch {
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database -> mDatabaseViewModel.databaseState.collect { database ->
mDatabase = database view.resetAppTimeoutWhenViewTouchedOrFocused(
view.resetAppTimeoutWhenViewTouchedOrFocused(requireContext(), database?.loaded) context = requireContext(),
onDatabaseRetrieved(database) databaseLoaded = database?.loaded
} )
}
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) {
onDatabaseActionFinished(it.database, it.actionTask, it.result)
} }
} }
@@ -167,29 +223,26 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
mDatabaseViewModel.reloadDatabase(false) mDatabaseViewModel.reloadDatabase(false)
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
mDatabase = database mDatabaseReadOnly = database.isReadOnly
mDatabaseReadOnly = database?.isReadOnly == true mMergeDataAllowed = database.isMergeDataAllowed()
mMergeDataAllowed = database?.isMergeDataAllowed() == true
mDatabase?.let { if (database.loaded) {
if (it.loaded) { when (mScreen) {
when (mScreen) { Screen.DATABASE -> {
Screen.DATABASE -> { onCreateDatabasePreference(database)
onCreateDatabasePreference(it) }
} Screen.DATABASE_SECURITY -> {
Screen.DATABASE_SECURITY -> { onCreateDatabaseSecurityPreference(database)
onCreateDatabaseSecurityPreference(it) }
} Screen.DATABASE_MASTER_KEY -> {
Screen.DATABASE_MASTER_KEY -> { onCreateDatabaseMasterKeyPreference(database)
onCreateDatabaseMasterKeyPreference(it) }
} else -> {
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) { if (result.isSuccess) {
newDefaultUsername newDefaultUsername
} else { } else {
mDatabase?.defaultUsername = oldDefaultUsername database.defaultUsername = oldDefaultUsername
oldDefaultUsername oldDefaultUsername
} }
dbDefaultUsernamePref?.summary = defaultUsernameToShow dbDefaultUsernamePref?.summary = defaultUsernameToShow
@@ -471,7 +524,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newColor newColor
} else { } else {
mDatabase?.customColor = Color.parseColor(oldColor) database.customColor = oldColor.toColorInt()
oldColor oldColor
} }
dbCustomColorPref?.summary = defaultColorToShow dbCustomColorPref?.summary = defaultColorToShow
@@ -483,7 +536,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newCompression newCompression
} else { } else {
mDatabase?.compressionAlgorithm = oldCompression database.compressionAlgorithm = oldCompression
oldCompression oldCompression
} }
dbDataCompressionPref?.summary = algorithmToShow?.getLocalizedName(resources) dbDataCompressionPref?.summary = algorithmToShow?.getLocalizedName(resources)
@@ -497,7 +550,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} else { } else {
oldRecycleBin oldRecycleBin
} }
mDatabase?.setRecycleBin(recycleBinToShow) database.setRecycleBin(recycleBinToShow)
refreshRecycleBinGroup(database) refreshRecycleBinGroup(database)
} }
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK -> { DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK -> {
@@ -509,7 +562,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} else { } else {
oldTemplatesGroup oldTemplatesGroup
} }
mDatabase?.setTemplatesGroup(templatesGroupToShow) database.setTemplatesGroup(templatesGroupToShow)
refreshTemplatesGroup(database) refreshTemplatesGroup(database)
} }
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK -> { DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK -> {
@@ -519,7 +572,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newMaxHistoryItems newMaxHistoryItems
} else { } else {
mDatabase?.historyMaxItems = oldMaxHistoryItems database.historyMaxItems = oldMaxHistoryItems
oldMaxHistoryItems oldMaxHistoryItems
} }
dbMaxHistoryItemsPref?.summary = maxHistoryItemsToShow.toString() dbMaxHistoryItemsPref?.summary = maxHistoryItemsToShow.toString()
@@ -531,7 +584,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newMaxHistorySize newMaxHistorySize
} else { } else {
mDatabase?.historyMaxSize = oldMaxHistorySize database.historyMaxSize = oldMaxHistorySize
oldMaxHistorySize oldMaxHistorySize
} }
dbMaxHistorySizePref?.summary = maxHistorySizeToShow.toString() dbMaxHistorySizePref?.summary = maxHistorySizeToShow.toString()
@@ -549,7 +602,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newEncryption newEncryption
} else { } else {
mDatabase?.encryptionAlgorithm = oldEncryption database.encryptionAlgorithm = oldEncryption
oldEncryption oldEncryption
} }
mEncryptionAlgorithmPref?.summary = algorithmToShow.toString() mEncryptionAlgorithmPref?.summary = algorithmToShow.toString()
@@ -561,7 +614,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newKeyDerivationEngine newKeyDerivationEngine
} else { } else {
mDatabase?.kdfEngine = oldKeyDerivationEngine database.kdfEngine = oldKeyDerivationEngine
oldKeyDerivationEngine oldKeyDerivationEngine
} }
mKeyDerivationPref?.summary = kdfEngineToShow.toString() mKeyDerivationPref?.summary = kdfEngineToShow.toString()
@@ -578,7 +631,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newIterations newIterations
} else { } else {
mDatabase?.numberKeyEncryptionRounds = oldIterations database.numberKeyEncryptionRounds = oldIterations
oldIterations oldIterations
} }
mRoundPref?.summary = roundsToShow.toString() mRoundPref?.summary = roundsToShow.toString()
@@ -590,7 +643,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newMemoryUsage newMemoryUsage
} else { } else {
mDatabase?.memoryUsage = oldMemoryUsage database.memoryUsage = oldMemoryUsage
oldMemoryUsage oldMemoryUsage
} }
mMemoryPref?.summary = memoryToShow.toString() mMemoryPref?.summary = memoryToShow.toString()
@@ -602,7 +655,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newParallelism newParallelism
} else { } else {
mDatabase?.parallelism = oldParallelism database.parallelism = oldParallelism
oldParallelism oldParallelism
} }
mParallelismPref?.summary = parallelismToShow.toString() mParallelismPref?.summary = parallelismToShow.toString()

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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()
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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"
}
}

View File

@@ -35,8 +35,8 @@ import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.education.Education import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.password.PassphraseGenerator import com.kunzisoft.keepass.password.PassphraseGenerator
import com.kunzisoft.keepass.timeout.TimeoutHelper 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.KeyboardUtil.isKeyboardActivatedInSettings
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
import java.util.Properties import java.util.Properties
object PreferencesUtil { object PreferencesUtil {
@@ -108,7 +108,7 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.auto_focus_search_default)) context.resources.getBoolean(R.bool.auto_focus_search_default))
} }
fun searchSubdomains(context: Context): Boolean { fun searchSubDomains(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.subdomain_search_key), return prefs.getBoolean(context.getString(R.string.subdomain_search_key),
context.resources.getBoolean(R.bool.subdomain_search_default)) context.resources.getBoolean(R.bool.subdomain_search_default))
@@ -352,6 +352,8 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.search_option_username_default)) context.resources.getBoolean(R.bool.search_option_username_default))
searchInPasswords = prefs.getBoolean(context.getString(R.string.search_option_password_key), searchInPasswords = prefs.getBoolean(context.getString(R.string.search_option_password_key),
context.resources.getBoolean(R.bool.search_option_password_default)) 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), searchInUrls = prefs.getBoolean(context.getString(R.string.search_option_url_key),
context.resources.getBoolean(R.bool.search_option_url_default)) context.resources.getBoolean(R.bool.search_option_url_default))
searchInExpired = prefs.getBoolean(context.getString(R.string.search_option_expired_key), searchInExpired = prefs.getBoolean(context.getString(R.string.search_option_expired_key),
@@ -389,6 +391,8 @@ object PreferencesUtil {
searchParameters.searchInUsernames) searchParameters.searchInUsernames)
putBoolean(context.getString(R.string.search_option_password_key), putBoolean(context.getString(R.string.search_option_password_key),
searchParameters.searchInPasswords) searchParameters.searchInPasswords)
putBoolean(context.getString(R.string.search_option_application_id_key),
searchParameters.searchInAppIds)
putBoolean(context.getString(R.string.search_option_url_key), putBoolean(context.getString(R.string.search_option_url_key),
searchParameters.searchInUrls) searchParameters.searchInUrls)
putBoolean(context.getString(R.string.search_option_expired_key), putBoolean(context.getString(R.string.search_option_expired_key),
@@ -686,6 +690,32 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.keyboard_previous_lock_default)) 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 { fun isAutofillCloseDatabaseEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.autofill_close_database_key), 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.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.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.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_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_notification_entry_clear_close_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_entry_timeout_key) -> editor.putString(name, value.toLong().toString()) context.getString(R.string.keyboard_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_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_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.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_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_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean())

View File

@@ -159,10 +159,6 @@ open class SettingsActivity
return coordinatorLayout return coordinatorLayout
} }
override fun finishActivityIfDatabaseNotLoaded(): Boolean {
return false
}
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
database: ContextualDatabase, database: ContextualDatabase,
actionTask: String, actionTask: String,
@@ -192,7 +188,7 @@ open class SettingsActivity
} }
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) { override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
assignPassword(mainCredential) assignMainCredential(mainCredential)
} }
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {} override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}

View File

@@ -52,6 +52,7 @@ class DurationDialogPreference @JvmOverloads constructor(context: Context,
notifyChanged() notifyChanged()
} }
@Deprecated(message = "")
override fun onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any?) { override fun onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any?) {
if (restorePersistedValue) { if (restorePersistedValue) {
mDuration = getPersistedString(mDuration.toString()).toLongOrNull() ?: mDuration mDuration = getPersistedString(mDuration.toString()).toLongOrNull() ?: mDuration

View File

@@ -95,20 +95,16 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
return dialog return dialog
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) var initColor = database.customColor
if (initColor != null) {
database?.let { enableSwitchView.isChecked = true
var initColor = it.customColor } else {
if (initColor != null) { enableSwitchView.isChecked = false
enableSwitchView.isChecked = true initColor = DEFAULT_COLOR
} else {
enableSwitchView.isChecked = false
initColor = DEFAULT_COLOR
}
chromaColorView.currentColor = initColor
arguments?.putInt(ARG_INITIAL_COLOR, initColor)
} }
chromaColorView.currentColor = initColor
arguments?.putInt(ARG_INITIAL_COLOR, initColor)
} }
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) { override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -50,16 +50,14 @@ class DatabaseDataCompressionPreferenceDialogFragmentCompat
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
setExplanationText(R.string.database_data_compression_summary) setExplanationText(R.string.database_data_compression_summary)
mRecyclerView?.adapter = mCompressionAdapter mRecyclerView?.adapter = mCompressionAdapter
compressionSelected = database.compressionAlgorithm
database?.let { mCompressionAdapter?.setItems(
compressionSelected = it.compressionAlgorithm items = database.availableCompressionAlgorithms,
mCompressionAdapter?.setItems(it.availableCompressionAlgorithms, compressionSelected) itemUsed = compressionSelected
} )
} }
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) { override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
class DatabaseDefaultUsernamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { class DatabaseDefaultUsernamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) inputText = database.defaultUsername
inputText = database?.defaultUsername?: ""
} }
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) { override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

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