mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
352 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
964f4ae236 | ||
|
|
9146315001 | ||
|
|
ed095ad0a7 | ||
|
|
82a8776911 | ||
|
|
753e9c4721 | ||
|
|
b64094ed20 | ||
|
|
bc854c63f7 | ||
|
|
3b793a72b8 | ||
|
|
f19afbdb2e | ||
|
|
622e9cefdd | ||
|
|
3ba56677ba | ||
|
|
39b4b4df70 | ||
|
|
4180ca92b0 | ||
|
|
bc9d00a1e1 | ||
|
|
5bdc72aa67 | ||
|
|
2be32e6884 | ||
|
|
612db4a6fc | ||
|
|
e74176f3bc | ||
|
|
af1fba42a0 | ||
|
|
bebf30aec1 | ||
|
|
321bb46df5 | ||
|
|
429f6db93f | ||
|
|
fc5a13160a | ||
|
|
c6eee8d449 | ||
|
|
7d227f372f | ||
|
|
3ac56b974f | ||
|
|
2e85ea401b | ||
|
|
fd080fb952 | ||
|
|
cc8e07366a | ||
|
|
c21bcbdbc2 | ||
|
|
e2ee17dae7 | ||
|
|
e68830fa25 | ||
|
|
9ddd66ce85 | ||
|
|
e3b69789bf | ||
|
|
54f2ed9fab | ||
|
|
2fea019b95 | ||
|
|
9ac7ef2d22 | ||
|
|
6d452fa49c | ||
|
|
d99edb6b4d | ||
|
|
cb679f0d59 | ||
|
|
2e237fba2d | ||
|
|
e68863a154 | ||
|
|
5dd9f75095 | ||
|
|
403021d38b | ||
|
|
fea7b30d6f | ||
|
|
ab5c859db4 | ||
|
|
3fcbc65de0 | ||
|
|
3f1ee6bbea | ||
|
|
37ce2ab781 | ||
|
|
ffaf4a761a | ||
|
|
56b7cc9118 | ||
|
|
987f3f9047 | ||
|
|
3039efc67c | ||
|
|
26daac4637 | ||
|
|
88a93829a9 | ||
|
|
7923a63d36 | ||
|
|
9a5c782d5d | ||
|
|
c39e4ba693 | ||
|
|
7db3d0502f | ||
|
|
d557e8b516 | ||
|
|
d6ae17657b | ||
|
|
3468b0f6f5 | ||
|
|
79777801e8 | ||
|
|
a202f66d48 | ||
|
|
ba58d5d47c | ||
|
|
46685592df | ||
|
|
ba9e2892ef | ||
|
|
a1da3b4fbd | ||
|
|
8bee0ec220 | ||
|
|
aebf6b21de | ||
|
|
0cf9253ea4 | ||
|
|
b63ceb37a4 | ||
|
|
c462dae6f5 | ||
|
|
ddf890b861 | ||
|
|
252eb30b13 | ||
|
|
62ab11cc56 | ||
|
|
e19ad3a8cc | ||
|
|
51fd8a77eb | ||
|
|
5ee0c2eb13 | ||
|
|
6d0ef8265c | ||
|
|
ea69d5acb2 | ||
|
|
1fb9595ec3 | ||
|
|
88e0bd51dc | ||
|
|
67477cc53b | ||
|
|
d2549d61d6 | ||
|
|
d6dc75961b | ||
|
|
f40c83812a | ||
|
|
b29c638d20 | ||
|
|
5bb03c2eef | ||
|
|
a76b1195e5 | ||
|
|
64da26f42c | ||
|
|
ef82552a0f | ||
|
|
d61b27ccd0 | ||
|
|
910ba99056 | ||
|
|
3de2a9acfd | ||
|
|
a48dccf27a | ||
|
|
2a561fb37e | ||
|
|
e27a329ac5 | ||
|
|
8e06a2a7cb | ||
|
|
ace82852af | ||
|
|
73369974b8 | ||
|
|
332eda8a7a | ||
|
|
e5ea1e35aa | ||
|
|
86aae9635a | ||
|
|
db3ccae87d | ||
|
|
4cec26967c | ||
|
|
a0368a4981 | ||
|
|
a1c7fe1e99 | ||
|
|
bf247ddeb7 | ||
|
|
1d2bc0fbfb | ||
|
|
85a12fe4ee | ||
|
|
a443ef996b | ||
|
|
c6995ad403 | ||
|
|
9018807eb8 | ||
|
|
b463106dd5 | ||
|
|
a23d28e1fa | ||
|
|
a0454f42d0 | ||
|
|
1c2ac88f47 | ||
|
|
11eb1bae45 | ||
|
|
089d86165a | ||
|
|
6a7362ad35 | ||
|
|
d2c10e2e4e | ||
|
|
0c20a14e67 | ||
|
|
acccf290de | ||
|
|
6ebe0f78af | ||
|
|
935c09ccd2 | ||
|
|
1eb10ad5bd | ||
|
|
ca4283151e | ||
|
|
8fb98ca4e7 | ||
|
|
be74c9710f | ||
|
|
24fb3c4c30 | ||
|
|
3bdc5fe600 | ||
|
|
c30884d6d0 | ||
|
|
5d26c3bd09 | ||
|
|
02e35cf5b7 | ||
|
|
085aefd2b9 | ||
|
|
1ea5b7a50c | ||
|
|
6cba96dd42 | ||
|
|
46238a76bc | ||
|
|
d5a9b664a1 | ||
|
|
6fdc4504d5 | ||
|
|
8a7c411a35 | ||
|
|
5ff9d5fa2f | ||
|
|
bb0f3c80d3 | ||
|
|
597d9c8274 | ||
|
|
4dd8c06fd2 | ||
|
|
72c66b3cd9 | ||
|
|
c2223afa6f | ||
|
|
d338d1340f | ||
|
|
ed4423666b | ||
|
|
d21fe662ff | ||
|
|
4da1c5bd92 | ||
|
|
18c18605fb | ||
|
|
988cb1a8d0 | ||
|
|
b6e01767e0 | ||
|
|
5414854e9c | ||
|
|
ae7f0732c6 | ||
|
|
d49d33fe3a | ||
|
|
5e7fc2d468 | ||
|
|
0d26e6a870 | ||
|
|
dd92f9ceb6 | ||
|
|
ff9239b9c4 | ||
|
|
319d35e485 | ||
|
|
28e65a4601 | ||
|
|
eb626e5bfe | ||
|
|
e1decf9a23 | ||
|
|
fff0e84b95 | ||
|
|
d73a7004b1 | ||
|
|
f71061e835 | ||
|
|
b2d25cc512 | ||
|
|
4d54b56c1d | ||
|
|
c764c6afff | ||
|
|
87b97a3849 | ||
|
|
5e6db44476 | ||
|
|
8615fa817f | ||
|
|
2f891bacd3 | ||
|
|
0d8a426df4 | ||
|
|
c952eb4415 | ||
|
|
2fd53b9416 | ||
|
|
244ca08890 | ||
|
|
208f1e97d5 | ||
|
|
e4e0628e20 | ||
|
|
f60f31771f | ||
|
|
ff6367bac4 | ||
|
|
540e72812e | ||
|
|
5fe4af8e9d | ||
|
|
ae42ab43b7 | ||
|
|
c463055971 | ||
|
|
1849dca81d | ||
|
|
b3dd3dcfb5 | ||
|
|
fef88ff270 | ||
|
|
f1f7dd1e6c | ||
|
|
409f290e33 | ||
|
|
96c3af097a | ||
|
|
4fe6b2e115 | ||
|
|
cc936b9304 | ||
|
|
e7f2a22583 | ||
|
|
4bf905ecda | ||
|
|
f8d80525d9 | ||
|
|
7ce6092270 | ||
|
|
65857596a6 | ||
|
|
e6253336bd | ||
|
|
e5595a3275 | ||
|
|
366e8bf1d7 | ||
|
|
fa63265599 | ||
|
|
755e0ea9a5 | ||
|
|
a819f2f8a8 | ||
|
|
c92da0a72f | ||
|
|
524963dbd8 | ||
|
|
50b1ac388e | ||
|
|
51c62034df | ||
|
|
e4d0cd89c6 | ||
|
|
bfe50fa985 | ||
|
|
3d798e6585 | ||
|
|
068c59ac98 | ||
|
|
34ec94a0c3 | ||
|
|
576a355342 | ||
|
|
aa19f11699 | ||
|
|
2fb4dff46d | ||
|
|
e6cf3f12a5 | ||
|
|
ca94ce86ba | ||
|
|
dea6b25bb4 | ||
|
|
c48f64d331 | ||
|
|
5e3a504c1f | ||
|
|
b9b7d7b2db | ||
|
|
e085d5d277 | ||
|
|
05336e93a0 | ||
|
|
90b3b56893 | ||
|
|
02c514272e | ||
|
|
989e47ed12 | ||
|
|
1caf132558 | ||
|
|
a337de3679 | ||
|
|
75b37f5a9f | ||
|
|
075f54b286 | ||
|
|
e07cbc2e14 | ||
|
|
ac29b7bac7 | ||
|
|
b9129cb941 | ||
|
|
6957fcd81a | ||
|
|
cfe56fc055 | ||
|
|
6f3e065ad1 | ||
|
|
abfa7a3f47 | ||
|
|
dd0d85e54e | ||
|
|
76c20263f7 | ||
|
|
e447388611 | ||
|
|
1bfec67c02 | ||
|
|
45041216d6 | ||
|
|
e075e9018c | ||
|
|
eed304ec40 | ||
|
|
5bcbbac97f | ||
|
|
ea4750fc11 | ||
|
|
5037821529 | ||
|
|
3a4c88f19a | ||
|
|
e960a8e169 | ||
|
|
1d4e1687cf | ||
|
|
033fa95285 | ||
|
|
cae69e7572 | ||
|
|
01d778650c | ||
|
|
dd389dbab1 | ||
|
|
272ebd0c3f | ||
|
|
0aecc21f43 | ||
|
|
1e7e464e65 | ||
|
|
d5c378ac85 | ||
|
|
672f1ca37d | ||
|
|
2f9e1e4bf2 | ||
|
|
25d97e4f2e | ||
|
|
f49dcbd654 | ||
|
|
bf2d56b4fd | ||
|
|
5893541dd2 | ||
|
|
803d637510 | ||
|
|
ccd5da0962 | ||
|
|
36e3b85400 | ||
|
|
cd73880e21 | ||
|
|
8337f98f3a | ||
|
|
47fbb562b7 | ||
|
|
ef98e8a2db | ||
|
|
e562694606 | ||
|
|
464bc1442d | ||
|
|
c1730353d0 | ||
|
|
55e32e4ac5 | ||
|
|
96ed9fc7a6 | ||
|
|
5fda628c9c | ||
|
|
17742e25a9 | ||
|
|
8086289e4b | ||
|
|
65157f661f | ||
|
|
5df637d01f | ||
|
|
8084920b9e | ||
|
|
a6af9976fc | ||
|
|
05c480b6d3 | ||
|
|
fb909dac52 | ||
|
|
a8130d67be | ||
|
|
754d195e26 | ||
|
|
074910ea19 | ||
|
|
988b18b515 | ||
|
|
8924254c25 | ||
|
|
2bedbf8a6c | ||
|
|
437a704bc8 | ||
|
|
a3bd5e1593 | ||
|
|
3feb177afc | ||
|
|
821f35fe05 | ||
|
|
d36f675da7 | ||
|
|
b7f9690a38 | ||
|
|
c861fe790c | ||
|
|
1a717bda03 | ||
|
|
b80acd5a2d | ||
|
|
7e41527cfe | ||
|
|
200881278c | ||
|
|
0d133ffdb0 | ||
|
|
f8787ba03d | ||
|
|
4f10d13691 | ||
|
|
98007c962d | ||
|
|
5f27f161a5 | ||
|
|
fcf723849b | ||
|
|
5bd866e104 | ||
|
|
9985c6065d | ||
|
|
1f2e4a3719 | ||
|
|
fa2555a3f7 | ||
|
|
b4de7afe77 | ||
|
|
736cafbcc2 | ||
|
|
f2f4c1e63d | ||
|
|
bc86ee87a0 | ||
|
|
5cbd60c024 | ||
|
|
5817273872 | ||
|
|
32d6a11353 | ||
|
|
9477fba704 | ||
|
|
80b16bccf1 | ||
|
|
6672085d84 | ||
|
|
05a39f6922 | ||
|
|
40e8dea485 | ||
|
|
7e09532d5d | ||
|
|
44e8f4f406 | ||
|
|
e3083c7773 | ||
|
|
d0c0c4a4d6 | ||
|
|
a9e8de26f8 | ||
|
|
c7a256ebf1 | ||
|
|
d0ab5267cf | ||
|
|
88b701fd39 | ||
|
|
4a1cee619c | ||
|
|
c7741115ff | ||
|
|
19c987abc3 | ||
|
|
d03693341e | ||
|
|
bf496333eb | ||
|
|
c6b01947b3 | ||
|
|
91781f36ac | ||
|
|
3fbdf78ba1 | ||
|
|
d1f463d497 | ||
|
|
488fd60d5d | ||
|
|
41025f64c0 | ||
|
|
a2eac2ff76 | ||
|
|
34f2a2391a | ||
|
|
67b09014aa | ||
|
|
c907750446 | ||
|
|
69114c3cc0 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -19,6 +19,9 @@ bin/
|
||||
gen/
|
||||
out/
|
||||
|
||||
# Kotlin folder
|
||||
.kotlin/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
37
CHANGELOG
37
CHANGELOG
@@ -1,3 +1,40 @@
|
||||
KeePassDX(4.3.0)
|
||||
* Manual change of app language #1884 #1990
|
||||
* Fix autofill username detection #2276
|
||||
* Fix Passkey in passwordless mode #2282
|
||||
|
||||
KeePassDX(4.2.4)
|
||||
* Fix remembering database location #2262
|
||||
|
||||
KeePassDX(4.2.3)
|
||||
* Fix multiple Passkey selection #2253
|
||||
* Fix database dialog subtitle #2254
|
||||
* Fix save search info if URL present #2255
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(4.2.2)
|
||||
* Fix database merge algorithm #2223
|
||||
* Fix save search info #2243
|
||||
* Fix Play Service as privileged app for Passkey Cross Device Authentication #2244
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(4.2.1)
|
||||
* Fix Magikeyboard autosearch #2233
|
||||
* Fix database merge #2223
|
||||
* Fix dialog database action #2234
|
||||
* Fix autofill selection #2238 #2235
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(4.2.0)
|
||||
* Passkeys management #1421 #2097 (@cali-95)
|
||||
* Confirm usage of passkey #2165 #2124
|
||||
* Dialog to manage missing signature #2152 #2155 #2161 #2160
|
||||
* Capture error #2159 #2215
|
||||
* Change Passkey Backup Eligibility & Backup State #2135 #2150 #2212
|
||||
* Search settings #2112 #2181 #2187 #2204
|
||||
* Autofill refactoring #765 #2196
|
||||
* Small fixes #2157 #2164 #2171 #2122 #2180 #2209 #2214
|
||||
|
||||
KeePassDX(4.1.9)
|
||||
* Fix landscape UI #2198 #2200 (@chenxiaolong)
|
||||
* Fix start loop and flash screen #2201
|
||||
|
||||
22
README.md
22
README.md
@@ -6,19 +6,21 @@
|
||||
|
||||
### Features
|
||||
|
||||
- Create database files / entries and groups.
|
||||
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
||||
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
|
||||
- **Passkeys** for authentication and **local storage of private keys**.
|
||||
- **Biometric recognition** for fast unlocking (fingerprint / face unlock / …).
|
||||
- **One-Time Password** management (HOTP / TOTP) for two-factor authentication (2FA).
|
||||
- **Autofill** for easy form filling with passwords.
|
||||
- **Magikeyboard** to efficiently fill in any field.
|
||||
- Create **encrypted database files**.
|
||||
- Organisation of credentials by **entry** and in **group** trees.
|
||||
- Allows opening and **copying URI / URL fields quickly**.
|
||||
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
|
||||
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
|
||||
- Material design with **themes**.
|
||||
- **Auto-Fill** and Integration.
|
||||
- Field filling **keyboard**.
|
||||
- Dynamic **templates**
|
||||
- Dynamic **templates** for each type of entry.
|
||||
- **History** of each entry.
|
||||
- Precise management of **settings**.
|
||||
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
||||
- Material design with **themes**.
|
||||
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
||||
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
|
||||
- Code written in **native languages** (Kotlin / Java / JNI / C).
|
||||
|
||||
KeePassDX is **open source** and **ad-free**.
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 35
|
||||
versionCode = 143
|
||||
versionName = "4.1.9"
|
||||
versionCode = 150
|
||||
versionName = "4.3.0"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
@@ -35,6 +35,10 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
// Disables dependency metadata when building APKs.
|
||||
includeInApk = false
|
||||
@@ -101,6 +105,15 @@ android {
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
packaging {
|
||||
// Bouncy castle bug https://github.com/bcgit/bc-java/issues/1685
|
||||
resources.pickFirsts.add('META-INF/versions/9/OSGI-INF/MANIFEST.MF')
|
||||
}
|
||||
|
||||
androidResources {
|
||||
generateLocaleConfig true
|
||||
}
|
||||
}
|
||||
|
||||
def room_version = "2.5.1"
|
||||
@@ -140,7 +153,10 @@ dependencies {
|
||||
implementation 'commons-codec:commons-codec:1.15'
|
||||
// Password generator
|
||||
implementation 'me.gosimple:nbvcxz:1.5.0'
|
||||
|
||||
|
||||
// Credentials Provider
|
||||
implementation "androidx.credentials:credentials:1.2.2"
|
||||
|
||||
// Modules import
|
||||
implementation project(path: ':database')
|
||||
implementation project(path: ':icon-pack')
|
||||
|
||||
76
app/src/free/assets/passkeys_privileged_apps_community.json
Normal file
76
app/src/free/assets/passkeys_privileged_apps_community.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"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.ironfoxoss.ironfox.nightly",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
820
app/src/free/assets/passkeys_privileged_apps_google.json
Normal file
820
app/src/free/assets/passkeys_privileged_apps_google.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -45,7 +45,8 @@
|
||||
android:resizeableActivity="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/KeepassDXStyle.Night"
|
||||
tools:targetApi="s">
|
||||
tools:targetApi="s"
|
||||
tools:ignore="CredentialDependency">
|
||||
<meta-data
|
||||
android:name="com.google.android.backup.api_key"
|
||||
android:value="${googleAndroidBackupAPIKey}" />
|
||||
@@ -158,25 +159,41 @@
|
||||
android:label="@string/about" />
|
||||
<activity
|
||||
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
|
||||
android:name="com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity" />
|
||||
<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
|
||||
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
|
||||
android:theme="@style/Theme.Transparent" />
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:exported="false"
|
||||
android:excludeFromRecents="true" />
|
||||
<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:launchMode="singleInstance"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:excludeFromRecents="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -192,14 +209,12 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
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>
|
||||
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:exported="false"
|
||||
android:excludeFromRecents="true"
|
||||
tools:targetApi="upside_down_cake" />
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
@@ -227,8 +242,8 @@
|
||||
android:exported="false" />
|
||||
<!-- Receiver for Autofill -->
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
||||
android:label="@string/autofill_service_name"
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService"
|
||||
android:label="@string/app_name"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
||||
<meta-data
|
||||
@@ -239,7 +254,7 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService"
|
||||
android:label="@string/keyboard_label"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_INPUT_METHOD" >
|
||||
@@ -249,6 +264,22 @@
|
||||
<action android:name="android.view.InputMethod" />
|
||||
</intent-filter>
|
||||
</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
|
||||
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
|
||||
android:exported="true">
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.Log
|
||||
@@ -32,7 +31,7 @@ import androidx.core.text.HtmlCompat
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
||||
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
||||
import com.kunzisoft.keepass.utils.getPackageInfoCompat
|
||||
import org.joda.time.DateTime
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,15 +50,15 @@ import com.google.android.material.tabs.TabLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.adapters.TagsAdapter
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.otp.OtpType
|
||||
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||
@@ -69,7 +69,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import com.kunzisoft.keepass.view.WindowInsetPosition
|
||||
import com.kunzisoft.keepass.view.applyWindowInsets
|
||||
@@ -125,6 +125,8 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
private var mBackgroundColor: Int? = null
|
||||
private var mForegroundColor: Int? = null
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -264,7 +266,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
mIcon = entryInfo.icon
|
||||
// Assign title text
|
||||
val entryTitle =
|
||||
if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
|
||||
entryInfo.title.ifEmpty { entryInfo.id.asHexString() }
|
||||
collapsingToolbarLayout?.title = entryTitle
|
||||
toolbar?.title = entryTitle
|
||||
// Assign tags
|
||||
@@ -312,11 +314,11 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
||||
mDatabase?.let { database ->
|
||||
launch(
|
||||
this,
|
||||
database,
|
||||
historySelected.nodeId,
|
||||
historySelected.historyPosition,
|
||||
mEntryActivityResultLauncher
|
||||
activity = this,
|
||||
database = database,
|
||||
entryId = historySelected.nodeId,
|
||||
historyPosition = historySelected.historyPosition,
|
||||
activityResultLauncher = mEntryActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -330,9 +332,8 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
return coordinatorLayout
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
mEntryViewModel.loadDatabase(database)
|
||||
}
|
||||
|
||||
@@ -478,11 +479,12 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
R.id.menu_edit -> {
|
||||
mDatabase?.let { database ->
|
||||
mMainEntryId?.let { entryId ->
|
||||
EntryEditActivity.launchToUpdate(
|
||||
this,
|
||||
database,
|
||||
entryId,
|
||||
mEntryActivityResultLauncher
|
||||
EntryEditActivity.launch(
|
||||
activity = this,
|
||||
database = database,
|
||||
registrationType = EntryEditActivity.RegistrationType.UPDATE,
|
||||
nodeId = entryId,
|
||||
activityResultLauncher = mEntryActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -520,7 +522,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
// Transit data in previous Activity after an update
|
||||
Intent().apply {
|
||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
||||
setResult(Activity.RESULT_OK, this)
|
||||
setResult(RESULT_OK, this)
|
||||
}
|
||||
super.finish()
|
||||
}
|
||||
@@ -534,34 +536,22 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
|
||||
|
||||
/**
|
||||
* Open standard Entry activity
|
||||
* Open standard or history Entry activity
|
||||
*/
|
||||
fun launch(activity: Activity,
|
||||
database: ContextualDatabase,
|
||||
entryId: NodeId<UUID>,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
fun launch(
|
||||
activity: Activity,
|
||||
database: ContextualDatabase,
|
||||
entryId: NodeId<UUID>,
|
||||
historyPosition: Int? = null,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>
|
||||
) {
|
||||
if (database.loaded) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entryId)
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open history Entry activity
|
||||
*/
|
||||
fun launch(activity: Activity,
|
||||
database: ContextualDatabase,
|
||||
entryId: NodeId<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)
|
||||
historyPosition?.let {
|
||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||
}
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,14 +36,14 @@ import android.widget.Spinner
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.timepicker.MaterialTimePicker
|
||||
@@ -55,22 +55,23 @@ import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Compani
|
||||
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildSpecialModeResponseAndSetResult
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.DataTime
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
@@ -79,9 +80,9 @@ import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getNewEntry
|
||||
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
@@ -100,6 +101,7 @@ import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
||||
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.EnumSet
|
||||
import java.util.UUID
|
||||
|
||||
@@ -155,8 +157,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
// To ask data lost only one time
|
||||
private var backPressedAlreadyApproved = false
|
||||
override fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -210,8 +211,8 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
mDatabase,
|
||||
entryId,
|
||||
parentId,
|
||||
EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent),
|
||||
EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||
intent.retrieveRegisterInfo()
|
||||
?: intent.retrieveSearchInfo()?.toRegisterInfo()
|
||||
)
|
||||
|
||||
// To retrieve attachment
|
||||
@@ -378,23 +379,30 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
} ?: run {
|
||||
updateEntry(entrySave.oldEntry, entrySave.newEntry)
|
||||
}
|
||||
}
|
||||
|
||||
// Don't wait for saving if it's to provide autofill
|
||||
mDatabase?.let { database ->
|
||||
EntrySelectionHelper.doSpecialAction(intent,
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{
|
||||
entryValidatedForKeyboardSelection(database, entrySave.newEntry)
|
||||
},
|
||||
{ _, _ ->
|
||||
entryValidatedForAutofillSelection(database, entrySave.newEntry)
|
||||
},
|
||||
{
|
||||
entryValidatedForAutofillRegistration(entrySave.newEntry)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mEntryEditViewModel.uiState.collect { uiState ->
|
||||
when (uiState) {
|
||||
EntryEditViewModel.UIState.Loading -> {}
|
||||
EntryEditViewModel.UIState.ShowOverwriteMessage -> {
|
||||
if (mEntryEditViewModel.warningOverwriteDataAlreadyApproved.not()) {
|
||||
AlertDialog.Builder(this@EntryEditActivity)
|
||||
.setTitle(R.string.warning_overwrite_data_title)
|
||||
.setMessage(R.string.warning_overwrite_data_description)
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
mEntryEditViewModel.backPressedAlreadyApproved = true
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mEntryEditViewModel.warningOverwriteDataAlreadyApproved = true
|
||||
}
|
||||
.create().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -407,13 +415,13 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
mAllowCustomFields = database?.allowEntryCustomFields() == true
|
||||
mAllowOTP = database?.allowOTP == true
|
||||
mEntryEditViewModel.loadDatabase(database)
|
||||
mAllowCustomFields = database.allowEntryCustomFields() == true
|
||||
mAllowOTP = database.allowOTP == true
|
||||
mEntryEditViewModel.loadTemplateEntry(database)
|
||||
mTemplatesSelectorAdapter?.apply {
|
||||
iconDrawableFactory = mDatabase?.iconDrawableFactory
|
||||
iconDrawableFactory = database.iconDrawableFactory
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -424,39 +432,45 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
mEntryEditViewModel.unlockAction()
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||
try {
|
||||
if (result.isSuccess) {
|
||||
var newNodes: List<Node> = ArrayList()
|
||||
result.data?.getBundle(DatabaseTaskNotificationService.NEW_NODES_KEY)?.let { newNodesBundle ->
|
||||
newNodes = DatabaseTaskNotificationService.getListNodesFromBundle(database, newNodesBundle)
|
||||
}
|
||||
if (newNodes.size == 1) {
|
||||
(newNodes[0] as? Entry?)?.let { entry ->
|
||||
EntrySelectionHelper.doSpecialAction(intent,
|
||||
{
|
||||
// Finish naturally
|
||||
finishForEntryResult(entry)
|
||||
},
|
||||
{
|
||||
// Nothing when search retrieved
|
||||
},
|
||||
{
|
||||
entryValidatedForSave(entry)
|
||||
},
|
||||
{
|
||||
entryValidatedForKeyboardSelection(database, entry)
|
||||
},
|
||||
{ _, _ ->
|
||||
entryValidatedForAutofillSelection(database, entry)
|
||||
},
|
||||
{
|
||||
entryValidatedForAutofillRegistration(entry)
|
||||
result.data?.getNewEntry(database)?.let { entry ->
|
||||
EntrySelectionHelper.doSpecialAction(
|
||||
intent = intent,
|
||||
defaultAction = {
|
||||
// Finish naturally
|
||||
finishForEntryResult(entry)
|
||||
},
|
||||
searchAction = {
|
||||
// Nothing when search retrieved
|
||||
},
|
||||
selectionAction = { intentSender, typeMode, searchInfo ->
|
||||
when(typeMode) {
|
||||
TypeMode.DEFAULT -> {}
|
||||
TypeMode.MAGIKEYBOARD ->
|
||||
entryValidatedForKeyboardSelection(database, entry)
|
||||
TypeMode.PASSKEY ->
|
||||
entryValidatedForPasskey(database, entry)
|
||||
TypeMode.AUTOFILL ->
|
||||
entryValidatedForAutofill(database, entry)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
registrationAction = { _, typeMode, _ ->
|
||||
when(typeMode) {
|
||||
TypeMode.DEFAULT ->
|
||||
entryValidatedForSave(entry)
|
||||
TypeMode.MAGIKEYBOARD -> {}
|
||||
TypeMode.PASSKEY ->
|
||||
entryValidatedForPasskey(database, entry)
|
||||
TypeMode.AUTOFILL ->
|
||||
entryValidatedForAutofill(database, entry)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -473,29 +487,33 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
|
||||
private fun entryValidatedForKeyboardSelection(database: ContextualDatabase, entry: Entry) {
|
||||
// Populate Magikeyboard with entry
|
||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||
this,
|
||||
entry.getEntryInfo(database)
|
||||
// Build Magikeyboard response with the entry selected
|
||||
this.buildSpecialModeResponseAndSetResult(
|
||||
entryInfo = entry.getEntryInfo(database),
|
||||
extras = buildEntryResult(entry)
|
||||
)
|
||||
onValidateSpecialMode()
|
||||
// Don't keep activity history for entry edition
|
||||
finishForEntryResult(entry)
|
||||
}
|
||||
|
||||
private fun entryValidatedForAutofillSelection(database: ContextualDatabase, entry: Entry) {
|
||||
private fun entryValidatedForAutofill(database: ContextualDatabase, entry: Entry) {
|
||||
// Build Autofill response with the entry selected
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity,
|
||||
database,
|
||||
entry.getEntryInfo(database))
|
||||
this.buildSpecialModeResponseAndSetResult(
|
||||
entryInfo = entry.getEntryInfo(database),
|
||||
extras = buildEntryResult(entry)
|
||||
)
|
||||
}
|
||||
onValidateSpecialMode()
|
||||
}
|
||||
|
||||
private fun entryValidatedForAutofillRegistration(entry: Entry) {
|
||||
private fun entryValidatedForPasskey(database: ContextualDatabase, entry: Entry) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
this.buildPasskeyResponseAndSetResult(
|
||||
entryInfo = entry.getEntryInfo(database),
|
||||
extras = buildEntryResult(entry) // To update the previous screen
|
||||
)
|
||||
}
|
||||
onValidateSpecialMode()
|
||||
finishForEntryResult(entry)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -729,13 +747,13 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
|
||||
private fun onApprovedBackPressed(approved: () -> Unit) {
|
||||
if (!backPressedAlreadyApproved) {
|
||||
if (mEntryEditViewModel.backPressedAlreadyApproved.not()) {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.discard_changes)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.discard) { _, _ ->
|
||||
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
||||
backPressedAlreadyApproved = true
|
||||
mEntryEditViewModel.backPressedAlreadyApproved = true
|
||||
approved.invoke()
|
||||
}.create().show()
|
||||
} else {
|
||||
@@ -743,14 +761,19 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildEntryResult(entry: Entry): Bundle {
|
||||
return Bundle().apply {
|
||||
putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun finishForEntryResult(entry: Entry) {
|
||||
// Assign entry callback as a result
|
||||
try {
|
||||
val bundle = Bundle()
|
||||
val bundle = buildEntryResult(entry)
|
||||
val intentEntry = Intent()
|
||||
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
|
||||
intentEntry.putExtras(bundle)
|
||||
setResult(Activity.RESULT_OK, intentEntry)
|
||||
setResult(RESULT_OK, intentEntry)
|
||||
super.finish()
|
||||
} catch (e: Exception) {
|
||||
// Exception when parcelable can't be done
|
||||
@@ -758,6 +781,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
enum class RegistrationType {
|
||||
UPDATE, CREATE
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = EntryEditActivity::class.java.name
|
||||
@@ -767,23 +794,12 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
const val KEY_PARENT = "parent"
|
||||
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
|
||||
|
||||
fun registerForEntryResult(fragment: Fragment,
|
||||
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
|
||||
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
entryAddedOrUpdatedListener.invoke(
|
||||
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
|
||||
)
|
||||
} else {
|
||||
entryAddedOrUpdatedListener.invoke(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun registerForEntryResult(activity: FragmentActivity,
|
||||
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
|
||||
fun registerForEntryResult(
|
||||
activity: FragmentActivity,
|
||||
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit
|
||||
): ActivityResultLauncher<Intent> {
|
||||
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
entryAddedOrUpdatedListener.invoke(
|
||||
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
|
||||
)
|
||||
@@ -794,151 +810,78 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to update an existing entry by his [entryId]
|
||||
* Launch EntryEditActivity to update an existing entry or to add a new entry in an existing group
|
||||
*/
|
||||
fun launchToUpdate(activity: Activity,
|
||||
database: ContextualDatabase,
|
||||
entryId: NodeId<UUID>,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
fun launch(
|
||||
activity: Activity,
|
||||
database: ContextualDatabase,
|
||||
registrationType: RegistrationType,
|
||||
nodeId: NodeId<*>,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>
|
||||
) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entryId)
|
||||
when (registrationType) {
|
||||
RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
|
||||
RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
|
||||
}
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to add a new entry in an existent group
|
||||
* Launch EntryEditActivity to add a new entry in special selection
|
||||
*/
|
||||
fun launchToCreate(activity: Activity,
|
||||
database: ContextualDatabase,
|
||||
groupId: NodeId<*>,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, groupId)
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun launchToUpdateForSave(context: Context,
|
||||
database: ContextualDatabase,
|
||||
entryId: NodeId<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) {
|
||||
fun launchForSelection(
|
||||
context: Context,
|
||||
database: ContextualDatabase,
|
||||
typeMode: TypeMode,
|
||||
groupId: NodeId<*>,
|
||||
searchInfo: SearchInfo? = null,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||
) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||
val intent = Intent(context, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, groupId)
|
||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
||||
context,
|
||||
intent,
|
||||
searchInfo
|
||||
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||
context = context,
|
||||
intent = intent,
|
||||
typeMode = typeMode,
|
||||
searchInfo = searchInfo,
|
||||
activityResultLauncher = activityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to add a new entry in keyboard selection
|
||||
* Launch EntryEditActivity to update an updated entry or register a new entry (from autofill)
|
||||
*/
|
||||
fun launchForKeyboardSelectionResult(context: Context,
|
||||
database: ContextualDatabase,
|
||||
groupId: NodeId<*>,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
fun launchForRegistration(
|
||||
context: Context,
|
||||
database: ContextualDatabase,
|
||||
nodeId: NodeId<*>,
|
||||
registerInfo: RegisterInfo? = null,
|
||||
typeMode: TypeMode,
|
||||
registrationType: RegistrationType,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||
) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||
val intent = Intent(context, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, groupId)
|
||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
||||
when (registrationType) {
|
||||
RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
|
||||
RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
|
||||
}
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
context,
|
||||
intent,
|
||||
searchInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to add a new entry in autofill selection
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
database: ContextualDatabase,
|
||||
activityResultLauncher: ActivityResultLauncher<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,
|
||||
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,
|
||||
registerInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to register a new entry (from autofill)
|
||||
*/
|
||||
fun launchToCreateForRegistration(context: Context,
|
||||
database: ContextualDatabase,
|
||||
groupId: NodeId<*>,
|
||||
registerInfo: RegisterInfo? = null) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||
val intent = Intent(context, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, groupId)
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
context,
|
||||
intent,
|
||||
registerInfo
|
||||
registerInfo,
|
||||
typeMode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,226 +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.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.WebDomain
|
||||
|
||||
/**
|
||||
* Activity to search or select entry in database,
|
||||
* Commonly used with Magikeyboard
|
||||
*/
|
||||
class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||
|
||||
override fun applyCustomStyle(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
|
||||
if (keySelectionBundle != null) {
|
||||
// To manage package name
|
||||
var searchInfo = SearchInfo()
|
||||
keySelectionBundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { mSearchInfo ->
|
||||
searchInfo = mSearchInfo
|
||||
}
|
||||
launch(database, searchInfo)
|
||||
} else {
|
||||
// To manage share
|
||||
var sharedWebDomain: String? = null
|
||||
var otpString: String? = null
|
||||
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_SEND -> {
|
||||
if ("text/plain" == intent.type) {
|
||||
// Retrieve web domain or OTP
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
|
||||
if (OtpEntryFields.isOTPUri(extra))
|
||||
otpString = extra
|
||||
else
|
||||
sharedWebDomain = Uri.parse(extra).host
|
||||
}
|
||||
}
|
||||
launchSelection(database, sharedWebDomain, otpString)
|
||||
}
|
||||
Intent.ACTION_VIEW -> {
|
||||
// Retrieve OTP
|
||||
intent.dataString?.let { extra ->
|
||||
if (OtpEntryFields.isOTPUri(extra))
|
||||
otpString = extra
|
||||
}
|
||||
launchSelection(database, sharedWebDomain, otpString)
|
||||
}
|
||||
else -> {
|
||||
if (database != null) {
|
||||
GroupActivity.launch(this, database)
|
||||
} else {
|
||||
FileDatabaseSelectActivity.launch(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun launchSelection(database: ContextualDatabase?,
|
||||
sharedWebDomain: String?,
|
||||
otpString: String?) {
|
||||
// Build domain search param
|
||||
val searchInfo = SearchInfo().apply {
|
||||
this.webDomain = sharedWebDomain
|
||||
this.otpString = otpString
|
||||
}
|
||||
|
||||
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launch(database, searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun launch(database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo) {
|
||||
|
||||
// Setting to integrate Magikeyboard
|
||||
val searchShareForMagikeyboard = isKeyboardActivatedInSettings()
|
||||
|
||||
// If database is open
|
||||
val readOnly = database?.isReadOnly != false
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
searchInfo,
|
||||
{ openedDatabase, items ->
|
||||
// Items found
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
Toast.makeText(applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
MagikeyboardService.performSelection(
|
||||
items,
|
||||
{ entryInfo ->
|
||||
// Automatically populate keyboard
|
||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||
this,
|
||||
entryInfo
|
||||
)
|
||||
},
|
||||
{ autoSearch ->
|
||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
autoSearch)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
GroupActivity.launchForSearchResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
true)
|
||||
}
|
||||
},
|
||||
{ openedDatabase ->
|
||||
// Show the database UI to select the entry
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
Toast.makeText(applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
GroupActivity.launchForSearchResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false)
|
||||
}
|
||||
},
|
||||
{
|
||||
// If database not open
|
||||
if (searchInfo.otpString != null) {
|
||||
FileDatabaseSelectActivity.launchForSaveResult(this,
|
||||
searchInfo)
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
|
||||
searchInfo)
|
||||
} else {
|
||||
FileDatabaseSelectActivity.launchForSearchResult(this,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
||||
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||
|
||||
fun launch(context: Context,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
val intent = Intent(context, EntrySelectionLauncherActivity::class.java).apply {
|
||||
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
||||
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
||||
})
|
||||
}
|
||||
// New task needed because don't launch from an Activity context
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@@ -33,8 +32,6 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.isVisible
|
||||
@@ -44,31 +41,28 @@ import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
||||
import com.kunzisoft.keepass.utils.DexUtil
|
||||
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
@@ -98,10 +92,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
|
||||
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 manageDatabaseInfo(): Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -132,7 +123,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
uri?.let {
|
||||
launchPasswordActivityWithPath(uri)
|
||||
launchMainCredentialActivityWithPath(uri)
|
||||
}
|
||||
}
|
||||
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
|
||||
@@ -161,7 +152,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
}
|
||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
||||
launchPasswordActivity(
|
||||
launchMainCredentialActivity(
|
||||
databaseFileUri,
|
||||
fileDatabaseHistoryEntityToOpen.keyFileUri,
|
||||
fileDatabaseHistoryEntityToOpen.hardwareKey
|
||||
@@ -180,7 +171,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
|
||||
// Load default database the first time
|
||||
databaseFilesViewModel.doForDefaultDatabase { databaseFileUri ->
|
||||
launchPasswordActivityWithPath(databaseFileUri)
|
||||
launchMainCredentialActivityWithPath(databaseFileUri)
|
||||
}
|
||||
|
||||
// Retrieve the database URI provided by file manager after an orientation change
|
||||
@@ -223,13 +214,16 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
// Retrieve settings for default database
|
||||
mAdapterDatabaseHistory?.setDefaultDatabase(it)
|
||||
}
|
||||
|
||||
// Remove all the remember locations if needed
|
||||
if (PreferencesUtil.rememberDatabaseLocations(applicationContext).not()) {
|
||||
FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||
.deleteAll()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
if (database != null) {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
@@ -237,25 +231,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
|
||||
if (result.isSuccess) {
|
||||
// Update list
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_TASK,
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
result.data?.getParcelableCompat<Uri>(DATABASE_URI_KEY)?.let { databaseUri ->
|
||||
val mainCredential =
|
||||
result.data?.getParcelableCompat(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY)
|
||||
?: MainCredential()
|
||||
databaseFilesViewModel.addDatabaseFile(
|
||||
databaseUri,
|
||||
mainCredential.keyFileUri,
|
||||
mainCredential.hardwareKey
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Launch activity
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_TASK -> {
|
||||
@@ -264,13 +240,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
database,
|
||||
false
|
||||
)
|
||||
coordinatorLayout.showActionErrorIfNeeded(result)
|
||||
}
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
coordinatorLayout.showActionErrorIfNeeded(result)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -288,17 +264,58 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
|
||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
|
||||
MainCredentialActivity.launch(this,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
{ exception ->
|
||||
fileNoFoundAction(exception)
|
||||
private fun launchMainCredentialActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
|
||||
try {
|
||||
EntrySelectionHelper.doSpecialAction(
|
||||
intent = this.intent,
|
||||
defaultAction = {
|
||||
MainCredentialActivity.launch(
|
||||
activity = this,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey
|
||||
)
|
||||
},
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() },
|
||||
mAutofillActivityResultLauncher)
|
||||
searchAction = { searchInfo ->
|
||||
MainCredentialActivity.launchForSearchResult(
|
||||
activity = this,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
selectionAction = { intentSenderMode, typeMode, searchInfo ->
|
||||
MainCredentialActivity.launchForSelection(
|
||||
activity = this,
|
||||
activityResultLauncher = if (intentSenderMode)
|
||||
mCredentialActivityResultLauncher else null,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
typeMode = typeMode,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
registrationAction = { intentSenderMode, typeMode, registerInfo ->
|
||||
MainCredentialActivity.launchForRegistration(
|
||||
activity = this,
|
||||
activityResultLauncher = if (intentSenderMode)
|
||||
mCredentialActivityResultLauncher else null,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
typeMode = typeMode,
|
||||
registerInfo = registerInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
}
|
||||
)
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||
@@ -308,12 +325,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
{ onValidateSpecialMode() },
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() },
|
||||
mAutofillActivityResultLauncher)
|
||||
mCredentialActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||
launchPasswordActivity(databaseUri, null, null)
|
||||
private fun launchMainCredentialActivityWithPath(databaseUri: Uri) {
|
||||
launchMainCredentialActivity(databaseUri, null, null)
|
||||
// Delete flickering for kitkat <=
|
||||
@Suppress("DEPRECATION")
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||
@@ -337,10 +355,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
mDatabase?.let { database ->
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
|
||||
// Show recent files if allowed
|
||||
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
|
||||
databaseFilesViewModel.loadListOfDatabases()
|
||||
@@ -359,7 +373,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
try {
|
||||
mDatabaseFileUri?.let { databaseUri ->
|
||||
// Create the new database
|
||||
createDatabase(databaseUri, mainCredential)
|
||||
mDatabaseViewModel.createDatabase(databaseUri, mainCredential)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
val error = getString(R.string.error_create_database_file)
|
||||
@@ -443,55 +457,36 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launchForSearchResult(context: Context,
|
||||
searchInfo: SearchInfo) {
|
||||
EntrySelectionHelper.startActivityForSearchModeResult(context,
|
||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo)
|
||||
fun launchForSearch(
|
||||
context: Context,
|
||||
searchInfo: SearchInfo
|
||||
) {
|
||||
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||
context = context,
|
||||
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Save Launch
|
||||
* Selection Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launchForSaveResult(context: Context,
|
||||
searchInfo: SearchInfo) {
|
||||
EntrySelectionHelper.startActivityForSaveModeResult(context,
|
||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Keyboard Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launchForKeyboardSelectionResult(activity: Activity,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(activity,
|
||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Autofill Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
AutofillHelper.startActivityForAutofillResult(activity,
|
||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||
activityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
fun launchForSelection(
|
||||
context: Context,
|
||||
typeMode: TypeMode,
|
||||
searchInfo: SearchInfo? = null,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
) {
|
||||
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||
context = context,
|
||||
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo = searchInfo,
|
||||
typeMode = typeMode,
|
||||
activityResultLauncher = activityResultLauncher
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -499,11 +494,19 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
* Registration Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launchForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo? = null) {
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
registerInfo)
|
||||
fun launchForRegistration(
|
||||
context: Context,
|
||||
typeMode: TypeMode,
|
||||
registerInfo: RegisterInfo? = null,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
) {
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
context = context,
|
||||
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
registerInfo = registerInfo,
|
||||
typeMode = typeMode,
|
||||
activityResultLauncher = activityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -78,6 +78,8 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -174,10 +176,10 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
if (database?.allowCustomIcons == true) {
|
||||
if (database.allowCustomIcons) {
|
||||
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
} else {
|
||||
uploadButton.visibility = View.GONE
|
||||
|
||||
@@ -45,6 +45,8 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
||||
private lateinit var imageView: ImageView
|
||||
private lateinit var progressView: View
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = false
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -101,7 +103,7 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
try {
|
||||
@@ -119,18 +121,16 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
||||
resources.displayMetrics.heightPixels * 2
|
||||
)
|
||||
|
||||
database?.let { database ->
|
||||
BinaryDatabaseManager.loadBitmap(
|
||||
database,
|
||||
attachment.binaryData,
|
||||
mImagePreviewMaxWidth
|
||||
) { bitmapLoaded ->
|
||||
if (bitmapLoaded == null) {
|
||||
finish()
|
||||
} else {
|
||||
progressView.visibility = View.GONE
|
||||
imageView.setImageBitmap(bitmapLoaded)
|
||||
}
|
||||
BinaryDatabaseManager.loadBitmap(
|
||||
database,
|
||||
attachment.binaryData,
|
||||
mImagePreviewMaxWidth
|
||||
) { bitmapLoaded ->
|
||||
if (bitmapLoaded == null) {
|
||||
finish()
|
||||
} else {
|
||||
progressView.visibility = View.GONE
|
||||
imageView.setImageBitmap(bitmapLoaded)
|
||||
}
|
||||
}
|
||||
} ?: finish()
|
||||
|
||||
@@ -28,6 +28,8 @@ class KeyGeneratorActivity : DatabaseLockActivity() {
|
||||
private lateinit var validationButton: View
|
||||
private var lockView: View? = null
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
private val keyGeneratorViewModel: KeyGeneratorViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -36,8 +36,6 @@ import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
@@ -49,16 +47,15 @@ import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||
import com.kunzisoft.keepass.biometric.deviceUnlockError
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||
@@ -127,10 +124,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
private var mReadOnly: 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 manageDatabaseInfo(): Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -313,20 +307,18 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
if (database != null) {
|
||||
// Trying to load another database
|
||||
if (mDatabaseFileUri != null
|
||||
&& database.fileUri != null
|
||||
&& mDatabaseFileUri != database.fileUri) {
|
||||
Toast.makeText(this,
|
||||
R.string.warning_database_already_opened,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
launchGroupActivityIfLoaded(database)
|
||||
// Trying to load another database
|
||||
if (mDatabaseFileUri != null
|
||||
&& database.fileUri != null
|
||||
&& mDatabaseFileUri != database.fileUri) {
|
||||
Toast.makeText(this,
|
||||
R.string.warning_database_already_opened,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
@@ -433,7 +425,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
{ onValidateSpecialMode() },
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() },
|
||||
mAutofillActivityResultLauncher
|
||||
mCredentialActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -511,10 +503,11 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
val password = intent.getStringExtra(KEY_PASSWORD)
|
||||
// Consume the intent extra password
|
||||
intent.removeExtra(KEY_PASSWORD)
|
||||
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
||||
if (password != null) {
|
||||
mainCredentialView?.populatePasswordTextView(password)
|
||||
}
|
||||
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
||||
intent.removeExtra(KEY_LAUNCH_IMMEDIATELY)
|
||||
if (launchImmediately) {
|
||||
loadDatabase()
|
||||
} else {
|
||||
@@ -569,13 +562,10 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
clearCredentialsViews()
|
||||
}
|
||||
|
||||
if (mReadOnly && (
|
||||
mSpecialMode == SpecialMode.SAVE
|
||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||
) {
|
||||
Log.e(TAG, getString(R.string.autofill_read_only_save))
|
||||
if (mReadOnly && mSpecialMode == SpecialMode.REGISTRATION) {
|
||||
Log.e(TAG, getString(R.string.error_save_read_only))
|
||||
Snackbar.make(coordinatorLayout,
|
||||
R.string.autofill_read_only_save,
|
||||
R.string.error_save_read_only,
|
||||
Snackbar.LENGTH_LONG).asError().show()
|
||||
} else {
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
@@ -596,7 +586,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
readOnly: Boolean,
|
||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
fixDuplicateUUID: Boolean) {
|
||||
loadDatabase(
|
||||
mDatabaseViewModel.loadDatabase(
|
||||
databaseUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
@@ -749,11 +739,13 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
private const val KEY_PASSWORD = "password"
|
||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||
|
||||
private fun buildAndLaunchIntent(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
private fun buildAndLaunchIntent(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
intentBuildLauncher: (Intent) -> Unit
|
||||
) {
|
||||
val intent = Intent(activity, MainCredentialActivity::class.java)
|
||||
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||
if (keyFile != null)
|
||||
@@ -770,10 +762,12 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launch(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?) {
|
||||
fun launch(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?
|
||||
) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
@@ -786,185 +780,73 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForSearchResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo) {
|
||||
fun launchForSearchResult(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo
|
||||
) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||
activity,
|
||||
intent,
|
||||
searchInfo)
|
||||
context = activity,
|
||||
intent = intent,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Save Launch
|
||||
* Selection Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForSaveResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo) {
|
||||
fun launchForSelection(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
typeMode: TypeMode,
|
||||
searchInfo: SearchInfo?,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?
|
||||
) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
||||
activity,
|
||||
intent,
|
||||
searchInfo)
|
||||
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||
context = activity,
|
||||
intent = intent,
|
||||
typeMode = typeMode,
|
||||
searchInfo = searchInfo,
|
||||
activityResultLauncher = activityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Keyboard Launch
|
||||
* Registration Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForKeyboardResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
||||
activity,
|
||||
intent,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Autofill Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
activityResultLauncher: ActivityResultLauncher<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?) {
|
||||
fun launchForRegistration(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
typeMode: TypeMode,
|
||||
registerInfo: RegisterInfo?,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?
|
||||
) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
activity,
|
||||
intent,
|
||||
registerInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Global Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launch(activity: AppCompatActivity,
|
||||
databaseUri: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||
onCancelSpecialMode: () -> Unit,
|
||||
onLaunchActivitySpecialMode: () -> Unit,
|
||||
autofillActivityResultLauncher: ActivityResultLauncher<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()
|
||||
}
|
||||
context = activity,
|
||||
intent = intent,
|
||||
typeMode = typeMode,
|
||||
registerInfo = registerInfo,
|
||||
activityResultLauncher = activityResultLauncher,
|
||||
)
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
||||
}
|
||||
builder.setMessage(stringBuilder)
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
actionDatabaseListener?.validateDatabaseChanged()
|
||||
actionDatabaseListener?.onDatabaseChangeValidated()
|
||||
}
|
||||
return builder.create()
|
||||
}
|
||||
@@ -76,7 +76,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
||||
}
|
||||
|
||||
interface ActionDatabaseChangedListener {
|
||||
fun validateDatabaseChanged()
|
||||
fun onDatabaseChangeValidated()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -86,9 +86,10 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
||||
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
|
||||
private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE"
|
||||
|
||||
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||
readOnly: Boolean
|
||||
fun getInstance(
|
||||
oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||
readOnly: Boolean
|
||||
)
|
||||
: DatabaseChangedDialogFragment {
|
||||
val fragment = DatabaseChangedDialogFragment()
|
||||
|
||||
@@ -5,6 +5,9 @@ import android.view.View
|
||||
import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
@@ -12,23 +15,40 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
||||
|
||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
private var mDatabase: ContextualDatabase? = null
|
||||
private val mDatabase: ContextualDatabase?
|
||||
get() = mDatabaseViewModel.database
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mDatabaseViewModel.database.observe(this) { database ->
|
||||
this.mDatabase = database
|
||||
resetAppTimeoutOnTouchOrFocus()
|
||||
onDatabaseRetrieved(database)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mDatabaseViewModel.actionState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||
onDatabaseActionFinished(
|
||||
uiState.database,
|
||||
uiState.actionTask,
|
||||
uiState.result
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mDatabaseViewModel.actionFinished.observe(this) { result ->
|
||||
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mDatabaseViewModel.databaseState.collect { database ->
|
||||
database?.let {
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,13 +65,14 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated(message = "")
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
resetAppTimeoutOnTouchOrFocus()
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
// Can be overridden by a subclass
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.model.GroupInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.view.DateTimeFieldView
|
||||
|
||||
@@ -62,14 +62,14 @@ class GroupDialogFragment : DatabaseDialogFragment() {
|
||||
private lateinit var uuidContainerView: ViewGroup
|
||||
private lateinit var uuidReferenceView: TextView
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
mPopulateIconMethod = { imageView, icon ->
|
||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
}
|
||||
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
|
||||
|
||||
if (database?.allowCustomSearchableGroup() == true) {
|
||||
if (database.allowCustomSearchableGroup()) {
|
||||
searchableLabelView.visibility = View.VISIBLE
|
||||
searchableView.visibility = View.VISIBLE
|
||||
} else {
|
||||
@@ -155,7 +155,7 @@ class GroupDialogFragment : DatabaseDialogFragment() {
|
||||
searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable)
|
||||
autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType,
|
||||
mGroupInfo.defaultAutoTypeSequence)
|
||||
val uuid = UuidUtil.toHexString(mGroupInfo.id)
|
||||
val uuid = mGroupInfo.id?.asHexString()
|
||||
if (uuid == null || uuid.isEmpty()) {
|
||||
uuidContainerView.visibility = View.GONE
|
||||
} else {
|
||||
|
||||
@@ -112,32 +112,32 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
mPopulateIconMethod = { imageView, icon ->
|
||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
}
|
||||
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||
|
||||
searchableContainerView.visibility = if (database?.allowCustomSearchableGroup() == true) {
|
||||
searchableContainerView.visibility = if (database.allowCustomSearchableGroup()) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
if (database?.allowAutoType() == true) {
|
||||
if (database.allowAutoType()) {
|
||||
autoTypeContainerView.visibility = View.VISIBLE
|
||||
} else {
|
||||
autoTypeContainerView.visibility = View.GONE
|
||||
}
|
||||
|
||||
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
|
||||
tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
|
||||
tagsCompletionView.apply {
|
||||
threshold = 1
|
||||
setAdapter(tagsAdapter)
|
||||
}
|
||||
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
|
||||
tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
@@ -45,10 +45,10 @@ class IconEditDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mCustomIcon: IconImageCustom? = null
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
mPopulateIconMethod = { imageView, icon ->
|
||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
|
||||
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon)
|
||||
}
|
||||
mCustomIcon?.let { customIcon ->
|
||||
populateViewsWithCustomIcon(customIcon)
|
||||
|
||||
@@ -35,9 +35,9 @@ import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
|
||||
import com.kunzisoft.keepass.password.PasswordEntropy
|
||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||
@@ -258,8 +258,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
showEmptyPasswordConfirmationDialog()
|
||||
} else if (!error
|
||||
&& hardwareKey != null
|
||||
&& !HardwareKeyActivity.isHardwareKeyAvailable(
|
||||
requireActivity(), hardwareKey, false)
|
||||
&& !HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)
|
||||
) {
|
||||
// show hardware driver dialog if required
|
||||
error = true
|
||||
|
||||
@@ -29,7 +29,11 @@ import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.*
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
@@ -40,15 +44,15 @@ import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_OTP_DIGITS
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_TOTP_PERIOD
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_HOTP_COUNTER
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_DIGITS
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_SECRET
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
|
||||
import com.kunzisoft.keepass.otp.OtpTokenType
|
||||
import com.kunzisoft.keepass.otp.OtpType
|
||||
import com.kunzisoft.keepass.otp.TokenCalculator
|
||||
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
||||
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
class SetOTPDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
|
||||
@@ -4,36 +4,59 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
|
||||
|
||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
protected var mDatabase: ContextualDatabase? = null
|
||||
protected val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
protected val mDatabase: ContextualDatabase?
|
||||
get() = mDatabaseViewModel.database
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
|
||||
if (mDatabase == null || mDatabase != database) {
|
||||
this.mDatabase = database
|
||||
onDatabaseRetrieved(database)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mDatabaseViewModel.actionState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||
onDatabaseActionFinished(
|
||||
uiState.database,
|
||||
uiState.actionTask,
|
||||
uiState.result
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result ->
|
||||
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mDatabaseViewModel.databaseState.collect { database ->
|
||||
database?.let {
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
|
||||
context?.let {
|
||||
view?.resetAppTimeoutWhenViewTouchedOrFocused(it, mDatabase?.loaded)
|
||||
view?.resetAppTimeoutWhenViewTouchedOrFocused(
|
||||
context = it,
|
||||
databaseLoaded = mDatabase?.loaded
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +67,4 @@ abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
|
||||
) {
|
||||
// Can be overridden by a subclass
|
||||
}
|
||||
|
||||
protected fun buildNewBinaryAttachment(): BinaryData? {
|
||||
return mDatabase?.buildNewBinaryAttachment()
|
||||
}
|
||||
}
|
||||
@@ -230,7 +230,7 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
val attachmentToUploadUri = it.attachmentToUploadUri
|
||||
val fileName = it.fileName
|
||||
|
||||
buildNewBinaryAttachment()?.let { binaryAttachment ->
|
||||
mDatabaseViewModel.buildNewAttachment()?.let { binaryAttachment ->
|
||||
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||
// Ask to replace the current attachment
|
||||
if ((!mAllowMultipleAttachments
|
||||
@@ -273,13 +273,13 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
|
||||
templateView.populateIconMethod = { imageView, icon ->
|
||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
}
|
||||
|
||||
mAllowMultipleAttachments = database?.allowMultipleAttachments == true
|
||||
mAllowMultipleAttachments = database.allowMultipleAttachments == true
|
||||
|
||||
attachmentsAdapter?.database = database
|
||||
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
|
||||
@@ -290,12 +290,12 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
|
||||
tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
|
||||
tagsCompletionView.apply {
|
||||
threshold = 1
|
||||
setAdapter(tagsAdapter)
|
||||
}
|
||||
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
|
||||
tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
||||
|
||||
@@ -24,7 +24,7 @@ import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||
import com.kunzisoft.keepass.view.TemplateView
|
||||
import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.showByFading
|
||||
@@ -133,7 +133,7 @@ class EntryFragment: DatabaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
context?.let { context ->
|
||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
||||
attachmentsAdapter?.database = database
|
||||
@@ -184,7 +184,7 @@ class EntryFragment: DatabaseFragment() {
|
||||
// customDataView.text = entryInfo?.customData?.toString()
|
||||
|
||||
// Assign special data
|
||||
uuidReferenceView.text = UuidUtil.toHexString(entryInfo?.id)
|
||||
uuidReferenceView.text = entryInfo?.id?.asHexString()
|
||||
}
|
||||
|
||||
private fun showClipboardDialog() {
|
||||
|
||||
@@ -36,9 +36,9 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.adapters.NodesAdapter
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||
@@ -154,46 +154,44 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
context?.let { context ->
|
||||
database?.let { database ->
|
||||
mAdapter = NodesAdapter(context, database).apply {
|
||||
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
||||
override fun onNodeClick(database: ContextualDatabase, node: Node) {
|
||||
if (nodeActionSelectionMode) {
|
||||
if (listActionNodes.contains(node)) {
|
||||
// Remove selected item if already selected
|
||||
listActionNodes.remove(node)
|
||||
} else {
|
||||
// Add selected item if not already selected
|
||||
listActionNodes.add(node)
|
||||
}
|
||||
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
||||
setActionNodes(listActionNodes)
|
||||
notifyNodeChanged(node)
|
||||
mAdapter = NodesAdapter(context, database).apply {
|
||||
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
||||
override fun onNodeClick(database: ContextualDatabase, node: Node) {
|
||||
if (nodeActionSelectionMode) {
|
||||
if (listActionNodes.contains(node)) {
|
||||
// Remove selected item if already selected
|
||||
listActionNodes.remove(node)
|
||||
} else {
|
||||
nodeClickListener?.onNodeClick(database, node)
|
||||
// Add selected item if not already selected
|
||||
listActionNodes.add(node)
|
||||
}
|
||||
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
||||
setActionNodes(listActionNodes)
|
||||
notifyNodeChanged(node)
|
||||
} else {
|
||||
nodeClickListener?.onNodeClick(database, node)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean {
|
||||
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
||||
// Select the first item after a long click
|
||||
if (!listActionNodes.contains(node))
|
||||
listActionNodes.add(node)
|
||||
override fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean {
|
||||
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
||||
// Select the first item after a long click
|
||||
if (!listActionNodes.contains(node))
|
||||
listActionNodes.add(node)
|
||||
|
||||
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
||||
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
||||
|
||||
setActionNodes(listActionNodes)
|
||||
notifyNodeChanged(node)
|
||||
activity?.hideKeyboard()
|
||||
}
|
||||
return true
|
||||
setActionNodes(listActionNodes)
|
||||
notifyNodeChanged(node)
|
||||
activity?.hideKeyboard()
|
||||
}
|
||||
})
|
||||
}
|
||||
mNodesRecyclerView?.adapter = mAdapter
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
mNodesRecyclerView?.adapter = mAdapter
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +246,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
|
||||
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
|
||||
activity?.intent?.let {
|
||||
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
|
||||
specialMode = it.retrieveSpecialMode()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,9 +297,9 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
}
|
||||
}
|
||||
|
||||
private fun containsRecycleBin(nodes: List<Node>): Boolean {
|
||||
return mDatabase?.isRecycleBinEnabled == true
|
||||
&& nodes.any { it == mDatabase?.recycleBin }
|
||||
private fun containsRecycleBin(database: ContextualDatabase?, nodes: List<Node>): Boolean {
|
||||
return database?.isRecycleBinEnabled == true
|
||||
&& nodes.any { it == database.recycleBin }
|
||||
}
|
||||
|
||||
fun actionNodesCallback(database: ContextualDatabase,
|
||||
@@ -328,7 +326,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
// Open and Edit for a single item
|
||||
if (nodes.size == 1) {
|
||||
// Edition
|
||||
if (database.isReadOnly || containsRecycleBin(nodes)) {
|
||||
if (database.isReadOnly || containsRecycleBin(database, nodes)) {
|
||||
menu?.removeItem(R.id.menu_edit)
|
||||
}
|
||||
} else {
|
||||
@@ -348,7 +346,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
}
|
||||
|
||||
// Deletion
|
||||
if (database.isReadOnly || containsRecycleBin(nodes)) {
|
||||
if (database.isReadOnly || containsRecycleBin(database, nodes)) {
|
||||
menu?.removeItem(R.id.menu_delete)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +71,8 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
iconPickerAdapter.iconDrawableFactory = database.iconDrawableFactory
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val populateList = launch {
|
||||
|
||||
@@ -48,9 +48,9 @@ class IconPickerFragment : DatabaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
||||
if (database?.allowCustomIcons == true) 2 else 1)
|
||||
if (database.allowCustomIcons) 2 else 1)
|
||||
viewPager.adapter = iconPickerPagerAdapter
|
||||
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
||||
tab.text = when (position) {
|
||||
|
||||
@@ -107,7 +107,7 @@ class KeyGeneratorFragment : DatabaseFragment() {
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
// Nothing here
|
||||
}
|
||||
|
||||
|
||||
@@ -244,7 +244,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
// Nothing here
|
||||
}
|
||||
|
||||
|
||||
@@ -293,20 +293,22 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
||||
private fun generatePassword() {
|
||||
var password = ""
|
||||
try {
|
||||
password = PasswordGenerator(resources).generatePassword(getPasswordLength(),
|
||||
uppercaseCompound.isChecked,
|
||||
lowercaseCompound.isChecked,
|
||||
digitsCompound.isChecked,
|
||||
minusCompound.isChecked,
|
||||
underlineCompound.isChecked,
|
||||
spaceCompound.isChecked,
|
||||
specialsCompound.isChecked,
|
||||
bracketsCompound.isChecked,
|
||||
extendedCompound.isChecked,
|
||||
getConsiderChars(),
|
||||
getIgnoreChars(),
|
||||
atLeastOneCompound.isChecked,
|
||||
excludeAmbiguousCompound.isChecked)
|
||||
password = PasswordGenerator(resources).generatePassword(
|
||||
length = getPasswordLength(),
|
||||
upperCase = uppercaseCompound.isChecked,
|
||||
lowerCase = lowercaseCompound.isChecked,
|
||||
digits = digitsCompound.isChecked,
|
||||
minus = minusCompound.isChecked,
|
||||
underline = underlineCompound.isChecked,
|
||||
space = spaceCompound.isChecked,
|
||||
specials = specialsCompound.isChecked,
|
||||
brackets = bracketsCompound.isChecked,
|
||||
extended = extendedCompound.isChecked,
|
||||
considerChars = getConsiderChars(),
|
||||
ignoreChars = getIgnoreChars(),
|
||||
atLeastOneFromEach = atLeastOneCompound.isChecked,
|
||||
excludeAmbiguousChar = excludeAmbiguousCompound.isChecked
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to generate a password", e)
|
||||
}
|
||||
@@ -318,7 +320,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
// Nothing here
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package com.kunzisoft.keepass.activities.helpers
|
||||
|
||||
enum class TypeMode {
|
||||
DEFAULT, MAGIKEYBOARD, AUTOFILL
|
||||
}
|
||||
@@ -1,96 +1,307 @@
|
||||
package com.kunzisoft.keepass.activities.legacy
|
||||
|
||||
import android.net.Uri
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
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.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider.Companion.startDatabaseService
|
||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.getBinaryDir
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||
abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
|
||||
|
||||
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
|
||||
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||
protected var mDatabase: ContextualDatabase? = null
|
||||
protected val mDatabase: ContextualDatabase?
|
||||
get() = mDatabaseViewModel.database
|
||||
|
||||
private val progressTaskViewModel: ProgressTaskViewModel by viewModels()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Useful to only waiting for the activity result and prevent any parallel action
|
||||
*/
|
||||
var credentialResultLaunched = false
|
||||
|
||||
/**
|
||||
* Utility activity result launcher,
|
||||
* Used recursively, close each activity with return data
|
||||
*/
|
||||
protected var mCredentialActivityResultLauncher: CredentialActivityResultLauncher =
|
||||
CredentialActivityResultLauncher(
|
||||
registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
setActivityResult(
|
||||
lockDatabase = false,
|
||||
resultCode = it.resultCode,
|
||||
data = it.data
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Custom ActivityResultLauncher to manage the database action
|
||||
*/
|
||||
protected inner class CredentialActivityResultLauncher(
|
||||
val builder: ActivityResultLauncher<Intent>
|
||||
) : ActivityResultLauncher<Intent>() {
|
||||
|
||||
override fun launch(
|
||||
input: Intent?,
|
||||
options: ActivityOptionsCompat?
|
||||
) {
|
||||
credentialResultLaunched = true
|
||||
builder.launch(input, options)
|
||||
}
|
||||
|
||||
override fun unregister() {
|
||||
builder.unregister()
|
||||
}
|
||||
|
||||
override fun getContract(): ActivityResultContract<Intent?, *> {
|
||||
return builder.getContract()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mDatabaseTaskProvider = DatabaseTaskProvider(this, showDatabaseDialog())
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(CREDENTIAL_RESULT_LAUNCHER_KEY)
|
||||
) {
|
||||
credentialResultLaunched = savedInstanceState.getBoolean(CREDENTIAL_RESULT_LAUNCHER_KEY)
|
||||
}
|
||||
|
||||
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||
val databaseWasReloaded = database?.wasReloaded == true
|
||||
if (databaseWasReloaded && finishActivityIfReloadRequested()) {
|
||||
finish()
|
||||
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) {
|
||||
database?.wasReloaded = false
|
||||
onDatabaseRetrieved(database)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mDatabaseViewModel.actionState.collect { uiState ->
|
||||
if (credentialResultLaunched.not()) {
|
||||
when (uiState) {
|
||||
is DatabaseViewModel.ActionState.Wait -> {}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseReloaded -> {
|
||||
if (finishActivityIfReloadRequested()) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseInfoChanged -> {
|
||||
if (manageDatabaseInfo()) {
|
||||
showDatabaseChangedDialog(
|
||||
uiState.previousDatabaseInfo,
|
||||
uiState.newDatabaseInfo,
|
||||
uiState.readOnlyDatabase
|
||||
)
|
||||
}
|
||||
}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionRequested -> {
|
||||
startDatabasePermissionService(
|
||||
uiState.bundle,
|
||||
uiState.actionTask
|
||||
)
|
||||
}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionStarted -> {
|
||||
progressTaskViewModel.show(uiState.progressMessage)
|
||||
}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionUpdated -> {
|
||||
progressTaskViewModel.show(uiState.progressMessage)
|
||||
}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionStopped -> {
|
||||
progressTaskViewModel.hide()
|
||||
}
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||
onDatabaseActionFinished(
|
||||
uiState.database,
|
||||
uiState.actionTask,
|
||||
uiState.result
|
||||
)
|
||||
progressTaskViewModel.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mDatabaseTaskProvider?.onActionFinish = { database, actionTask, result ->
|
||||
onDatabaseActionFinished(database, actionTask, result)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
progressTaskViewModel.progressTaskState.collect { state ->
|
||||
when (state) {
|
||||
is ProgressTaskViewModel.ProgressTaskState.Show ->
|
||||
startDialog()
|
||||
is ProgressTaskViewModel.ProgressTaskState.Hide ->
|
||||
stopDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mDatabaseViewModel.databaseState.collect { database ->
|
||||
if (credentialResultLaunched.not()) {
|
||||
// Nullable function
|
||||
onUnknownDatabaseRetrieved(database)
|
||||
database?.let {
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun showDatabaseDialog(): Boolean {
|
||||
return true
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(CREDENTIAL_RESULT_LAUNCHER_KEY, credentialResultLaunched)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mDatabaseTaskProvider?.destroy()
|
||||
mDatabaseTaskProvider = null
|
||||
mDatabase = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
mDatabase = database
|
||||
mDatabaseViewModel.defineDatabase(database)
|
||||
/**
|
||||
* Nullable function to retrieve a database
|
||||
*/
|
||||
open fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
// optional method implementation
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
// optional method implementation
|
||||
}
|
||||
|
||||
open fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
mDatabaseViewModel.onActionFinished(database, actionTask, result)
|
||||
// optional method implementation
|
||||
}
|
||||
|
||||
fun createDatabase(
|
||||
databaseUri: Uri,
|
||||
mainCredential: MainCredential
|
||||
private fun startDatabasePermissionService(bundle: Bundle?, actionTask: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||
== PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
startDatabaseService(bundle, actionTask)
|
||||
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
|
||||
this,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
) {
|
||||
// it's not the first time, so the user deliberately chooses not to display the notification
|
||||
startDatabaseService(bundle, actionTask)
|
||||
} else {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.warning_database_notification_permission)
|
||||
.setNegativeButton(R.string.later) { _, _ ->
|
||||
// Refuses the notification, so start the service
|
||||
startDatabaseService(bundle, actionTask)
|
||||
}
|
||||
.setPositiveButton(R.string.ask) { _, _ ->
|
||||
// Save the temp parameters to ask the permission
|
||||
tempServiceParameters.add(Pair(bundle, actionTask))
|
||||
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}.create().show()
|
||||
}
|
||||
} else {
|
||||
startDatabaseService(bundle, actionTask)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDatabaseChangedDialog(
|
||||
previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||
newDatabaseInfo: SnapFileDatabaseInfo,
|
||||
readOnlyDatabase: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential)
|
||||
lifecycleScope.launch {
|
||||
if (databaseChangedDialogFragment == null) {
|
||||
databaseChangedDialogFragment = supportFragmentManager
|
||||
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
|
||||
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||
mActionDatabaseListener
|
||||
}
|
||||
if (progressTaskDialogFragment == null) {
|
||||
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
|
||||
previousDatabaseInfo,
|
||||
newDatabaseInfo,
|
||||
readOnlyDatabase
|
||||
)
|
||||
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||
mActionDatabaseListener
|
||||
databaseChangedDialogFragment?.show(
|
||||
supportFragmentManager,
|
||||
DATABASE_CHANGED_DIALOG_TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadDatabase(
|
||||
databaseUri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
readOnly: Boolean,
|
||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
fixDuplicateUuid: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
|
||||
private fun startDialog() {
|
||||
lifecycleScope.launch {
|
||||
if (showDatabaseDialog()) {
|
||||
if (progressTaskDialogFragment == null) {
|
||||
progressTaskDialogFragment = supportFragmentManager
|
||||
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
|
||||
}
|
||||
if (progressTaskDialogFragment == null) {
|
||||
progressTaskDialogFragment = ProgressTaskDialogFragment()
|
||||
progressTaskDialogFragment?.show(
|
||||
supportFragmentManager,
|
||||
PROGRESS_TASK_DIALOG_TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun closeDatabase() {
|
||||
mDatabase?.clearAndClose(this.getBinaryDir())
|
||||
private fun stopDialog() {
|
||||
progressTaskDialogFragment?.dismissAllowingStateLoss()
|
||||
progressTaskDialogFragment = null
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
mDatabaseTaskProvider?.registerProgressTask()
|
||||
protected open fun showDatabaseDialog(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
mDatabaseTaskProvider?.unregisterProgressTask()
|
||||
super.onPause()
|
||||
companion object {
|
||||
const val CREDENTIAL_RESULT_LAUNCHER_KEY = "com.kunzisoft.keepass.CREDENTIAL_RESULT_LAUNCHER_KEY"
|
||||
}
|
||||
}
|
||||
@@ -34,8 +34,8 @@ import androidx.appcompat.app.AlertDialog
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
@@ -87,128 +87,44 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
deleteDatabaseNodes(nodes)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveDatabase.observe(this) { save ->
|
||||
mDatabaseTaskProvider?.startDatabaseSave(save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.mergeDatabase.observe(this) { save ->
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
||||
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
||||
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
|
||||
}
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveName.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveDescription.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveDescription(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveDefaultUsername.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveColor.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveColor(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveCompression.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveCompression(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.removeUnlinkData.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseRemoveUnlinkedData(it)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveRecycleBin.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveRecycleBin(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveTemplatesGroup.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveTemplatesGroup(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveMaxHistoryItems.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveMaxHistoryItems(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveMaxHistorySize.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveEncryption.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveEncryption(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveKeyDerivation.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveKeyDerivation(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveIterations.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveIterations(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveMemoryUsage.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveParallelism.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveParallelism(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mExitLock = false
|
||||
}
|
||||
|
||||
open fun finishActivityIfDatabaseNotLoaded(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
// End activity if database not loaded
|
||||
if (finishActivityIfDatabaseNotLoaded() && (database == null || !database.loaded)) {
|
||||
if (database.loaded.not())
|
||||
finish()
|
||||
}
|
||||
|
||||
// Focus view to reinitialize timeout,
|
||||
// view is not necessary loaded so retry later in resume
|
||||
viewToInvalidateTimeout()
|
||||
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database?.loaded)
|
||||
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded)
|
||||
|
||||
database?.let {
|
||||
// check timeout
|
||||
if (mTimeoutEnable) {
|
||||
if (mLockReceiver == null) {
|
||||
mLockReceiver = LockReceiver {
|
||||
mDatabase = null
|
||||
closeDatabase(database)
|
||||
mExitLock = true
|
||||
closeOptionsMenu()
|
||||
finish()
|
||||
}
|
||||
registerLockReceiver(mLockReceiver)
|
||||
// check timeout
|
||||
if (mTimeoutEnable) {
|
||||
if (mLockReceiver == null) {
|
||||
mLockReceiver = LockReceiver {
|
||||
closeDatabase(database)
|
||||
mExitLock = true
|
||||
closeOptionsMenu()
|
||||
finish()
|
||||
}
|
||||
|
||||
// After the first creation
|
||||
// or If simply swipe with another application
|
||||
// If the time is out -> close the Activity
|
||||
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||
// If onCreate already record time
|
||||
if (!mExitLock)
|
||||
TimeoutHelper.recordTime(this, database.loaded)
|
||||
registerLockReceiver(mLockReceiver)
|
||||
}
|
||||
|
||||
mDatabaseReadOnly = database.isReadOnly
|
||||
mMergeDataAllowed = database.isMergeDataAllowed()
|
||||
|
||||
checkRegister()
|
||||
// After the first creation
|
||||
// or If simply swipe with another application
|
||||
// If the time is out -> close the Activity
|
||||
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||
// If onCreate already record time
|
||||
if (!mExitLock)
|
||||
TimeoutHelper.recordTime(this, database.loaded)
|
||||
}
|
||||
|
||||
mDatabaseReadOnly = database.isReadOnly
|
||||
mMergeDataAllowed = database.isMergeDataAllowed()
|
||||
|
||||
checkRegister()
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
@@ -227,7 +143,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
when (actionTask) {
|
||||
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
|
||||
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
||||
@@ -249,24 +164,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
databaseUri: Uri?,
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
assignDatabasePassword(databaseUri, mainCredential)
|
||||
mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
|
||||
}
|
||||
|
||||
private fun assignDatabasePassword(
|
||||
databaseUri: Uri?,
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
if (databaseUri != null) {
|
||||
mDatabaseTaskProvider?.startDatabaseAssignCredential(databaseUri, mainCredential)
|
||||
}
|
||||
}
|
||||
|
||||
fun assignPassword(mainCredential: MainCredential) {
|
||||
fun assignMainCredential(mainCredential: MainCredential) {
|
||||
mDatabase?.let { database ->
|
||||
database.fileUri?.let { databaseUri ->
|
||||
// Show the progress dialog now or after dialog confirmation
|
||||
if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) {
|
||||
assignDatabasePassword(databaseUri, mainCredential)
|
||||
mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
|
||||
} else {
|
||||
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
|
||||
.show(supportFragmentManager, "passwordEncodingTag")
|
||||
@@ -276,45 +182,51 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
}
|
||||
|
||||
fun saveDatabase() {
|
||||
mDatabaseTaskProvider?.startDatabaseSave(true)
|
||||
mDatabaseViewModel.saveDatabase(save = true)
|
||||
}
|
||||
|
||||
fun saveDatabaseTo(uri: Uri) {
|
||||
mDatabaseTaskProvider?.startDatabaseSave(true, uri)
|
||||
mDatabaseViewModel.saveDatabase(save = true, saveToUri = uri)
|
||||
}
|
||||
|
||||
fun mergeDatabase() {
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable)
|
||||
mDatabaseViewModel.mergeDatabase(save = mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable, uri, mainCredential)
|
||||
mDatabaseViewModel.mergeDatabase(mAutoSaveEnable, uri, mainCredential)
|
||||
}
|
||||
|
||||
fun reloadDatabase() {
|
||||
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
||||
mDatabaseTaskProvider?.startDatabaseReload(false)
|
||||
}
|
||||
mDatabaseViewModel.reloadDatabase(fixDuplicateUuid = false)
|
||||
}
|
||||
|
||||
fun createEntry(newEntry: Entry,
|
||||
parent: Group) {
|
||||
mDatabaseTaskProvider?.startDatabaseCreateEntry(newEntry, parent, mAutoSaveEnable)
|
||||
fun createEntry(
|
||||
newEntry: Entry,
|
||||
parent: Group
|
||||
) {
|
||||
mDatabaseViewModel.createEntry(newEntry, parent, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun updateEntry(oldEntry: Entry,
|
||||
entryToUpdate: Entry) {
|
||||
mDatabaseTaskProvider?.startDatabaseUpdateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
|
||||
fun updateEntry(
|
||||
oldEntry: Entry,
|
||||
entryToUpdate: Entry
|
||||
) {
|
||||
mDatabaseViewModel.updateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun copyNodes(nodesToCopy: List<Node>,
|
||||
newParent: Group) {
|
||||
mDatabaseTaskProvider?.startDatabaseCopyNodes(nodesToCopy, newParent, mAutoSaveEnable)
|
||||
fun copyNodes(
|
||||
nodesToCopy: List<Node>,
|
||||
newParent: Group
|
||||
) {
|
||||
mDatabaseViewModel.copyNodes(nodesToCopy, newParent, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun moveNodes(nodesToMove: List<Node>,
|
||||
newParent: Group) {
|
||||
mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable)
|
||||
fun moveNodes(
|
||||
nodesToMove: List<Node>,
|
||||
newParent: Group
|
||||
) {
|
||||
mDatabaseViewModel.moveNodes(nodesToMove, newParent, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
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) {
|
||||
// TODO Move in ViewModel
|
||||
mDatabase?.let { database ->
|
||||
// If recycle bin enabled, ensure it exists
|
||||
if (database.isRecycleBinEnabled) {
|
||||
@@ -350,11 +263,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
}
|
||||
|
||||
private fun deleteDatabaseNodes(nodes: List<Node>) {
|
||||
mDatabaseTaskProvider?.startDatabaseDeleteNodes(nodes, mAutoSaveEnable)
|
||||
mDatabaseViewModel.deleteNodes(nodes, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun createGroup(parent: Group,
|
||||
groupInfo: GroupInfo?) {
|
||||
fun createGroup(
|
||||
parent: Group,
|
||||
groupInfo: GroupInfo?
|
||||
) {
|
||||
// TODO Move in ViewModel
|
||||
// Build the group
|
||||
mDatabase?.createGroup()?.let { newGroup ->
|
||||
groupInfo?.let { info ->
|
||||
@@ -362,12 +278,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
}
|
||||
// Not really needed here because added in runnable but safe
|
||||
newGroup.parent = parent
|
||||
mDatabaseTaskProvider?.startDatabaseCreateGroup(newGroup, parent, mAutoSaveEnable)
|
||||
mDatabaseViewModel.createGroup(newGroup, parent, mAutoSaveEnable)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateGroup(oldGroup: Group,
|
||||
groupInfo: GroupInfo) {
|
||||
fun updateGroup(
|
||||
oldGroup: Group,
|
||||
groupInfo: GroupInfo
|
||||
) {
|
||||
// TODO Move in ViewModel
|
||||
// If group updated save it in the database
|
||||
val updateGroup = Group(oldGroup).let { updateGroup ->
|
||||
updateGroup.apply {
|
||||
@@ -377,27 +296,28 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
this.setGroupInfo(groupInfo)
|
||||
}
|
||||
}
|
||||
mDatabaseTaskProvider?.startDatabaseUpdateGroup(oldGroup, updateGroup, mAutoSaveEnable)
|
||||
mDatabaseViewModel.updateGroup(oldGroup, updateGroup, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun restoreEntryHistory(mainEntryId: NodeId<UUID>,
|
||||
entryHistoryPosition: Int) {
|
||||
mDatabaseTaskProvider
|
||||
?.startDatabaseRestoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||
fun restoreEntryHistory(
|
||||
mainEntryId: NodeId<UUID>,
|
||||
entryHistoryPosition: Int
|
||||
) {
|
||||
mDatabaseViewModel.restoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun deleteEntryHistory(mainEntryId: NodeId<UUID>,
|
||||
entryHistoryPosition: Int) {
|
||||
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||
fun deleteEntryHistory(
|
||||
mainEntryId: NodeId<UUID>,
|
||||
entryHistoryPosition: Int
|
||||
) {
|
||||
mDatabaseViewModel.deleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
private fun checkRegister() {
|
||||
// If in ave or registration mode, don't allow read only
|
||||
if ((mSpecialMode == SpecialMode.SAVE
|
||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||
&& mDatabaseReadOnly) {
|
||||
// If in registration mode, don't allow read only
|
||||
if (mSpecialMode == SpecialMode.REGISTRATION && mDatabaseReadOnly) {
|
||||
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
intent.removeModes()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -450,9 +370,11 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.lock) { _, _ ->
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
finish()
|
||||
}.create().show()
|
||||
} else {
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,16 @@ import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.helpers.TypeMode
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveTypeMode
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.ToolbarSpecial
|
||||
@@ -19,7 +26,7 @@ import com.kunzisoft.keepass.view.ToolbarSpecial
|
||||
abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
|
||||
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
||||
private var mTypeMode: TypeMode = TypeMode.DEFAULT
|
||||
protected var mTypeMode: TypeMode = TypeMode.DEFAULT
|
||||
|
||||
private var mToolbarSpecial: ToolbarSpecial? = null
|
||||
|
||||
@@ -42,20 +49,14 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
/**
|
||||
* Intent sender uses special retains data in callback
|
||||
*/
|
||||
private fun isIntentSender(): Boolean {
|
||||
return (mSpecialMode == SpecialMode.SELECTION
|
||||
&& mTypeMode == TypeMode.AUTOFILL)
|
||||
/* TODO Registration callback #765
|
||||
|| (mSpecialMode == SpecialMode.REGISTRATION
|
||||
&& mTypeMode == TypeMode.AUTOFILL
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
*/
|
||||
protected fun isIntentSender(): Boolean {
|
||||
return isIntentSenderMode(mSpecialMode, mTypeMode)
|
||||
}
|
||||
|
||||
fun onLaunchActivitySpecialMode() {
|
||||
if (!isIntentSender()) {
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
intent.removeModes()
|
||||
intent.removeInfo()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -64,8 +65,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
if (isIntentSender()) {
|
||||
super.finish()
|
||||
} else {
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
intent.removeModes()
|
||||
intent.removeInfo()
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
backToTheMainAppAndFinish()
|
||||
}
|
||||
@@ -77,8 +78,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
// To get the app caller, only for IntentSender
|
||||
onRegularBackPressed()
|
||||
} else {
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
intent.removeModes()
|
||||
intent.removeInfo()
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
backToTheMainAppAndFinish()
|
||||
}
|
||||
@@ -109,17 +110,18 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
}
|
||||
})
|
||||
|
||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
||||
mSpecialMode = intent.retrieveSpecialMode()
|
||||
mTypeMode = intent.retrieveTypeMode()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
||||
val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)?.searchInfo
|
||||
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||
mSpecialMode = intent.retrieveSpecialMode()
|
||||
mTypeMode = intent.retrieveTypeMode()
|
||||
val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
|
||||
val searchInfo: SearchInfo? = registerInfo?.searchInfo
|
||||
?: intent.retrieveSearchInfo()
|
||||
|
||||
// To show the selection mode
|
||||
mToolbarSpecial = findViewById(R.id.special_mode_view)
|
||||
@@ -128,26 +130,25 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
val selectionModeStringId = when (mSpecialMode) {
|
||||
SpecialMode.DEFAULT, // Not important because hidden
|
||||
SpecialMode.SEARCH -> R.string.search_mode
|
||||
SpecialMode.SAVE -> R.string.save_mode
|
||||
SpecialMode.SELECTION -> R.string.selection_mode
|
||||
SpecialMode.REGISTRATION -> R.string.registration_mode
|
||||
SpecialMode.REGISTRATION -> R.string.save_mode // Save is registration mode
|
||||
}
|
||||
val typeModeStringId = when (mTypeMode) {
|
||||
TypeMode.DEFAULT, // Not important because hidden
|
||||
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
|
||||
TypeMode.AUTOFILL -> R.string.autofill
|
||||
TypeMode.PASSKEY -> R.string.passkey
|
||||
}
|
||||
title = getString(selectionModeStringId)
|
||||
if (mTypeMode != TypeMode.DEFAULT)
|
||||
title = "$title (${getString(typeModeStringId)})"
|
||||
// Populate subtitle
|
||||
subtitle = searchInfo?.getName(resources)
|
||||
subtitle = registerInfo?.getName(resources) ?: searchInfo?.getName(resources)
|
||||
|
||||
// Show the toolbar or not
|
||||
visible = when (mSpecialMode) {
|
||||
SpecialMode.DEFAULT -> false
|
||||
SpecialMode.SEARCH -> true
|
||||
SpecialMode.SAVE -> true
|
||||
SpecialMode.SELECTION -> true
|
||||
SpecialMode.REGISTRATION -> true
|
||||
}
|
||||
|
||||
@@ -4,8 +4,11 @@ import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
interface DatabaseRetrieval {
|
||||
fun onDatabaseRetrieved(database: ContextualDatabase?)
|
||||
fun onDatabaseActionFinished(database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result)
|
||||
fun onDatabaseRetrieved(database: ContextualDatabase)
|
||||
|
||||
fun onDatabaseActionFinished(
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
)
|
||||
}
|
||||
@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.adapters
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
@@ -418,6 +417,7 @@ class NodesAdapter (
|
||||
}
|
||||
}
|
||||
|
||||
// OTP
|
||||
val otpElement = entry.getOtpElement()
|
||||
holder.otpContainer?.removeCallbacks(holder.otpRunnable)
|
||||
if (otpElement != null
|
||||
@@ -438,7 +438,11 @@ class NodesAdapter (
|
||||
holder.otpContainer?.visibility = View.GONE
|
||||
}
|
||||
holder.attachmentIcon?.visibility =
|
||||
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||
|
||||
// Passkey
|
||||
holder.passkeyIcon?.visibility =
|
||||
if (entry.getPasskey() != null) View.VISIBLE else View.GONE
|
||||
|
||||
// Assign colors
|
||||
assignBackgroundColor(holder.container, entry)
|
||||
@@ -451,6 +455,7 @@ class NodesAdapter (
|
||||
holder.otpToken?.setTextColor(foregroundColor)
|
||||
holder.otpProgress?.setIndicatorColor(foregroundColor)
|
||||
holder.attachmentIcon?.setColorFilter(foregroundColor)
|
||||
holder.passkeyIcon?.setColorFilter(foregroundColor)
|
||||
holder.meta.setTextColor(foregroundColor)
|
||||
iconColor = foregroundColor
|
||||
} else {
|
||||
@@ -459,6 +464,7 @@ class NodesAdapter (
|
||||
holder.otpToken?.setTextColor(mTextColorSecondary)
|
||||
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
|
||||
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
|
||||
holder.passkeyIcon?.setColorFilter(mTextColorSecondary)
|
||||
holder.meta.setTextColor(mTextColor)
|
||||
}
|
||||
} else {
|
||||
@@ -467,6 +473,7 @@ class NodesAdapter (
|
||||
holder.otpToken?.setTextColor(mColorOnSecondary)
|
||||
holder.otpProgress?.setIndicatorColor(mColorOnSecondary)
|
||||
holder.attachmentIcon?.setColorFilter(mColorOnSecondary)
|
||||
holder.passkeyIcon?.setColorFilter(mColorOnSecondary)
|
||||
holder.meta.setTextColor(mColorOnSecondary)
|
||||
}
|
||||
|
||||
@@ -611,6 +618,7 @@ class NodesAdapter (
|
||||
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
|
||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
||||
var passkeyIcon: ImageView? = itemView.findViewById(R.id.node_passkey_icon)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package com.kunzisoft.keepass.autofill
|
||||
|
||||
import android.app.assist.AssistStructure
|
||||
|
||||
data class AutofillComponent(val assistStructure: AssistStructure,
|
||||
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)
|
||||
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
* 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.database.element.node.NodeIdUUID
|
||||
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.getEnum
|
||||
import com.kunzisoft.keepass.utils.getEnumExtra
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import com.kunzisoft.keepass.utils.getParcelableList
|
||||
import com.kunzisoft.keepass.utils.putEnum
|
||||
import com.kunzisoft.keepass.utils.putEnumExtra
|
||||
import com.kunzisoft.keepass.utils.putParcelableList
|
||||
import java.io.IOException
|
||||
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 Bundle.addSearchInfo(searchInfo: SearchInfo?): Bundle {
|
||||
searchInfo?.let {
|
||||
putParcelable(KEY_SEARCH_INFO, it)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun Intent.retrieveSearchInfo(): SearchInfo? {
|
||||
return getParcelableExtraCompat(KEY_SEARCH_INFO)
|
||||
}
|
||||
|
||||
fun Bundle.getSearchInfo(): SearchInfo? {
|
||||
return getParcelableCompat(KEY_SEARCH_INFO)
|
||||
}
|
||||
|
||||
fun Intent.addRegisterInfo(registerInfo: RegisterInfo?): Intent {
|
||||
registerInfo?.let {
|
||||
putExtra(KEY_REGISTER_INFO, it)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun Bundle.addRegisterInfo(registerInfo: RegisterInfo?): Bundle {
|
||||
registerInfo?.let {
|
||||
putParcelable(KEY_REGISTER_INFO, it)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun Intent.retrieveRegisterInfo(): RegisterInfo? {
|
||||
return getParcelableExtraCompat(KEY_REGISTER_INFO)
|
||||
}
|
||||
|
||||
fun Bundle.getRegisterInfo(): RegisterInfo? {
|
||||
return getParcelableCompat(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 Bundle.addSpecialMode(specialMode: SpecialMode): Bundle {
|
||||
this.putEnum(KEY_SPECIAL_MODE, specialMode)
|
||||
return this
|
||||
}
|
||||
|
||||
fun Intent.retrieveSpecialMode(): SpecialMode {
|
||||
return this.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
|
||||
}
|
||||
|
||||
fun Bundle.getSpecialMode(): SpecialMode {
|
||||
return this.getEnum<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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve nodes ids from intent and get the corresponding entry info list in [database]
|
||||
*/
|
||||
fun Intent.retrieveAndRemoveEntries(database: ContextualDatabase): List<EntryInfo> {
|
||||
val nodesIds = retrieveNodesIds()
|
||||
?: throw IOException("NodesIds is null")
|
||||
removeNodesIds()
|
||||
return nodesIds.mapNotNull { nodeId ->
|
||||
database
|
||||
.getEntryById(NodeIdUUID(nodeId))
|
||||
?.getEntryInfo(database)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Intent sender uses special retains data in callback
|
||||
*/
|
||||
fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
|
||||
return (specialMode == SpecialMode.SELECTION
|
||||
&& (typeMode == TypeMode.MAGIKEYBOARD || 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
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
package com.kunzisoft.keepass.activities.helpers
|
||||
package com.kunzisoft.keepass.credentialprovider
|
||||
|
||||
enum class SpecialMode {
|
||||
DEFAULT,
|
||||
SEARCH,
|
||||
SAVE,
|
||||
SELECTION,
|
||||
REGISTRATION;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.kunzisoft.keepass.credentialprovider
|
||||
|
||||
enum class TypeMode {
|
||||
DEFAULT, MAGIKEYBOARD, PASSKEY, AUTOFILL
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* 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.getRegisterInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.getSearchInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.getSpecialMode
|
||||
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.autofill.AutofillHelper.retrieveAutofillComponent
|
||||
import com.kunzisoft.keepass.credentialprovider.viewmodel.AutofillLauncherViewModel
|
||||
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
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 = false
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean = true
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// To apply the bypass https://github.com/Kunzisoft/KeePassDX/issues/2238
|
||||
// before managing intent in super class
|
||||
intent.retrieveSelectionBundle()?.apply {
|
||||
intent.addSpecialMode(getSpecialMode())
|
||||
intent.addSearchInfo(getSearchInfo())
|
||||
intent.addRegisterInfo(getRegisterInfo())
|
||||
intent.addAutofillComponent(retrieveAutofillComponent())
|
||||
}
|
||||
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.ShowAutofillSuggestionMessage -> {
|
||||
showAutofillSuggestionMessage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
// Retrieve the UI
|
||||
autofillLauncherViewModel.credentialUiState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is CredentialLauncherViewModel.CredentialState.Loading -> {}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
|
||||
GroupActivity.launchForSelection(
|
||||
context = this@AutofillLauncherActivity,
|
||||
database = uiState.database,
|
||||
searchInfo = uiState.searchInfo,
|
||||
typeMode = uiState.typeMode,
|
||||
activityResultLauncher = mAutofillSelectionActivityResultLauncher,
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
|
||||
GroupActivity.launchForRegistration(
|
||||
context = this@AutofillLauncherActivity,
|
||||
database = uiState.database,
|
||||
registerInfo = uiState.registerInfo,
|
||||
typeMode = uiState.typeMode,
|
||||
activityResultLauncher = mAutofillRegistrationActivityResultLauncher
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
|
||||
FileDatabaseSelectActivity.launchForSelection(
|
||||
context = this@AutofillLauncherActivity,
|
||||
searchInfo = uiState.searchInfo,
|
||||
typeMode = uiState.typeMode,
|
||||
activityResultLauncher = mAutofillSelectionActivityResultLauncher
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForRegistration -> {
|
||||
FileDatabaseSelectActivity.launchForRegistration(
|
||||
context = this@AutofillLauncherActivity,
|
||||
registerInfo = uiState.registerInfo,
|
||||
typeMode = uiState.typeMode,
|
||||
activityResultLauncher = mAutofillRegistrationActivityResultLauncher,
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
|
||||
setActivityResult(
|
||||
lockDatabase = uiState.lockDatabase,
|
||||
resultCode = uiState.resultCode,
|
||||
data = uiState.data
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.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()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_PENDING_INTENT_BUNDLE = "com.kunzisoft.keepass.extra.BUNDLE"
|
||||
private val TAG = AutofillLauncherActivity::class.java.name
|
||||
|
||||
fun Intent.retrieveSelectionBundle(): Bundle? {
|
||||
return this.getBundleExtra(KEY_PENDING_INTENT_BUNDLE)
|
||||
}
|
||||
|
||||
fun getPendingIntentForSelection(
|
||||
context: Context,
|
||||
searchInfo: SearchInfo? = null,
|
||||
autofillComponent: AutofillComponent
|
||||
): PendingIntent? {
|
||||
try {
|
||||
// Doesn't work with direct extra Parcelable in Android 11 (don't know why?)
|
||||
// https://github.com/Kunzisoft/KeePassDX/issues/2238
|
||||
// Wrap into a bundle to bypass the problem
|
||||
val tempBundle = Bundle().apply {
|
||||
addSpecialMode(SpecialMode.SELECTION)
|
||||
addSearchInfo(searchInfo)
|
||||
addAutofillComponent(autofillComponent)
|
||||
}
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
randomRequestCode(),
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
putExtra(KEY_PENDING_INTENT_BUNDLE, tempBundle)
|
||||
},
|
||||
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 {
|
||||
// Bypass intent issue
|
||||
val tempBundle = Bundle().apply {
|
||||
addSpecialMode(SpecialMode.REGISTRATION)
|
||||
addRegisterInfo(registerInfo)
|
||||
}
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
randomRequestCode(),
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
putExtra(KEY_PENDING_INTENT_BUNDLE, tempBundle)
|
||||
},
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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.content.Context
|
||||
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.lifecycle.lifecycleScope
|
||||
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.addSearchInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
|
||||
import com.kunzisoft.keepass.credentialprovider.viewmodel.EntrySelectionViewModel
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.view.toastError
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Activity to search or select entry in database,
|
||||
* Commonly used with Magikeyboard
|
||||
*/
|
||||
class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||
|
||||
private val entrySelectionViewModel: EntrySelectionViewModel by viewModels()
|
||||
|
||||
private var mEntrySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
entrySelectionViewModel.manageSelectionResult(it)
|
||||
}
|
||||
|
||||
override fun applyCustomStyle() = false
|
||||
|
||||
override fun finishActivityIfReloadRequested() = false
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
entrySelectionViewModel.initialize()
|
||||
lifecycleScope.launch {
|
||||
// Initialize the parameters
|
||||
entrySelectionViewModel.uiState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is EntrySelectionViewModel.UIState.Loading -> {}
|
||||
is EntrySelectionViewModel.UIState.PopulateKeyboard -> {
|
||||
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(
|
||||
context = this@EntrySelectionLauncherActivity,
|
||||
entry = uiState.entryInfo,
|
||||
toast = true
|
||||
)
|
||||
}
|
||||
is EntrySelectionViewModel.UIState.LaunchFileDatabaseSelectForSearch -> {
|
||||
FileDatabaseSelectActivity.launchForSearch(
|
||||
context = this@EntrySelectionLauncherActivity,
|
||||
searchInfo = uiState.searchInfo
|
||||
)
|
||||
finish()
|
||||
}
|
||||
is EntrySelectionViewModel.UIState.LaunchGroupActivityForSearch -> {
|
||||
GroupActivity.launchForSearch(
|
||||
context = this@EntrySelectionLauncherActivity,
|
||||
database = uiState.database,
|
||||
searchInfo = uiState.searchInfo
|
||||
)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
// Retrieve the UI
|
||||
entrySelectionViewModel.credentialUiState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is CredentialLauncherViewModel.CredentialState.Loading -> {}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
|
||||
GroupActivity.launchForSelection(
|
||||
context = this@EntrySelectionLauncherActivity,
|
||||
database = uiState.database,
|
||||
searchInfo = uiState.searchInfo,
|
||||
typeMode = uiState.typeMode,
|
||||
activityResultLauncher = mEntrySelectionActivityResultLauncher
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
|
||||
GroupActivity.launchForRegistration(
|
||||
context = this@EntrySelectionLauncherActivity,
|
||||
database = uiState.database,
|
||||
registerInfo = uiState.registerInfo,
|
||||
typeMode = uiState.typeMode,
|
||||
activityResultLauncher = null // Null to not get any callback
|
||||
)
|
||||
finish()
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
|
||||
FileDatabaseSelectActivity.launchForSelection(
|
||||
context = this@EntrySelectionLauncherActivity,
|
||||
searchInfo = uiState.searchInfo,
|
||||
typeMode = uiState.typeMode,
|
||||
activityResultLauncher = mEntrySelectionActivityResultLauncher
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForRegistration -> {
|
||||
FileDatabaseSelectActivity.launchForRegistration(
|
||||
context = this@EntrySelectionLauncherActivity,
|
||||
registerInfo = uiState.registerInfo,
|
||||
typeMode = uiState.typeMode,
|
||||
activityResultLauncher = null // Null to not get any callback
|
||||
)
|
||||
finish()
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
|
||||
setActivityResult(
|
||||
lockDatabase = uiState.lockDatabase,
|
||||
resultCode = uiState.resultCode,
|
||||
data = uiState.data
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.ShowError -> {
|
||||
toastError(uiState.error)
|
||||
entrySelectionViewModel.cancelResult()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onUnknownDatabaseRetrieved(database)
|
||||
entrySelectionViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun launch(
|
||||
context: Context,
|
||||
searchInfo: SearchInfo? = null
|
||||
) {
|
||||
context.startActivity(Intent(
|
||||
context,
|
||||
EntrySelectionLauncherActivity::class.java
|
||||
).apply {
|
||||
addSearchInfo(searchInfo)
|
||||
// New task needed because don't launch from an Activity context
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.CredentialState.SetActivityResult -> {
|
||||
setActivityResult(
|
||||
lockDatabase = uiState.lockDatabase,
|
||||
resultCode = uiState.resultCode,
|
||||
data = uiState.data
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.CredentialState.Loading -> {}
|
||||
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
|
||||
setActivityResult(
|
||||
lockDatabase = uiState.lockDatabase,
|
||||
resultCode = uiState.resultCode,
|
||||
data = uiState.data
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.ShowError -> {
|
||||
toastError(uiState.error)
|
||||
passkeyLauncherViewModel.cancelResult()
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
|
||||
GroupActivity.launchForSelection(
|
||||
context = this@PasskeyLauncherActivity,
|
||||
database = uiState.database,
|
||||
typeMode = uiState.typeMode,
|
||||
searchInfo = uiState.searchInfo,
|
||||
activityResultLauncher = mPasskeySelectionActivityResultLauncher
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
|
||||
GroupActivity.launchForRegistration(
|
||||
context = this@PasskeyLauncherActivity,
|
||||
database = uiState.database,
|
||||
typeMode = uiState.typeMode,
|
||||
registerInfo = uiState.registerInfo,
|
||||
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
|
||||
FileDatabaseSelectActivity.launchForSelection(
|
||||
context = this@PasskeyLauncherActivity,
|
||||
typeMode = uiState.typeMode,
|
||||
searchInfo = uiState.searchInfo,
|
||||
activityResultLauncher = mPasskeySelectionActivityResultLauncher
|
||||
)
|
||||
}
|
||||
is CredentialLauncherViewModel.CredentialState.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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||
|
||||
import android.app.assist.AssistStructure
|
||||
|
||||
data class AutofillComponent(
|
||||
val assistStructure: AssistStructure,
|
||||
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?
|
||||
)
|
||||
@@ -17,10 +17,9 @@
|
||||
* 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.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Context
|
||||
@@ -28,6 +27,7 @@ import android.content.Intent
|
||||
import android.graphics.BlendMode
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.service.autofill.Dataset
|
||||
import android.service.autofill.Field
|
||||
import android.service.autofill.FillResponse
|
||||
@@ -38,19 +38,14 @@ import android.view.autofill.AutofillId
|
||||
import android.view.autofill.AutofillManager
|
||||
import android.view.autofill.AutofillValue
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.Toast
|
||||
import android.widget.inline.InlinePresentationSpec
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.autofill.inline.UiVersions
|
||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
|
||||
import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
@@ -58,22 +53,63 @@ import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import java.io.IOException
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
object AutofillHelper {
|
||||
|
||||
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
||||
private const val EXTRA_BASE_STRUCTURE = "com.kunzisoft.keepass.autofill.BASE_STRUCTURE"
|
||||
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
||||
|
||||
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
|
||||
intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
||||
fun Intent.addAutofillComponent(autofillComponent: AutofillComponent?): Intent {
|
||||
autofillComponent?.let {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun Intent.retrieveAutofillComponent(): AutofillComponent? {
|
||||
this.getParcelableExtraCompat<AssistStructure>(EXTRA_BASE_STRUCTURE)?.let { assistStructure ->
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
AutofillComponent(assistStructure,
|
||||
intent.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
|
||||
AutofillComponent(
|
||||
assistStructure,
|
||||
this.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
|
||||
} else {
|
||||
AutofillComponent(assistStructure, null)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun Bundle.addAutofillComponent(autofillComponent: AutofillComponent?): Bundle {
|
||||
autofillComponent?.let {
|
||||
this.putParcelable(EXTRA_BASE_STRUCTURE, autofillComponent.assistStructure)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
autofillComponent.compatInlineSuggestionsRequest?.let {
|
||||
this.putParcelable(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun Bundle.retrieveAutofillComponent(): AutofillComponent? {
|
||||
this.getParcelableCompat<AssistStructure>(EXTRA_BASE_STRUCTURE)?.let { assistStructure ->
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
AutofillComponent(
|
||||
assistStructure,
|
||||
this.getParcelableCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
||||
)
|
||||
} else {
|
||||
AutofillComponent(assistStructure, null)
|
||||
}
|
||||
@@ -132,11 +168,13 @@ object AutofillHelper {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun buildDatasetForEntry(context: Context,
|
||||
database: ContextualDatabase,
|
||||
entryInfo: EntryInfo,
|
||||
struct: StructureParser.Result,
|
||||
inlinePresentation: InlinePresentation?): Dataset {
|
||||
private fun buildDatasetForEntry(
|
||||
context: Context,
|
||||
database: ContextualDatabase,
|
||||
entryInfo: EntryInfo,
|
||||
struct: StructureParser.Result,
|
||||
inlinePresentation: InlinePresentation?
|
||||
): Dataset {
|
||||
val remoteViews: RemoteViews = newRemoteViews(context, database, makeEntryTitle(entryInfo), entryInfo.icon)
|
||||
|
||||
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
@@ -263,7 +301,7 @@ object AutofillHelper {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (field in entryInfo.customFields) {
|
||||
for (field in entryInfo.getCustomFieldsForFilling()) {
|
||||
if (field.name == TemplateField.LABEL_HOLDER) {
|
||||
struct.creditCardHolderId?.let { ccNameId ->
|
||||
datasetBuilder.addValueToDatasetBuilder(
|
||||
@@ -294,30 +332,15 @@ object AutofillHelper {
|
||||
return dataset
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to assign a drawable to a new icon from a database icon
|
||||
*/
|
||||
private fun buildIconFromEntry(context: Context,
|
||||
database: ContextualDatabase,
|
||||
entryInfo: EntryInfo): Icon? {
|
||||
try {
|
||||
database.iconDrawableFactory.getBitmapFromIcon(context,
|
||||
entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
||||
return Icon.createWithBitmap(bitmap)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun buildInlinePresentationForEntry(context: Context,
|
||||
database: ContextualDatabase,
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
||||
positionItem: Int,
|
||||
entryInfo: EntryInfo): InlinePresentation? {
|
||||
private fun buildInlinePresentationForEntry(
|
||||
context: Context,
|
||||
database: ContextualDatabase,
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
||||
positionItem: Int,
|
||||
entryInfo: EntryInfo
|
||||
): InlinePresentation? {
|
||||
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||
@@ -336,13 +359,9 @@ object AutofillHelper {
|
||||
// Build the content for IME UI
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
randomRequestCode(),
|
||||
Intent(context, AutofillSettingsActivity::class.java),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
return InlinePresentation(
|
||||
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
|
||||
@@ -353,7 +372,7 @@ object AutofillHelper {
|
||||
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
|
||||
entryInfo.buildIcon(context, database)?.let { icon ->
|
||||
setEndIcon(icon.apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
@@ -367,9 +386,11 @@ object AutofillHelper {
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun buildInlinePresentationForManualSelection(context: Context,
|
||||
inlinePresentationSpec: InlinePresentationSpec,
|
||||
pendingIntent: PendingIntent): InlinePresentation? {
|
||||
private fun buildInlinePresentationForManualSelection(
|
||||
context: Context,
|
||||
inlinePresentationSpec: InlinePresentationSpec,
|
||||
pendingIntent: PendingIntent
|
||||
): InlinePresentation? {
|
||||
// Make sure that the IME spec claims support for v1 UI template.
|
||||
val imeStyle = inlinePresentationSpec.style
|
||||
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
|
||||
@@ -386,11 +407,13 @@ object AutofillHelper {
|
||||
}.build().slice, inlinePresentationSpec, false)
|
||||
}
|
||||
|
||||
fun buildResponse(context: Context,
|
||||
database: ContextualDatabase,
|
||||
entriesInfo: List<EntryInfo>,
|
||||
parseResult: StructureParser.Result,
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
|
||||
fun buildResponse(
|
||||
context: Context,
|
||||
database: ContextualDatabase,
|
||||
entriesInfo: List<EntryInfo>,
|
||||
parseResult: StructureParser.Result,
|
||||
autofillComponent: AutofillComponent
|
||||
): FillResponse? {
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
// Add Header
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
@@ -411,7 +434,8 @@ object AutofillHelper {
|
||||
// Add inline suggestion for new IME and dataset
|
||||
var numberInlineSuggestions = 0
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
autofillComponent.compatInlineSuggestionsRequest
|
||||
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
|
||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
|
||||
@@ -427,21 +451,27 @@ object AutofillHelper {
|
||||
var inlinePresentation: InlinePresentation? = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& numberInlineSuggestions > 0
|
||||
&& compatInlineSuggestionsRequest != null) {
|
||||
&& autofillComponent.compatInlineSuggestionsRequest != null) {
|
||||
inlinePresentation = buildInlinePresentationForEntry(
|
||||
context,
|
||||
database,
|
||||
compatInlineSuggestionsRequest,
|
||||
autofillComponent.compatInlineSuggestionsRequest,
|
||||
numberInlineSuggestions--,
|
||||
entry
|
||||
)
|
||||
}
|
||||
// Create dataset for each entry
|
||||
responseBuilder.addDataset(
|
||||
buildDatasetForEntry(context, database, entry, parseResult, inlinePresentation)
|
||||
buildDatasetForEntry(
|
||||
context = context,
|
||||
database = database,
|
||||
entryInfo = entry,
|
||||
struct = parseResult,
|
||||
inlinePresentation = inlinePresentation
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to add dataset")
|
||||
Log.e(TAG, "Unable to add dataset", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,21 +483,28 @@ object AutofillHelper {
|
||||
webScheme = parseResult.webScheme
|
||||
manualSelection = true
|
||||
}
|
||||
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
|
||||
AutofillLauncherActivity.getPendingIntentForSelection(context,
|
||||
searchInfo, compatInlineSuggestionsRequest)?.let { pendingIntent ->
|
||||
val manualSelectionView = RemoteViews(
|
||||
context.packageName,
|
||||
R.layout.item_autofill_select_entry
|
||||
)
|
||||
AutofillLauncherActivity.getPendingIntentForSelection(
|
||||
context,
|
||||
searchInfo,
|
||||
autofillComponent
|
||||
)?.let { pendingIntent ->
|
||||
|
||||
var inlinePresentation: InlinePresentation? = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpec =
|
||||
inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||
inlinePresentation = buildInlinePresentationForManualSelection(
|
||||
context,
|
||||
inlinePresentationSpec,
|
||||
pendingIntent
|
||||
)
|
||||
}
|
||||
autofillComponent.compatInlineSuggestionsRequest
|
||||
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpec =
|
||||
inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||
inlinePresentation = buildInlinePresentationForManualSelection(
|
||||
context,
|
||||
inlinePresentationSpec,
|
||||
pendingIntent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
@@ -512,92 +549,33 @@ object AutofillHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Autofill response for one entry
|
||||
* Build the Autofill response
|
||||
*/
|
||||
fun buildResponseAndSetResult(activity: Activity,
|
||||
database: ContextualDatabase,
|
||||
entryInfo: EntryInfo) {
|
||||
buildResponseAndSetResult(activity, database, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Autofill response for many entry
|
||||
*/
|
||||
fun buildResponseAndSetResult(activity: Activity,
|
||||
database: ContextualDatabase,
|
||||
entriesInfo: List<EntryInfo>) {
|
||||
fun buildResponse(
|
||||
context: Context,
|
||||
autofillComponent: AutofillComponent,
|
||||
database: ContextualDatabase,
|
||||
entriesInfo: List<EntryInfo>,
|
||||
onIntentCreated: (Intent) -> Unit
|
||||
) {
|
||||
if (entriesInfo.isEmpty()) {
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
throw IOException("No entries found")
|
||||
} else {
|
||||
var setResultOk = false
|
||||
activity.intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
|
||||
StructureParser(structure).parse()?.let { result ->
|
||||
// New Response
|
||||
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat<CompatInlineSuggestionsRequest>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
||||
if (compatInlineSuggestionsRequest != null) {
|
||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest)
|
||||
} else {
|
||||
buildResponse(activity, database, entriesInfo, result, null)
|
||||
}
|
||||
val mReplyIntent = Intent()
|
||||
Log.d(activity.javaClass.name, "Success Autofill auth.")
|
||||
mReplyIntent.putExtra(
|
||||
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
||||
response)
|
||||
setResultOk = true
|
||||
activity.setResult(Activity.RESULT_OK, mReplyIntent)
|
||||
}
|
||||
}
|
||||
if (!setResultOk) {
|
||||
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
}
|
||||
StructureParser(autofillComponent.assistStructure).parse()?.let { result ->
|
||||
// New Response
|
||||
onIntentCreated(Intent().putExtra(
|
||||
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
||||
buildResponse(
|
||||
context = context,
|
||||
database = database,
|
||||
entriesInfo = entriesInfo,
|
||||
parseResult = result,
|
||||
autofillComponent = autofillComponent
|
||||
)
|
||||
))
|
||||
} ?: throw IOException("Unable to parse the structure")
|
||||
}
|
||||
}
|
||||
|
||||
fun buildActivityResultLauncher(activity: AppCompatActivity,
|
||||
lockDatabase: Boolean = false): ActivityResultLauncher<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
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
* 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.os.Build
|
||||
@@ -17,7 +17,7 @@
|
||||
* 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.app.PendingIntent
|
||||
@@ -43,8 +43,8 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.autofill.inline.UiVersions
|
||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||
import com.kunzisoft.keepass.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
|
||||
import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
|
||||
import com.kunzisoft.keepass.credentialprovider.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||
@@ -53,7 +53,7 @@ import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.WebDomain
|
||||
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||
import org.joda.time.DateTime
|
||||
|
||||
|
||||
@@ -92,10 +92,11 @@ class KeeAutofillService : AutofillService() {
|
||||
autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this)
|
||||
}
|
||||
|
||||
override fun onFillRequest(request: FillRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: FillCallback) {
|
||||
|
||||
override fun onFillRequest(
|
||||
request: FillRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: FillCallback
|
||||
) {
|
||||
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
||||
|
||||
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
|
||||
@@ -120,64 +121,64 @@ class KeeAutofillService : AutofillService() {
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
}
|
||||
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
|
||||
searchInfo.webDomain = webDomainWithoutSubDomain
|
||||
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& autofillInlineSuggestionsEnabled) {
|
||||
CompatInlineSuggestionsRequest(request)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
launchSelection(mDatabase,
|
||||
searchInfo,
|
||||
parseResult,
|
||||
inlineSuggestionsRequest,
|
||||
callback)
|
||||
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& autofillInlineSuggestionsEnabled) {
|
||||
CompatInlineSuggestionsRequest(request)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val autofillComponent = AutofillComponent(
|
||||
latestStructure,
|
||||
inlineSuggestionsRequest
|
||||
)
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = this,
|
||||
database = mDatabase,
|
||||
searchInfo = searchInfo,
|
||||
onItemsFound = { openedDatabase, items ->
|
||||
callback.onSuccess(
|
||||
AutofillHelper.buildResponse(
|
||||
context = this,
|
||||
database = openedDatabase,
|
||||
entriesInfo = items,
|
||||
parseResult = parseResult,
|
||||
autofillComponent = autofillComponent
|
||||
)
|
||||
)
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
// Show UI if no search result
|
||||
showUIForEntrySelection(parseResult, openedDatabase,
|
||||
searchInfo, autofillComponent, callback)
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// Show UI if database not open
|
||||
showUIForEntrySelection(parseResult, null,
|
||||
searchInfo, autofillComponent, callback)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchSelection(database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo,
|
||||
parseResult: StructureParser.Result,
|
||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||
callback: FillCallback) {
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
searchInfo,
|
||||
{ openedDatabase, items ->
|
||||
callback.onSuccess(
|
||||
AutofillHelper.buildResponse(this, openedDatabase,
|
||||
items, parseResult, inlineSuggestionsRequest)
|
||||
)
|
||||
},
|
||||
{ openedDatabase ->
|
||||
// Show UI if no search result
|
||||
showUIForEntrySelection(parseResult, openedDatabase,
|
||||
searchInfo, inlineSuggestionsRequest, callback)
|
||||
},
|
||||
{
|
||||
// Show UI if database not open
|
||||
showUIForEntrySelection(parseResult, null,
|
||||
searchInfo, inlineSuggestionsRequest, callback)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||
database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo,
|
||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||
callback: FillCallback) {
|
||||
private fun showUIForEntrySelection(
|
||||
parseResult: StructureParser.Result,
|
||||
database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo,
|
||||
autofillComponent: AutofillComponent,
|
||||
callback: FillCallback
|
||||
) {
|
||||
var success = false
|
||||
parseResult.allAutofillIds().let { autofillIds ->
|
||||
if (autofillIds.isNotEmpty()) {
|
||||
// If the entire Autofill Response is authenticated, AuthActivity is used
|
||||
// to generate Response.
|
||||
AutofillLauncherActivity.getPendingIntentForSelection(this,
|
||||
searchInfo, inlineSuggestionsRequest)?.intentSender?.let { intentSender ->
|
||||
AutofillLauncherActivity.getPendingIntentForSelection(
|
||||
this,
|
||||
searchInfo,
|
||||
autofillComponent
|
||||
)?.intentSender?.let { intentSender ->
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
val remoteViewsUnlock: RemoteViews = if (database == null) {
|
||||
if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||
@@ -268,7 +269,8 @@ class KeeAutofillService : AutofillService() {
|
||||
&& autofillInlineSuggestionsEnabled
|
||||
) {
|
||||
var inlinePresentation: InlinePresentation? = null
|
||||
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
autofillComponent.compatInlineSuggestionsRequest
|
||||
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpecs =
|
||||
inlineSuggestionsRequest.inlinePresentationSpecs
|
||||
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
||||
@@ -286,7 +288,7 @@ class KeeAutofillService : AutofillService() {
|
||||
InlineSuggestionUi.newContentBuilder(
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
randomRequestCode(),
|
||||
Intent(this, AutofillSettingsActivity::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
@@ -358,7 +360,7 @@ class KeeAutofillService : AutofillService() {
|
||||
|
||||
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
||||
var success = false
|
||||
if (askToSaveData) {
|
||||
if (askToSaveData && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val latestStructure = request.fillContexts.last().structure
|
||||
StructureParser(latestStructure).parse(true)?.let { parseResult ->
|
||||
|
||||
@@ -384,30 +386,32 @@ class KeeAutofillService : AutofillService() {
|
||||
}
|
||||
|
||||
// Show UI to save data
|
||||
val searchInfo = SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
}
|
||||
val registerInfo = RegisterInfo(
|
||||
SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
},
|
||||
parseResult.usernameValue?.textValue?.toString(),
|
||||
parseResult.passwordValue?.textValue?.toString(),
|
||||
searchInfo = searchInfo,
|
||||
username = parseResult.usernameValue?.textValue?.toString(),
|
||||
password = parseResult.passwordValue?.textValue?.toString(),
|
||||
creditCard = parseResult.creditCardNumber?.let { cardNumber ->
|
||||
CreditCard(
|
||||
parseResult.creditCardHolder,
|
||||
parseResult.creditCardNumber,
|
||||
cardNumber,
|
||||
expiration,
|
||||
parseResult.cardVerificationValue
|
||||
))
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// TODO Callback in each activity #765
|
||||
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
|
||||
// registerInfo))
|
||||
//} else {
|
||||
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
|
||||
success = true
|
||||
callback.onSuccess()
|
||||
//}
|
||||
AutofillLauncherActivity.getPendingIntentForRegistration(
|
||||
this,
|
||||
registerInfo
|
||||
)?.intentSender?.let { intentSender ->
|
||||
success = true
|
||||
callback.onSuccess(intentSender)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
* 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.autofill
|
||||
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||
|
||||
import android.app.assist.AssistStructure
|
||||
import android.os.Build
|
||||
@@ -154,7 +154,7 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
|
||||
// Password Id changed if it's the second times we are here,
|
||||
// So the last username candidate is most appropriate
|
||||
if (result?.passwordId != null) {
|
||||
if (result?.passwordId != null && usernameIdCandidate != null) {
|
||||
result?.usernameId = usernameIdCandidate
|
||||
result?.usernameValue = usernameValueCandidate
|
||||
}
|
||||
@@ -362,8 +362,8 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
if (result?.passwordId == null) {
|
||||
usernameIdCandidate = autofillId
|
||||
usernameValueCandidate = node.autofillValue
|
||||
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
||||
}
|
||||
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
||||
}
|
||||
inputIsVariationType(inputType,
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
|
||||
@@ -14,7 +14,7 @@
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.kunzisoft.keepass.magikeyboard;
|
||||
package com.kunzisoft.keepass.credentialprovider.magikeyboard;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
@@ -14,14 +14,14 @@
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.kunzisoft.keepass.magikeyboard;
|
||||
package com.kunzisoft.keepass.credentialprovider.magikeyboard;
|
||||
|
||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
|
||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
|
||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY;
|
||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
|
||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP;
|
||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
|
||||
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
|
||||
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
|
||||
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY;
|
||||
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
|
||||
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP;
|
||||
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
@@ -52,7 +52,7 @@ import android.widget.TextView;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.kunzisoft.keepass.R;
|
||||
import com.kunzisoft.keepass.magikeyboard.Keyboard.Key;
|
||||
import com.kunzisoft.keepass.credentialprovider.magikeyboard.Keyboard.Key;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
@@ -18,7 +18,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
package com.kunzisoft.keepass.magikeyboard
|
||||
package com.kunzisoft.keepass.credentialprovider.magikeyboard
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
@@ -41,9 +41,10 @@ import androidx.core.graphics.BlendModeCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
|
||||
import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
@@ -324,9 +325,9 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
actionGoAutomatically()
|
||||
}
|
||||
KEY_FIELDS -> {
|
||||
getEntryInfo()?.customFields?.let { customFields ->
|
||||
getEntryInfo()?.getCustomFieldsForFilling()?.let { customFields ->
|
||||
fieldsAdapter?.apply {
|
||||
setFields(customFields.filter { it.name != OTP_TOKEN_FIELD})
|
||||
setFields(customFields)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -341,10 +342,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
}
|
||||
|
||||
private fun actionKeyEntry(searchInfo: SearchInfo? = null) {
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
mDatabase,
|
||||
searchInfo,
|
||||
{ _, items ->
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = this,
|
||||
database = mDatabase,
|
||||
searchInfo = searchInfo,
|
||||
onItemsFound = { _, items ->
|
||||
performSelection(
|
||||
items,
|
||||
{
|
||||
@@ -361,11 +363,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
}
|
||||
)
|
||||
},
|
||||
{
|
||||
onItemNotFound = {
|
||||
// Select if not found
|
||||
launchEntrySelection(searchInfo)
|
||||
},
|
||||
{
|
||||
onDatabaseClosed = {
|
||||
// Select if database not opened
|
||||
removeEntryInfo()
|
||||
launchEntrySelection(searchInfo)
|
||||
@@ -460,34 +462,23 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
|
||||
}
|
||||
|
||||
fun performSelection(items: List<EntryInfo>,
|
||||
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
||||
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
|
||||
if (items.size == 1) {
|
||||
val itemFound = items[0]
|
||||
if (entryUUID != itemFound.id) {
|
||||
actionPopulateKeyboard.invoke(itemFound)
|
||||
} else {
|
||||
// Force selection if magikeyboard already populated
|
||||
actionEntrySelection.invoke(false)
|
||||
}
|
||||
} else if (items.size > 1) {
|
||||
// Select the one we want in the selection
|
||||
actionEntrySelection.invoke(true)
|
||||
} else {
|
||||
// Select an arbitrary one
|
||||
actionEntrySelection.invoke(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
||||
entry: EntryInfo,
|
||||
toast: Boolean = true) {
|
||||
// Populate Magikeyboard with entry
|
||||
addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
||||
// Consume the selection mode
|
||||
EntrySelectionHelper.removeModesFromIntent(activity.intent)
|
||||
activity.moveTaskToBack(true)
|
||||
fun performSelection(
|
||||
items: List<EntryInfo>,
|
||||
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
||||
actionEntrySelection: (autoSearch: Boolean) -> Unit
|
||||
) {
|
||||
EntrySelectionHelper.performSelection(
|
||||
items = items,
|
||||
actionPopulateCredentialProvider = { itemFound ->
|
||||
if (entryUUID != itemFound.id) {
|
||||
actionPopulateKeyboard.invoke(itemFound)
|
||||
} else {
|
||||
// Force selection if magikeyboard already populated
|
||||
actionEntrySelection.invoke(false)
|
||||
}
|
||||
},
|
||||
actionEntrySelection = actionEntrySelection
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
/*
|
||||
* 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.encrypt.Base64Helper.Companion.b64Encode
|
||||
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,
|
||||
credentialIds: List<String> = listOf()
|
||||
): SearchInfo {
|
||||
return SearchInfo().apply {
|
||||
this.relyingParty = relyingParty
|
||||
this.credentialIds = credentialIds
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
toastError(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 publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions(option.requestJson)
|
||||
val relyingPartyId = publicKeyCredentialRequestOptions.rpId
|
||||
val credentialIdList = publicKeyCredentialRequestOptions.allowCredentials
|
||||
.map { b64Encode(it.id) }
|
||||
val searchInfo = buildPasskeySearchInfo(relyingPartyId, credentialIdList)
|
||||
Log.d(TAG, "Build passkey search for relying party $relyingPartyId, credentialIds $credentialIdList")
|
||||
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")
|
||||
if (credentialIdList.isEmpty()) {
|
||||
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)
|
||||
} else {
|
||||
throw IOException(
|
||||
getString(
|
||||
R.string.error_passkey_credential_id,
|
||||
relyingPartyId,
|
||||
credentialIdList
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
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 publicKeyCredentialCreationOptions = PublicKeyCredentialCreationOptions(
|
||||
requestJson = request.requestJson,
|
||||
clientDataHash = request.clientDataHash
|
||||
)
|
||||
val relyingPartyId = publicKeyCredentialCreationOptions.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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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>"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* 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 com.kunzisoft.encrypt.Base64Helper
|
||||
import org.json.JSONObject
|
||||
|
||||
data class PublicKeyCredentialRpEntity(
|
||||
val name: String,
|
||||
val id: String
|
||||
) {
|
||||
companion object {
|
||||
fun JSONObject.getPublicKeyCredentialRpEntity(
|
||||
parameterName: String
|
||||
): PublicKeyCredentialRpEntity {
|
||||
val rpJson = this.getJSONObject(parameterName)
|
||||
return PublicKeyCredentialRpEntity(
|
||||
rpJson.getString("name"),
|
||||
rpJson.getString("id")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun JSONObject.getPublicKeyCredentialUserEntity(
|
||||
parameterName: String
|
||||
): PublicKeyCredentialUserEntity {
|
||||
val rpUser = this.getJSONObject(parameterName)
|
||||
return PublicKeyCredentialUserEntity(
|
||||
rpUser.getString("name"),
|
||||
Base64Helper.b64Decode(rpUser.getString("id")),
|
||||
rpUser.getString("displayName")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class PublicKeyCredentialParameters(
|
||||
val type: String,
|
||||
val alg: Long
|
||||
) {
|
||||
companion object {
|
||||
fun JSONObject.getPublicKeyCredentialParametersList(
|
||||
parameterName: String
|
||||
): List<PublicKeyCredentialParameters> {
|
||||
val pubKeyCredParamsJson = this.getJSONArray(parameterName)
|
||||
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"))
|
||||
)
|
||||
}
|
||||
return pubKeyCredParamsTmp.toList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun JSONObject.getPublicKeyCredentialDescriptorList(
|
||||
parameterName: String
|
||||
): List<PublicKeyCredentialDescriptor> {
|
||||
val credentialsJson = this.getJSONArray(parameterName)
|
||||
val credentialsTmp: MutableList<PublicKeyCredentialDescriptor> = mutableListOf()
|
||||
for (i in 0 until credentialsJson.length()) {
|
||||
val credentialJson = credentialsJson.getJSONObject(i)
|
||||
|
||||
val transports: MutableList<String> = mutableListOf()
|
||||
val transportsJson = credentialJson.getJSONArray("transports")
|
||||
for (j in 0 until transportsJson.length()) {
|
||||
transports.add(transportsJson.getString(j))
|
||||
}
|
||||
credentialsTmp.add(
|
||||
PublicKeyCredentialDescriptor(
|
||||
type = credentialJson.getString("type"),
|
||||
id = Base64Helper.b64Decode(credentialJson.getString("id")),
|
||||
transports = transports
|
||||
)
|
||||
)
|
||||
}
|
||||
return credentialsTmp.toList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class AuthenticatorSelectionCriteria(
|
||||
val authenticatorAttachment: String? = null,
|
||||
val residentKey: ResidentKeyRequirement? = null,
|
||||
val requireResidentKey: Boolean?,
|
||||
val userVerification: UserVerificationRequirement? = UserVerificationRequirement.PREFERRED
|
||||
) {
|
||||
companion object {
|
||||
fun JSONObject.getAuthenticatorSelectionCriteria(
|
||||
parameterName: String
|
||||
): AuthenticatorSelectionCriteria {
|
||||
val authenticatorSelection = this.optJSONObject(parameterName)
|
||||
?: return AuthenticatorSelectionCriteria(requireResidentKey = null)
|
||||
val authenticatorAttachment = if (!authenticatorSelection.isNull("authenticatorAttachment"))
|
||||
authenticatorSelection.getString("authenticatorAttachment") else null
|
||||
var residentKey = if (!authenticatorSelection.isNull("residentKey"))
|
||||
ResidentKeyRequirement.fromString(authenticatorSelection.getString("residentKey"))
|
||||
else null
|
||||
val requireResidentKey = authenticatorSelection.optBoolean("requireResidentKey", false)
|
||||
val userVerification = UserVerificationRequirement.fromString(authenticatorSelection.optString("userVerification", "preferred"))
|
||||
// https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement
|
||||
if (residentKey == null) {
|
||||
residentKey = if (requireResidentKey) {
|
||||
ResidentKeyRequirement.REQUIRED
|
||||
} else {
|
||||
ResidentKeyRequirement.DISCOURAGED
|
||||
}
|
||||
}
|
||||
return AuthenticatorSelectionCriteria(
|
||||
authenticatorAttachment = authenticatorAttachment,
|
||||
residentKey = residentKey,
|
||||
requireResidentKey = requireResidentKey,
|
||||
userVerification = userVerification
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement
|
||||
enum class ResidentKeyRequirement(val value: String) {
|
||||
DISCOURAGED("discouraged"),
|
||||
PREFERRED("preferred"),
|
||||
REQUIRED("required");
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
companion object {
|
||||
fun fromString(value: String): ResidentKeyRequirement? {
|
||||
return ResidentKeyRequirement.entries.firstOrNull { it.value == value }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement
|
||||
enum class UserVerificationRequirement(val value: String) {
|
||||
REQUIRED("required"),
|
||||
PREFERRED("preferred"),
|
||||
DISCOURAGED("discouraged");
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
companion object {
|
||||
fun fromString(value: String): UserVerificationRequirement? {
|
||||
return UserVerificationRequirement.entries.firstOrNull { it.value == value }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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.Base64Helper
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorSelectionCriteria.Companion.getAuthenticatorSelectionCriteria
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialDescriptor.Companion.getPublicKeyCredentialDescriptorList
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialParameters.Companion.getPublicKeyCredentialParametersList
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRpEntity.Companion.getPublicKeyCredentialRpEntity
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUserEntity.Companion.getPublicKeyCredentialUserEntity
|
||||
import org.json.JSONObject
|
||||
|
||||
class PublicKeyCredentialCreationOptions(
|
||||
requestJson: String,
|
||||
var clientDataHash: ByteArray?
|
||||
) {
|
||||
private val json: JSONObject = JSONObject(requestJson)
|
||||
|
||||
val relyingPartyEntity: PublicKeyCredentialRpEntity =
|
||||
json.getPublicKeyCredentialRpEntity("rp")
|
||||
|
||||
val userEntity: PublicKeyCredentialUserEntity =
|
||||
json.getPublicKeyCredentialUserEntity("user")
|
||||
|
||||
val challenge: ByteArray =
|
||||
Base64Helper.b64Decode(json.getString("challenge"))
|
||||
|
||||
val pubKeyCredParams: List<PublicKeyCredentialParameters> =
|
||||
json.getPublicKeyCredentialParametersList("pubKeyCredParams")
|
||||
|
||||
var timeout: Long =
|
||||
json.optLong("timeout", 0)
|
||||
|
||||
var excludeCredentials: List<PublicKeyCredentialDescriptor> =
|
||||
json.getPublicKeyCredentialDescriptorList("excludeCredentials")
|
||||
|
||||
var authenticatorSelection: AuthenticatorSelectionCriteria =
|
||||
json.getAuthenticatorSelectionCriteria("authenticatorSelection")
|
||||
|
||||
var attestation: String =
|
||||
json.optString("attestation", "none")
|
||||
|
||||
companion object {
|
||||
private val TAG = PublicKeyCredentialCreationOptions::class.simpleName
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialDescriptor.Companion.getPublicKeyCredentialDescriptorList
|
||||
import org.json.JSONObject
|
||||
|
||||
// https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement
|
||||
class PublicKeyCredentialRequestOptions(requestJson: String) {
|
||||
private 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 allowCredentials: List<PublicKeyCredentialDescriptor> =
|
||||
json.getPublicKeyCredentialDescriptorList("allowCredentials")
|
||||
|
||||
val userVerification: UserVerificationRequirement =
|
||||
UserVerificationRequirement.fromString(
|
||||
json.optString("userVerification", "preferred"))
|
||||
?: UserVerificationRequirement.PREFERRED
|
||||
|
||||
// TODO Hints
|
||||
val hints: List<String> = listOf()
|
||||
|
||||
// TODO Extensions
|
||||
// val extensions: AuthenticationExtensionsClientInputs
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1,627 @@
|
||||
/*
|
||||
* 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.content.res.Resources
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Passkey error response
|
||||
*/
|
||||
fun Activity.buildPasskeyErrorAndSetResult(
|
||||
resources: Resources,
|
||||
relyingPartyId: String?,
|
||||
credentialIds: List<String>
|
||||
) {
|
||||
val error = resources.getString(
|
||||
R.string.error_passkey_credential_id,
|
||||
relyingPartyId,
|
||||
credentialIds
|
||||
)
|
||||
Log.e(javaClass.name, error)
|
||||
Toast.makeText(
|
||||
this,
|
||||
error,
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
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.retrieveAndRemoveEntries
|
||||
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.exception.RegisterInReadOnlyDatabaseException
|
||||
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 =
|
||||
CredentialState.LaunchGroupActivityForSelection(
|
||||
database = openedDatabase,
|
||||
searchInfo = searchInfo,
|
||||
typeMode = TypeMode.AUTOFILL
|
||||
)
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// If database not open
|
||||
mCredentialUiState.value =
|
||||
CredentialState.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 entries = intent.retrieveAndRemoveEntries(database)
|
||||
val autofillComponent = mAutofillComponent
|
||||
if (autofillComponent == null)
|
||||
throw IOException("Autofill component is null")
|
||||
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 =
|
||||
CredentialState.LaunchGroupActivityForRegistration(
|
||||
database = openedDatabase,
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.AUTOFILL
|
||||
)
|
||||
} else {
|
||||
mCredentialUiState.value = CredentialState.ShowError(
|
||||
RegisterInReadOnlyDatabaseException()
|
||||
)
|
||||
}
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
if (!readOnly) {
|
||||
// Show the database UI to select the entry
|
||||
mCredentialUiState.value =
|
||||
CredentialState.LaunchGroupActivityForRegistration(
|
||||
database = openedDatabase,
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.AUTOFILL
|
||||
)
|
||||
} else {
|
||||
mCredentialUiState.value = CredentialState.ShowError(
|
||||
RegisterInReadOnlyDatabaseException()
|
||||
)
|
||||
}
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// If database not open
|
||||
mCredentialUiState.value =
|
||||
CredentialState.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 ShowAutofillSuggestionMessage: UIState()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = AutofillLauncherViewModel::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -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<CredentialState>(CredentialState.Loading)
|
||||
val credentialUiState: StateFlow<CredentialState> = mCredentialUiState
|
||||
|
||||
fun showError(error: Throwable) {
|
||||
Log.e(TAG, "Error on credential provider launch", error)
|
||||
mCredentialUiState.value = CredentialState.ShowError(error)
|
||||
}
|
||||
|
||||
open fun onResult() {
|
||||
isResultLauncherRegistered = false
|
||||
mSelectionResult = null
|
||||
}
|
||||
|
||||
fun setResult(intent: Intent, lockDatabase: Boolean = false) {
|
||||
// Remove the launcher register
|
||||
onResult()
|
||||
mCredentialUiState.value = CredentialState.SetActivityResult(
|
||||
lockDatabase = lockDatabase,
|
||||
resultCode = RESULT_OK,
|
||||
data = intent
|
||||
)
|
||||
}
|
||||
|
||||
fun cancelResult(lockDatabase: Boolean = false) {
|
||||
onResult()
|
||||
mCredentialUiState.value = CredentialState.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 CredentialState {
|
||||
object Loading : CredentialState()
|
||||
data class LaunchGroupActivityForSelection(
|
||||
val database: ContextualDatabase,
|
||||
val searchInfo: SearchInfo?,
|
||||
val typeMode: TypeMode
|
||||
): CredentialState()
|
||||
data class LaunchGroupActivityForRegistration(
|
||||
val database: ContextualDatabase,
|
||||
val registerInfo: RegisterInfo?,
|
||||
val typeMode: TypeMode
|
||||
): CredentialState()
|
||||
data class LaunchFileDatabaseSelectActivityForSelection(
|
||||
val searchInfo: SearchInfo?,
|
||||
val typeMode: TypeMode
|
||||
): CredentialState()
|
||||
data class LaunchFileDatabaseSelectActivityForRegistration(
|
||||
val registerInfo: RegisterInfo?,
|
||||
val typeMode: TypeMode
|
||||
): CredentialState()
|
||||
data class SetActivityResult(
|
||||
val lockDatabase: Boolean,
|
||||
val resultCode: Int,
|
||||
val data: Intent? = null
|
||||
): CredentialState()
|
||||
data class ShowError(
|
||||
val error: Throwable
|
||||
): CredentialState()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = CredentialLauncherViewModel::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
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.core.net.toUri
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveAndRemoveEntries
|
||||
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.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
|
||||
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
|
||||
|
||||
class EntrySelectionViewModel(application: Application): CredentialLauncherViewModel(application) {
|
||||
|
||||
private var searchShareForMagikeyboard: Boolean = false
|
||||
private var mLockDatabaseAfterSelection: Boolean = false
|
||||
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
|
||||
val uiState: StateFlow<UIState> = mUiState
|
||||
|
||||
fun initialize() {
|
||||
searchShareForMagikeyboard = getApplication<Application>().isKeyboardActivatedInSettings()
|
||||
mLockDatabaseAfterSelection = false // TODO Close database after selection
|
||||
}
|
||||
|
||||
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: SearchInfo? = intent.retrieveSearchInfo()
|
||||
if (searchInfo != null) {
|
||||
launch(database, searchInfo)
|
||||
} else {
|
||||
// To manage share
|
||||
var sharedWebDomain: String? = null
|
||||
var otpString: String? = null
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_SEND -> {
|
||||
if ("text/plain" == intent.type) {
|
||||
// Retrieve web domain or OTP
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
|
||||
if (OtpEntryFields.isOTPUri(extra))
|
||||
otpString = extra
|
||||
else
|
||||
sharedWebDomain = extra.toUri().host
|
||||
}
|
||||
}
|
||||
launchSelection(database, sharedWebDomain, otpString)
|
||||
}
|
||||
Intent.ACTION_VIEW -> {
|
||||
// Retrieve OTP
|
||||
intent.dataString?.let { extra ->
|
||||
if (OtpEntryFields.isOTPUri(extra))
|
||||
otpString = extra
|
||||
}
|
||||
launchSelection(database, null, otpString)
|
||||
}
|
||||
else -> {
|
||||
if (database != null && database.loaded) {
|
||||
mUiState.value = UIState.LaunchGroupActivityForSearch(
|
||||
database = database,
|
||||
searchInfo = SearchInfo()
|
||||
)
|
||||
} else {
|
||||
mUiState.value = UIState.LaunchFileDatabaseSelectForSearch(
|
||||
searchInfo = SearchInfo()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------
|
||||
// Selection
|
||||
// -------------
|
||||
|
||||
private fun launchSelection(
|
||||
database: ContextualDatabase?,
|
||||
sharedWebDomain: String?,
|
||||
otpString: String?
|
||||
) {
|
||||
// Build domain search param
|
||||
val searchInfo = SearchInfo().apply {
|
||||
this.webDomain = sharedWebDomain
|
||||
this.otpString = otpString
|
||||
}
|
||||
launch(database, searchInfo)
|
||||
}
|
||||
|
||||
private fun launch(
|
||||
database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo
|
||||
) {
|
||||
// If database is open
|
||||
val readOnly = database?.isReadOnly != false
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = getApplication(),
|
||||
database = database,
|
||||
searchInfo = searchInfo,
|
||||
onItemsFound = { openedDatabase, items ->
|
||||
// Items found
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
mCredentialUiState.value =
|
||||
CredentialState.LaunchGroupActivityForRegistration(
|
||||
database = openedDatabase,
|
||||
registerInfo = searchInfo.toRegisterInfo(),
|
||||
typeMode = TypeMode.DEFAULT
|
||||
)
|
||||
} else {
|
||||
mCredentialUiState.value = CredentialState.ShowError(
|
||||
RegisterInReadOnlyDatabaseException()
|
||||
)
|
||||
}
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
MagikeyboardService.performSelection(
|
||||
items,
|
||||
{ entryInfo ->
|
||||
populateKeyboard(entryInfo)
|
||||
},
|
||||
{ autoSearch ->
|
||||
mCredentialUiState.value = CredentialState.LaunchGroupActivityForSelection(
|
||||
database = openedDatabase,
|
||||
searchInfo = searchInfo,
|
||||
typeMode = TypeMode.MAGIKEYBOARD
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
mUiState.value = UIState.LaunchGroupActivityForSearch(
|
||||
database = openedDatabase,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
}
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
// Show the database UI to select the entry
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
mCredentialUiState.value =
|
||||
CredentialState.LaunchGroupActivityForRegistration(
|
||||
database = openedDatabase,
|
||||
registerInfo = searchInfo.toRegisterInfo(),
|
||||
typeMode = TypeMode.DEFAULT
|
||||
)
|
||||
} else {
|
||||
mCredentialUiState.value = CredentialState.ShowError(
|
||||
RegisterInReadOnlyDatabaseException()
|
||||
)
|
||||
}
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
mCredentialUiState.value = CredentialState.LaunchGroupActivityForSelection(
|
||||
database = openedDatabase,
|
||||
searchInfo = searchInfo,
|
||||
typeMode = TypeMode.MAGIKEYBOARD
|
||||
)
|
||||
} else {
|
||||
mUiState.value = UIState.LaunchGroupActivityForSearch(
|
||||
database = openedDatabase,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
}
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// If database not open
|
||||
if (searchInfo.otpString != null) {
|
||||
mCredentialUiState.value = CredentialState.LaunchFileDatabaseSelectActivityForRegistration(
|
||||
registerInfo = searchInfo.toRegisterInfo(),
|
||||
typeMode = TypeMode.DEFAULT
|
||||
)
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
mCredentialUiState.value = CredentialState.LaunchFileDatabaseSelectActivityForSelection(
|
||||
searchInfo = searchInfo,
|
||||
typeMode = TypeMode.MAGIKEYBOARD
|
||||
)
|
||||
} else {
|
||||
mUiState.value = UIState.LaunchFileDatabaseSelectForSearch(
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun populateKeyboard(entryInfo: EntryInfo) {
|
||||
// Automatically populate keyboard
|
||||
mUiState.value = UIState.PopulateKeyboard(entryInfo)
|
||||
setResult(Intent(), lockDatabase = mLockDatabaseAfterSelection)
|
||||
}
|
||||
|
||||
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 Magikeyboard", e)
|
||||
showError(e)
|
||||
}) {
|
||||
when (activityResult.resultCode) {
|
||||
RESULT_OK -> {
|
||||
withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "Magikeyboard selection result")
|
||||
if (intent == null)
|
||||
throw IOException("Intent is null")
|
||||
val entries = intent.retrieveAndRemoveEntries(database)
|
||||
withContext(Dispatchers.Main) {
|
||||
// Populate Magikeyboard with entry
|
||||
entries.firstOrNull()?.let { entryInfo ->
|
||||
populateKeyboard(entryInfo)
|
||||
} // TODO Manage multiple entries in Magikeyboard
|
||||
}
|
||||
}
|
||||
}
|
||||
RESULT_CANCELED -> {
|
||||
withContext(Dispatchers.Main) {
|
||||
cancelResult()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun manageRegistrationResult(activityResult: ActivityResult) {
|
||||
super.manageRegistrationResult(activityResult)
|
||||
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||
Log.e(TAG, "Unable to create selection response for Magikeyboard", e)
|
||||
showError(e)
|
||||
}) {
|
||||
when (activityResult.resultCode) {
|
||||
RESULT_OK -> {
|
||||
// Empty data result
|
||||
// TODO Show Toast indicating value is saved
|
||||
withContext(Dispatchers.Main) {
|
||||
setResult(Intent(), lockDatabase = false)
|
||||
}
|
||||
}
|
||||
RESULT_CANCELED -> {
|
||||
withContext(Dispatchers.Main) {
|
||||
cancelResult()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class UIState {
|
||||
object Loading: UIState()
|
||||
data class PopulateKeyboard(
|
||||
val entryInfo: EntryInfo
|
||||
): UIState()
|
||||
data class LaunchFileDatabaseSelectForSearch(
|
||||
val searchInfo: SearchInfo
|
||||
): UIState()
|
||||
data class LaunchGroupActivityForSearch(
|
||||
val database: ContextualDatabase,
|
||||
val searchInfo: SearchInfo
|
||||
): UIState()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = EntrySelectionViewModel::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 =
|
||||
CredentialState.LaunchGroupActivityForSelection(
|
||||
database = openedDatabase,
|
||||
searchInfo = searchInfo,
|
||||
typeMode = TypeMode.PASSKEY
|
||||
)
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
Log.d(TAG, "Manual passkey selection in closed database")
|
||||
mCredentialUiState.value =
|
||||
CredentialState.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 =
|
||||
CredentialState.LaunchGroupActivityForRegistration(
|
||||
database = openedDatabase,
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.PASSKEY
|
||||
)
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
Log.d(TAG, "Launch new manual registration in opened database")
|
||||
mCredentialUiState.value =
|
||||
CredentialState.LaunchGroupActivityForRegistration(
|
||||
database = openedDatabase,
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.PASSKEY
|
||||
)
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
Log.d(TAG, "Manual passkey registration in closed database")
|
||||
mCredentialUiState.value =
|
||||
CredentialState.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
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database
|
||||
|
||||
import android.Manifest
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
@@ -29,23 +28,15 @@ import android.content.Context.BIND_IMPORTANT
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
@@ -55,7 +46,6 @@ import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK
|
||||
@@ -89,13 +79,9 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
||||
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
||||
import com.kunzisoft.keepass.utils.putParcelableList
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
@@ -103,175 +89,58 @@ import java.util.UUID
|
||||
* Useful to retrieve a database instance and sending tasks commands
|
||||
*/
|
||||
class DatabaseTaskProvider(
|
||||
private var context: Context,
|
||||
private var showDialog: Boolean = true
|
||||
private var context: Context
|
||||
) {
|
||||
|
||||
// To show dialog only if context is an activity
|
||||
private var activity: FragmentActivity? = try { context as? FragmentActivity? }
|
||||
catch (_: Exception) { null }
|
||||
|
||||
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
|
||||
|
||||
var onActionFinish: ((database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result) -> Unit)? = null
|
||||
|
||||
private var intentDatabaseTask: Intent = Intent(
|
||||
context.applicationContext,
|
||||
DatabaseTaskNotificationService::class.java
|
||||
)
|
||||
var onStartActionRequested: ((bundle: Bundle?, actionTask: String) -> Unit)? = null
|
||||
var actionTaskListener: DatabaseTaskNotificationService.ActionTaskListener? = null
|
||||
var databaseInfoListener: DatabaseTaskNotificationService.DatabaseInfoListener? = null
|
||||
|
||||
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
|
||||
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
|
||||
|
||||
private var serviceConnection: ServiceConnection? = null
|
||||
|
||||
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
|
||||
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
|
||||
|
||||
fun destroy() {
|
||||
this.activity = null
|
||||
this.onDatabaseRetrieved = null
|
||||
this.onActionFinish = null
|
||||
this.databaseTaskBroadcastReceiver = null
|
||||
this.mBinder = null
|
||||
this.serviceConnection = null
|
||||
this.progressTaskDialogFragment = null
|
||||
this.databaseChangedDialogFragment = null
|
||||
}
|
||||
|
||||
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
||||
override fun onActionStarted(
|
||||
database: ContextualDatabase,
|
||||
progressMessage: ProgressMessage
|
||||
) {
|
||||
if (showDialog)
|
||||
startDialog(progressMessage)
|
||||
}
|
||||
|
||||
override fun onActionUpdated(
|
||||
database: ContextualDatabase,
|
||||
progressMessage: ProgressMessage
|
||||
) {
|
||||
if (showDialog)
|
||||
updateDialog(progressMessage)
|
||||
}
|
||||
|
||||
override fun onActionStopped(
|
||||
database: ContextualDatabase
|
||||
) {
|
||||
// Remove the progress task
|
||||
stopDialog()
|
||||
}
|
||||
|
||||
override fun onActionFinished(
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
onActionFinish?.invoke(database, actionTask, result)
|
||||
onActionStopped(database)
|
||||
}
|
||||
fun onDatabaseChangeValidated() {
|
||||
mBinder?.getService()?.saveDatabaseInfo()
|
||||
}
|
||||
|
||||
private val mActionDatabaseListener = object: DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
|
||||
override fun validateDatabaseChanged() {
|
||||
mBinder?.getService()?.saveDatabaseInfo()
|
||||
}
|
||||
}
|
||||
|
||||
private var databaseInfoListener = object:
|
||||
DatabaseTaskNotificationService.DatabaseInfoListener {
|
||||
override fun onDatabaseInfoChanged(
|
||||
previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||
newDatabaseInfo: SnapFileDatabaseInfo,
|
||||
readOnlyDatabase: Boolean
|
||||
) {
|
||||
activity?.let { activity ->
|
||||
activity.lifecycleScope.launch {
|
||||
if (databaseChangedDialogFragment == null) {
|
||||
databaseChangedDialogFragment = activity.supportFragmentManager
|
||||
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
|
||||
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||
mActionDatabaseListener
|
||||
}
|
||||
if (progressTaskDialogFragment == null) {
|
||||
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
|
||||
previousDatabaseInfo,
|
||||
newDatabaseInfo,
|
||||
readOnlyDatabase
|
||||
)
|
||||
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||
mActionDatabaseListener
|
||||
databaseChangedDialogFragment?.show(
|
||||
activity.supportFragmentManager,
|
||||
DATABASE_CHANGED_DIALOG_TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var databaseListener = object: DatabaseTaskNotificationService.DatabaseListener {
|
||||
private var databaseListener = object : DatabaseTaskNotificationService.DatabaseListener {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
onDatabaseRetrieved?.invoke(database)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startDialog(progressMessage: ProgressMessage) {
|
||||
activity?.let { activity ->
|
||||
activity.lifecycleScope.launch {
|
||||
if (progressTaskDialogFragment == null) {
|
||||
progressTaskDialogFragment = activity.supportFragmentManager
|
||||
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
|
||||
}
|
||||
if (progressTaskDialogFragment == null) {
|
||||
progressTaskDialogFragment = ProgressTaskDialogFragment()
|
||||
progressTaskDialogFragment?.show(
|
||||
activity.supportFragmentManager,
|
||||
PROGRESS_TASK_DIALOG_TAG
|
||||
)
|
||||
}
|
||||
updateDialog(progressMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDialog(progressMessage: ProgressMessage) {
|
||||
progressTaskDialogFragment?.apply {
|
||||
updateTitle(progressMessage.titleId)
|
||||
updateMessage(progressMessage.messageId)
|
||||
updateWarning(progressMessage.warningId)
|
||||
setCancellable(progressMessage.cancelable)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopDialog() {
|
||||
progressTaskDialogFragment?.dismissAllowingStateLoss()
|
||||
progressTaskDialogFragment = null
|
||||
}
|
||||
|
||||
private fun initServiceConnection() {
|
||||
stopDialog()
|
||||
if (serviceConnection == null) {
|
||||
serviceConnection = object : ServiceConnection {
|
||||
override fun onBindingDied(name: ComponentName?) {
|
||||
stopDialog()
|
||||
actionTaskListener?.onActionStopped()
|
||||
onDatabaseRetrieved?.invoke(null)
|
||||
}
|
||||
|
||||
override fun onNullBinding(name: ComponentName?) {
|
||||
stopDialog()
|
||||
actionTaskListener?.onActionStopped()
|
||||
onDatabaseRetrieved?.invoke(null)
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
||||
addServiceListeners(this)
|
||||
getService().checkDatabase()
|
||||
getService().checkDatabaseInfo()
|
||||
getService().checkAction()
|
||||
}
|
||||
mBinder =
|
||||
(serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
||||
addServiceListeners(this)
|
||||
getService().checkDatabase()
|
||||
getService().checkDatabaseInfo()
|
||||
getService().checkAction()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
@@ -284,20 +153,36 @@ class DatabaseTaskProvider(
|
||||
|
||||
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
||||
service?.addDatabaseListener(databaseListener)
|
||||
service?.addDatabaseFileInfoListener(databaseInfoListener)
|
||||
service?.addActionTaskListener(actionTaskListener)
|
||||
databaseInfoListener?.let { infoListener ->
|
||||
service?.addDatabaseFileInfoListener(infoListener)
|
||||
}
|
||||
actionTaskListener?.let { taskListener ->
|
||||
service?.addActionTaskListener(taskListener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
||||
service?.removeActionTaskListener(actionTaskListener)
|
||||
service?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||
actionTaskListener?.let { taskListener ->
|
||||
service?.removeActionTaskListener(taskListener)
|
||||
}
|
||||
databaseInfoListener?.let { infoListener ->
|
||||
service?.removeDatabaseFileInfoListener(infoListener)
|
||||
}
|
||||
service?.removeDatabaseListener(databaseListener)
|
||||
onDatabaseRetrieved?.invoke(null)
|
||||
}
|
||||
|
||||
private fun bindService() {
|
||||
initServiceConnection()
|
||||
serviceConnection?.let {
|
||||
context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT)
|
||||
context.bindService(
|
||||
Intent(
|
||||
context.applicationContext,
|
||||
DatabaseTaskNotificationService::class.java
|
||||
),
|
||||
it,
|
||||
BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +210,7 @@ class DatabaseTaskProvider(
|
||||
// Bind to the service when is starting
|
||||
bindService()
|
||||
}
|
||||
|
||||
DATABASE_STOP_TASK_ACTION -> {
|
||||
// Remove the progress task
|
||||
unBindService()
|
||||
@@ -332,7 +218,8 @@ class DatabaseTaskProvider(
|
||||
}
|
||||
}
|
||||
}
|
||||
ContextCompat.registerReceiver(context, databaseTaskBroadcastReceiver,
|
||||
ContextCompat.registerReceiver(
|
||||
context, databaseTaskBroadcastReceiver,
|
||||
IntentFilter().apply {
|
||||
addAction(DATABASE_START_TASK_ACTION)
|
||||
addAction(DATABASE_STOP_TASK_ACTION)
|
||||
@@ -356,58 +243,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) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val contextActivity = activity
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
|
||||
== PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
startService(bundle, actionTask)
|
||||
} else if (contextActivity != null && shouldShowRequestPermissionRationale(
|
||||
contextActivity,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
) {
|
||||
// it's not the first time, so the user deliberately chooses not to display the notification
|
||||
startService(bundle, actionTask)
|
||||
} else {
|
||||
AlertDialog.Builder(context)
|
||||
.setMessage(R.string.warning_database_notification_permission)
|
||||
.setNegativeButton(R.string.later) { _, _ ->
|
||||
// Refuses the notification, so start the service
|
||||
startService(bundle, actionTask)
|
||||
}
|
||||
.setPositiveButton(R.string.ask) { _, _ ->
|
||||
// Save the temp parameters to ask the permission
|
||||
tempServiceParameters.add(Pair(bundle, actionTask))
|
||||
requestPermissionLauncher?.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}.create().show()
|
||||
}
|
||||
} else {
|
||||
startService(bundle, actionTask)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startService(bundle: Bundle? = null, actionTask: String) {
|
||||
try {
|
||||
if (bundle != null)
|
||||
intentDatabaseTask.putExtras(bundle)
|
||||
intentDatabaseTask.action = actionTask
|
||||
context.startService(intentDatabaseTask)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to perform database action", e)
|
||||
Toast.makeText(context, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
|
||||
onStartActionRequested?.invoke(bundle, actionTask) ?: run {
|
||||
context.startDatabaseService(bundle, actionTask)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,47 +255,51 @@ class DatabaseTaskProvider(
|
||||
----
|
||||
*/
|
||||
|
||||
fun startDatabaseCreate(databaseUri: Uri,
|
||||
mainCredential: MainCredential
|
||||
fun startDatabaseCreate(
|
||||
databaseUri: Uri,
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||
}
|
||||
, ACTION_DATABASE_CREATE_TASK)
|
||||
}, ACTION_DATABASE_CREATE_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseLoad(databaseUri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
readOnly: Boolean,
|
||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
fixDuplicateUuid: Boolean) {
|
||||
fun startDatabaseLoad(
|
||||
databaseUri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
readOnly: Boolean,
|
||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
fixDuplicateUuid: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
||||
putParcelable(DatabaseTaskNotificationService.CIPHER_DATABASE_KEY, cipherEncryptDatabase)
|
||||
putParcelable(
|
||||
DatabaseTaskNotificationService.CIPHER_DATABASE_KEY,
|
||||
cipherEncryptDatabase
|
||||
)
|
||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||
}
|
||||
, ACTION_DATABASE_LOAD_TASK)
|
||||
}, ACTION_DATABASE_LOAD_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseMerge(save: Boolean,
|
||||
fromDatabaseUri: Uri? = null,
|
||||
mainCredential: MainCredential? = null) {
|
||||
fun startDatabaseMerge(
|
||||
save: Boolean,
|
||||
fromDatabaseUri: Uri? = null,
|
||||
mainCredential: MainCredential? = null
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
|
||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||
}
|
||||
, ACTION_DATABASE_MERGE_TASK)
|
||||
}, ACTION_DATABASE_MERGE_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||
}
|
||||
, ACTION_DATABASE_RELOAD_TASK)
|
||||
}, ACTION_DATABASE_RELOAD_TASK)
|
||||
}
|
||||
|
||||
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
|
||||
@@ -473,15 +315,15 @@ class DatabaseTaskProvider(
|
||||
}
|
||||
}
|
||||
|
||||
fun startDatabaseAssignCredential(databaseUri: Uri,
|
||||
mainCredential: MainCredential
|
||||
fun startDatabaseAssignCredential(
|
||||
databaseUri: Uri,
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||
}
|
||||
, ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK)
|
||||
}, ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -490,54 +332,60 @@ class DatabaseTaskProvider(
|
||||
----
|
||||
*/
|
||||
|
||||
fun startDatabaseCreateGroup(newGroup: Group,
|
||||
parent: Group,
|
||||
save: Boolean) {
|
||||
fun startDatabaseCreateGroup(
|
||||
newGroup: Group,
|
||||
parent: Group,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, newGroup)
|
||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_CREATE_GROUP_TASK)
|
||||
}, ACTION_DATABASE_CREATE_GROUP_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseUpdateGroup(oldGroup: Group,
|
||||
groupToUpdate: Group,
|
||||
save: Boolean) {
|
||||
fun startDatabaseUpdateGroup(
|
||||
oldGroup: Group,
|
||||
groupToUpdate: Group,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.GROUP_ID_KEY, oldGroup.nodeId)
|
||||
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_GROUP_TASK)
|
||||
}, ACTION_DATABASE_UPDATE_GROUP_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseCreateEntry(newEntry: Entry,
|
||||
parent: Group,
|
||||
save: Boolean) {
|
||||
fun startDatabaseCreateEntry(
|
||||
newEntry: Entry,
|
||||
parent: Group,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, newEntry)
|
||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_CREATE_ENTRY_TASK)
|
||||
}, ACTION_DATABASE_CREATE_ENTRY_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseUpdateEntry(oldEntry: Entry,
|
||||
entryToUpdate: Entry,
|
||||
save: Boolean) {
|
||||
fun startDatabaseUpdateEntry(
|
||||
oldEntry: Entry,
|
||||
entryToUpdate: Entry,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, oldEntry.nodeId)
|
||||
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_ENTRY_TASK)
|
||||
}, ACTION_DATABASE_UPDATE_ENTRY_TASK)
|
||||
}
|
||||
|
||||
private fun startDatabaseActionListNodes(actionTask: String,
|
||||
nodesPaste: List<Node>,
|
||||
newParent: Group?,
|
||||
save: Boolean) {
|
||||
private fun startDatabaseActionListNodes(
|
||||
actionTask: String,
|
||||
nodesPaste: List<Node>,
|
||||
newParent: Group?,
|
||||
save: Boolean
|
||||
) {
|
||||
val groupsIdToCopy = ArrayList<NodeId<*>>()
|
||||
val entriesIdToCopy = ArrayList<NodeId<UUID>>()
|
||||
nodesPaste.forEach { nodeVersioned ->
|
||||
@@ -545,6 +393,7 @@ class DatabaseTaskProvider(
|
||||
Type.GROUP -> {
|
||||
groupsIdToCopy.add((nodeVersioned as Group).nodeId)
|
||||
}
|
||||
|
||||
Type.ENTRY -> {
|
||||
entriesIdToCopy.add((nodeVersioned as Entry).nodeId)
|
||||
}
|
||||
@@ -559,24 +408,29 @@ class DatabaseTaskProvider(
|
||||
if (newParentId != null)
|
||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, actionTask)
|
||||
}, actionTask)
|
||||
}
|
||||
|
||||
fun startDatabaseCopyNodes(nodesToCopy: List<Node>,
|
||||
newParent: Group,
|
||||
save: Boolean) {
|
||||
fun startDatabaseCopyNodes(
|
||||
nodesToCopy: List<Node>,
|
||||
newParent: Group,
|
||||
save: Boolean
|
||||
) {
|
||||
startDatabaseActionListNodes(ACTION_DATABASE_COPY_NODES_TASK, nodesToCopy, newParent, save)
|
||||
}
|
||||
|
||||
fun startDatabaseMoveNodes(nodesToMove: List<Node>,
|
||||
newParent: Group,
|
||||
save: Boolean) {
|
||||
fun startDatabaseMoveNodes(
|
||||
nodesToMove: List<Node>,
|
||||
newParent: Group,
|
||||
save: Boolean
|
||||
) {
|
||||
startDatabaseActionListNodes(ACTION_DATABASE_MOVE_NODES_TASK, nodesToMove, newParent, save)
|
||||
}
|
||||
|
||||
fun startDatabaseDeleteNodes(nodesToDelete: List<Node>,
|
||||
save: Boolean) {
|
||||
fun startDatabaseDeleteNodes(
|
||||
nodesToDelete: List<Node>,
|
||||
save: Boolean
|
||||
) {
|
||||
startDatabaseActionListNodes(ACTION_DATABASE_DELETE_NODES_TASK, nodesToDelete, null, save)
|
||||
}
|
||||
|
||||
@@ -586,26 +440,28 @@ class DatabaseTaskProvider(
|
||||
-----------------
|
||||
*/
|
||||
|
||||
fun startDatabaseRestoreEntryHistory(mainEntryId: NodeId<UUID>,
|
||||
entryHistoryPosition: Int,
|
||||
save: Boolean) {
|
||||
fun startDatabaseRestoreEntryHistory(
|
||||
mainEntryId: NodeId<UUID>,
|
||||
entryHistoryPosition: Int,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
|
||||
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
|
||||
}, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
|
||||
}
|
||||
|
||||
fun startDatabaseDeleteEntryHistory(mainEntryId: NodeId<UUID>,
|
||||
entryHistoryPosition: Int,
|
||||
save: Boolean) {
|
||||
fun startDatabaseDeleteEntryHistory(
|
||||
mainEntryId: NodeId<UUID>,
|
||||
entryHistoryPosition: Int,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
|
||||
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
|
||||
}, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -614,110 +470,118 @@ class DatabaseTaskProvider(
|
||||
-----------------
|
||||
*/
|
||||
|
||||
fun startDatabaseSaveName(oldName: String,
|
||||
newName: String,
|
||||
save: Boolean) {
|
||||
fun startDatabaseSaveName(
|
||||
oldName: String,
|
||||
newName: String,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldName)
|
||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_NAME_TASK)
|
||||
}, ACTION_DATABASE_UPDATE_NAME_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveDescription(oldDescription: String,
|
||||
newDescription: String,
|
||||
save: Boolean) {
|
||||
fun startDatabaseSaveDescription(
|
||||
oldDescription: String,
|
||||
newDescription: String,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDescription)
|
||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
|
||||
}, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveDefaultUsername(oldDefaultUsername: String,
|
||||
newDefaultUsername: String,
|
||||
save: Boolean) {
|
||||
fun startDatabaseSaveDefaultUsername(
|
||||
oldDefaultUsername: String,
|
||||
newDefaultUsername: String,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDefaultUsername)
|
||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
|
||||
}, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveColor(oldColor: String,
|
||||
newColor: String,
|
||||
save: Boolean) {
|
||||
fun startDatabaseSaveColor(
|
||||
oldColor: String,
|
||||
newColor: String,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldColor)
|
||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_COLOR_TASK)
|
||||
}, ACTION_DATABASE_UPDATE_COLOR_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveCompression(oldCompression: CompressionAlgorithm,
|
||||
newCompression: CompressionAlgorithm,
|
||||
save: Boolean) {
|
||||
fun startDatabaseSaveCompression(
|
||||
oldCompression: CompressionAlgorithm,
|
||||
newCompression: CompressionAlgorithm,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldCompression)
|
||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
||||
}, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
||||
}, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveRecycleBin(oldRecycleBin: Group?,
|
||||
newRecycleBin: Group?,
|
||||
save: Boolean) {
|
||||
fun startDatabaseSaveRecycleBin(
|
||||
oldRecycleBin: Group?,
|
||||
newRecycleBin: Group?,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldRecycleBin)
|
||||
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newRecycleBin)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
|
||||
}, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveTemplatesGroup(oldTemplatesGroup: Group?,
|
||||
newTemplatesGroup: Group?,
|
||||
save: Boolean) {
|
||||
fun startDatabaseSaveTemplatesGroup(
|
||||
oldTemplatesGroup: Group?,
|
||||
newTemplatesGroup: Group?,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldTemplatesGroup)
|
||||
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newTemplatesGroup)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
|
||||
}, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
|
||||
newMaxHistoryItems: Int,
|
||||
save: Boolean) {
|
||||
fun startDatabaseSaveMaxHistoryItems(
|
||||
oldMaxHistoryItems: Int,
|
||||
newMaxHistoryItems: Int,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistoryItems)
|
||||
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
|
||||
}, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveMaxHistorySize(oldMaxHistorySize: Long,
|
||||
newMaxHistorySize: Long,
|
||||
save: Boolean) {
|
||||
fun startDatabaseSaveMaxHistorySize(
|
||||
oldMaxHistorySize: Long,
|
||||
newMaxHistorySize: Long,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistorySize)
|
||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
|
||||
}, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -726,59 +590,64 @@ class DatabaseTaskProvider(
|
||||
-------------------
|
||||
*/
|
||||
|
||||
fun startDatabaseSaveEncryption(oldEncryption: EncryptionAlgorithm,
|
||||
newEncryption: EncryptionAlgorithm,
|
||||
save: Boolean) {
|
||||
fun startDatabaseSaveEncryption(
|
||||
oldEncryption: EncryptionAlgorithm,
|
||||
newEncryption: EncryptionAlgorithm,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldEncryption)
|
||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
|
||||
}, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveKeyDerivation(oldKeyDerivation: KdfEngine,
|
||||
newKeyDerivation: KdfEngine,
|
||||
save: Boolean) {
|
||||
fun startDatabaseSaveKeyDerivation(
|
||||
oldKeyDerivation: KdfEngine,
|
||||
newKeyDerivation: KdfEngine,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldKeyDerivation)
|
||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
|
||||
}, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveIterations(oldIterations: Long,
|
||||
newIterations: Long,
|
||||
save: Boolean) {
|
||||
fun startDatabaseSaveIterations(
|
||||
oldIterations: Long,
|
||||
newIterations: Long,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldIterations)
|
||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
|
||||
}, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveMemoryUsage(oldMemoryUsage: Long,
|
||||
newMemoryUsage: Long,
|
||||
save: Boolean) {
|
||||
fun startDatabaseSaveMemoryUsage(
|
||||
oldMemoryUsage: Long,
|
||||
newMemoryUsage: Long,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMemoryUsage)
|
||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
|
||||
}, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveParallelism(oldParallelism: Long,
|
||||
newParallelism: Long,
|
||||
save: Boolean) {
|
||||
fun startDatabaseSaveParallelism(
|
||||
oldParallelism: Long,
|
||||
newParallelism: Long,
|
||||
save: Boolean
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism)
|
||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
|
||||
}, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -788,18 +657,32 @@ class DatabaseTaskProvider(
|
||||
start(Bundle().apply {
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
|
||||
}
|
||||
, ACTION_DATABASE_SAVE)
|
||||
}, ACTION_DATABASE_SAVE)
|
||||
}
|
||||
|
||||
fun startChallengeResponded(response: ByteArray?) {
|
||||
start(Bundle().apply {
|
||||
putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response)
|
||||
}
|
||||
, ACTION_CHALLENGE_RESPONDED)
|
||||
}, ACTION_CHALLENGE_RESPONDED)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = DatabaseTaskProvider::class.java.name
|
||||
|
||||
fun Context.startDatabaseService(bundle: Bundle? = null, actionTask: String) {
|
||||
try {
|
||||
val intentDatabaseTask = Intent(
|
||||
applicationContext,
|
||||
DatabaseTaskNotificationService::class.java
|
||||
)
|
||||
if (bundle != null)
|
||||
intentDatabaseTask.putExtras(bundle)
|
||||
intentDatabaseTask.action = actionTask
|
||||
startService(intentDatabaseTask)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to perform database action", e)
|
||||
Toast.makeText(this, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.annotation.StringRes
|
||||
|
||||
data class ProgressMessage(
|
||||
@StringRes
|
||||
var titleId: Int,
|
||||
var titleId: Int? = null,
|
||||
@StringRes
|
||||
var messageId: Int? = null,
|
||||
@StringRes
|
||||
|
||||
@@ -24,14 +24,44 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateEngine
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.database.exception.*
|
||||
import com.kunzisoft.keepass.database.exception.CopyEntryDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.CopyGroupDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.CorruptedDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseInputException
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.EmptyKeyDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.HardwareKeyDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.InvalidAlgorithmDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.InvalidCredentialsDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.KDFMemoryDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.LocalizedException
|
||||
import com.kunzisoft.keepass.database.exception.MergeDatabaseKDBException
|
||||
import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.NoMemoryDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.SignatureDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
||||
import com.kunzisoft.keepass.database.exception.VersionDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.XMLMalformedDatabaseException
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_CREDENTIAL_ID
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_FLAG_BE
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_FLAG_BS
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_PRIVATE_KEY
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_RELYING_PARTY
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USERNAME
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USER_HANDLE
|
||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.PASSKEY_FIELD
|
||||
|
||||
fun DatabaseException.getLocalizedMessage(resources: Resources): String? =
|
||||
fun LocalizedException.getLocalizedMessage(resources: Resources): String? =
|
||||
when (this) {
|
||||
is FileNotFoundDatabaseException -> resources.getString(R.string.file_not_found_content)
|
||||
is CorruptedDatabaseException -> resources.getString(R.string.corrupted_file)
|
||||
is InvalidAlgorithmDatabaseException -> resources.getString(R.string.invalid_algorithm)
|
||||
is UnknownDatabaseLocationException -> resources.getString(R.string.error_location_unknown)
|
||||
is RegisterInReadOnlyDatabaseException -> resources.getString(R.string.error_save_read_only)
|
||||
is HardwareKeyDatabaseException -> resources.getString(R.string.error_hardware_key_unsupported)
|
||||
is EmptyKeyDatabaseException -> resources.getString(R.string.error_empty_key)
|
||||
is SignatureDatabaseException -> resources.getString(R.string.invalid_db_sig)
|
||||
@@ -63,6 +93,11 @@ fun TemplateField.isStandardPasswordName(context: Context, name: String): Boolea
|
||||
|| name == getLocalizedName(context, LABEL_PASSWORD)
|
||||
}
|
||||
|
||||
fun TemplateField.isPasskeyLabel(context: Context, name: String): Boolean {
|
||||
return name.equals(PASSKEY_FIELD, true)
|
||||
|| name == getLocalizedName(context, PASSKEY_FIELD)
|
||||
}
|
||||
|
||||
fun TemplateField.getLocalizedName(context: Context?, name: String): String {
|
||||
if (context == null
|
||||
|| TemplateEngine.containsTemplateDecorator(name)
|
||||
@@ -107,6 +142,15 @@ fun TemplateField.getLocalizedName(context: Context?, name: String): String {
|
||||
LABEL_SECURE_NOTE.equals(name, true) -> context.getString(R.string.secure_note)
|
||||
LABEL_MEMBERSHIP.equals(name, true) -> context.getString(R.string.membership)
|
||||
|
||||
PASSKEY_FIELD.equals(name, true) -> context.getString(R.string.passkey)
|
||||
FIELD_USERNAME.equals(name, true) -> context.getString(R.string.passkey_username)
|
||||
FIELD_PRIVATE_KEY.equals(name, true) -> context.getString(R.string.passkey_private_key)
|
||||
FIELD_CREDENTIAL_ID.equals(name, true) -> context.getString(R.string.passkey_credential_id)
|
||||
FIELD_USER_HANDLE.equals(name, true) -> context.getString(R.string.passkey_user_handle)
|
||||
FIELD_RELYING_PARTY.equals(name, true) -> context.getString(R.string.passkey_relying_party)
|
||||
FIELD_FLAG_BE.equals(name, true) -> context.getString(R.string.passkey_backup_eligibility)
|
||||
FIELD_FLAG_BS.equals(name, true) -> context.getString(R.string.passkey_backup_state)
|
||||
|
||||
else -> name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,16 @@ package com.kunzisoft.keepass.database.helper
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil.searchSubDomains
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||
|
||||
object SearchHelper {
|
||||
|
||||
@@ -41,38 +48,113 @@ object SearchHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to perform actions if item is found or not after an auto search in [database]
|
||||
* Get the concrete web domain AKA without sub domain if needed
|
||||
*/
|
||||
fun checkAutoSearchInfo(context: Context,
|
||||
database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo?,
|
||||
onItemsFound: (openedDatabase: ContextualDatabase,
|
||||
items: List<EntryInfo>) -> Unit,
|
||||
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
|
||||
onDatabaseClosed: () -> Unit) {
|
||||
if (database == null || !database.loaded) {
|
||||
onDatabaseClosed.invoke()
|
||||
} else if (TimeoutHelper.checkTime(context)) {
|
||||
var searchWithoutUI = false
|
||||
if (searchInfo != null
|
||||
&& !searchInfo.manualSelection
|
||||
&& !searchInfo.containsOnlyNullValues()) {
|
||||
// If search provide results
|
||||
database.createVirtualGroupFromSearchInfo(
|
||||
searchInfoString = searchInfo.toString(),
|
||||
searchInfoByDomain = searchInfo.isASearchByDomain(),
|
||||
max = MAX_SEARCH_ENTRY
|
||||
)?.let { searchGroup ->
|
||||
if (searchGroup.numberOfChildEntries > 0) {
|
||||
searchWithoutUI = true
|
||||
onItemsFound.invoke(database,
|
||||
searchGroup.getChildEntriesInfo(database))
|
||||
private fun getConcreteWebDomain(
|
||||
context: Context,
|
||||
webDomain: String?,
|
||||
concreteWebDomain: (searchSubDomains: Boolean, concreteWebDomain: String?) -> Unit
|
||||
) {
|
||||
val domain = webDomain
|
||||
val searchSubDomains = searchSubDomains(context)
|
||||
if (domain != null) {
|
||||
// Warning, web domain can contains IP, don't crop in this case
|
||||
if (searchSubDomains
|
||||
|| Regex(SearchInfo.WEB_IP_REGEX).matches(domain)) {
|
||||
concreteWebDomain.invoke(searchSubDomains, webDomain)
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val publicSuffixList = PublicSuffixList(context)
|
||||
val publicSuffix = publicSuffixList
|
||||
.getPublicSuffixPlusOne(domain).await()
|
||||
withContext(Dispatchers.Main) {
|
||||
concreteWebDomain.invoke(false, publicSuffix)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!searchWithoutUI) {
|
||||
} else {
|
||||
concreteWebDomain.invoke(searchSubDomains, null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create search parameters asynchronously from [SearchInfo]
|
||||
*/
|
||||
fun SearchInfo.getSearchParametersFromSearchInfo(
|
||||
context: Context,
|
||||
callback: (SearchParameters) -> Unit
|
||||
) {
|
||||
getConcreteWebDomain(
|
||||
context,
|
||||
webDomain
|
||||
) { searchSubDomains, concreteDomain ->
|
||||
var query = this.toString()
|
||||
if (isDomainSearch && concreteDomain != null)
|
||||
query = concreteDomain
|
||||
callback.invoke(
|
||||
SearchParameters().apply {
|
||||
searchQuery = query
|
||||
searchOptions = optionsString()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,20 +33,22 @@ import java.util.*
|
||||
class PasswordGenerator(private val resources: Resources) {
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun generatePassword(length: Int,
|
||||
upperCase: Boolean,
|
||||
lowerCase: Boolean,
|
||||
digits: Boolean,
|
||||
minus: Boolean,
|
||||
underline: Boolean,
|
||||
space: Boolean,
|
||||
specials: Boolean,
|
||||
brackets: Boolean,
|
||||
extended: Boolean,
|
||||
considerChars: String,
|
||||
ignoreChars: String,
|
||||
atLeastOneFromEach: Boolean,
|
||||
excludeAmbiguousChar: Boolean): String {
|
||||
fun generatePassword(
|
||||
length: Int,
|
||||
upperCase: Boolean,
|
||||
lowerCase: Boolean,
|
||||
digits: Boolean,
|
||||
minus: Boolean,
|
||||
underline: Boolean,
|
||||
space: Boolean,
|
||||
specials: Boolean,
|
||||
brackets: Boolean,
|
||||
extended: Boolean,
|
||||
considerChars: String,
|
||||
ignoreChars: String,
|
||||
atLeastOneFromEach: Boolean,
|
||||
excludeAmbiguousChar: Boolean
|
||||
): String {
|
||||
// Desired password length is 0 or less
|
||||
if (length <= 0) {
|
||||
throw IllegalArgumentException(resources.getString(R.string.error_wrong_length))
|
||||
@@ -228,7 +230,7 @@ class PasswordGenerator(private val resources: Resources) {
|
||||
private const val MINUS_CHAR = "-"
|
||||
private const val UNDERLINE_CHAR = "_"
|
||||
private const val SPACE_CHAR = " "
|
||||
private const val SPECIAL_CHARS = "!\"#$%&'*+,./:;=?@\\^`"
|
||||
private const val SPECIAL_CHARS = "&/,^@.#:%\\='$!?*`;+\"|~"
|
||||
private const val BRACKET_CHARS = "[]{}()<>"
|
||||
private const val AMBIGUOUS_CHARS = "iI|lLoO01"
|
||||
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
package com.kunzisoft.keepass.receivers
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.utils.DexUtil
|
||||
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
||||
|
||||
class DexModeReceiver : BroadcastReceiver() {
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -164,7 +165,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
||||
}
|
||||
}
|
||||
if (attachmentNotificationList.isEmpty()) {
|
||||
stopSelf()
|
||||
stopService()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,7 +195,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
||||
private fun newNotification(attachmentNotification: AttachmentNotification) {
|
||||
|
||||
val pendingContentIntent = PendingIntent.getActivity(this,
|
||||
0,
|
||||
randomRequestCode(),
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
setDataAndType(attachmentNotification.uri,
|
||||
@@ -208,7 +209,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
||||
)
|
||||
|
||||
val pendingDeleteIntent = PendingIntent.getService(this,
|
||||
0,
|
||||
randomRequestCode(),
|
||||
Intent(this, AttachmentFileNotificationService::class.java).apply {
|
||||
// No action to delete the service
|
||||
putExtra(FILE_URI_KEY, attachmentNotification.uri)
|
||||
|
||||
@@ -62,7 +62,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
}
|
||||
// Stop the service
|
||||
stopSelf()
|
||||
stopService()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
@@ -274,10 +274,12 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
val containsPasswordToCopy = entry.password.isNotEmpty()
|
||||
&& PreferencesUtil.allowCopyProtectedFields(context)
|
||||
val containsOTPToCopy = entry.containsCustomField(OTP_TOKEN_FIELD)
|
||||
val containsExtraFieldToCopy = entry.customFields.isNotEmpty()
|
||||
&& (entry.containsCustomFieldsNotProtected()
|
||||
val customFields = entry.getCustomFieldsForFilling()
|
||||
val containsExtraFieldToCopy = customFields.isNotEmpty()
|
||||
&& (customFields.any { !it.protectedValue.isProtected }
|
||||
||
|
||||
(entry.containsCustomFieldsProtected() && PreferencesUtil.allowCopyProtectedFields(context))
|
||||
(customFields.any { it.protectedValue.isProtected }
|
||||
&& PreferencesUtil.allowCopyProtectedFields(context))
|
||||
)
|
||||
|
||||
var startService = false
|
||||
@@ -320,7 +322,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
if (containsExtraFieldToCopy) {
|
||||
try {
|
||||
var anonymousFieldNumber = 0
|
||||
entry.customFields.forEach { field ->
|
||||
entry.getCustomFieldsForFilling().forEach { field ->
|
||||
//If value is not protected or allowed
|
||||
if ((!field.protectedValue.isProtected
|
||||
|| PreferencesUtil.allowCopyProtectedFields(context))
|
||||
|
||||
@@ -61,13 +61,14 @@ import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
|
||||
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
@@ -175,7 +176,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
progressMessage: ProgressMessage
|
||||
)
|
||||
fun onActionStopped(
|
||||
database: ContextualDatabase
|
||||
database: ContextualDatabase? = null
|
||||
)
|
||||
fun onActionFinished(
|
||||
database: ContextualDatabase,
|
||||
@@ -218,7 +219,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
Log.i(TAG, "Database file modified " +
|
||||
"$previousDatabaseInfo != $lastFileDatabaseInfo ")
|
||||
// Call listener to indicate a change in database info
|
||||
if (!mSaveState && previousDatabaseInfo != null) {
|
||||
if (!mSaveState) {
|
||||
mDatabaseInfoListeners.forEach { listener ->
|
||||
listener.onDatabaseInfoChanged(
|
||||
previousDatabaseInfo,
|
||||
@@ -261,11 +262,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
)
|
||||
}
|
||||
} else {
|
||||
/* Do not stopped here, service cannot be connected
|
||||
mActionTaskListeners.forEach { actionTaskListener ->
|
||||
actionTaskListener.onActionStopped(
|
||||
database
|
||||
)
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,7 +339,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
val intentAction = intent?.action
|
||||
|
||||
if (intentAction == null && !database.loaded) {
|
||||
stopSelf()
|
||||
stopService()
|
||||
}
|
||||
|
||||
val actionRunnable: ActionRunnable? = when (intentAction) {
|
||||
@@ -446,10 +448,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
TimeoutHelper.releaseTemporarilyDisableTimeout()
|
||||
// Stop service after save if user remove task
|
||||
if (save && mTaskRemovedRequested) {
|
||||
actionOnLock()
|
||||
stopService()
|
||||
} else if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) {
|
||||
if (!database.loaded) {
|
||||
stopSelf()
|
||||
stopService()
|
||||
} else {
|
||||
// Restart the service to open lock notification
|
||||
try {
|
||||
@@ -534,11 +536,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
|
||||
val notificationBuilder = buildNewNotification().apply {
|
||||
setSmallIcon(iconId)
|
||||
intent?.let {
|
||||
setContentTitle(getString(
|
||||
intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mProgressMessage.titleId))
|
||||
)
|
||||
}
|
||||
val titleId = mProgressMessage.titleId?.let {
|
||||
intent?.getIntExtra(DATABASE_TASK_TITLE_KEY, it)
|
||||
} ?: R.string.app_name
|
||||
setContentTitle(getString(titleId))
|
||||
setAutoCancel(false)
|
||||
setContentIntent(null)
|
||||
}
|
||||
@@ -550,7 +551,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
// Build Intents for notification action
|
||||
val pendingDatabaseIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
randomRequestCode(),
|
||||
Intent(this, GroupActivity::class.java),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
@@ -660,7 +661,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
}
|
||||
|
||||
private fun updateMessage(resId: Int) {
|
||||
mProgressMessage.messageId = resId
|
||||
mProgressMessage = mProgressMessage.copy(
|
||||
messageId = resId
|
||||
)
|
||||
notifyProgressMessage()
|
||||
}
|
||||
|
||||
@@ -672,13 +675,19 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
updateMessage(R.string.decrypting_db)
|
||||
}
|
||||
|
||||
override fun actionOnLock() {
|
||||
override fun stopService() {
|
||||
if (!TimeoutHelper.temporarilyDisableLock) {
|
||||
closeDatabase(mDatabase)
|
||||
// Remove the database during the lock
|
||||
// And notify each subscriber
|
||||
mDatabase = null
|
||||
mDatabaseListeners.forEach { listener ->
|
||||
listener.onDatabaseRetrieved(null)
|
||||
}
|
||||
// Remove the lock timer (no more needed if it exists)
|
||||
TimeoutHelper.cancelLockTimer(this)
|
||||
// Service is stopped after receive the broadcast
|
||||
super.actionOnLock()
|
||||
super.stopService()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -709,9 +718,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
notifyProgressMessage()
|
||||
HardwareKeyActivity
|
||||
.launchHardwareKeyActivity(
|
||||
this@DatabaseTaskNotificationService,
|
||||
hardwareKey,
|
||||
seed
|
||||
context = this@DatabaseTaskNotificationService,
|
||||
hardwareKey = hardwareKey,
|
||||
seed = seed
|
||||
)
|
||||
// Wait the response
|
||||
mProgressMessage.apply {
|
||||
@@ -722,7 +731,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
// Close channels
|
||||
closeChallengeResponse()
|
||||
// Restore previous message
|
||||
mProgressMessage = previousMessage
|
||||
mProgressMessage = previousMessage.apply {
|
||||
cancelable = null
|
||||
}
|
||||
notifyProgressMessage()
|
||||
}
|
||||
return response
|
||||
@@ -1385,6 +1396,15 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
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 {
|
||||
val groupsId = mutableListOf<NodeId<*>>()
|
||||
val entriesId = mutableListOf<NodeId<UUID>>()
|
||||
|
||||
@@ -27,7 +27,7 @@ import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
@@ -55,7 +55,7 @@ class KeyboardEntryNotificationService : LockNotificationService() {
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
}
|
||||
// Stop the service
|
||||
stopSelf()
|
||||
stopService()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
package com.kunzisoft.keepass.services
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.app.ServiceCompat
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.LockReceiver
|
||||
import com.kunzisoft.keepass.utils.registerLockReceiver
|
||||
@@ -29,13 +28,7 @@ import com.kunzisoft.keepass.utils.unregisterLockReceiver
|
||||
abstract class LockNotificationService : NotificationService() {
|
||||
|
||||
private var mLockReceiver: LockReceiver = LockReceiver {
|
||||
actionOnLock()
|
||||
}
|
||||
|
||||
protected open fun actionOnLock() {
|
||||
// Stop the service in all cases
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
stopService()
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -46,7 +39,7 @@ abstract class LockNotificationService : NotificationService() {
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
if (!TimeoutHelper.temporarilyDisableLock) {
|
||||
actionOnLock()
|
||||
stopService()
|
||||
}
|
||||
super.onTaskRemoved(rootIntent)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import android.util.TypedValue
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||
@@ -114,6 +115,12 @@ abstract class NotificationService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun stopService() {
|
||||
// Stop the service in all cases
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
protected fun defineTimerJob(builder: NotificationCompat.Builder,
|
||||
type: NotificationServiceType,
|
||||
timeoutMilliseconds: Long,
|
||||
|
||||
@@ -19,9 +19,12 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.settings
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
class AutofillSettingsActivity : ExternalSettingsActivity() {
|
||||
|
||||
override fun retrieveTitle(): Int {
|
||||
|
||||
@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.settings
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
@@ -29,6 +30,7 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistAppIdPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistWebDomainPreferenceDialogFragmentCompat
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
class AutofillSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
@@ -39,11 +41,14 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
autofillInlineSuggestionsPreference?.isVisible = false
|
||||
}
|
||||
|
||||
val autofillAskSaveDataPreference: TwoStatePreference? = findPreference(getString(R.string.autofill_ask_to_save_data_key))
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
autofillAskSaveDataPreference?.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDisplayPreferenceDialog(preference: Preference) {
|
||||
var otherDialogFragment = false
|
||||
|
||||
var dialogFragment: DialogFragment? = null
|
||||
|
||||
when (preference.key) {
|
||||
@@ -53,7 +58,7 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
|
||||
getString(R.string.autofill_web_domain_blocklist_key) -> {
|
||||
dialogFragment = AutofillBlocklistWebDomainPreferenceDialogFragmentCompat.newInstance(preference.key)
|
||||
}
|
||||
else -> otherDialogFragment = true
|
||||
else -> {}
|
||||
}
|
||||
|
||||
if (dialogFragment != null) {
|
||||
@@ -62,7 +67,7 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
|
||||
dialogFragment.show(parentFragmentManager, TAG_AUTOFILL_PREF_FRAGMENT)
|
||||
}
|
||||
// Could not be handled here. Try with the super method.
|
||||
else if (otherDialogFragment) {
|
||||
else {
|
||||
super.onDisplayPreferenceDialog(preference)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ abstract class ExternalSettingsActivity : DatabaseModeActivity() {
|
||||
|
||||
private var lockView: FloatingActionButton? = null
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
||||
@@ -21,20 +21,25 @@ package com.kunzisoft.keepass.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainPreferenceFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private var mCallback: Callback? = null
|
||||
|
||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
private var mDatabaseLoaded: Boolean = false
|
||||
private val mDatabase: ContextualDatabase?
|
||||
get() = mDatabaseViewModel.database
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
@@ -50,20 +55,24 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
|
||||
mCallback = null
|
||||
super.onDetach()
|
||||
}
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
|
||||
mDatabaseLoaded = database?.loaded == true
|
||||
checkDatabaseLoaded()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mDatabaseViewModel.databaseState.collect { database ->
|
||||
checkDatabaseLoaded(database?.loaded == true)
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
private fun checkDatabaseLoaded() {
|
||||
private fun checkDatabaseLoaded(isDatabaseLoaded: Boolean) {
|
||||
findPreference<Preference>(getString(R.string.settings_database_key))
|
||||
?.isEnabled = mDatabaseLoaded
|
||||
?.isEnabled = isDatabaseLoaded
|
||||
|
||||
findPreference<PreferenceCategory>(getString(R.string.settings_database_category_key))
|
||||
?.isVisible = mDatabaseLoaded
|
||||
?.isVisible = isDatabaseLoaded
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
@@ -119,7 +128,7 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
checkDatabaseLoaded()
|
||||
checkDatabaseLoaded(mDatabase?.loaded == true)
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
|
||||
@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.settings
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
@@ -30,6 +29,7 @@ import android.view.autofill.AutofillManager
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.preference.ListPreference
|
||||
@@ -47,7 +47,7 @@ import com.kunzisoft.keepass.icons.IconPackChooser
|
||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.settings.preference.IconPackListPreference
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DurationDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
||||
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||
import com.kunzisoft.keepass.utils.UriUtil.releaseAllUnnecessaryPermissionUris
|
||||
|
||||
@@ -119,7 +119,16 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
|
||||
activity?.let { activity ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val autoFillEnablePreference: TwoStatePreference? = findPreference(getString(R.string.settings_autofill_enable_key))
|
||||
|
||||
// Hide Passkeys settings if needed
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
findPreference<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 ->
|
||||
if (autofillManager.hasEnabledAutofillServices())
|
||||
autoFillEnablePreference?.isChecked = autofillManager.hasEnabledAutofillServices()
|
||||
@@ -161,7 +170,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
val intent =
|
||||
Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
|
||||
intent.data =
|
||||
Uri.parse("package:com.kunzisoft.keepass.autofill.KeeAutofillService")
|
||||
"package:com.kunzisoft.keepass.autofill.KeeAutofillService".toUri()
|
||||
Log.d(javaClass.name, "Autofill enable service: intent=$intent")
|
||||
startActivity(intent)
|
||||
} else {
|
||||
@@ -171,7 +180,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
findPreference<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
|
||||
}
|
||||
|
||||
findPreference<Preference>(getString(R.string.autofill_explanation_key))?.setOnPreferenceClickListener {
|
||||
context?.openUrl(R.string.autofill_explanation_url)
|
||||
false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
findPreference<Preference>(getString(R.string.passkeys_explanation_key))?.setOnPreferenceClickListener {
|
||||
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 {
|
||||
startActivity(Intent(context, AutofillSettingsActivity::class.java))
|
||||
false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
findPreference<Preference>(getString(R.string.autofill_explanation_key))?.setOnPreferenceClickListener {
|
||||
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 ->
|
||||
@@ -530,7 +553,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
super.onResume()
|
||||
activity?.let { activity ->
|
||||
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)
|
||||
autoFillEnablePreference.isChecked = autofillManager != null
|
||||
&& autofillManager.hasEnabledAutofillServices()
|
||||
|
||||
@@ -19,13 +19,21 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.settings
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.toColorInt
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.TwoStatePreference
|
||||
@@ -39,19 +47,40 @@ import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.helper.*
|
||||
import com.kunzisoft.keepass.database.helper.getLocalizedName
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.settings.preference.*
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.*
|
||||
import com.kunzisoft.keepass.settings.preference.DialogColorPreference
|
||||
import com.kunzisoft.keepass.settings.preference.DialogListExplanationPreference
|
||||
import com.kunzisoft.keepass.settings.preference.InputKdfNumberPreference
|
||||
import com.kunzisoft.keepass.settings.preference.InputKdfSizePreference
|
||||
import com.kunzisoft.keepass.settings.preference.InputNumberPreference
|
||||
import com.kunzisoft.keepass.settings.preference.InputTextPreference
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseColorPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDataCompressionPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDefaultUsernamePreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDescriptionPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseKeyDerivationPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMaxHistorySizePreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMemoryUsagePreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseNamePreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseParallelismPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRecycleBinGroupPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRoundsPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseTemplatesGroupPreferenceDialogFragmentCompat
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.getSerializableCompat
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval {
|
||||
|
||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
private var mDatabase: ContextualDatabase? = null
|
||||
private val mDatabase: ContextualDatabase?
|
||||
get() = mDatabaseViewModel.database
|
||||
private var mDatabaseReadOnly: Boolean = false
|
||||
private var mMergeDataAllowed: Boolean = false
|
||||
private var mDatabaseAutoSaveEnabled: Boolean = true
|
||||
@@ -114,19 +143,46 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mDatabaseViewModel.actionState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||
onDatabaseActionFinished(
|
||||
uiState.database,
|
||||
uiState.actionTask,
|
||||
uiState.result
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mDatabaseViewModel.databaseState.collect { database ->
|
||||
database?.let {
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
|
||||
|
||||
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
|
||||
mDatabase = database
|
||||
view.resetAppTimeoutWhenViewTouchedOrFocused(requireContext(), database?.loaded)
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) {
|
||||
onDatabaseActionFinished(it.database, it.actionTask, it.result)
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
mDatabaseViewModel.databaseState.collect { database ->
|
||||
view.resetAppTimeoutWhenViewTouchedOrFocused(
|
||||
context = requireContext(),
|
||||
databaseLoaded = database?.loaded
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,29 +223,26 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
mDatabaseViewModel.reloadDatabase(false)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
mDatabase = database
|
||||
mDatabaseReadOnly = database?.isReadOnly == true
|
||||
mMergeDataAllowed = database?.isMergeDataAllowed() == true
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
mDatabaseReadOnly = database.isReadOnly
|
||||
mMergeDataAllowed = database.isMergeDataAllowed()
|
||||
|
||||
mDatabase?.let {
|
||||
if (it.loaded) {
|
||||
when (mScreen) {
|
||||
Screen.DATABASE -> {
|
||||
onCreateDatabasePreference(it)
|
||||
}
|
||||
Screen.DATABASE_SECURITY -> {
|
||||
onCreateDatabaseSecurityPreference(it)
|
||||
}
|
||||
Screen.DATABASE_MASTER_KEY -> {
|
||||
onCreateDatabaseMasterKeyPreference(it)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
if (database.loaded) {
|
||||
when (mScreen) {
|
||||
Screen.DATABASE -> {
|
||||
onCreateDatabasePreference(database)
|
||||
}
|
||||
Screen.DATABASE_SECURITY -> {
|
||||
onCreateDatabaseSecurityPreference(database)
|
||||
}
|
||||
Screen.DATABASE_MASTER_KEY -> {
|
||||
onCreateDatabaseMasterKeyPreference(database)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
} else {
|
||||
Log.e(javaClass.name, "Database isn't ready")
|
||||
}
|
||||
} else {
|
||||
Log.e(javaClass.name, "Database isn't ready")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,7 +511,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newDefaultUsername
|
||||
} else {
|
||||
mDatabase?.defaultUsername = oldDefaultUsername
|
||||
database.defaultUsername = oldDefaultUsername
|
||||
oldDefaultUsername
|
||||
}
|
||||
dbDefaultUsernamePref?.summary = defaultUsernameToShow
|
||||
@@ -471,7 +524,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newColor
|
||||
} else {
|
||||
mDatabase?.customColor = Color.parseColor(oldColor)
|
||||
database.customColor = oldColor.toColorInt()
|
||||
oldColor
|
||||
}
|
||||
dbCustomColorPref?.summary = defaultColorToShow
|
||||
@@ -483,7 +536,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newCompression
|
||||
} else {
|
||||
mDatabase?.compressionAlgorithm = oldCompression
|
||||
database.compressionAlgorithm = oldCompression
|
||||
oldCompression
|
||||
}
|
||||
dbDataCompressionPref?.summary = algorithmToShow?.getLocalizedName(resources)
|
||||
@@ -497,7 +550,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
} else {
|
||||
oldRecycleBin
|
||||
}
|
||||
mDatabase?.setRecycleBin(recycleBinToShow)
|
||||
database.setRecycleBin(recycleBinToShow)
|
||||
refreshRecycleBinGroup(database)
|
||||
}
|
||||
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK -> {
|
||||
@@ -509,7 +562,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
} else {
|
||||
oldTemplatesGroup
|
||||
}
|
||||
mDatabase?.setTemplatesGroup(templatesGroupToShow)
|
||||
database.setTemplatesGroup(templatesGroupToShow)
|
||||
refreshTemplatesGroup(database)
|
||||
}
|
||||
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK -> {
|
||||
@@ -519,7 +572,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newMaxHistoryItems
|
||||
} else {
|
||||
mDatabase?.historyMaxItems = oldMaxHistoryItems
|
||||
database.historyMaxItems = oldMaxHistoryItems
|
||||
oldMaxHistoryItems
|
||||
}
|
||||
dbMaxHistoryItemsPref?.summary = maxHistoryItemsToShow.toString()
|
||||
@@ -531,7 +584,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newMaxHistorySize
|
||||
} else {
|
||||
mDatabase?.historyMaxSize = oldMaxHistorySize
|
||||
database.historyMaxSize = oldMaxHistorySize
|
||||
oldMaxHistorySize
|
||||
}
|
||||
dbMaxHistorySizePref?.summary = maxHistorySizeToShow.toString()
|
||||
@@ -549,7 +602,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newEncryption
|
||||
} else {
|
||||
mDatabase?.encryptionAlgorithm = oldEncryption
|
||||
database.encryptionAlgorithm = oldEncryption
|
||||
oldEncryption
|
||||
}
|
||||
mEncryptionAlgorithmPref?.summary = algorithmToShow.toString()
|
||||
@@ -561,7 +614,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newKeyDerivationEngine
|
||||
} else {
|
||||
mDatabase?.kdfEngine = oldKeyDerivationEngine
|
||||
database.kdfEngine = oldKeyDerivationEngine
|
||||
oldKeyDerivationEngine
|
||||
}
|
||||
mKeyDerivationPref?.summary = kdfEngineToShow.toString()
|
||||
@@ -578,7 +631,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newIterations
|
||||
} else {
|
||||
mDatabase?.numberKeyEncryptionRounds = oldIterations
|
||||
database.numberKeyEncryptionRounds = oldIterations
|
||||
oldIterations
|
||||
}
|
||||
mRoundPref?.summary = roundsToShow.toString()
|
||||
@@ -590,7 +643,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newMemoryUsage
|
||||
} else {
|
||||
mDatabase?.memoryUsage = oldMemoryUsage
|
||||
database.memoryUsage = oldMemoryUsage
|
||||
oldMemoryUsage
|
||||
}
|
||||
mMemoryPref?.summary = memoryToShow.toString()
|
||||
@@ -602,7 +655,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (result.isSuccess) {
|
||||
newParallelism
|
||||
} else {
|
||||
mDatabase?.parallelism = oldParallelism
|
||||
database.parallelism = oldParallelism
|
||||
oldParallelism
|
||||
}
|
||||
mParallelismPref?.summary = parallelismToShow.toString()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user