Compare commits

...

479 Commits
3.0.3 ... 3.3.1

Author SHA1 Message Date
J-Jamet
d7decba3f5 Merge branch 'release/3.3.1' 2022-02-26 18:16:59 +01:00
Oğuz Ersen
671364f395 Translated using Weblate (Turkish)
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-02-26 17:20:50 +01:00
Eric
d0072237e7 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-02-26 17:20:50 +01:00
Ihor Hordiichuk
d11c001e0d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-02-26 17:20:50 +01:00
solokot
a197453ce0 Translated using Weblate (Russian)
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-02-26 17:20:49 +01:00
Matthaiks
d1586a8d80 Translated using Weblate (Polish)
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-02-26 17:20:49 +01:00
Kunzisoft
6e959e415f Translated using Weblate (French)
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2022-02-26 17:20:49 +01:00
J-Jamet
8a8e14701b Merge branch 'translations' into develop 2022-02-26 17:14:07 +01:00
J-Jamet
60bb144020 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2022-02-26 17:13:29 +01:00
J-Jamet
b561db3a67 Workaround to fill OTP token in multiple fields with Magikeyboard (long press) #1158 2022-02-26 15:19:06 +01:00
J-Jamet
b0e722acce Best autofill recognition #1250 2022-02-26 15:18:08 +01:00
J-Jamet
f79d32b22b Fix temp advanced unlocking #1245 2022-02-26 12:45:59 +01:00
Hosted Weblate
3579606d1e Merge branch 'origin/develop' into Weblate. 2022-02-26 12:31:54 +01:00
J-Jamet
b93c6266a6 Upgrade CHANGELOG 2022-02-26 12:30:28 +01:00
J-Jamet
ea9c530667 Fix "other" search filter 2022-02-26 12:29:07 +01:00
J-Jamet
6ea95c050a Fix filter string #1249 2022-02-26 12:14:04 +01:00
SC
5a99a28195 Translated using Weblate (Portuguese)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2022-02-25 20:55:58 +01:00
Vitor Henrique
0cd5351c07 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.8% (585 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-02-25 20:55:58 +01:00
VfBFan
148bed801b Translated using Weblate (German)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-02-25 20:55:57 +01:00
Larry Day
87a1a3ba1b Translated using Weblate (Czech)
Currently translated at 96.7% (573 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2022-02-25 20:55:57 +01:00
J-Jamet
20d2c10bba Upgrade room to 2.4.2 2022-02-25 12:50:01 +01:00
J-Jamet
3eed5e395e Upgrade to 3.3.1
Fix Japanese keyboard in search #1248
2022-02-25 12:44:38 +01:00
Oğuz Ersen
870fbaa05c Translated using Weblate (Turkish)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-02-22 01:54:58 +01:00
Eric
55868f68da Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-02-22 01:54:58 +01:00
Ihor Hordiichuk
e6f0fbeab5 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-02-22 01:54:58 +01:00
solokot
8a554349b5 Translated using Weblate (Russian)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-02-22 01:54:57 +01:00
Matthaiks
ab87d4e564 Translated using Weblate (Polish)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-02-22 01:54:57 +01:00
Éfrit
8400a83b70 Translated using Weblate (French)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2022-02-22 01:54:57 +01:00
Jeffree Romero
5d2caa37a9 Translated using Weblate (Spanish)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2022-02-22 01:54:57 +01:00
Retrial
a6b7cfc2d3 Translated using Weblate (Greek)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-02-22 01:54:56 +01:00
J-Jamet
74107b90bb Max binary byte as 10 MB to prevent OOM #256 2022-02-20 17:16:36 +01:00
Hosted Weblate
9f4a915d43 Merge branch 'origin/develop' into Weblate. 2022-02-19 11:29:30 +01:00
Ismael Mirabile
3f1f22e1c3 Translated using Weblate (German)
Currently translated at 99.6% (590 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-02-19 11:29:30 +01:00
J-Jamet
1d98eb2b95 Merge tag '3.3.0' into develop
3.3.0
2022-02-19 11:01:11 +01:00
J-Jamet
f1b56fed16 Merge branch 'release/3.3.0' 2022-02-19 11:01:02 +01:00
J-Jamet
23e75c56ed Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2022-02-19 10:35:52 +01:00
J-Jamet
eafc9cc1fa Fix merge in kdb database 2022-02-19 10:31:29 +01:00
J-Jamet
3e3dbb22df Upgrade to version code 102 2022-02-19 10:21:24 +01:00
J-Jamet
127b8ba0bd Smaller time to the back to app handler 2022-02-18 17:46:12 +01:00
J-Jamet
8a8e588a01 Fix save search info 2022-02-18 17:30:25 +01:00
J-Jamet
3c1b010821 Default https if scheme is null 2022-02-18 15:27:49 +01:00
J-Jamet
739c938576 Fix save and app instance in selection mode 2022-02-18 15:09:15 +01:00
J-Jamet
69fed0c347 Remove redundant label 2022-02-18 13:37:25 +01:00
J-Jamet
d040258296 Fix lock button in settings 2022-02-18 13:14:33 +01:00
J-Jamet
abee18839b Fix selection mode 2022-02-18 13:12:01 +01:00
J-Jamet
9c0eb4e27e Upgrade to version 3.3.0_beta04 2022-02-18 11:32:00 +01:00
J-Jamet
10598ef5e5 Fix education hints #1192 2022-02-18 10:31:24 +01:00
J-Jamet
fa09e2d21d Refactoring education 2022-02-18 09:39:29 +01:00
Oğuz Ersen
11ddfe3445 Translated using Weblate (Turkish)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-02-17 20:55:25 +01:00
Eric
9abbc8876b Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-02-17 20:55:24 +01:00
Ihor Hordiichuk
c5cf99e13d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-02-17 20:55:24 +01:00
solokot
82ac1c8f5e Translated using Weblate (Russian)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-02-17 20:55:24 +01:00
Matthaiks
837f8146e4 Translated using Weblate (Polish)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-02-17 20:55:24 +01:00
Kunzisoft
9698c3f2ee Translated using Weblate (French)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2022-02-17 20:55:23 +01:00
Retrial
85290b8f9d Translated using Weblate (Greek)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-02-17 20:55:23 +01:00
J-Jamet
1ccf48d7a5 Simpler fragment main credential view 2022-02-16 19:40:47 +01:00
J-Jamet
2cbec9f2b6 Change version code -> 100 Happy Bithcode 2022-02-16 19:30:12 +01:00
J-Jamet
692be415fa Fix small string 2022-02-16 19:16:45 +01:00
J-Jamet
83d5218f92 Fix view init for timeout 2022-02-16 19:13:41 +01:00
J-Jamet
324b016324 Remove unused file 2022-02-16 19:09:38 +01:00
J-Jamet
005c9673f7 Fix history banner color 2022-02-16 18:55:04 +01:00
J-Jamet
b8762dc6e0 Upgrade CHANGELOG number 2022-02-16 13:31:56 +01:00
J-Jamet
93abff0768 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2022-02-16 13:19:49 +01:00
Hosted Weblate
ac1cfb43d8 Merge branch 'origin/develop' into Weblate. 2022-02-16 13:19:07 +01:00
Oğuz Ersen
30ec712782 Translated using Weblate (Turkish)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-02-16 13:18:57 +01:00
Eric
bbd93fbc5b Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-02-16 13:18:57 +01:00
Ihor Hordiichuk
c46cbaff4b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-02-16 13:18:56 +01:00
solokot
1e979b256a Translated using Weblate (Russian)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-02-16 13:18:56 +01:00
Vitor Henrique
50429b419f Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.6% (578 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-02-16 13:18:55 +01:00
Matthaiks
2c540be5a3 Translated using Weblate (Polish)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-02-16 13:18:54 +01:00
random r
155d3d222f Translated using Weblate (Italian)
Currently translated at 99.8% (591 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2022-02-16 13:18:54 +01:00
Kunzisoft
3fc3bf1302 Translated using Weblate (French)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2022-02-16 13:18:53 +01:00
Óscar Fernández Díaz
f0fafbbc6e Translated using Weblate (Spanish)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2022-02-16 13:18:53 +01:00
Retrial
5f7d980d89 Translated using Weblate (Greek)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-02-16 13:18:52 +01:00
C. Rüdinger
c3799ed7fe Translated using Weblate (German)
Currently translated at 99.4% (589 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-02-16 13:18:52 +01:00
J-Jamet
f7533af5d8 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2022-02-16 13:12:58 +01:00
J-Jamet
2c9504fb93 Upgrade strings 2022-02-16 13:12:26 +01:00
J-Jamet
c22b3f5335 Upgrade libs 2022-02-16 13:01:05 +01:00
J-Jamet
2aea7fd46d Upgrade to 3.3.0_beta02 2022-02-16 13:00:49 +01:00
J-Jamet
bb9dda0cf1 Fix warning 2022-02-16 12:07:48 +01:00
VfBFan
ced6f05e59 Translated using Weblate (German)
Currently translated at 99.4% (589 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-02-15 22:13:47 +01:00
J-Jamet
cc14b5d4d4 Fix bad folder modification #1239 2022-02-15 11:29:50 +01:00
J-Jamet
2922f7110d Merge branch 'develop' into release/3.3.0 2022-02-14 16:35:08 +01:00
J-Jamet
762a877c5e Fix compilation 2022-02-14 16:34:42 +01:00
J-Jamet
aace7a8260 Change version to 3.3.0_beta01 2022-02-14 16:30:31 +01:00
J-Jamet
ac91abcb45 Fix color in Switch 2022-02-14 16:24:19 +01:00
J-Jamet
5d6ccd84c9 Fix icon selection colors 2022-02-14 16:20:31 +01:00
J-Jamet
bd2f980f8e Add new theme "Reply" and fix colors 2022-02-14 16:00:27 +01:00
J-Jamet
69af330ba1 Fix nav header 2022-02-14 15:24:11 +01:00
J-Jamet
b7bc3bfa8f Fix color flickering 2022-02-14 14:10:31 +01:00
J-Jamet
678663ad66 Add "Simple" theme 2022-02-14 12:51:27 +01:00
Hosted Weblate
ec3aa14112 Merge branch 'origin/develop' into Weblate. 2022-02-14 10:58:41 +01:00
Milo Ivir
3cdd98dc1a Translated using Weblate (Croatian)
Currently translated at 100.0% (582 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2022-02-14 10:58:41 +01:00
Vitor Henrique
fc3270efd2 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (582 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-02-14 10:58:40 +01:00
J-Jamet
0dd871d3b5 Harmonize "cannot" 2022-02-14 10:53:33 +01:00
J-Jamet
41108ff407 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2022-02-14 10:49:42 +01:00
J-Jamet
99de8cf220 Update CHANGELOG 2022-02-13 17:06:44 +01:00
J-Jamet
a60e887eb3 Fix custom data parcelable #1236 2022-02-13 17:03:36 +01:00
J-Jamet
5133cb5c8f Fix auto type 2022-02-13 15:52:30 +01:00
J-Jamet
1607d84c21 Fix entry properties #1236 2022-02-13 15:47:20 +01:00
J-Jamet
cc0a990af8 Fix TOKEN translation #1232 2022-02-13 15:08:38 +01:00
J-Jamet
fec5bdcfc8 Harmonisation of main credential views 2022-02-13 14:59:39 +01:00
J-Jamet
648514d150 Refactor Password in MainCredential 2022-02-13 14:36:45 +01:00
J-Jamet
37b9745b6b Add lock in drawer menu 2022-02-13 14:21:49 +01:00
J-Jamet
f858b1144c Merge develop and selectable text 2022-02-13 13:49:41 +01:00
J-Jamet
c7cd7b83fa Change navigation view 2022-02-12 19:34:48 +01:00
J-Jamet
3cef086b69 Fix merge and save copy 2022-02-12 19:17:32 +01:00
J-Jamet
ca51f591de Visual point to see if a database is modified 2022-02-12 18:49:38 +01:00
Óscar Fernández Díaz
da63404a84 Translated using Weblate (Spanish)
Currently translated at 100.0% (582 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2022-02-12 17:56:13 +01:00
J-Jamet
de2160f992 Fix small bugs 2022-02-12 16:01:30 +01:00
J-Jamet
8b7bb36e66 Merge branch 'feature/Merge_from' into develop #1221 #1204 #840 2022-02-12 14:57:44 +01:00
J-Jamet
6ade91dafa Update CHANGELOG 2022-02-12 14:57:29 +01:00
J-Jamet
2ef66f3011 Add data modified after a merge 2022-02-12 14:50:54 +01:00
J-Jamet
5b2fd0bfbb Fix small issues 2022-02-12 14:44:47 +01:00
J-Jamet
ab392e2cf3 Show URI in drawer 2022-02-12 13:53:05 +01:00
J-Jamet
8eb922e93f Fix menu 2022-02-12 13:34:36 +01:00
J-Jamet
20900dce07 Merge branch 'develop' into feature/Merge_from 2022-02-12 13:27:41 +01:00
J-Jamet
c2705ff5d2 Fix small bugs 2022-02-12 13:27:22 +01:00
J-Jamet
8af70fa7b5 Fix merge and save copy in KDB database 2022-02-12 13:20:22 +01:00
J-Jamet
e7eb8099ac Revert code to merge same KDB database 2022-02-12 13:02:34 +01:00
J-Jamet
cedf2eafbb Revert "Remove unused code"
This reverts commit cea7f1c2d1.
2022-02-12 12:57:57 +01:00
J-Jamet
e7553c68a0 Merge attachment from KDB database 2022-02-12 12:57:26 +01:00
J-Jamet
cea7f1c2d1 Remove unused code 2022-02-12 12:40:34 +01:00
J-Jamet
45937f9c9c Do not import meta stream entries 2022-02-12 12:37:49 +01:00
J-Jamet
ee2a1ea924 Add merge for KDB database 2022-02-12 12:33:40 +01:00
J-Jamet
1851c205e9 Merge branch 'develop' into feature/Merge_from 2022-02-12 11:51:16 +01:00
J-Jamet
c4a3947cb3 Fix null pointer with cipher database entity 2022-02-12 11:51:05 +01:00
J-Jamet
d3e76bcf21 Ask for new credential with a merge #1221 2022-02-12 11:42:36 +01:00
J-Jamet
cf4e64d6c5 Merge branch 'develop' into feature/Merge_from 2022-02-12 00:29:19 +01:00
J-Jamet
4cac571f1b Refactoring password view to prepare hardware key #8 2022-02-12 00:24:58 +01:00
J-Jamet
445ed92ff7 Update open database methods 2022-02-11 20:58:23 +01:00
J-Jamet
6686ce15c1 Implementation fo "Merge from" and "Save a copy to" 2022-02-11 19:40:56 +01:00
J-Jamet
59f134e0cd Change icons 2022-02-11 18:33:22 +01:00
J-Jamet
aab3f8c56f Nav header as dedicated view 2022-02-11 15:53:21 +01:00
J-Jamet
17c26e2a96 Add navigation drawer 2022-02-11 15:24:59 +01:00
Ciki Momogi
aec9124ef5 Translated using Weblate (Indonesian)
Currently translated at 90.3% (526 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2022-02-10 19:54:43 +01:00
random r
2df1e9bc2e Translated using Weblate (Italian)
Currently translated at 100.0% (582 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2022-02-08 20:12:00 +01:00
J-Jamet
6ceecbfd0f Upgrade libs 2022-02-08 18:33:49 +01:00
J-Jamet
460726cadb Upgrade gradle 2022-02-08 18:14:09 +01:00
J-Jamet
32baa7b9f1 Fix searchable group 2022-02-08 15:45:24 +01:00
J-Jamet
f44e648ccc Fix and update max search 2022-02-08 14:49:25 +01:00
J-Jamet
2ad385acb6 Fix search filters depending on database version 2022-02-08 14:11:24 +01:00
J-Jamet
ebaec2eaf0 Fix visible available features 2022-02-08 13:39:16 +01:00
J-Jamet
dac8c2c15d Fix database v1 available features 2022-02-08 13:25:56 +01:00
J-Jamet
60965491b0 Add background to simple button 2022-02-08 12:50:51 +01:00
SC
19a3faa85a Translated using Weblate (Portuguese)
Currently translated at 100.0% (582 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2022-02-08 00:15:38 +01:00
SC
3779346eed Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (582 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-02-08 00:15:38 +01:00
Stephan Paternotte
185e263ddd Translated using Weblate (Dutch)
Currently translated at 100.0% (582 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2022-02-08 00:15:38 +01:00
J-Jamet
8c0d984955 Fix expires padding 2022-02-07 20:15:12 +01:00
J-Jamet
8d1012eda0 Upgrade CHANGELOG 2022-02-07 19:37:00 +01:00
J-Jamet
03f147e3d0 Merge branch 'feature/Searchable_Auto_Type' into develop 2022-02-07 19:35:07 +01:00
J-Jamet
13ef5d640c Add searchable group #1006 #905 2022-02-07 19:34:47 +01:00
J-Jamet
2e8eed5afe Visually remove auto type 2022-02-07 17:25:21 +01:00
J-Jamet
54653a5bea Add searchable and Auto-type in Group 2022-02-07 17:02:39 +01:00
Gabriel Cardoso
e29082eba3 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (582 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-02-05 18:57:49 +01:00
hokonch
b635e9bb0d Translated using Weblate (Japanese)
Currently translated at 100.0% (582 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2022-02-05 18:57:48 +01:00
J-Jamet
179bfdc3d2 Update CHANGELOG 2022-02-05 16:55:29 +01:00
J-Jamet
631cb104cd Fix styles 2022-02-05 16:48:30 +01:00
J-Jamet
1aeaf6855e Change search filters positions 2022-02-05 15:30:55 +01:00
J-Jamet
6bac1ace30 Fix multiple calls in search view 2022-02-05 15:20:51 +01:00
J-Jamet
488bab7c4d Fix UUID search 2022-02-05 14:43:53 +01:00
J-Jamet
69fbfd7723 Fix tag filter 2022-02-05 14:34:10 +01:00
J-Jamet
7697713c44 Search with regular expression #175 2022-02-05 14:23:30 +01:00
J-Jamet
ee9b072f45 Fix filter selection 2022-02-05 13:38:42 +01:00
J-Jamet
741defd31e Keep search context #1141 2022-02-05 13:33:57 +01:00
J-Jamet
e259b37f74 Add green in divine theme 2022-02-04 22:09:18 +01:00
Jérémy JAMET
9a87de797c Update README 2022-02-04 21:52:29 +01:00
J-Jamet
5d44ba658e Move case sensitive filter 2022-02-03 20:02:19 +01:00
J-Jamet
00518b8231 Default activity animation 2022-02-03 19:58:16 +01:00
J-Jamet
6a3a99d2ac Fix orange color and FAB color 2022-02-03 19:28:16 +01:00
J-Jamet
819434968c Fix and simpler colors for divine theme 2022-02-03 19:12:40 +01:00
J-Jamet
9f55ca2fdb Fix status bar and navigation bar color 2022-02-03 18:25:30 +01:00
J-Jamet
7f69563edb Upgrade CHANGELOG and version to 3.3.0 2022-02-03 17:25:36 +01:00
J-Jamet
b5cf0f987e Merge branch 'feature/Search_Refactoring' into develop 2022-02-03 17:21:43 +01:00
J-Jamet
28ad0b39c3 Remove search view hint 2022-02-03 17:21:25 +01:00
J-Jamet
b4e9040d5c Code encapsulation 2022-02-03 17:17:16 +01:00
J-Jamet
18a6ff0aa5 Fix quick search 2022-02-03 16:06:30 +01:00
J-Jamet
66e8c25265 Show current group title 2022-02-03 15:45:07 +01:00
J-Jamet
aba1f2d35b Fix orientation change 2022-02-03 15:31:05 +01:00
J-Jamet
5f29bcea8f Fix orientation change 2022-02-03 15:12:06 +01:00
J-Jamet
fac6fd7926 Dynamic height of filters 2022-02-03 14:56:05 +01:00
J-Jamet
5f4ab201af Merge branch 'develop' into feature/Search_Refactoring 2022-02-02 20:25:54 +01:00
J-Jamet
9591d36f9d Fix small translation 2022-02-02 20:25:38 +01:00
J-Jamet
a38f34995d Manage search keyboard 2022-02-02 20:25:08 +01:00
J-Jamet
f72564e48d Fix node action 2022-02-02 19:39:50 +01:00
J-Jamet
39ae743c64 Reorder filters 2022-02-02 19:26:58 +01:00
J-Jamet
9cdb355878 Enable filters depending on database 2022-02-02 19:14:23 +01:00
J-Jamet
6bdabbc96b Refactor group elements 2022-02-02 19:02:00 +01:00
solokot
d9e7d5ff6f Translated using Weblate (Russian)
Currently translated at 100.0% (582 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-02-02 18:38:07 +01:00
J-Jamet
9f41da7868 Reorganize code 2022-02-02 18:36:07 +01:00
J-Jamet
d4f7258ed1 Search in current group 2022-02-02 18:32:43 +01:00
J-Jamet
aa34d78052 Fix chip style 2022-02-02 17:30:10 +01:00
J-Jamet
72712e8e0e Merge branch 'develop' into feature/Search_Refactoring 2022-02-02 12:39:59 +01:00
Hosted Weblate
bf25054ef2 Merge branch 'origin/develop' into Weblate. 2022-02-02 11:48:44 +01:00
Stephan Paternotte
32efafb404 Translated using Weblate (Dutch)
Currently translated at 100.0% (582 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2022-02-02 11:48:44 +01:00
J-Jamet
3748ba1afa Merge tag '3.2.0' into develop
3.2.0
2022-02-02 11:30:55 +01:00
J-Jamet
5baf91dc77 Merge branch 'release/3.2.0' 2022-02-02 11:30:47 +01:00
J-Jamet
a152a48402 Upgrade to 3.2.0 2022-02-01 17:10:04 +01:00
J-Jamet
5fb4c4c20c Fix dialog when no modification performed 2022-02-01 17:06:51 +01:00
J-Jamet
80cf4f05f8 Better unknown KDF exception 2022-02-01 16:03:54 +01:00
J-Jamet
ce8e532f61 Larger view for breadcrumb 2022-02-01 15:50:20 +01:00
Allan Nordhøy
7e04fcbb6c Translated using Weblate (Norwegian Bokmål)
Currently translated at 88.8% (517 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2022-02-01 05:53:21 +01:00
J-Jamet
71c37d0b8b Fix orientation change 2022-01-31 22:09:12 +01:00
J-Jamet
518375e29a Fix add button listeners 2022-01-31 14:23:46 +01:00
J-Jamet
25c845c1c7 Change max entry search 2022-01-31 14:21:38 +01:00
J-Jamet
6a468b339f Add search filters view 2022-01-31 13:46:17 +01:00
J-Jamet
05a52dd482 Fix search empty string 2022-01-31 11:23:52 +01:00
J-Jamet
5f1413ea1f Fix collapse animation 2022-01-31 00:07:48 +01:00
J-Jamet
84e45482a4 Fix fade animation 2022-01-30 23:45:19 +01:00
J-Jamet
32bfdca562 Advance search expand animation 2022-01-30 23:26:47 +01:00
J-Jamet
547971545e Merge branch 'develop' into feature/Search_Refactoring 2022-01-30 22:33:56 +01:00
J-Jamet
c12b16faf9 Merge branch 'develop' into feature/Search_Refactoring 2022-01-30 22:33:14 +01:00
J-Jamet
858d6c8723 Upgrade to 3.2.0_beta4 2022-01-30 19:44:46 +01:00
J-Jamet
0f021fae9f Fix Kdbx4 tag in Kdbx3 database #1222 2022-01-30 19:34:59 +01:00
J-Jamet
5cb42264c5 First commit for new search 2022-01-30 18:37:06 +01:00
Salih Ail
c17af4098f Translated using Weblate (Arabic)
Currently translated at 75.6% (440 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2022-01-30 15:57:02 +01:00
VfBFan
e6c094b433 Translated using Weblate (German)
Currently translated at 100.0% (582 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-01-30 15:57:02 +01:00
Oğuz Ersen
b15a86618b Translated using Weblate (Turkish)
Currently translated at 100.0% (582 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-01-29 03:55:09 +01:00
Eric
2e66cee551 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (582 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-01-29 03:55:09 +01:00
Ihor Hordiichuk
c6e3d0125a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (582 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-01-29 03:55:09 +01:00
solokot
f60f7d32dc Translated using Weblate (Russian)
Currently translated at 100.0% (582 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-01-29 03:55:08 +01:00
Matthaiks
dd7175d0c6 Translated using Weblate (Polish)
Currently translated at 100.0% (582 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-01-29 03:55:08 +01:00
J-Jamet
ba0e87f32a Fix warning 2022-01-28 15:56:16 +01:00
J-Jamet
cebba5a09d Upgrade to 3.2.0 beta3 2022-01-28 15:49:59 +01:00
J-Jamet
7b0c31c641 Fix search #1219 2022-01-28 15:49:00 +01:00
Kunzisoft
487be74e83 Translated using Weblate (English)
Currently translated at 99.8% (581 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2022-01-28 15:46:34 +01:00
J-Jamet
3c7c88e0f5 Hot fix for search #1219 2022-01-28 12:01:32 +01:00
J-Jamet
e876e89b41 Upgrade version code 2022-01-28 10:09:45 +01:00
J-Jamet
065fd9632c Update string 2022-01-27 18:56:13 +01:00
J-Jamet
f7d8641e31 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2022-01-27 18:50:34 +01:00
J-Jamet
ed6456599b Update string 2022-01-27 18:47:48 +01:00
Hosted Weblate
649538846a Merge branch 'origin/develop' into Weblate. 2022-01-27 18:44:48 +01:00
Kunzisoft
7468db5269 Translated using Weblate (French)
Currently translated at 99.6% (580 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2022-01-27 18:44:47 +01:00
Retrial
8291e94de4 Translated using Weblate (Greek)
Currently translated at 100.0% (582 of 582 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-01-27 18:44:47 +01:00
J-Jamet
94d30fc7ec Change string 2022-01-27 18:44:06 +01:00
Hosted Weblate
36c6f371c5 Merge branch 'origin/develop' into Weblate. 2022-01-27 18:32:27 +01:00
Balázs Meskó
94b3f66e14 Translated using Weblate (Hungarian)
Currently translated at 100.0% (576 of 576 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2022-01-27 18:32:27 +01:00
J-Jamet
c9527ddacd Update CHANGELOG 2022-01-27 18:31:20 +01:00
J-Jamet
805c728e92 Change "show" strings by "displays" 2022-01-27 18:29:00 +01:00
J-Jamet
7812a86adb Add entry colors setting 2022-01-27 18:26:01 +01:00
J-Jamet
d22defcd83 Move settings position 2022-01-27 18:06:38 +01:00
J-Jamet
d06829ff7b Fix empty tag 2022-01-27 17:33:05 +01:00
J-Jamet
9110a1aca6 Fix checkboxes position with an error 2022-01-27 16:51:19 +01:00
J-Jamet
d7c90601d0 Add warning integrity message when keyfile is an image #1188 2022-01-27 16:47:47 +01:00
J-Jamet
7b5fc600f5 Merge branch 'feature/Tags' into develop 2022-01-27 16:12:01 +01:00
J-Jamet
0e699a1918 Add Tokenautocomplete from sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed 2022-01-27 16:08:34 +01:00
J-Jamet
49f9964bc7 Add pool of tags 2022-01-27 15:37:37 +01:00
J-Jamet
ee3ec4b14d Add tags to group 2022-01-26 19:40:28 +01:00
J-Jamet
cdcc9f0aff Change tags views 2022-01-26 19:02:54 +01:00
J-Jamet
d56a01998f Merge branch 'develop' into feature/Tags 2022-01-26 18:43:21 +01:00
J-Jamet
4e7f7f7fa0 Fix lock in settings 2022-01-26 18:16:33 +01:00
J-Jamet
201d8f8aee Fix merge menu 2022-01-26 17:47:40 +01:00
J-Jamet
3847cb4d2d Merge branch 'feature/Keyboard_Refactoring' into develop 2022-01-26 15:12:01 +01:00
J-Jamet
89a338ac33 Suppress deprecation 2022-01-26 15:11:11 +01:00
J-Jamet
0084d113d3 Remove unused deprecated attribute 2022-01-26 15:10:21 +01:00
J-Jamet
6d49cc3577 Remove unused deprecated attribute 2022-01-26 15:10:06 +01:00
J-Jamet
910c45a6be Fix key press 2022-01-26 14:23:45 +01:00
J-Jamet
1d16ad764f First KeyboardView implementation 2022-01-26 14:01:08 +01:00
J-Jamet
142021c849 Update CHANGELOG 2022-01-25 18:18:40 +01:00
J-Jamet
c87b9768b1 small changelog change 2022-01-25 18:11:05 +01:00
J-Jamet
fbc3c1a9a4 Merge branch 'feature/Keep_Screen_On' of git://github.com/SUPERYAO541/KeePassDX into SUPERYAO541-feature/Keep_Screen_On 2022-01-25 18:04:49 +01:00
J-Jamet
05d1656a9e Inherit colors and icon from template #1213 #1130 2022-01-25 17:59:06 +01:00
J-Jamet
98804db478 Better kdf engine implementation 2022-01-25 16:37:18 +01:00
J-Jamet
c90f18c45a Better cipher engine implementation 2022-01-25 16:11:40 +01:00
J-Jamet
553783bfce Revert encryption commit 3426b3cdeb to prevent crash 2022-01-25 14:51:27 +01:00
J-Jamet
fbf67f28f5 Upgrade database version after a modification 2022-01-25 14:22:26 +01:00
J-Jamet
b8005466cd Inherit color from template #1213 2022-01-25 12:41:44 +01:00
J-Jamet
82177a386e Add copyright 2022-01-25 11:25:38 +01:00
J-Jamet
176b276835 Upgrade CHANGELOG 2022-01-25 11:04:18 +01:00
J-Jamet
a4732194ef Better quick search implementation and add path 2022-01-25 10:59:39 +01:00
I. Musthafa
4a815daf4d Translated using Weblate (Indonesian)
Currently translated at 89.9% (518 of 576 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2022-01-25 06:53:13 +01:00
Victor Mihalache
8aa7804e8d Translated using Weblate (Romanian)
Currently translated at 69.6% (401 of 576 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2022-01-25 06:53:13 +01:00
Oğuz Ersen
3b7086627d Translated using Weblate (Turkish)
Currently translated at 100.0% (576 of 576 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-01-25 06:53:12 +01:00
Eric
20fb4036d1 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (576 of 576 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-01-25 06:53:11 +01:00
Ihor Hordiichuk
3679ad3d70 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (576 of 576 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-01-25 06:53:11 +01:00
solokot
63f23c571f Translated using Weblate (Russian)
Currently translated at 100.0% (576 of 576 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-01-25 06:53:11 +01:00
André Marcelo Alvarenga
7e96303c7d Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (576 of 576 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-01-25 06:53:11 +01:00
Matthaiks
4f5879179e Translated using Weblate (Polish)
Currently translated at 100.0% (576 of 576 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-01-25 06:53:10 +01:00
Stephan Paternotte
5a81f54a1b Translated using Weblate (Dutch)
Currently translated at 100.0% (576 of 576 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2022-01-25 06:53:10 +01:00
Retrial
d8ccaf3578 Translated using Weblate (Greek)
Currently translated at 100.0% (576 of 576 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-01-25 06:53:09 +01:00
C. Rüdinger
7d0848f22b Translated using Weblate (German)
Currently translated at 100.0% (576 of 576 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-01-25 06:53:09 +01:00
André Marcelo Alvarenga
b02cffab56 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (576 of 576 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-01-23 23:49:47 +01:00
J-Jamet
59b6deb7be Fix color during selection 2022-01-23 12:38:35 +01:00
J-Jamet
1b734051d5 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2022-01-23 11:30:37 +01:00
J-Jamet
037fe41961 Merge branch 'Nickoriginal-patch-1' into develop 2022-01-23 11:23:56 +01:00
Nickoriginal
7bf1263c04 Update strings.xml
Fix mistyping
2022-01-22 16:01:13 +02:00
random r
0e4afd4681 Translated using Weblate (Italian)
Currently translated at 100.0% (574 of 574 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2022-01-22 14:52:47 +01:00
Balázs Meskó
21930021ed Translated using Weblate (Hungarian)
Currently translated at 88.3% (507 of 574 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2022-01-22 14:52:46 +01:00
Oğuz Ersen
748b5c54c9 Translated using Weblate (Turkish)
Currently translated at 100.0% (574 of 574 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-01-20 00:01:00 +01:00
Eric
82d305008b Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (574 of 574 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-01-20 00:01:00 +01:00
Ihor Hordiichuk
1f24955533 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (574 of 574 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-01-20 00:00:59 +01:00
solokot
ccfa28b895 Translated using Weblate (Russian)
Currently translated at 100.0% (574 of 574 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-01-20 00:00:59 +01:00
Matthaiks
2e3562d87e Translated using Weblate (Polish)
Currently translated at 100.0% (574 of 574 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-01-20 00:00:59 +01:00
Oliver Cervera
f1be109c15 Translated using Weblate (Italian)
Currently translated at 99.6% (572 of 574 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2022-01-20 00:00:59 +01:00
Balázs Meskó
b09bd39ad4 Translated using Weblate (Hungarian)
Currently translated at 83.2% (478 of 574 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2022-01-20 00:00:58 +01:00
Kunzisoft
d812c3d61b Translated using Weblate (French)
Currently translated at 100.0% (574 of 574 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2022-01-20 00:00:58 +01:00
Óscar Fernández Díaz
0d7772a4c6 Translated using Weblate (Spanish)
Currently translated at 100.0% (574 of 574 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2022-01-20 00:00:58 +01:00
Retrial
a5abf4d186 Translated using Weblate (Greek)
Currently translated at 100.0% (574 of 574 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-01-20 00:00:57 +01:00
VfBFan
c938a13482 Translated using Weblate (German)
Currently translated at 100.0% (574 of 574 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-01-20 00:00:57 +01:00
J-Jamet
0bfc395986 Merge branch 'feature/Merge_Data' into develop #840 2022-01-19 20:40:47 +01:00
J-Jamet
60c536f444 Fix save database info 2022-01-19 20:33:54 +01:00
J-Jamet
8129e6e0c1 First code to merge KDB and add doc 2022-01-19 20:10:50 +01:00
J-Jamet
223a8e9a5e Fix concurrent modification exception 2022-01-19 19:12:44 +01:00
J-Jamet
0e21c75007 replace updateGroup by addGroupIndex 2022-01-19 18:28:21 +01:00
J-Jamet
6ca031e8fd Merge custom data 2022-01-19 16:33:15 +01:00
J-Jamet
f02e86fb50 Confirm reload dialog if local modification #1196 2022-01-19 15:11:17 +01:00
J-Jamet
b17f8244da Remove merge menu in read only mode 2022-01-18 14:41:12 +01:00
J-Jamet
4c02ce138b Remove merge with kdb database 2022-01-18 14:37:46 +01:00
J-Jamet
5eba208b00 Upgrade to 3.2.0 and update CHANGELOG 2022-01-18 13:09:28 +01:00
J-Jamet
2a4fbbfb35 Merge branch 'develop' into feature/Merge_Data 2022-01-18 13:05:07 +01:00
Hosted Weblate
5dd8bdcae6 Merge branch 'origin/develop' into Weblate. 2022-01-18 13:04:51 +01:00
I. Musthafa
c3e82eea5d Translated using Weblate (Indonesian)
Currently translated at 90.8% (519 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2022-01-18 13:04:50 +01:00
Milo Ivir
2cfb96be33 Translated using Weblate (Croatian)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2022-01-18 13:04:49 +01:00
Stephan Paternotte
715eedfab1 Translated using Weblate (Dutch)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2022-01-18 13:04:49 +01:00
J-Jamet
7f3a1c9a7d Merge tag '3.1' into develop
3.1
2022-01-18 12:44:57 +01:00
J-Jamet
8413d2b31a Merge branch 'release/3.1' 2022-01-18 12:44:44 +01:00
J-Jamet
04a03da382 Merge branch 'master' into release/3.1 2022-01-18 12:44:30 +01:00
J-Jamet
e3274657ea /bin/bash: q: command not found 2022-01-18 12:13:03 +01:00
J-Jamet
f3b25cb792 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2022-01-18 12:08:48 +01:00
J-Jamet
d181f886fe Update CHANGELOG 2022-01-18 12:02:33 +01:00
J-Jamet
616d073395 Capture duplicate error exception 2022-01-18 11:44:45 +01:00
J-Jamet
d36fc19585 Add try catch exception when populate view 2022-01-18 11:32:11 +01:00
J-Jamet
95d9e07e2f TODO fix meta stream 2022-01-17 23:51:37 +01:00
J-Jamet
25077a7b9a Fix history merge 2022-01-17 23:33:20 +01:00
J-Jamet
6971cd1a6b Start merge database v1 2022-01-17 22:35:27 +01:00
J-Jamet
1e43a65743 Do not close the database when a merge failed 2022-01-17 22:18:46 +01:00
J-Jamet
2f2cbf343e Fix object deletion 2022-01-17 22:07:32 +01:00
J-Jamet
3426b3cdeb Better algorithm implementation 2022-01-17 21:31:05 +01:00
J-Jamet
e1d4c172f4 Merge settings 2022-01-17 20:29:10 +01:00
J-Jamet
4c4a67afaf Merge icons 2022-01-17 17:23:33 +01:00
J-Jamet
d33f210940 Add TODO and copyright 2022-01-15 22:31:28 +01:00
J-Jamet
ead59bd410 Merge entry history 2022-01-15 21:37:24 +01:00
J-Jamet
f9445de71f Merge database metadata 2022-01-15 21:03:40 +01:00
J-Jamet
c26995779a Change strings 2022-01-15 20:31:41 +01:00
J-Jamet
c6277453a3 Better binary management 2022-01-15 20:31:31 +01:00
solokot
91ebf2ba6f Translated using Weblate (Russian)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-01-15 11:13:08 +01:00
J-Jamet
0f87bdd5a3 Merge branch 'develop' into feature/Merge_Data 2022-01-14 18:52:55 +01:00
J-Jamet
9e97042dd1 Larger database color view 2022-01-14 18:42:32 +01:00
J-Jamet
b7c4a99e71 Upgrade README Year 2022-01-14 18:37:29 +01:00
J-Jamet
48e315453e Smooth counter 2022-01-14 18:11:36 +01:00
J-Jamet
a8a6d14ca3 Merge branch 'feature/Entry_Color' into develop 2022-01-14 18:05:21 +01:00
J-Jamet
e895dd3430 Upgrade CHANGELOG 2022-01-14 18:05:03 +01:00
J-Jamet
f59859137a Upgrade libs 2022-01-14 18:03:00 +01:00
J-Jamet
dee92e9e40 Upgrade libs 2022-01-14 18:01:35 +01:00
J-Jamet
6701f4f95e Fix color in KitKat 2022-01-14 17:55:54 +01:00
J-Jamet
e20f769854 Fix theme refresh 2022-01-14 16:00:36 +01:00
J-Jamet
4f762a9432 Change background color icon 2022-01-14 15:54:41 +01:00
J-Jamet
3c49eb1635 Fix color picker 2022-01-14 15:45:54 +01:00
J-Jamet
bdc6a282e2 Colorize entry view 2022-01-14 14:25:06 +01:00
J-Jamet
8392ab2cc4 Fix icon color 2022-01-14 01:13:37 +01:00
Allan Nordhøy
93c7c09f8c Translated using Weblate (Norwegian Bokmål)
Currently translated at 90.0% (514 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2022-01-14 00:53:27 +01:00
Allan Nordhøy
120116414f Translated using Weblate (Danish)
Currently translated at 91.5% (523 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2022-01-14 00:53:24 +01:00
J-Jamet
11794e5819 Add color change listener 2022-01-14 00:39:20 +01:00
J-Jamet
edce3d7bec Add color in entry 2022-01-13 20:22:10 +01:00
J-Jamet
e133e32e7c Add color in app bar 2022-01-13 19:15:18 +01:00
J-Jamet
471859e448 Add color in entry edit 2022-01-13 18:08:20 +01:00
Óscar Fernández Díaz
d8de66eb14 Translated using Weblate (Spanish)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2022-01-12 22:22:29 +01:00
J-Jamet
cfdc0237d7 Add foreground and background colors in list 2022-01-12 19:50:38 +01:00
J-Jamet
05fad24eda Fix color picker fragment 2022-01-12 17:01:35 +01:00
J-Jamet
d4818c5567 Select entry colors 2022-01-12 14:19:04 +01:00
J-Jamet
8e8e6a7b93 Invert info container visibility 2022-01-11 18:02:43 +01:00
J-Jamet
6547f0ffad First entry color test 2022-01-11 17:46:50 +01:00
Ngô Ngọc Đức Huy
6f172fffa8 Translated using Weblate (Vietnamese)
Currently translated at 29.5% (169 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2022-01-11 13:49:38 +01:00
Matthaiks
ed16e06676 Translated using Weblate (Polish)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-01-11 13:49:38 +01:00
Ngô Ngọc Đức Huy
1874f06f42 Translated using Weblate (Vietnamese)
Currently translated at 29.4% (168 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2022-01-09 01:54:52 +01:00
zeritti
e9db24429a Translated using Weblate (Czech)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2022-01-09 01:54:51 +01:00
J-Jamet
a59f4d45ca Better color implementation 2022-01-08 22:29:43 +01:00
J-Jamet
c7b3e0926c Fix save database color 2022-01-08 21:45:11 +01:00
J-Jamet
f0f5258bc9 Fix refresh UI 2022-01-08 20:36:59 +01:00
J-Jamet
12c07cf793 Fix refresh database metadata 2022-01-08 20:22:39 +01:00
J-Jamet
d2b8c85015 Add database color #913 2022-01-08 20:03:56 +01:00
J-Jamet
b9652291bd Manage default user name and color in KDB 2022-01-08 19:35:42 +01:00
J-Jamet
b0d1f93bfc Change header sig 2022-01-08 16:06:10 +01:00
J-Jamet
a6d6c247a8 Merge branch 'develop' into feature/Merge_Data 2022-01-07 19:02:22 +01:00
J-Jamet
553416c927 Fix number of entries and refactor GroupFragment 2022-01-07 18:59:23 +01:00
J-Jamet
b83696bc60 Fix create entry in the right group 2022-01-07 18:28:44 +01:00
J-Jamet
23ce320d75 Fix output KDB and private indexes 2022-01-07 18:12:45 +01:00
J-Jamet
dd170aafee Merge branch 'develop' into feature/Merge_Data 2022-01-07 14:39:17 +01:00
J-Jamet
27d5733dbc Strike out expires group 2022-01-07 14:38:18 +01:00
J-Jamet
9ba769c53e Fix merge and clear 2022-01-04 21:50:35 +01:00
J-Jamet
8ff57e3004 Better database loader implementation 2022-01-04 21:00:23 +01:00
J-Jamet
5c75c6c7d3 First pass to manage deleted objects 2022-01-04 16:06:39 +01:00
J-Jamet
56efb20ffa Fix delete methods 2022-01-04 15:12:58 +01:00
J-Jamet
699578bb59 Better recycle bin implementation 2022-01-04 13:31:18 +01:00
ssantos
8d7d01bf88 Translated using Weblate (Portuguese)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2022-01-03 21:55:51 +01:00
solokot
0bc37d2fc2 Translated using Weblate (Russian)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-01-03 21:55:50 +01:00
André Marcelo Alvarenga
aaa1655af1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-01-03 21:55:50 +01:00
Mr-Update
f1bd4e1bba Translated using Weblate (German)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-01-03 21:55:50 +01:00
J-Jamet
f74147070a Add TODO for merge code 2022-01-03 19:53:19 +01:00
J-Jamet
20f4ea93e4 Merge branch 'develop' into feature/Merge_Data 2022-01-03 19:52:23 +01:00
J-Jamet
15a28e7c83 Fix action in breadcrumb 2022-01-03 19:51:59 +01:00
J-Jamet
c550e1de54 First merge implementation 2022-01-03 19:25:48 +01:00
Serdar Sağlam
ab26e561fd Translated using Weblate (Turkish)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-01-01 13:56:33 +01:00
Eric
66968a28a3 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-01-01 13:56:32 +01:00
Ihor Hordiichuk
37d1f91224 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-01-01 13:56:32 +01:00
Gabriel Cardoso
9e69068d42 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-01-01 13:56:32 +01:00
Matthaiks
8e2a9fcd01 Translated using Weblate (Polish)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-01-01 13:56:31 +01:00
Retrial
1753887916 Translated using Weblate (Greek)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-01-01 13:56:31 +01:00
Hosted Weblate
21bcffcc87 Merge branch 'origin/develop' into Weblate. 2021-12-31 17:27:28 +01:00
I. Musthafa
1caed49c75 Translated using Weblate (Indonesian)
Currently translated at 84.7% (482 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-12-31 09:20:33 +01:00
Vitor Henrique
619ea35168 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-12-31 09:20:32 +01:00
Giai Ngo
877f913e8f Translated using Weblate (Vietnamese)
Currently translated at 25.8% (147 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2021-12-23 03:50:28 +01:00
I. Musthafa
25c47390c0 Translated using Weblate (Indonesian)
Currently translated at 81.1% (462 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-12-23 03:50:26 +01:00
J-Jamet
b3c46348a1 Allow to change root group 2021-12-22 19:01:51 +01:00
J-Jamet
6f154194f1 Upgrade bouncy castle #833 2021-12-22 17:40:18 +01:00
J-Jamet
c3ab08ce17 Upgrade to SDK 31 2021-12-22 17:28:29 +01:00
J-Jamet
004fffa992 Add exact alarm message 2021-12-22 17:23:57 +01:00
J-Jamet
d6bd80c9c0 Fix UI in Android 8 #509 2021-12-22 14:12:30 +01:00
J-Jamet
318bcdd011 Remove WRITE_EXTERNAL_STORAGE permission 2021-12-22 13:57:05 +01:00
J-Jamet
3076f2af68 Add backup rules 2021-12-21 17:14:30 +01:00
Jérémy JAMET
3dd9ef5564 Update bug_report.md 2021-12-21 10:01:34 +01:00
I. Musthafa
367e5fa84e Translated using Weblate (Indonesian)
Currently translated at 81.0% (461 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-12-20 15:50:26 +01:00
J-Jamet
97cd61fd13 First pass to update API 31 2021-12-17 17:57:09 +01:00
Y. Sakamoto
869bf7a345 Translated using Weblate (Japanese)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-12-17 12:53:25 +01:00
J-Jamet
9e15ac242d Fix small element 2021-12-16 21:36:55 +01:00
J-Jamet
84943e58f1 Fix small elements 2021-12-16 21:30:40 +01:00
J-Jamet
2289bf0a27 Fix show UUID in V1 format 2021-12-16 21:15:23 +01:00
J-Jamet
7fda40c983 Show UUID in group 2021-12-16 20:52:49 +01:00
J-Jamet
7eeed8f670 Edit group on long click 2021-12-16 20:20:46 +01:00
J-Jamet
4d3f4ed5c2 Add group info dialog #1177 2021-12-16 19:12:31 +01:00
J-Jamet
145a4f5c20 Merge branch 'feature/Breadcrumb' into develop 2021-12-16 17:44:20 +01:00
J-Jamet
9afe3d26e9 Arrow as breadcrumb delimiter 2021-12-16 17:37:17 +01:00
J-Jamet
b73a7f1ed8 Upgrade version to 3.1.0 and changelog 2021-12-16 17:29:17 +01:00
J-Jamet
91a2bc3862 Fix virtual content 2021-12-16 17:25:41 +01:00
J-Jamet
78a8a840b0 Add path in search result 2021-12-16 17:20:33 +01:00
J-Jamet
f4d54b6ca3 Fix margin 2021-12-16 16:54:53 +01:00
J-Jamet
bc7a1c332c Breadcrunb toolbar animation and remove back button 2021-12-16 16:49:56 +01:00
J-Jamet
0e75cb9095 Change parallax 2021-12-16 16:23:09 +01:00
J-Jamet
41b6fb6dcd Add selectable background 2021-12-16 13:23:18 +01:00
J-Jamet
2ca3cbc88f Fix database title 2021-12-16 13:12:08 +01:00
J-Jamet
d05641a3d6 Change toolbar parallax 2021-12-16 13:02:04 +01:00
J-Jamet
28bf84e05c Fix search 2021-12-16 12:56:14 +01:00
J-Jamet
ff51b53660 Back group in breadcrumb 2021-12-16 12:25:25 +01:00
J-Jamet
8b8e034b18 Refresh breadcrumb 2021-12-16 12:06:42 +01:00
J-Jamet
39927b06e3 Change breadcrumb UI 2021-12-16 12:01:26 +01:00
J-Jamet
66db2e7d16 Better breadcrumb implementation 2021-12-16 11:26:17 +01:00
Kunzisoft
a927c33ef1 Translated using Weblate (French)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2021-12-15 12:50:48 +01:00
J-Jamet
17bc18b881 Add breadcrumb for group 2021-12-13 14:24:12 +01:00
Oymate
aa643c4a82 Translated using Weblate (Bengali (Bangladesh))
Currently translated at 7.7% (44 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bn_BD/
2021-12-12 07:52:01 +01:00
J-Jamet
836fbea676 Merge tag '3.0.4' into develop
3.0.4
2021-12-09 20:11:08 +01:00
J-Jamet
045049243c Merge branch 'release/3.0.4' 2021-12-09 20:10:57 +01:00
J-Jamet
b9813a3494 Upgrade version code 2021-12-09 19:54:12 +01:00
J-Jamet
9b42a93ce1 Change lock button 2021-12-09 19:49:38 +01:00
J-Jamet
8502bceef1 Fix search 2021-12-09 18:38:38 +01:00
J-Jamet
663387476f Change select entry min height 2021-12-09 14:33:07 +01:00
J-Jamet
daafd83df9 Better extra key implementation 2021-12-09 14:29:41 +01:00
J-Jamet
f780f2725b Fix compat inline suggestions request 2021-12-09 14:15:40 +01:00
J-Jamet
483aca871a Upgrade to version 3.0.4 and fix inline autofill suggestion #1165 2021-12-09 13:38:42 +01:00
J-Jamet
352e709c3b Merge branch 'master' into develop 2021-12-09 11:53:40 +01:00
J-Jamet
629057b2c1 Fix autofill exception #1173 2021-12-09 11:46:39 +01:00
J-Jamet
0e5f53596d Merge tag '3.0.3' into develop
3.0.3
2021-12-08 17:58:49 +01:00
Ihor Hordiichuk
35c8ea22b1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-12-08 03:51:55 +01:00
Óscar Fernández Díaz
23a548f9b4 Translated using Weblate (Spanish)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-12-02 19:54:32 +01:00
Milo Ivir
7169b15fd8 Translated using Weblate (Croatian)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-11-29 16:54:00 +01:00
abidin toumi
f9def8c96f Translated using Weblate (Arabic)
Currently translated at 78.7% (448 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-11-29 16:53:59 +01:00
Mr-Update
ef43837af1 Translated using Weblate (German)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-11-25 10:50:58 +01:00
random r
0979ca607d Translated using Weblate (Italian)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-11-23 15:40:37 +01:00
Beytullah AKYÜZ
98fb27f77d Translated using Weblate (Turkish)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-11-22 14:52:03 +01:00
JY3
d68510bbaa Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-11-22 14:52:03 +01:00
SC
4177d34b00 Translated using Weblate (Portuguese)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2021-11-19 19:52:59 +01:00
Serdar Sağlam
3ec5c04bf6 Translated using Weblate (Turkish)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-11-19 19:52:59 +01:00
Eric
a877c068b6 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-11-19 19:52:58 +01:00
Ihor Hordiichuk
6a3db90c1e Translated using Weblate (Ukrainian)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-11-19 19:52:58 +01:00
solokot
a079e0d864 Translated using Weblate (Russian)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-11-19 19:52:58 +01:00
Matthaiks
719776d66e Translated using Weblate (Polish)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-11-19 19:52:58 +01:00
Stephan Paternotte
c5af1241e9 Translated using Weblate (Dutch)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2021-11-19 19:52:57 +01:00
Retrial
27e4d7b563 Translated using Weblate (Greek)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-11-19 19:52:57 +01:00
VfBFan
450ab34721 Translated using Weblate (German)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-11-19 19:52:57 +01:00
Hosted Weblate
3e2d4eae2c Merge branch 'origin/develop' into Weblate. 2021-11-17 20:33:06 +01:00
SUPERYAO
c3542224ae Merge branch 'develop' into feature/Keep_Screen_On 2021-10-25 18:03:04 +08:00
J-Jamet
429faf44db Allow to delete tags 2021-10-22 15:23:44 +02:00
J-Jamet
69ad7979ae Manually add token tags 2021-10-17 13:40:49 +02:00
J-Jamet
e87caac723 Put tag if not empty 2021-10-13 16:45:41 +02:00
J-Jamet
494544a4c2 Fix tags view position 2021-10-08 17:44:47 +02:00
J-Jamet
94b6118bf7 Fix theme and add autocomplete view 2021-10-05 14:30:49 +02:00
J-Jamet
7d6d86c6ff Merge branch 'develop' into feature/Tags 2021-10-04 16:49:38 +02:00
J-Jamet
bedc327e65 Manage Tags #633 2021-09-28 19:12:34 +02:00
SUPERYAO
40a893a8c9 There is no need to clear the flag when the option is not enabled 2021-09-24 22:30:21 +08:00
SUPERYAO
37ebd30a4d Keep the screen on when watching the entry 2021-09-24 20:33:57 +08:00
343 changed files with 15572 additions and 4157 deletions

View File

@@ -24,6 +24,7 @@ A clear and concise description of what you expected to happen.
- Created with: [e.g Windows KeePass 2.42] - Created with: [e.g Windows KeePass 2.42]
- Version: [e.g. 2] - Version: [e.g. 2]
- Location: [e.g. Remote file retrieved with GDrive app] - Location: [e.g. Remote file retrieved with GDrive app]
- File provider (`content://` URI): [e.g. `content://com.google.android.apps.docs.storage/5`]
- Size: [e.g. 150Mo] - Size: [e.g. 150Mo]
- Contains attachment: [e.g. Yes] - Contains attachment: [e.g. Yes]

View File

@@ -1,3 +1,46 @@
KeePassDX(3.3.1)
* Fix Japanese keyboard in search #1248
* Better OOM management #256
* Fix filters #1249
* Fix temp advanced unlocking #1245
* Best autofill recognition #1250
* Workaround to fill OTP token in multiple fields with Magikeyboard (long press) #1158
KeePassDX(3.3.0)
* Quick search and dynamic filters #163 #462 #521
* Keep search context #1141
* Add searchable groups #905 #1006
* Search with regular expression #175
* Merge from file and save as copy #1221 #1204 #840
* Fix custom data #1236
* Fix education hints #1192
* Fix save and app instance in selection mode
* New UI and fix styles
* Add "Simple" and "Reply" themes
KeePassDX(3.2.0)
* Manage data merge #840 #977
* Manage Tags #633
* Inherit colors and icon from template #1213 #1130
* Entry colors setting #1207
* Setting to keep the screen on when watching the entry #1119
* Add path in quick search
* Small fixes
KeePassDX(3.1.0)
* Add breadcrumb
* Add path in search results #1148
* Add group info dialog #1177
* Manage colors #64 #913
* Fix UI in Android 8 #509
* Upgrade libs and SDK to 31 #833
* Fix parser of database v1 #1201
* Stop asking WRITE_EXTERNAL_STORAGE permission
KeePassDX(3.0.4)
* Fix autofill inline bugs #1173 #1165
* Small UI change
KeePassDX(3.0.3) KeePassDX(3.0.3)
* Change default Argon2 parameters #1098 * Change default Argon2 parameters #1098
* Add & edit custom icon name #976 * Add & edit custom icon name #976

View File

@@ -43,7 +43,7 @@ Optional visual styles are accessible after a contribution (and a congratulatory
* Add features by making a **[pull request](https://help.github.com/articles/about-pull-requests/)**. * Add features by making a **[pull request](https://help.github.com/articles/about-pull-requests/)**.
* Help to **[translate](https://hosted.weblate.org/projects/keepass-dx/strings/)** KeePassDX to your language (on [Weblate](https://hosted.weblate.org/projects/keepass-dx/) or by sending a [pull request](https://help.github.com/articles/about-pull-requests/)). * Help to **[translate](https://hosted.weblate.org/projects/keepass-dx/strings/)** KeePassDX to your language (on [Weblate](https://hosted.weblate.org/projects/keepass-dx/) or by sending a [pull request](https://help.github.com/articles/about-pull-requests/)).
* **[Donate](https://www.kunzisoft.com/donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features. * **[Donate](https://www.keepassdx.com/#donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePassDX. * Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePassDX.
## Download ## Download
@@ -72,7 +72,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
## License ## License
Copyright © 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com). Copyright © 2022 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
This file is part of KeePassDX. This file is part of KeePassDX.

View File

@@ -1,18 +1,19 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
android { android {
compileSdkVersion 30 compileSdkVersion 31
buildToolsVersion "30.0.3" buildToolsVersion "31.0.0"
ndkVersion "21.4.7075529" ndkVersion "21.4.7075529"
defaultConfig { defaultConfig {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 30 targetSdkVersion 31
versionCode = 90 versionCode = 103
versionName = "3.0.3" versionName = "3.3.1"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"
@@ -68,10 +69,14 @@ android {
buildConfigField "boolean", "FULL_VERSION", "false" buildConfigField "boolean", "FULL_VERSION", "false"
buildConfigField "boolean", "CLOSED_STORE", "true" buildConfigField "boolean", "CLOSED_STORE", "true"
buildConfigField "String[]", "STYLES_DISABLED", buildConfigField "String[]", "STYLES_DISABLED",
"{\"KeepassDXStyle_Blue\"," + "{\"KeepassDXStyle_Simple\"," +
"\"KeepassDXStyle_Simple_Night\"," +
"\"KeepassDXStyle_Blue\"," +
"\"KeepassDXStyle_Blue_Night\"," + "\"KeepassDXStyle_Blue_Night\"," +
"\"KeepassDXStyle_Red\"," + "\"KeepassDXStyle_Red\"," +
"\"KeepassDXStyle_Red_Night\"," + "\"KeepassDXStyle_Red_Night\"," +
"\"KeepassDXStyle_Reply\"," +
"\"KeepassDXStyle_Reply_Night\"," +
"\"KeepassDXStyle_Purple\"," + "\"KeepassDXStyle_Purple\"," +
"\"KeepassDXStyle_Purple_Dark\"}" "\"KeepassDXStyle_Purple_Dark\"}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}" buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
@@ -99,22 +104,26 @@ android {
} }
} }
def room_version = "2.3.0" def room_version = "2.4.2"
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "com.android.support:multidex:1.0.3"
implementation "androidx.appcompat:appcompat:$android_appcompat_version" implementation "androidx.appcompat:appcompat:$android_appcompat_version"
implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.2' implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.1.0' implementation 'androidx.biometric:biometric:1.1.0'
implementation 'androidx.media:media:1.4.3' implementation 'androidx.media:media:1.5.0'
// Lifecycle - LiveData - ViewModel - Coroutines // Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:$android_core_version" implementation "androidx.core:core-ktx:$android_core_version"
implementation 'androidx.fragment:fragment-ktx:1.3.6' implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation "com.google.android.material:material:$android_material_version" implementation "com.google.android.material:material:$android_material_version"
// Token auto complete
// From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed
// implementation "com.splitwise:tokenautocomplete:4.0.0-beta04"
// Database // Database
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
@@ -123,7 +132,7 @@ dependencies {
// Time // Time
implementation 'joda-time:joda-time:2.10.13' implementation 'joda-time:joda-time:2.10.13'
// Color // Color
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4' implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6'
// Education // Education
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3' implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3'
// Apache Commons // Apache Commons

View File

@@ -10,15 +10,12 @@
android:anyDensity="true" /> android:anyDensity="true" />
<uses-permission <uses-permission
android:name="android.permission.FOREGROUND_SERVICE" /> android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission <uses-permission
android:name="android.permission.USE_BIOMETRIC" /> android:name="android.permission.USE_BIOMETRIC" />
<uses-permission <uses-permission
android:name="android.permission.VIBRATE"/> android:name="android.permission.VIBRATE"/>
<!-- Write permission until Android 10 -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"
tools:ignore="ScopedStorage" />
<!-- Open apps from links --> <!-- Open apps from links -->
<uses-permission <uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES" android:name="android.permission.QUERY_ALL_PACKAGES"
@@ -30,19 +27,19 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:name="com.kunzisoft.keepass.app.App" android:name="com.kunzisoft.keepass.app.App"
android:allowBackup="true" android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/old_backup_rules"
android:dataExtractionRules="@xml/backup_rules"
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent" android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
android:largeHeap="true" android:largeHeap="true"
android:resizeableActivity="true" android:resizeableActivity="true"
android:theme="@style/KeepassDXStyle.Night" android:theme="@style/KeepassDXStyle.Night"
tools:targetApi="n"> tools:targetApi="s">
<meta-data <meta-data
android:name="com.google.android.backup.api_key" android:name="com.google.android.backup.api_key"
android:value="${googleAndroidBackupAPIKey}" /> android:value="${googleAndroidBackupAPIKey}" />
<activity <activity
android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity" android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
android:theme="@style/KeepassDXStyle.SplashScreen" android:theme="@style/KeepassDXStyle.SplashScreen"
android:label="@string/app_name"
android:launchMode="singleTop" android:launchMode="singleTop"
android:exported="true" android:exported="true"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
@@ -53,7 +50,7 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name="com.kunzisoft.keepass.activities.PasswordActivity" android:name="com.kunzisoft.keepass.activities.MainCredentialActivity"
android:exported="true" android:exported="true"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustResize|stateUnchanged"> android:windowSoftInputMode="adjustResize|stateUnchanged">

View File

@@ -24,7 +24,7 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.view.inputmethod.InlineSuggestionsRequest import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@@ -32,8 +32,9 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
import com.kunzisoft.keepass.autofill.KeeAutofillService import com.kunzisoft.keepass.autofill.KeeAutofillService
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchHelper
@@ -64,17 +65,37 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode -> EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
when (specialMode) { when (specialMode) {
SpecialMode.SELECTION -> { SpecialMode.SELECTION -> {
// Build search param intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle ->
val searchInfo = SearchInfo().apply { // To pass extra inline request
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID) var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME) compatInlineSuggestionsRequest = bundle.getParcelable(KEY_INLINE_SUGGESTION)
manualSelection = intent.getBooleanExtra(KEY_MANUAL_SELECTION, false) }
} // Build search param
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain -> bundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
searchInfo.webDomain = concreteWebDomain SearchInfo.getConcreteWebDomain(
launchSelection(database, searchInfo) 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 -> { SpecialMode.REGISTRATION -> {
// To register info // To register info
@@ -95,10 +116,8 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
} }
private fun launchSelection(database: Database?, private fun launchSelection(database: Database?,
autofillComponent: AutofillComponent?,
searchInfo: SearchInfo) { searchInfo: SearchInfo) {
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
if (autofillComponent == null) { if (autofillComponent == null) {
setResult(Activity.RESULT_CANCELED) setResult(Activity.RESULT_CANCELED)
finish() finish()
@@ -194,34 +213,28 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
companion object { companion object {
private const val KEY_MANUAL_SELECTION = "KEY_MANUAL_SELECTION" private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID" private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN" private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO" private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
fun getPendingIntentForSelection(context: Context, fun getPendingIntentForSelection(context: Context,
searchInfo: SearchInfo? = null, searchInfo: SearchInfo? = null,
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): PendingIntent { compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent {
return PendingIntent.getActivity(context, 0, return PendingIntent.getActivity(context, 0,
// Doesn't work with Parcelable (don't know why?) // 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 { Intent(context, AutofillLauncherActivity::class.java).apply {
searchInfo?.let { putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId) putParcelable(KEY_SEARCH_INFO, searchInfo)
putExtra(KEY_SEARCH_DOMAIN, it.webDomain) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putExtra(KEY_SEARCH_SCHEME, it.webScheme) putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
putExtra(KEY_MANUAL_SELECTION, it.manualSelection)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.let {
putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
} }
} })
}, },
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// TODO Mutable PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
PendingIntent.FLAG_CANCEL_CURRENT
} else { } else {
PendingIntent.FLAG_CANCEL_CURRENT PendingIntent.FLAG_CANCEL_CURRENT
}) })
@@ -234,9 +247,8 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION) EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
putExtra(KEY_REGISTER_INFO, registerInfo) putExtra(KEY_REGISTER_INFO, registerInfo)
}, },
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// TODO Mutable PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
PendingIntent.FLAG_CANCEL_CURRENT
} else { } else {
PendingIntent.FLAG_CANCEL_CURRENT PendingIntent.FLAG_CANCEL_CURRENT
}) })

View File

@@ -36,12 +36,20 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.graphics.ColorUtils
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.appbar.CollapsingToolbarLayout
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.fragments.EntryFragment import com.kunzisoft.keepass.activities.fragments.EntryFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TagsAdapter
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
@@ -58,7 +66,10 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.view.changeControlColor
import com.kunzisoft.keepass.view.changeTitleColor
import com.kunzisoft.keepass.view.hideByFading import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.showActionErrorIfNeeded import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.EntryViewModel import com.kunzisoft.keepass.viewmodels.EntryViewModel
@@ -68,15 +79,20 @@ class EntryActivity : DatabaseLockActivity() {
private var coordinatorLayout: CoordinatorLayout? = null private var coordinatorLayout: CoordinatorLayout? = null
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var appBarLayout: AppBarLayout? = null
private var titleIconView: ImageView? = null private var titleIconView: ImageView? = null
private var historyView: View? = null private var historyView: View? = null
private var entryProgress: ProgressBar? = null private var tagsListView: RecyclerView? = null
private var tagsAdapter: TagsAdapter? = null
private var entryProgress: LinearProgressIndicator? = null
private var lockView: View? = null private var lockView: View? = null
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
private var loadingView: ProgressBar? = null private var loadingView: ProgressBar? = null
private val mEntryViewModel: EntryViewModel by viewModels() private val mEntryViewModel: EntryViewModel by viewModels()
private val mEntryActivityEducation = EntryActivityEducation(this)
private var mMainEntryId: NodeId<UUID>? = null private var mMainEntryId: NodeId<UUID>? = null
private var mHistoryPosition: Int = -1 private var mHistoryPosition: Int = -1
private var mEntryIsHistory: Boolean = false private var mEntryIsHistory: Boolean = false
@@ -93,7 +109,12 @@ class EntryActivity : DatabaseLockActivity() {
} }
private var mIcon: IconImage? = null private var mIcon: IconImage? = null
private var mIconColor: Int = 0 private var mColorAccent: Int = 0
private var mControlColor: Int = 0
private var mColorPrimary: Int = 0
private var mColorBackground: Int = 0
private var mBackgroundColor: Int? = null
private var mForegroundColor: Int? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -108,8 +129,10 @@ class EntryActivity : DatabaseLockActivity() {
// Get views // Get views
coordinatorLayout = findViewById(R.id.toolbar_coordinator) coordinatorLayout = findViewById(R.id.toolbar_coordinator)
collapsingToolbarLayout = findViewById(R.id.toolbar_layout) collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
appBarLayout = findViewById(R.id.app_bar)
titleIconView = findViewById(R.id.entry_icon) titleIconView = findViewById(R.id.entry_icon)
historyView = findViewById(R.id.history_container) historyView = findViewById(R.id.history_container)
tagsListView = findViewById(R.id.entry_tags_list_view)
entryProgress = findViewById(R.id.entry_progress) entryProgress = findViewById(R.id.entry_progress)
lockView = findViewById(R.id.lock_button) lockView = findViewById(R.id.lock_button)
loadingView = findViewById(R.id.loading) loadingView = findViewById(R.id.loading)
@@ -118,10 +141,26 @@ class EntryActivity : DatabaseLockActivity() {
collapsingToolbarLayout?.title = " " collapsingToolbarLayout?.title = " "
toolbar?.title = " " toolbar?.title = " "
// Retrieve the textColor to tint the icon // Retrieve the textColor to tint the toolbar
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent)) val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
mIconColor = taIconColor.getColor(0, Color.BLACK) val taControlColor = theme.obtainStyledAttributes(intArrayOf(R.attr.toolbarColorControl))
taIconColor.recycle() val taColorPrimary = theme.obtainStyledAttributes(intArrayOf(R.attr.colorPrimary))
val taColorBackground = theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
mColorAccent = taColorAccent.getColor(0, Color.BLACK)
mControlColor = taControlColor.getColor(0, Color.BLACK)
mColorPrimary = taColorPrimary.getColor(0, Color.BLACK)
mColorBackground = taColorBackground.getColor(0, Color.BLACK)
taColorAccent.recycle()
taControlColor.recycle()
taColorPrimary.recycle()
taColorBackground.recycle()
// Init Tags adapter
tagsAdapter = TagsAdapter(this)
tagsListView?.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
adapter = tagsAdapter
}
// Get Entry from UUID // Get Entry from UUID
try { try {
@@ -166,10 +205,8 @@ class EntryActivity : DatabaseLockActivity() {
// Assign history dedicated view // Assign history dedicated view
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
if (entryIsHistory) { if (entryIsHistory) {
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
collapsingToolbarLayout?.contentScrim = collapsingToolbarLayout?.contentScrim =
ColorDrawable(taColorAccent.getColor(0, Color.BLACK)) ColorDrawable(mColorAccent)
taColorAccent.recycle()
} }
val entryInfo = entryInfoHistory.entryInfo val entryInfo = entryInfoHistory.entryInfo
@@ -184,15 +221,20 @@ class EntryActivity : DatabaseLockActivity() {
} }
// Assign title icon // Assign title icon
mIcon = entryInfo.icon mIcon = entryInfo.icon
titleIconView?.let { iconView ->
mIconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, mIconColor)
}
// Assign title text // Assign title text
val entryTitle = val entryTitle =
if (entryInfo.title.isNotEmpty()) entryInfo.title else entryInfo.id.toString() if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
collapsingToolbarLayout?.title = entryTitle collapsingToolbarLayout?.title = entryTitle
toolbar?.title = entryTitle toolbar?.title = entryTitle
mUrl = entryInfo.url mUrl = entryInfo.url
// Assign tags
val tags = entryInfo.tags
tagsListView?.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
tagsAdapter?.setTags(tags)
// Assign colors
val showEntryColors = PreferencesUtil.showEntryColors(this)
mBackgroundColor = if (showEntryColors) entryInfo.backgroundColor else null
mForegroundColor = if (showEntryColors) entryInfo.foregroundColor else null
loadingView?.hideByFading() loadingView?.hideByFading()
mEntryLoaded = true mEntryLoaded = true
@@ -204,9 +246,9 @@ class EntryActivity : DatabaseLockActivity() {
} }
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement -> mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
if (otpElement == null) if (otpElement == null) {
entryProgress?.visibility = View.GONE entryProgress?.visibility = View.GONE
when (otpElement?.type) { } else when (otpElement.type) {
// Only add token if HOTP // Only add token if HOTP
OtpType.HOTP -> { OtpType.HOTP -> {
entryProgress?.visibility = View.GONE entryProgress?.visibility = View.GONE
@@ -215,7 +257,7 @@ class EntryActivity : DatabaseLockActivity() {
OtpType.TOTP -> { OtpType.TOTP -> {
entryProgress?.apply { entryProgress?.apply {
max = otpElement.period max = otpElement.period
progress = otpElement.secondsRemaining setProgressCompat(otpElement.secondsRemaining, true)
visibility = View.VISIBLE visibility = View.VISIBLE
} }
} }
@@ -252,13 +294,6 @@ class EntryActivity : DatabaseLockActivity() {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
mEntryViewModel.loadDatabase(database) mEntryViewModel.loadDatabase(database)
// Assign title icon
mIcon?.let { icon ->
titleIconView?.let { iconView ->
mIconDrawableFactory?.assignDatabaseIcon(iconView, icon, mIconColor)
}
}
} }
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
@@ -296,6 +331,11 @@ class EntryActivity : DatabaseLockActivity() {
} }
} }
} }
// Keep the screen on
if (PreferencesUtil.isKeepScreenOnEnabled(this)) {
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
} }
override fun onPause() { override fun onPause() {
@@ -304,11 +344,33 @@ class EntryActivity : DatabaseLockActivity() {
super.onPause() super.onPause()
} }
private fun applyToolbarColors() {
appBarLayout?.setBackgroundColor(mBackgroundColor ?: mColorPrimary)
collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorPrimary)
val backgroundDarker = if (mBackgroundColor != null) {
ColorUtils.blendARGB(mBackgroundColor!!, Color.WHITE, 0.1f)
} else {
mColorBackground
}
titleIconView?.background?.colorFilter = BlendModeColorFilterCompat
.createBlendModeColorFilterCompat(backgroundDarker, BlendModeCompat.SRC_IN)
mIcon?.let { icon ->
titleIconView?.let { iconView ->
mIconDrawableFactory?.assignDatabaseIcon(
iconView,
icon,
mForegroundColor ?: mColorAccent
)
}
}
toolbar?.changeControlColor(mForegroundColor ?: mControlColor)
collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mControlColor)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
if (mEntryLoaded) { if (mEntryLoaded) {
val inflater = menuInflater val inflater = menuInflater
MenuUtil.contributionMenuInflater(inflater, menu)
inflater.inflate(R.menu.entry, menu) inflater.inflate(R.menu.entry, menu)
inflater.inflate(R.menu.database, menu) inflater.inflate(R.menu.database, menu)
@@ -319,11 +381,7 @@ class EntryActivity : DatabaseLockActivity() {
// Show education views // Show education views
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
performedNextEducation( performedNextEducation(menu)
EntryActivityEducation(
this
), menu
)
} }
} }
return true return true
@@ -335,39 +393,44 @@ class EntryActivity : DatabaseLockActivity() {
} }
if (mEntryIsHistory || mDatabaseReadOnly) { if (mEntryIsHistory || mDatabaseReadOnly) {
menu?.findItem(R.id.menu_save_database)?.isVisible = false menu?.findItem(R.id.menu_save_database)?.isVisible = false
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
menu?.findItem(R.id.menu_edit)?.isVisible = false menu?.findItem(R.id.menu_edit)?.isVisible = false
} }
if (!mMergeDataAllowed) {
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
}
if (mSpecialMode != SpecialMode.DEFAULT) { if (mSpecialMode != SpecialMode.DEFAULT) {
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
menu?.findItem(R.id.menu_reload_database)?.isVisible = false menu?.findItem(R.id.menu_reload_database)?.isVisible = false
} }
applyToolbarColors()
return super.onPrepareOptionsMenu(menu) return super.onPrepareOptionsMenu(menu)
} }
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation, private fun performedNextEducation(menu: Menu) {
menu: Menu) {
val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG) val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG)
as? EntryFragment? as? EntryFragment?
val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView() val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView()
val entryCopyEducationPerformed = entryFieldCopyView != null val entryCopyEducationPerformed = entryFieldCopyView != null
&& entryActivityEducation.checkAndPerformedEntryCopyEducation( && mEntryActivityEducation.checkAndPerformedEntryCopyEducation(
entryFieldCopyView, entryFieldCopyView,
{ {
entryFragment.launchEntryCopyEducationAction() entryFragment.launchEntryCopyEducationAction()
}, },
{ {
performedNextEducation(entryActivityEducation, menu) performedNextEducation(menu)
}) })
if (!entryCopyEducationPerformed) { if (!entryCopyEducationPerformed) {
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit) val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
// entryEditEducationPerformed // entryEditEducationPerformed
menuEditView != null && entryActivityEducation.checkAndPerformedEntryEditEducation( menuEditView != null && mEntryActivityEducation.checkAndPerformedEntryEditEducation(
menuEditView, menuEditView,
{ {
onOptionsItemSelected(menu.findItem(R.id.menu_edit)) onOptionsItemSelected(menu.findItem(R.id.menu_edit))
}, },
{ {
performedNextEducation(entryActivityEducation, menu) performedNextEducation(menu)
} }
) )
} }
@@ -375,10 +438,6 @@ class EntryActivity : DatabaseLockActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_contribute -> {
MenuUtil.onContributionItemSelected(this)
return true
}
R.id.menu_edit -> { R.id.menu_edit -> {
mDatabase?.let { database -> mDatabase?.let { database ->
mMainEntryId?.let { entryId -> mMainEntryId?.let { entryId ->
@@ -415,6 +474,9 @@ class EntryActivity : DatabaseLockActivity() {
R.id.menu_save_database -> { R.id.menu_save_database -> {
saveDatabase() saveDatabase()
} }
R.id.menu_merge_database -> {
mergeDatabase()
}
R.id.menu_reload_database -> { R.id.menu_reload_database -> {
reloadDatabase() reloadDatabase()
} }

View File

@@ -74,6 +74,7 @@ import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.* import com.kunzisoft.keepass.view.*
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
import org.joda.time.DateTime import org.joda.time.DateTime
import java.util.* import java.util.*
@@ -103,6 +104,8 @@ class EntryEditActivity : DatabaseLockActivity(),
private var mEntryLoaded: Boolean = false private var mEntryLoaded: Boolean = false
private var mTemplatesSelectorAdapter: TemplatesSelectorAdapter? = null private var mTemplatesSelectorAdapter: TemplatesSelectorAdapter? = null
private val mColorPickerViewModel: ColorPickerViewModel by viewModels()
private var mAllowCustomFields = false private var mAllowCustomFields = false
private var mAllowOTP = false private var mAllowOTP = false
@@ -110,7 +113,7 @@ class EntryEditActivity : DatabaseLockActivity(),
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
// Education // Education
private var entryEditActivityEducation: EntryEditActivityEducation? = null private var mEntryEditActivityEducation = EntryEditActivityEducation(this)
private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon -> private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon ->
mEntryEditViewModel.selectIcon(icon) mEntryEditViewModel.selectIcon(icon)
@@ -180,8 +183,6 @@ class EntryEditActivity : DatabaseLockActivity(),
} }
mAttachmentFileBinderManager = AttachmentFileBinderManager(this) mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
// Verify the education views
entryEditActivityEducation = EntryEditActivityEducation(this)
// Lock button // Lock button
lockView?.setOnClickListener { lockAndExit() } lockView?.setOnClickListener { lockAndExit() }
@@ -243,6 +244,15 @@ class EntryEditActivity : DatabaseLockActivity(),
IconPickerActivity.launch(this@EntryEditActivity, iconImage, mIconSelectionActivityResultLauncher) IconPickerActivity.launch(this@EntryEditActivity, iconImage, mIconSelectionActivityResultLauncher)
} }
mEntryEditViewModel.requestColorSelection.observe(this) { color ->
ColorPickerDialogFragment.newInstance(color)
.show(supportFragmentManager, "ColorPickerFragment")
}
mColorPickerViewModel.colorPicked.observe(this) { color ->
mEntryEditViewModel.selectColor(color)
}
mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant -> mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
if (dateInstant.type == DateInstant.Type.TIME) { if (dateInstant.type == DateInstant.Type.TIME) {
// Launch the time picker // Launch the time picker
@@ -526,10 +536,8 @@ class EntryEditActivity : DatabaseLockActivity(),
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
if (mEntryLoaded) { if (mEntryLoaded) {
menuInflater.inflate(R.menu.entry_edit, menu) menuInflater.inflate(R.menu.entry_edit, menu)
entryEditActivityEducation?.let { Handler(Looper.getMainLooper()).post {
Handler(Looper.getMainLooper()).post { performedNextEducation()
performedNextEducation(it)
}
} }
} }
return true return true
@@ -556,19 +564,19 @@ class EntryEditActivity : DatabaseLockActivity(),
return super.onPrepareOptionsMenu(menu) return super.onPrepareOptionsMenu(menu)
} }
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) { private fun performedNextEducation() {
val entryEditFragment = supportFragmentManager.findFragmentById(R.id.entry_edit_content) val entryEditFragment = supportFragmentManager.findFragmentById(R.id.entry_edit_content)
as? EntryEditFragment? as? EntryEditFragment?
val generatePasswordView = entryEditFragment?.getActionImageView() val generatePasswordView = entryEditFragment?.getActionImageView()
val generatePasswordEductionPerformed = generatePasswordView != null val generatePasswordEductionPerformed = generatePasswordView != null
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation( && mEntryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
generatePasswordView, generatePasswordView,
{ {
entryEditFragment.launchGeneratePasswordEductionAction() entryEditFragment.launchGeneratePasswordEductionAction()
}, },
{ {
performedNextEducation(entryEditActivityEducation) performedNextEducation()
} }
) )
@@ -577,33 +585,33 @@ class EntryEditActivity : DatabaseLockActivity(),
val addNewFieldEducationPerformed = mAllowCustomFields val addNewFieldEducationPerformed = mAllowCustomFields
&& addNewFieldView != null && addNewFieldView != null
&& addNewFieldView.isVisible && addNewFieldView.isVisible
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation( && mEntryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
addNewFieldView, addNewFieldView,
{ {
addNewCustomField() addNewCustomField()
}, },
{ {
performedNextEducation(entryEditActivityEducation) performedNextEducation()
} }
) )
if (!addNewFieldEducationPerformed) { if (!addNewFieldEducationPerformed) {
val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment) val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment)
val addAttachmentEducationPerformed = attachmentView != null val addAttachmentEducationPerformed = attachmentView != null
&& attachmentView.isVisible && attachmentView.isVisible
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation( && mEntryEditActivityEducation.checkAndPerformedAttachmentEducation(
attachmentView, attachmentView,
{ {
addNewAttachment() addNewAttachment()
}, },
{ {
performedNextEducation(entryEditActivityEducation) performedNextEducation()
} }
) )
if (!addAttachmentEducationPerformed) { if (!addAttachmentEducationPerformed) {
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp) val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
setupOtpView != null setupOtpView != null
&& setupOtpView.isVisible && setupOtpView.isVisible
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation( && mEntryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
setupOtpView, setupOtpView,
{ {
setupOtp() setupOtp()
@@ -650,8 +658,8 @@ class EntryEditActivity : DatabaseLockActivity(),
override fun acceptPassword(passwordField: Field) { override fun acceptPassword(passwordField: Field) {
mEntryEditViewModel.selectPassword(passwordField) mEntryEditViewModel.selectPassword(passwordField)
entryEditActivityEducation?.let { Handler(Looper.getMainLooper()).post {
Handler(Looper.getMainLooper()).post { performedNextEducation(it) } performedNextEducation()
} }
} }

View File

@@ -42,7 +42,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
@@ -69,7 +69,7 @@ import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
import java.io.FileNotFoundException import java.io.FileNotFoundException
class FileDatabaseSelectActivity : DatabaseModeActivity(), class FileDatabaseSelectActivity : DatabaseModeActivity(),
AssignMasterKeyDialogFragment.AssignPasswordDialogListener { SetMainCredentialDialogFragment.AssignMainCredentialDialogListener {
// Views // Views
private lateinit var coordinatorLayout: CoordinatorLayout private lateinit var coordinatorLayout: CoordinatorLayout
@@ -78,6 +78,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels() private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels()
private val mFileDatabaseSelectActivityEducation = FileDatabaseSelectActivityEducation(this)
// Adapter to manage database history list // Adapter to manage database history list
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
@@ -124,7 +126,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri -> mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
mDatabaseFileUri = databaseFileCreatedUri mDatabaseFileUri = databaseFileCreatedUri
if (mDatabaseFileUri != null) { if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment.getInstance(true) SetMainCredentialDialogFragment.getInstance(true)
.show(supportFragmentManager, "passwordDialog") .show(supportFragmentManager, "passwordDialog")
} else { } else {
val error = getString(R.string.error_create_database) val error = getString(R.string.error_create_database)
@@ -132,7 +134,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
Log.e(TAG, error) Log.e(TAG, error)
} }
} }
openDatabaseButtonView = findViewById(R.id.open_keyfile_button) openDatabaseButtonView = findViewById(R.id.open_database_button)
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper) openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
// History list // History list
@@ -291,7 +293,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
} }
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) { private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
PasswordActivity.launch(this, MainCredentialActivity.launch(this,
databaseUri, databaseUri,
keyFile, keyFile,
{ exception -> { exception ->
@@ -392,39 +394,40 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
MenuUtil.defaultMenuInflater(menuInflater, menu) MenuUtil.defaultMenuInflater(menuInflater, menu)
} }
Handler(Looper.getMainLooper()).post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) } Handler(Looper.getMainLooper()).post {
performedNextEducation()
}
return true return true
} }
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) { private fun performedNextEducation() {
// If no recent files // If no recent files
val createDatabaseEducationPerformed = val createDatabaseEducationPerformed =
createDatabaseButtonView != null createDatabaseButtonView != null
&& createDatabaseButtonView!!.visibility == View.VISIBLE && createDatabaseButtonView!!.visibility == View.VISIBLE
&& mAdapterDatabaseHistory != null && mFileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
&& mAdapterDatabaseHistory!!.itemCount == 0
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
createDatabaseButtonView!!, createDatabaseButtonView!!,
{ {
createNewFile() createNewFile()
}, },
{ {
// But if the user cancel, it can also select a database // But if the user cancel, it can also select a database
performedNextEducation(fileDatabaseSelectActivityEducation) performedNextEducation()
}) })
if (!createDatabaseEducationPerformed) { if (!createDatabaseEducationPerformed) {
// selectDatabaseEducationPerformed // selectDatabaseEducationPerformed
openDatabaseButtonView != null openDatabaseButtonView != null
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation( && mFileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
openDatabaseButtonView!!, openDatabaseButtonView!!,
{ tapTargetView -> { tapTargetView ->
tapTargetView?.let { tapTargetView?.let {
mExternalFileHelper?.openDocument() mExternalFileHelper?.openDocument()
} }
}, },
{} {
)
})
} }
} }

View File

@@ -21,35 +21,34 @@ package com.kunzisoft.keepass.activities
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.text.Editable
import android.text.TextWatcher
import android.util.Log import android.util.Log
import android.view.* import android.view.Menu
import android.view.KeyEvent.KEYCODE_ENTER import android.view.MenuItem
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.ViewGroup
import android.widget.* import android.widget.Button
import android.widget.CompoundButton
import android.widget.TextView
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ActivityCompat
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
import com.kunzisoft.keepass.activities.helpers.* import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
@@ -57,11 +56,9 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
@@ -70,23 +67,20 @@ import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.MainCredentialView
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import java.io.FileNotFoundException import java.io.FileNotFoundException
class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener { class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
// Views // Views
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
private var filenameView: TextView? = null private var filenameView: TextView? = null
private var passwordView: EditText? = null private var mainCredentialView: MainCredentialView? = null
private var keyFileSelectionView: KeyFileSelectionView? = null
private var confirmButtonView: Button? = null private var confirmButtonView: Button? = null
private var checkboxPasswordView: CompoundButton? = null
private var checkboxKeyFileView: CompoundButton? = null
private var infoContainerView: ViewGroup? = null private var infoContainerView: ViewGroup? = null
private lateinit var coordinatorLayout: CoordinatorLayout private lateinit var coordinatorLayout: CoordinatorLayout
private var advancedUnlockFragment: AdvancedUnlockFragment? = null private var advancedUnlockFragment: AdvancedUnlockFragment? = null
@@ -94,25 +88,16 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels() private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels() private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels()
private val mPasswordActivityEducation = PasswordActivityEducation(this)
private var mDefaultDatabase: Boolean = false private var mDefaultDatabase: Boolean = false
private var mDatabaseFileUri: Uri? = null private var mDatabaseFileUri: Uri? = null
private var mDatabaseKeyFileUri: Uri? = null
private var mRememberKeyFile: Boolean = false private var mRememberKeyFile: Boolean = false
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var mPermissionAsked = false
private var mReadOnly: Boolean = false private var mReadOnly: Boolean = false
private var mForceReadOnly: Boolean = false private var mForceReadOnly: Boolean = false
set(value) {
infoContainerView?.visibility = if (value) {
mReadOnly = true
View.VISIBLE
} else {
View.GONE
}
field = value
}
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? = private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
@@ -122,7 +107,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_password) setContentView(R.layout.activity_main_credential)
toolbar = findViewById(R.id.toolbar) toolbar = findViewById(R.id.toolbar)
toolbar?.title = getString(R.string.app_name) toolbar?.title = getString(R.string.app_name)
@@ -130,16 +115,12 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
confirmButtonView = findViewById(R.id.activity_password_open_button)
filenameView = findViewById(R.id.filename) filenameView = findViewById(R.id.filename)
passwordView = findViewById(R.id.password) mainCredentialView = findViewById(R.id.activity_password_credentials)
keyFileSelectionView = findViewById(R.id.keyfile_selection) confirmButtonView = findViewById(R.id.activity_password_open_button)
checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
infoContainerView = findViewById(R.id.activity_password_info_container) infoContainerView = findViewById(R.id.activity_password_info_container)
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout) coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) { mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
savedInstanceState.getBoolean(KEY_READ_ONLY) savedInstanceState.getBoolean(KEY_READ_ONLY)
} else { } else {
@@ -147,41 +128,19 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
} }
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this) mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity) mExternalFileHelper = ExternalFileHelper(this@MainCredentialActivity)
mExternalFileHelper?.buildOpenDocument { uri -> mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) { if (uri != null) {
mDatabaseKeyFileUri = uri mainCredentialView?.populateKeyFileTextView(uri)
populateKeyFileTextView(uri)
} }
} }
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper) mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
mainCredentialView?.onValidateListener = {
passwordView?.setOnEditorActionListener(onEditorActionListener) loadDatabase()
passwordView?.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
if (editable.toString().isNotEmpty() && checkboxPasswordView?.isChecked != true)
checkboxPasswordView?.isChecked = true
}
})
passwordView?.setOnKeyListener { _, _, keyEvent ->
var handled = false
if (keyEvent.action == KeyEvent.ACTION_DOWN
&& keyEvent?.keyCode == KEYCODE_ENTER) {
verifyCheckboxesAndLoadDatabase()
handled = true
}
handled
} }
// If is a view intent // If is a view intent
getUriFromIntent(intent) getUriFromIntent(intent)
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
}
// Init Biometric elements // Init Biometric elements
advancedUnlockFragment = supportFragmentManager advancedUnlockFragment = supportFragmentManager
@@ -196,10 +155,11 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
} }
// Listen password checkbox to init advanced unlock and confirmation button // Listen password checkbox to init advanced unlock and confirmation button
checkboxPasswordView?.setOnCheckedChangeListener { _, _ -> mainCredentialView?.onPasswordChecked =
mAdvancedUnlockViewModel.checkUnlockAvailability() CompoundButton.OnCheckedChangeListener { _, _ ->
enableOrNotTheConfirmationButton() mAdvancedUnlockViewModel.checkUnlockAvailability()
} enableConfirmationButton()
}
// Observe if default database // Observe if default database
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase -> mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
@@ -208,19 +168,29 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
// Observe database file change // Observe database file change
mDatabaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile -> mDatabaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
// Force read only if the file does not exists // Force read only if the file does not exists
mForceReadOnly = databaseFile?.let { val databaseFileNotExists = databaseFile?.let {
!it.databaseFileExists !it.databaseFileExists
} ?: true } ?: true
infoContainerView?.visibility = if (databaseFileNotExists) {
mReadOnly = true
View.VISIBLE
} else {
View.GONE
}
mForceReadOnly = databaseFileNotExists
invalidateOptionsMenu() invalidateOptionsMenu()
// Post init uri with KeyFile only if needed // Post init uri with KeyFile only if needed
val databaseKeyFileUri = mainCredentialView?.getMainCredential()?.keyFileUri
val keyFileUri = val keyFileUri =
if (mRememberKeyFile if (mRememberKeyFile
&& (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) { && (databaseKeyFileUri == null || databaseKeyFileUri.toString().isEmpty())) {
databaseFile?.keyFileUri databaseFile?.keyFileUri
} else { } else {
mDatabaseKeyFileUri databaseKeyFileUri
} }
// Define title // Define title
@@ -233,10 +203,10 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@PasswordActivity) mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
// Back to previous keyboard is setting activated // Back to previous keyboard is setting activated
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@PasswordActivity)) { if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) {
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION)) sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
} }
@@ -249,8 +219,6 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri) mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
} }
checkPermission()
mDatabase?.let { database -> mDatabase?.let { database ->
launchGroupActivityIfLoaded(database) launchGroupActivityIfLoaded(database)
} }
@@ -277,7 +245,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
if (result.isSuccess) { if (result.isSuccess) {
launchGroupActivityIfLoaded(database) launchGroupActivityIfLoaded(database)
} else { } else {
passwordView?.requestFocusFromTouch() mainCredentialView?.requestPasswordFocus()
var resultError = "" var resultError = ""
val resultException = result.exception val resultException = result.exception
@@ -294,7 +262,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
var databaseUri: Uri? = null var databaseUri: Uri? = null
var mainCredential = MainCredential() var mainCredential = MainCredential()
var readOnly = true var readOnly = true
var cipherEntity: CipherDatabaseEntity? = null var cipherEncryptDatabase: CipherEncryptDatabase? = null
result.data?.let { resultData -> result.data?.let { resultData ->
databaseUri = resultData.getParcelable(DATABASE_URI_KEY) databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
@@ -302,8 +270,8 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
resultData.getParcelable(MAIN_CREDENTIAL_KEY) resultData.getParcelable(MAIN_CREDENTIAL_KEY)
?: mainCredential ?: mainCredential
readOnly = resultData.getBoolean(READ_ONLY_KEY) readOnly = resultData.getBoolean(READ_ONLY_KEY)
cipherEntity = cipherEncryptDatabase =
resultData.getParcelable(CIPHER_ENTITY_KEY) resultData.getParcelable(CIPHER_DATABASE_KEY)
} }
databaseUri?.let { databaseFileUri -> databaseUri?.let { databaseFileUri ->
@@ -311,7 +279,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
databaseFileUri, databaseFileUri,
mainCredential, mainCredential,
readOnly, readOnly,
cipherEntity, cipherEncryptDatabase,
true true
) )
} }
@@ -347,11 +315,16 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
if (action != null if (action != null
&& action == VIEW_INTENT) { && action == VIEW_INTENT) {
mDatabaseFileUri = intent.data mDatabaseFileUri = intent.data
mDatabaseKeyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE) mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE))
} else { } else {
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME) mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE) intent?.getParcelableExtra<Uri?>(KEY_KEYFILE)?.let {
mainCredentialView?.populateKeyFileTextView(it)
}
} }
try {
intent?.removeExtra(KEY_KEYFILE)
} catch (e: Exception) {}
mDatabaseFileUri?.let { mDatabaseFileUri?.let {
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it) mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
} }
@@ -386,51 +359,68 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
finish() finish()
} }
override fun retrieveCredentialForEncryption(): String { override fun retrieveCredentialForEncryption(): ByteArray {
return passwordView?.text?.toString() ?: "" return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
?: byteArrayOf()
} }
override fun conditionToStoreCredential(): Boolean { override fun conditionToStoreCredential(): Boolean {
return checkboxPasswordView?.isChecked == true return mainCredentialView?.conditionToStoreCredential() == true
} }
override fun onCredentialEncrypted(databaseUri: Uri, override fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
encryptedCredential: String,
ivSpec: String) {
// Load the database if password is registered with biometric // Load the database if password is registered with biometric
verifyCheckboxesAndLoadDatabase( loadDatabase(mDatabaseFileUri,
CipherDatabaseEntity( mainCredentialView?.getMainCredential(),
databaseUri.toString(), cipherEncryptDatabase
encryptedCredential,
ivSpec)
) )
} }
override fun onCredentialDecrypted(databaseUri: Uri, private val credentialStorageListener = object: MainCredentialView.CredentialStorageListener {
decryptedCredential: String) { override fun passwordToStore(password: String?): ByteArray? {
// Load the database if password is retrieve from biometric return password?.toByteArray()
// Retrieve from biometric }
verifyKeyFileCheckboxAndLoadDatabase(decryptedCredential)
override fun keyfileToStore(keyfile: Uri?): ByteArray? {
// TODO create byte array to store keyfile
return null
}
override fun hardwareKeyToStore(): ByteArray? {
// TODO create byte array to store hardware key
return null
}
} }
private val onEditorActionListener = object : TextView.OnEditorActionListener { override fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { // Load the database if password is retrieve from biometric
if (actionId == IME_ACTION_DONE) { // Retrieve from biometric
verifyCheckboxesAndLoadDatabase() val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
return true when (cipherDecryptDatabase.credentialStorage) {
CredentialStorage.PASSWORD -> {
mainCredential.masterPassword = String(cipherDecryptDatabase.decryptedValue)
}
CredentialStorage.KEY_FILE -> {
// TODO advanced unlock key file
}
CredentialStorage.HARDWARE_KEY -> {
// TODO advanced unlock hardware key
} }
return false
} }
loadDatabase(mDatabaseFileUri,
mainCredential,
null
)
} }
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) { private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
// Define Key File text // Define Key File text
if (mRememberKeyFile) { if (mRememberKeyFile) {
populateKeyFileTextView(keyFileUri) mainCredentialView?.populateKeyFileTextView(keyFileUri)
} }
// Define listener for validate button // Define listener for validate button
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() } confirmButtonView?.setOnClickListener { loadDatabase() }
// If Activity is launch with a password and want to open directly // If Activity is launch with a password and want to open directly
val intent = intent val intent = intent
@@ -439,66 +429,33 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
intent.removeExtra(KEY_PASSWORD) intent.removeExtra(KEY_PASSWORD)
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false) val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
if (password != null) { if (password != null) {
populatePasswordTextView(password) mainCredentialView?.populatePasswordTextView(password)
} }
if (launchImmediately) { if (launchImmediately) {
verifyCheckboxesAndLoadDatabase(password, keyFileUri) loadDatabase()
} else { } else {
// Init Biometric elements // Init Biometric elements
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri) mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
} }
enableOrNotTheConfirmationButton() enableConfirmationButton()
// Auto select the password field and open keyboard mainCredentialView?.focusPasswordFieldAndOpenKeyboard()
passwordView?.postDelayed({
passwordView?.requestFocusFromTouch()
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager?
inputMethodManager?.showSoftInput(passwordView, InputMethodManager.SHOW_IMPLICIT)
}, 100)
} }
private fun enableOrNotTheConfirmationButton() { private fun enableConfirmationButton() {
// Enable or not the open button if setting is checked // Enable or not the open button if setting is checked
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) { if (!PreferencesUtil.emptyPasswordAllowed(this@MainCredentialActivity)) {
checkboxPasswordView?.let { confirmButtonView?.isEnabled = mainCredentialView?.isFill() ?: false
confirmButtonView?.isEnabled = (checkboxPasswordView?.isChecked == true
|| checkboxKeyFileView?.isChecked == true)
}
} else { } else {
confirmButtonView?.isEnabled = true confirmButtonView?.isEnabled = true
} }
} }
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) { private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
populatePasswordTextView(null) mainCredentialView?.populatePasswordTextView(null)
if (clearKeyFile) { if (clearKeyFile) {
mDatabaseKeyFileUri = null mainCredentialView?.populateKeyFileTextView(null)
populateKeyFileTextView(null)
}
}
private fun populatePasswordTextView(text: String?) {
if (text == null || text.isEmpty()) {
passwordView?.setText("")
if (checkboxPasswordView?.isChecked == true)
checkboxPasswordView?.isChecked = false
} else {
passwordView?.setText(text)
if (checkboxPasswordView?.isChecked != true)
checkboxPasswordView?.isChecked = true
}
}
private fun populateKeyFileTextView(uri: Uri?) {
if (uri == null || uri.toString().isEmpty()) {
keyFileSelectionView?.uri = null
if (checkboxKeyFileView?.isChecked == true)
checkboxKeyFileView?.isChecked = false
} else {
keyFileSelectionView?.uri = uri
if (checkboxKeyFileView?.isChecked != true)
checkboxKeyFileView?.isChecked = true
} }
} }
@@ -510,42 +467,20 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
mDatabaseKeyFileUri?.let {
outState.putString(KEY_KEYFILE, it.toString())
}
outState.putBoolean(KEY_READ_ONLY, mReadOnly) outState.putBoolean(KEY_READ_ONLY, mReadOnly)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) { private fun loadDatabase() {
val password: String? = passwordView?.text?.toString() loadDatabase(mDatabaseFileUri,
val keyFile: Uri? = keyFileSelectionView?.uri mainCredentialView?.getMainCredential(),
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity) null
} )
private fun verifyCheckboxesAndLoadDatabase(password: String?,
keyFile: Uri?,
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
verifyKeyFileCheckbox(keyFile)
loadDatabase(mDatabaseFileUri, keyPassword, mDatabaseKeyFileUri, cipherDatabaseEntity)
}
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
val keyFile: Uri? = keyFileSelectionView?.uri
verifyKeyFileCheckbox(keyFile)
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
}
private fun verifyKeyFileCheckbox(keyFile: Uri?) {
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
} }
private fun loadDatabase(databaseFileUri: Uri?, private fun loadDatabase(databaseFileUri: Uri?,
password: String?, mainCredential: MainCredential?,
keyFileUri: Uri?, cipherEncryptDatabase: CipherEncryptDatabase?) {
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) { if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
clearCredentialsViews() clearCredentialsViews()
@@ -563,11 +498,12 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
databaseFileUri?.let { databaseUri -> databaseFileUri?.let { databaseUri ->
// Show the progress dialog and load the database // Show the progress dialog and load the database
showProgressDialogAndLoadDatabase( showProgressDialogAndLoadDatabase(
databaseUri, databaseUri,
MainCredential(password, keyFileUri), mainCredential ?: MainCredential(),
mReadOnly, mReadOnly,
cipherDatabaseEntity, cipherEncryptDatabase,
false) false
)
} }
} }
} }
@@ -575,14 +511,14 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri, private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
mainCredential: MainCredential, mainCredential: MainCredential,
readOnly: Boolean, readOnly: Boolean,
cipherDatabaseEntity: CipherDatabaseEntity?, cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUUID: Boolean) { fixDuplicateUUID: Boolean) {
loadDatabase( loadDatabase(
databaseUri, databaseUri,
mainCredential, mainCredential,
readOnly, readOnly,
cipherDatabaseEntity, cipherEncryptDatabase,
fixDuplicateUUID fixDuplicateUUID
) )
} }
@@ -613,61 +549,33 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
return true return true
} }
// Check permission
private fun checkPermission() {
if (Build.VERSION.SDK_INT in 23..28
&& !mReadOnly
&& !mPermissionAsked) {
mPermissionAsked = true
// Check self permission to show or not the dialog
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
val permissions = arrayOf(writePermission)
if (toolbar != null
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
WRITE_EXTERNAL_STORAGE_REQUEST -> {
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
Toast.makeText(this, R.string.read_only_warning, Toast.LENGTH_LONG).show()
}
}
}
}
// To fix multiple view education // To fix multiple view education
private var performedEductionInProgress = false private var performedEductionInProgress = false
private fun launchEducation(menu: Menu) { private fun launchEducation(menu: Menu) {
if (!performedEductionInProgress) { if (!performedEductionInProgress) {
performedEductionInProgress = true performedEductionInProgress = true
// Show education views // Show education views
Handler(Looper.getMainLooper()).post { performedNextEducation(PasswordActivityEducation(this), menu) } Handler(Looper.getMainLooper()).post {
performedNextEducation(menu)
}
} }
} }
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation, private fun performedNextEducation(menu: Menu) {
menu: Menu) {
val educationToolbar = toolbar val educationToolbar = toolbar
val unlockEducationPerformed = educationToolbar != null val unlockEducationPerformed = educationToolbar != null
&& passwordActivityEducation.checkAndPerformedUnlockEducation( && mPasswordActivityEducation.checkAndPerformedUnlockEducation(
educationToolbar, educationToolbar,
{ {
performedNextEducation(passwordActivityEducation, menu) performedNextEducation(menu)
}, },
{ {
performedNextEducation(passwordActivityEducation, menu) performedNextEducation(menu)
}) })
if (!unlockEducationPerformed) { if (!unlockEducationPerformed) {
val readOnlyEducationPerformed = val readOnlyEducationPerformed =
educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation( && mPasswordActivityEducation.checkAndPerformedReadOnlyEducation(
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key), educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
{ {
try { try {
@@ -675,19 +583,19 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to find read mode menu") Log.e(TAG, "Unable to find read mode menu")
} }
performedNextEducation(passwordActivityEducation, menu) performedNextEducation(menu)
}, },
{ {
performedNextEducation(passwordActivityEducation, menu) performedNextEducation(menu)
}) })
advancedUnlockFragment?.performEducation(passwordActivityEducation, advancedUnlockFragment?.performEducation(mPasswordActivityEducation,
readOnlyEducationPerformed, readOnlyEducationPerformed,
{ {
performedNextEducation(passwordActivityEducation, menu) performedNextEducation(menu)
}, },
{ {
performedNextEducation(passwordActivityEducation, menu) performedNextEducation(menu)
}) })
} }
} }
@@ -718,7 +626,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
companion object { companion object {
private val TAG = PasswordActivity::class.java.name private val TAG = MainCredentialActivity::class.java.name
private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG" private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG"
@@ -729,12 +637,10 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
private const val KEY_READ_ONLY = "KEY_READ_ONLY" private const val KEY_READ_ONLY = "KEY_READ_ONLY"
private const val KEY_PASSWORD = "password" private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately" private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?, private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
intentBuildLauncher: (Intent) -> Unit) { intentBuildLauncher: (Intent) -> Unit) {
val intent = Intent(activity, PasswordActivity::class.java) val intent = Intent(activity, MainCredentialActivity::class.java)
intent.putExtra(KEY_FILENAME, databaseFile) intent.putExtra(KEY_FILENAME, databaseFile)
if (keyFile != null) if (keyFile != null)
intent.putExtra(KEY_KEYFILE, keyFile) intent.putExtra(KEY_KEYFILE, keyFile)
@@ -870,30 +776,30 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
try { try {
EntrySelectionHelper.doSpecialAction(activity.intent, EntrySelectionHelper.doSpecialAction(activity.intent,
{ {
PasswordActivity.launch(activity, MainCredentialActivity.launch(activity,
databaseUri, keyFile) databaseUri, keyFile)
}, },
{ searchInfo -> // Search Action { searchInfo -> // Search Action
PasswordActivity.launchForSearchResult(activity, MainCredentialActivity.launchForSearchResult(activity,
databaseUri, keyFile, databaseUri, keyFile,
searchInfo) searchInfo)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
}, },
{ searchInfo -> // Save Action { searchInfo -> // Save Action
PasswordActivity.launchForSaveResult(activity, MainCredentialActivity.launchForSaveResult(activity,
databaseUri, keyFile, databaseUri, keyFile,
searchInfo) searchInfo)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
}, },
{ searchInfo -> // Keyboard Selection Action { searchInfo -> // Keyboard Selection Action
PasswordActivity.launchForKeyboardResult(activity, MainCredentialActivity.launchForKeyboardResult(activity,
databaseUri, keyFile, databaseUri, keyFile,
searchInfo) searchInfo)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
}, },
{ searchInfo, autofillComponent -> // Autofill Selection Action { searchInfo, autofillComponent -> // Autofill Selection Action
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PasswordActivity.launchForAutofillResult(activity, MainCredentialActivity.launchForAutofillResult(activity,
databaseUri, keyFile, databaseUri, keyFile,
autofillActivityResultLauncher, autofillActivityResultLauncher,
autofillComponent, autofillComponent,
@@ -904,7 +810,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
} }
}, },
{ registerInfo -> // Registration Action { registerInfo -> // Registration Action
PasswordActivity.launchForRegistration(activity, MainCredentialActivity.launchForRegistration(activity,
databaseUri, keyFile, databaseUri, keyFile,
registerInfo) registerInfo)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()

View File

@@ -0,0 +1,95 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.graphics.Color
import android.os.Bundle
import android.widget.CompoundButton
import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import com.kunzisoft.androidclearchroma.view.ChromaColorView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
class ColorPickerDialogFragment : DatabaseDialogFragment() {
private val mColorPickerViewModel: ColorPickerViewModel by activityViewModels()
private lateinit var enableSwitchView: CompoundButton
private lateinit var chromaColorView: ChromaColorView
private var mDefaultColor = Color.WHITE
private var mActivated = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_color_picker, null)
enableSwitchView = root.findViewById(R.id.switch_element)
chromaColorView = root.findViewById(R.id.chroma_color_view)
if (savedInstanceState != null) {
if (savedInstanceState.containsKey(ARG_INITIAL_COLOR)) {
mDefaultColor = savedInstanceState.getInt(ARG_INITIAL_COLOR)
}
if (savedInstanceState.containsKey(ARG_ACTIVATED)) {
mActivated = savedInstanceState.getBoolean(ARG_ACTIVATED)
}
} else {
arguments?.apply {
if (containsKey(ARG_INITIAL_COLOR)) {
mDefaultColor = getInt(ARG_INITIAL_COLOR)
}
if (containsKey(ARG_ACTIVATED)) {
mActivated = getBoolean(ARG_ACTIVATED)
}
}
}
enableSwitchView.isChecked = mActivated
chromaColorView.currentColor = mDefaultColor
chromaColorView.setOnColorChangedListener {
if (!enableSwitchView.isChecked)
enableSwitchView.isChecked = true
}
val builder = AlertDialog.Builder(activity)
builder.setView(root)
.setPositiveButton(android.R.string.ok) { _, _ ->
val color: Int? = if (enableSwitchView.isChecked)
chromaColorView.currentColor
else
null
mColorPickerViewModel.pickColor(color)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
// Do nothing
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(ARG_INITIAL_COLOR, chromaColorView.currentColor)
outState.putBoolean(ARG_ACTIVATED, mActivated)
}
companion object {
private const val ARG_INITIAL_COLOR = "ARG_INITIAL_COLOR"
private const val ARG_ACTIVATED = "ARG_ACTIVATED"
fun newInstance(
@ColorInt initialColor: Int?,
): ColorPickerDialogFragment {
return ColorPickerDialogFragment().apply {
arguments = Bundle().apply {
putInt(ARG_INITIAL_COLOR, initialColor ?: Color.WHITE)
putBoolean(ARG_ACTIVATED, initialColor != null)
}
}
}
}
}

View File

@@ -0,0 +1,204 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.TagsAdapter
import com.kunzisoft.keepass.database.element.Database
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.UuidUtil
import com.kunzisoft.keepass.view.DateTimeFieldView
class GroupDialogFragment : DatabaseDialogFragment() {
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
private var mGroupInfo = GroupInfo()
private lateinit var iconView: ImageView
private var mIconColor: Int = 0
private lateinit var nameTextView: TextView
private lateinit var tagsListView: RecyclerView
private var tagsAdapter: TagsAdapter? = null
private lateinit var notesTextLabelView: TextView
private lateinit var notesTextView: TextView
private lateinit var expirationView: DateTimeFieldView
private lateinit var creationView: TextView
private lateinit var modificationView: TextView
private lateinit var searchableLabelView: TextView
private lateinit var searchableView: TextView
private lateinit var autoTypeLabelView: TextView
private lateinit var autoTypeView: TextView
private lateinit var uuidContainerView: ViewGroup
private lateinit var uuidReferenceView: TextView
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
}
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
if (database?.allowCustomSearchableGroup() == true) {
searchableLabelView.visibility = View.VISIBLE
searchableView.visibility = View.VISIBLE
} else {
searchableLabelView.visibility = View.GONE
searchableView.visibility = View.GONE
}
// TODO Auto-Type
/*
if (database?.allowAutoType() == true) {
autoTypeLabelView.visibility = View.VISIBLE
autoTypeView.visibility = View.VISIBLE
} else {
autoTypeLabelView.visibility = View.GONE
autoTypeView.visibility = View.GONE
}
*/
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_group, null)
iconView = root.findViewById(R.id.group_icon)
nameTextView = root.findViewById(R.id.group_name)
tagsListView = root.findViewById(R.id.group_tags_list_view)
notesTextLabelView = root.findViewById(R.id.group_note_label)
notesTextView = root.findViewById(R.id.group_note)
expirationView = root.findViewById(R.id.group_expiration)
creationView = root.findViewById(R.id.group_created)
modificationView = root.findViewById(R.id.group_modified)
searchableLabelView = root.findViewById(R.id.group_searchable_label)
searchableView = root.findViewById(R.id.group_searchable)
autoTypeLabelView = root.findViewById(R.id.group_auto_type_label)
autoTypeView = root.findViewById(R.id.group_auto_type)
uuidContainerView = root.findViewById(R.id.group_UUID_container)
uuidReferenceView = root.findViewById(R.id.group_UUID_reference)
// Retrieve the textColor to tint the icon
val ta = activity.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
mIconColor = ta.getColor(0, Color.WHITE)
ta.recycle()
if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
} else {
arguments?.apply {
if (containsKey(KEY_GROUP_INFO)) {
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
}
}
}
// populate info in views
val title = mGroupInfo.title
if (title.isEmpty()) {
nameTextView.visibility = View.GONE
} else {
nameTextView.text = title
nameTextView.visibility = View.VISIBLE
}
tagsAdapter = TagsAdapter(activity)
tagsListView.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
adapter = tagsAdapter
}
val tags = mGroupInfo.tags
tagsListView.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
tagsAdapter?.setTags(tags)
val notes = mGroupInfo.notes
if (notes == null || notes.isEmpty()) {
notesTextLabelView.visibility = View.GONE
notesTextView.visibility = View.GONE
} else {
notesTextView.text = notes
notesTextLabelView.visibility = View.VISIBLE
notesTextView.visibility = View.VISIBLE
}
expirationView.activation = mGroupInfo.expires
expirationView.dateTime = mGroupInfo.expiryTime
creationView.text = mGroupInfo.creationTime.getDateTimeString(resources)
modificationView.text = mGroupInfo.lastModificationTime.getDateTimeString(resources)
searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable)
autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType,
mGroupInfo.defaultAutoTypeSequence)
val uuid = UuidUtil.toHexString(mGroupInfo.id)
if (uuid == null || uuid.isEmpty()) {
uuidContainerView.visibility = View.GONE
} else {
uuidReferenceView.text = uuid
uuidContainerView.apply {
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
}
}
val builder = AlertDialog.Builder(activity)
builder.setView(root)
.setPositiveButton(android.R.string.ok){ _, _ ->
// Do nothing
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun stringFromInheritableBoolean(enable: Boolean?, value: String? = null): String {
val valueString = if (value != null && value.isNotEmpty()) " [$value]" else ""
return when {
enable == null -> getString(R.string.inherited) + valueString
enable -> getString(R.string.enable) + valueString
else -> getString(R.string.disable)
}
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
super.onSaveInstanceState(outState)
}
data class Error(val isError: Boolean, val messageId: Int?)
companion object {
const val TAG_SHOW_GROUP = "TAG_SHOW_GROUP"
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
fun launch(groupInfo: GroupInfo): GroupDialogFragment {
val bundle = Bundle()
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
val fragment = GroupDialogFragment()
fragment.arguments = bundle
return fragment
}
}
}

View File

@@ -23,20 +23,23 @@ import android.app.Dialog
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Button import android.view.ViewGroup
import android.widget.ImageView import android.widget.*
import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.* import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.*
import com.kunzisoft.keepass.adapters.TagsProposalAdapter
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.model.GroupInfo import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.view.DateTimeEditFieldView import com.kunzisoft.keepass.view.DateTimeEditFieldView
import com.kunzisoft.keepass.view.InheritedCompletionView
import com.kunzisoft.keepass.view.TagsCompletionView
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
import com.tokenautocomplete.FilteredArrayAdapter
import org.joda.time.DateTime import org.joda.time.DateTime
class GroupEditDialogFragment : DatabaseDialogFragment() { class GroupEditDialogFragment : DatabaseDialogFragment() {
@@ -55,6 +58,14 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
private lateinit var notesTextLayoutView: TextInputLayout private lateinit var notesTextLayoutView: TextInputLayout
private lateinit var notesTextView: TextView private lateinit var notesTextView: TextView
private lateinit var expirationView: DateTimeEditFieldView private lateinit var expirationView: DateTimeEditFieldView
private lateinit var searchableContainerView: TextInputLayout
private lateinit var searchableView: InheritedCompletionView
private lateinit var autoTypeContainerView: ViewGroup
private lateinit var autoTypeInheritedView: InheritedCompletionView
private lateinit var autoTypeSequenceView: TextView
private lateinit var tagsContainerView: TextInputLayout
private lateinit var tagsCompletionView: TagsCompletionView
private var tagsAdapter: FilteredArrayAdapter<String>? = null
enum class EditGroupDialogAction { enum class EditGroupDialogAction {
CREATION, UPDATE, NONE; CREATION, UPDATE, NONE;
@@ -107,10 +118,30 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
override fun onDatabaseRetrieved(database: Database?) { override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon -> mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor) database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
} }
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon) mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
searchableContainerView.visibility = if (database?.allowCustomSearchableGroup() == true) {
View.VISIBLE
} else {
View.GONE
}
if (database?.allowAutoType() == true) {
autoTypeContainerView.visibility = View.VISIBLE
} else {
autoTypeContainerView.visibility = View.GONE
}
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
tagsCompletionView.apply {
threshold = 1
setAdapter(tagsAdapter)
}
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@@ -122,6 +153,13 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
notesTextLayoutView = root.findViewById(R.id.group_edit_note_container) notesTextLayoutView = root.findViewById(R.id.group_edit_note_container)
notesTextView = root.findViewById(R.id.group_edit_note) notesTextView = root.findViewById(R.id.group_edit_note)
expirationView = root.findViewById(R.id.group_edit_expiration) expirationView = root.findViewById(R.id.group_edit_expiration)
searchableContainerView = root.findViewById(R.id.group_edit_searchable_container)
searchableView = root.findViewById(R.id.group_edit_searchable)
autoTypeContainerView = root.findViewById(R.id.group_edit_auto_type_container)
autoTypeInheritedView = root.findViewById(R.id.group_edit_auto_type_inherited)
autoTypeSequenceView = root.findViewById(R.id.group_edit_auto_type_sequence)
tagsContainerView = root.findViewById(R.id.group_tags_label)
tagsCompletionView = root.findViewById(R.id.group_tags_completion_view)
// Retrieve the textColor to tint the icon // Retrieve the textColor to tint the icon
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
@@ -197,6 +235,19 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
} }
expirationView.activation = groupInfo.expires expirationView.activation = groupInfo.expires
expirationView.dateTime = groupInfo.expiryTime expirationView.dateTime = groupInfo.expiryTime
// Set searchable
searchableView.setValue(groupInfo.searchable)
// Set auto-type
autoTypeInheritedView.setValue(groupInfo.enableAutoType)
autoTypeSequenceView.text = groupInfo.defaultAutoTypeSequence
// Set Tags
groupInfo.tags.let { tags ->
tagsCompletionView.setText("")
for (i in 0 until tags.size()) {
tagsCompletionView.addObjectSync(tags.get(i))
}
}
} }
private fun retrieveGroupInfoFromViews() { private fun retrieveGroupInfoFromViews() {
@@ -208,6 +259,10 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
} }
mGroupInfo.expires = expirationView.activation mGroupInfo.expires = expirationView.activation
mGroupInfo.expiryTime = expirationView.dateTime mGroupInfo.expiryTime = expirationView.dateTime
mGroupInfo.searchable = searchableView.getValue()
mGroupInfo.enableAutoType = autoTypeInheritedView.getValue()
mGroupInfo.defaultAutoTypeSequence = autoTypeSequenceView.text.toString()
mGroupInfo.tags = tagsCompletionView.getTags()
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@@ -246,8 +301,8 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
companion object { companion object {
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP" const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
const val KEY_ACTION_ID = "KEY_ACTION_ID" private const val KEY_ACTION_ID = "KEY_ACTION_ID"
const val KEY_GROUP_INFO = "KEY_GROUP_INFO" private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
fun create(groupInfo: GroupInfo): GroupEditDialogFragment { fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
val bundle = Bundle() val bundle = Bundle()

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2022 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.MainCredentialView
class MainCredentialDialogFragment : DatabaseDialogFragment() {
private var mainCredentialView: MainCredentialView? = null
private var mListener: AskMainCredentialDialogListener? = null
private var mExternalFileHelper: ExternalFileHelper? = null
interface AskMainCredentialDialogListener {
fun onAskMainCredentialDialogPositiveClick(databaseUri: Uri?, mainCredential: MainCredential)
fun onAskMainCredentialDialogNegativeClick(databaseUri: Uri?, mainCredential: MainCredential)
}
override fun onAttach(activity: Context) {
super.onAttach(activity)
try {
mListener = activity as AskMainCredentialDialogListener
} catch (e: ClassCastException) {
throw ClassCastException(activity.toString()
+ " must implement " + AskMainCredentialDialogListener::class.java.name)
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
var databaseUri: Uri? = null
arguments?.apply {
if (containsKey(KEY_ASK_CREDENTIAL_URI))
databaseUri = getParcelable(KEY_ASK_CREDENTIAL_URI)
}
val builder = AlertDialog.Builder(activity)
val root = activity.layoutInflater.inflate(R.layout.fragment_main_credential, null)
mainCredentialView = root.findViewById(R.id.main_credential_view)
databaseUri?.let {
root.findViewById<TextView>(R.id.title_database)?.text =
UriUtil.getFileData(requireContext(), it)?.name
}
builder.setView(root)
// Add action buttons
.setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.onAskMainCredentialDialogPositiveClick(
databaseUri,
retrieveMainCredential()
)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
mListener?.onAskMainCredentialDialogNegativeClick(
databaseUri,
retrieveMainCredential()
)
}
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) {
mainCredentialView?.populateKeyFileTextView(uri)
}
}
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun retrieveMainCredential(): MainCredential {
return mainCredentialView?.getMainCredential() ?: MainCredential()
}
companion object {
private const val KEY_ASK_CREDENTIAL_URI = "KEY_ASK_CREDENTIAL_URI"
const val TAG_ASK_MAIN_CREDENTIAL = "TAG_ASK_MAIN_CREDENTIAL"
fun getInstance(uri: Uri?): MainCredentialDialogFragment {
val fragment = MainCredentialDialogFragment()
val args = Bundle()
args.putParcelable(KEY_ASK_CREDENTIAL_URI, uri)
fragment.arguments = args
return fragment
}
}
}

View File

@@ -39,7 +39,7 @@ import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.KeyFileSelectionView
class AssignMasterKeyDialogFragment : DatabaseDialogFragment() { class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private var mMasterPassword: String? = null private var mMasterPassword: String? = null
private var mKeyFile: Uri? = null private var mKeyFile: Uri? = null
@@ -56,7 +56,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
private var keyFileCheckBox: CompoundButton? = null private var keyFileCheckBox: CompoundButton? = null
private var keyFileSelectionView: KeyFileSelectionView? = null private var keyFileSelectionView: KeyFileSelectionView? = null
private var mListener: AssignPasswordDialogListener? = null private var mListener: AssignMainCredentialDialogListener? = null
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
@@ -74,7 +74,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
} }
} }
interface AssignPasswordDialogListener { interface AssignMainCredentialDialogListener {
fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential)
fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential)
} }
@@ -82,10 +82,10 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
override fun onAttach(activity: Context) { override fun onAttach(activity: Context) {
super.onAttach(activity) super.onAttach(activity)
try { try {
mListener = activity as AssignPasswordDialogListener mListener = activity as AssignMainCredentialDialogListener
} catch (e: ClassCastException) { } catch (e: ClassCastException) {
throw ClassCastException(activity.toString() throw ClassCastException(activity.toString()
+ " must implement " + AssignPasswordDialogListener::class.java.name) + " must implement " + AssignMainCredentialDialogListener::class.java.name)
} }
} }
@@ -112,7 +112,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater val inflater = activity.layoutInflater
rootView = inflater.inflate(R.layout.fragment_set_password, null) rootView = inflater.inflate(R.layout.fragment_set_main_credential, null)
builder.setView(rootView) builder.setView(rootView)
// Add action buttons // Add action buttons
.setPositiveButton(android.R.string.ok) { _, _ -> } .setPositiveButton(android.R.string.ok) { _, _ -> }
@@ -254,7 +254,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
if (!verifyKeyFile()) { if (!verifyKeyFile()) {
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
this@AssignMasterKeyDialogFragment.dismiss() this@SetMainCredentialDialogFragment.dismiss()
} }
} }
.setNegativeButton(android.R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
@@ -269,7 +269,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
builder.setMessage(R.string.warning_no_encryption_key) builder.setMessage(R.string.warning_no_encryption_key)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
this@AssignMasterKeyDialogFragment.dismiss() this@SetMainCredentialDialogFragment.dismiss()
} }
.setNegativeButton(android.R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
mNoKeyConfirmationDialog = builder.create() mNoKeyConfirmationDialog = builder.create()
@@ -301,8 +301,8 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG" private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
fun getInstance(allowNoMasterKey: Boolean): AssignMasterKeyDialogFragment { fun getInstance(allowNoMasterKey: Boolean): SetMainCredentialDialogFragment {
val fragment = AssignMasterKeyDialogFragment() val fragment = SetMainCredentialDialogFragment()
val args = Bundle() val args = Bundle()
args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey) args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey)
fragment.arguments = args fragment.arguments = args

View File

@@ -29,10 +29,12 @@ import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.adapters.TagsProposalAdapter
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.template.Template import com.kunzisoft.keepass.database.element.template.Template
@@ -40,11 +42,10 @@ import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.StreamDirection import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.view.TemplateEditView import com.kunzisoft.keepass.view.*
import com.kunzisoft.keepass.view.collapse
import com.kunzisoft.keepass.view.expand
import com.kunzisoft.keepass.view.showByFading
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
import com.tokenautocomplete.FilteredArrayAdapter
class EntryEditFragment: DatabaseFragment() { class EntryEditFragment: DatabaseFragment() {
@@ -55,6 +56,9 @@ class EntryEditFragment: DatabaseFragment() {
private lateinit var attachmentsContainerView: ViewGroup private lateinit var attachmentsContainerView: ViewGroup
private lateinit var attachmentsListView: RecyclerView private lateinit var attachmentsListView: RecyclerView
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
private lateinit var tagsContainerView: TextInputLayout
private lateinit var tagsCompletionView: TagsCompletionView
private var tagsAdapter: FilteredArrayAdapter<String>? = null
private var mTemplate: Template? = null private var mTemplate: Template? = null
private var mAllowMultipleAttachments: Boolean = false private var mAllowMultipleAttachments: Boolean = false
@@ -87,6 +91,8 @@ class EntryEditFragment: DatabaseFragment() {
templateView = view.findViewById(R.id.template_view) templateView = view.findViewById(R.id.template_view)
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container) attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
attachmentsListView = view.findViewById(R.id.entry_attachments_list) attachmentsListView = view.findViewById(R.id.entry_attachments_list)
tagsContainerView = view.findViewById(R.id.entry_tags_label)
tagsCompletionView = view.findViewById(R.id.entry_tags_completion_view)
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext()) attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
attachmentsListView.apply { attachmentsListView.apply {
@@ -99,6 +105,12 @@ class EntryEditFragment: DatabaseFragment() {
setOnIconClickListener { setOnIconClickListener {
mEntryEditViewModel.requestIconSelection(templateView.getIcon()) mEntryEditViewModel.requestIconSelection(templateView.getIcon())
} }
setOnBackgroundColorClickListener {
mEntryEditViewModel.requestBackgroundColorSelection(templateView.getBackgroundColor())
}
setOnForegroundColorClickListener {
mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor())
}
setOnCustomEditionActionClickListener { field -> setOnCustomEditionActionClickListener { field ->
mEntryEditViewModel.requestCustomFieldEdition(field) mEntryEditViewModel.requestCustomFieldEdition(field)
} }
@@ -140,13 +152,22 @@ class EntryEditFragment: DatabaseFragment() {
} }
mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) { mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) {
mEntryEditViewModel.saveEntryInfo(it.database, it.entry, it.parent, retrieveEntryInfo()) val entryInfo = retrieveEntryInfo()
mEntryEditViewModel.saveEntryInfo(it.database, it.entry, it.parent, entryInfo)
} }
mEntryEditViewModel.onIconSelected.observe(viewLifecycleOwner) { iconImage -> mEntryEditViewModel.onIconSelected.observe(viewLifecycleOwner) { iconImage ->
templateView.setIcon(iconImage) templateView.setIcon(iconImage)
} }
mEntryEditViewModel.onBackgroundColorSelected.observe(viewLifecycleOwner) { color ->
templateView.setBackgroundColor(color)
}
mEntryEditViewModel.onForegroundColorSelected.observe(viewLifecycleOwner) { color ->
templateView.setForegroundColor(color)
}
mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField -> mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField ->
templateView.setPasswordField(passwordField) templateView.setPasswordField(passwordField)
} }
@@ -263,18 +284,34 @@ class EntryEditFragment: DatabaseFragment() {
attachmentsContainerView.expand(true) attachmentsContainerView.expand(true)
} }
} }
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
tagsCompletionView.apply {
threshold = 1
setAdapter(tagsAdapter)
}
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
} }
private fun assignEntryInfo(entryInfo: EntryInfo?) { private fun assignEntryInfo(entryInfo: EntryInfo?) {
// Populate entry views // Populate entry views
templateView.setEntryInfo(entryInfo) templateView.setEntryInfo(entryInfo)
// Set Tags
entryInfo?.tags?.let { tags ->
tagsCompletionView.setText("")
for (i in 0 until tags.size()) {
tagsCompletionView.addObjectSync(tags.get(i))
}
}
// Manage attachments // Manage attachments
setAttachments(entryInfo?.attachments ?: listOf()) setAttachments(entryInfo?.attachments ?: listOf())
} }
private fun retrieveEntryInfo(): EntryInfo { private fun retrieveEntryInfo(): EntryInfo {
val entryInfo = templateView.getEntryInfo() val entryInfo = templateView.getEntryInfo()
entryInfo.tags = tagsCompletionView.getTags()
entryInfo.attachments = getAttachments().toMutableList() entryInfo.attachments = getAttachments().toMutableList()
return entryInfo return entryInfo
} }

View File

@@ -41,8 +41,9 @@ class EntryFragment: DatabaseFragment() {
private lateinit var attachmentsListView: RecyclerView private lateinit var attachmentsListView: RecyclerView
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
private lateinit var customDataView: TextView
private lateinit var uuidContainerView: View private lateinit var uuidContainerView: View
private lateinit var uuidView: TextView
private lateinit var uuidReferenceView: TextView private lateinit var uuidReferenceView: TextView
private var mClipboardHelper: ClipboardHelper? = null private var mClipboardHelper: ClipboardHelper? = null
@@ -84,11 +85,13 @@ class EntryFragment: DatabaseFragment() {
creationDateView = view.findViewById(R.id.entry_created) creationDateView = view.findViewById(R.id.entry_created)
modificationDateView = view.findViewById(R.id.entry_modified) modificationDateView = view.findViewById(R.id.entry_modified)
// TODO Custom data
// customDataView = view.findViewById(R.id.entry_custom_data)
uuidContainerView = view.findViewById(R.id.entry_UUID_container) uuidContainerView = view.findViewById(R.id.entry_UUID_container)
uuidContainerView.apply { uuidContainerView.apply {
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
} }
uuidView = view.findViewById(R.id.entry_UUID)
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference) uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory -> mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory ->
@@ -156,11 +159,14 @@ class EntryFragment: DatabaseFragment() {
assignAttachments(entryInfo?.attachments ?: listOf()) assignAttachments(entryInfo?.attachments ?: listOf())
// Assign dates // Assign dates
assignCreationDate(entryInfo?.creationTime) creationDateView.text = entryInfo?.creationTime?.getDateTimeString(resources)
assignModificationDate(entryInfo?.lastModificationTime) modificationDateView.text = entryInfo?.lastModificationTime?.getDateTimeString(resources)
// TODO Custom data
// customDataView.text = entryInfo?.customData?.toString()
// Assign special data // Assign special data
assignUUID(entryInfo?.id) uuidReferenceView.text = UuidUtil.toHexString(entryInfo?.id)
} }
private fun showClipboardDialog() { private fun showClipboardDialog() {
@@ -191,19 +197,6 @@ class EntryFragment: DatabaseFragment() {
templateView.reload() templateView.reload()
} }
private fun assignCreationDate(date: DateInstant?) {
creationDateView.text = date?.getDateTimeString(resources)
}
private fun assignModificationDate(date: DateInstant?) {
modificationDateView.text = date?.getDateTimeString(resources)
}
private fun assignUUID(uuid: UUID?) {
uuidView.text = uuid?.toString()
uuidReferenceView.text = UuidUtil.toHexString(uuid)
}
/* ------------- /* -------------
* Attachments * Attachments
* ------------- * -------------

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.activities.fragments package com.kunzisoft.keepass.activities.fragments
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.* import android.view.*
@@ -34,12 +33,11 @@ import com.kunzisoft.keepass.activities.EntryEditActivity
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.adapters.NodeAdapter import com.kunzisoft.keepass.adapters.NodesAdapter
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -50,10 +48,11 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
private var nodeClickListener: NodeClickListener? = null private var nodeClickListener: NodeClickListener? = null
private var onScrollListener: OnScrollListener? = null private var onScrollListener: OnScrollListener? = null
private var groupRefreshed: GroupRefreshedListener? = null
private var mNodesRecyclerView: RecyclerView? = null private var mNodesRecyclerView: RecyclerView? = null
private var mLayoutManager: LinearLayoutManager? = null private var mLayoutManager: LinearLayoutManager? = null
private var mAdapter: NodeAdapter? = null private var mAdapter: NodesAdapter? = null
private val mGroupViewModel: GroupViewModel by activityViewModels() private val mGroupViewModel: GroupViewModel by activityViewModels()
@@ -102,12 +101,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
// TODO Change to ViewModel
try { try {
nodeClickListener = context as NodeClickListener nodeClickListener = context as NodeClickListener
} catch (e: ClassCastException) { } catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception // The activity doesn't implement the interface, throw exception
throw ClassCastException(context.toString() throw ClassCastException(context.toString()
+ " must implement " + NodeAdapter.NodeClickCallback::class.java.name) + " must implement " + NodesAdapter.NodeClickCallback::class.java.name)
} }
try { try {
@@ -115,14 +116,24 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
} catch (e: ClassCastException) { } catch (e: ClassCastException) {
onScrollListener = null onScrollListener = null
// Context menu can be omit // Context menu can be omit
Log.w(TAG, context.toString() Log.w(
TAG, context.toString()
+ " must implement " + RecyclerView.OnScrollListener::class.java.name) + " must implement " + RecyclerView.OnScrollListener::class.java.name)
} }
try {
groupRefreshed = context as GroupRefreshedListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context.toString()
+ " must implement " + GroupRefreshedListener::class.java.name)
}
} }
override fun onDetach() { override fun onDetach() {
nodeClickListener = null nodeClickListener = null
onScrollListener = null onScrollListener = null
groupRefreshed = null
super.onDetach() super.onDetach()
} }
@@ -138,10 +149,11 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
contextThemed?.let { context -> contextThemed?.let { context ->
database?.let { database -> database?.let { database ->
mAdapter = NodeAdapter(context, database).apply { mAdapter = NodesAdapter(context, database).apply {
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback { setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
override fun onNodeClick(database: Database, node: Node) { override fun onNodeClick(database: Database, node: Node) {
if (nodeActionSelectionMode) { if (mCurrentGroup?.isVirtual == false
&& nodeActionSelectionMode) {
if (listActionNodes.contains(node)) { if (listActionNodes.contains(node)) {
// Remove selected item if already selected // Remove selected item if already selected
listActionNodes.remove(node) listActionNodes.remove(node)
@@ -158,7 +170,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
} }
override fun onNodeLongClick(database: Database, node: Node): Boolean { override fun onNodeLongClick(database: Database, node: Node): Boolean {
if (nodeActionPasteMode == PasteMode.UNDEFINED) { if (mCurrentGroup?.isVirtual == false
&& nodeActionPasteMode == PasteMode.UNDEFINED) {
// Select the first item after a long click // Select the first item after a long click
if (!listActionNodes.contains(node)) if (!listActionNodes.contains(node))
listActionNodes.add(node) listActionNodes.add(node)
@@ -195,7 +208,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
// To apply theme // To apply theme
return inflater.cloneInContext(contextThemed) return inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_group, container, false) .inflate(R.layout.fragment_nodes, container, false)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -246,9 +259,9 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
private fun rebuildList() { private fun rebuildList() {
try { try {
// Add elements to the list // Add elements to the list
mCurrentGroup?.let { mainGroup -> mCurrentGroup?.let { currentGroup ->
// Thrown an exception when sort cannot be performed // Thrown an exception when sort cannot be performed
mAdapter?.rebuildList(mainGroup) mAdapter?.rebuildList(currentGroup)
} }
} catch (e:Exception) { } catch (e:Exception) {
Log.e(TAG, "Unable to rebuild the list", e) Log.e(TAG, "Unable to rebuild the list", e)
@@ -260,6 +273,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
} else { } else {
notFoundView?.visibility = View.GONE notFoundView?.visibility = View.GONE
} }
groupRefreshed?.onGroupRefreshed()
} }
override fun onSortSelected(sortNodeEnum: SortNodeEnum, override fun onSortSelected(sortNodeEnum: SortNodeEnum,
@@ -292,15 +307,17 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
val sortDialogFragment: SortDialogFragment = val sortDialogFragment: SortDialogFragment =
if (mRecycleBinEnable) { if (mRecycleBinEnable) {
SortDialogFragment.getInstance( SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context), PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context), PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context), PreferencesUtil.getGroupsBeforeSort(context),
PreferencesUtil.getRecycleBinBottomSort(context)) PreferencesUtil.getRecycleBinBottomSort(context)
)
} else { } else {
SortDialogFragment.getInstance( SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context), PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context), PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context)) PreferencesUtil.getGroupsBeforeSort(context)
)
} }
sortDialogFragment.show(childFragmentManager, "sortDialog") sortDialogFragment.show(childFragmentManager, "sortDialog")
@@ -447,6 +464,10 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
fun onScrolled(dy: Int) fun onScrolled(dy: Int)
} }
interface GroupRefreshedListener {
fun onGroupRefreshed()
}
companion object { companion object {
private val TAG = GroupFragment::class.java.name private val TAG = GroupFragment::class.java.name
} }

View File

@@ -4,9 +4,9 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.activity.viewModels import androidx.activity.viewModels
import com.kunzisoft.keepass.activities.stylish.StylishActivity import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
@@ -59,9 +59,9 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
fun loadDatabase(databaseUri: Uri, fun loadDatabase(databaseUri: Uri,
mainCredential: MainCredential, mainCredential: MainCredential,
readOnly: Boolean, readOnly: Boolean,
cipherEntity: CipherDatabaseEntity?, cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean) { fixDuplicateUuid: Boolean) {
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEntity, fixDuplicateUuid) mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
} }
protected fun closeDatabase() { protected fun closeDatabase() {

View File

@@ -62,6 +62,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
private var mExitLock: Boolean = false private var mExitLock: Boolean = false
protected var mDatabaseReadOnly: Boolean = true protected var mDatabaseReadOnly: Boolean = true
protected var mMergeDataAllowed: Boolean = false
private var mAutoSaveEnable: Boolean = true private var mAutoSaveEnable: Boolean = true
protected var mIconDrawableFactory: IconDrawableFactory? = null protected var mIconDrawableFactory: IconDrawableFactory? = null
@@ -87,8 +88,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabaseTaskProvider?.startDatabaseSave(save) mDatabaseTaskProvider?.startDatabaseSave(save)
} }
mDatabaseViewModel.mergeDatabase.observe(this) {
mDatabaseTaskProvider?.startDatabaseMerge()
}
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid -> mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid) mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
}
} }
mDatabaseViewModel.saveName.observe(this) { mDatabaseViewModel.saveName.observe(this) {
@@ -197,6 +204,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
} }
mDatabaseReadOnly = database.isReadOnly mDatabaseReadOnly = database.isReadOnly
mMergeDataAllowed = database.isMergeDataAllowed()
mIconDrawableFactory = database.iconDrawableFactory mIconDrawableFactory = database.iconDrawableFactory
checkRegister() checkRegister()
@@ -212,6 +220,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
) { ) {
super.onDatabaseActionFinished(database, actionTask, result) super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) { when (actionTask) {
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> { DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
// Reload the current activity // Reload the current activity
if (result.isSuccess) { if (result.isSuccess) {
@@ -254,8 +263,22 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabaseTaskProvider?.startDatabaseSave(true) mDatabaseTaskProvider?.startDatabaseSave(true)
} }
fun saveDatabaseTo(uri: Uri) {
mDatabaseTaskProvider?.startDatabaseSave(true, uri)
}
fun mergeDatabase() {
mDatabaseTaskProvider?.startDatabaseMerge()
}
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
mDatabaseTaskProvider?.startDatabaseMerge(uri, mainCredential)
}
fun reloadDatabase() { fun reloadDatabase() {
mDatabaseTaskProvider?.startDatabaseReload(false) mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
mDatabaseTaskProvider?.startDatabaseReload(false)
}
} }
fun createEntry(newEntry: Entry, fun createEntry(newEntry: Entry,

View File

@@ -1,6 +1,8 @@
package com.kunzisoft.keepass.activities.legacy package com.kunzisoft.keepass.activities.legacy
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
@@ -11,6 +13,7 @@ import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.SpecialModeView import com.kunzisoft.keepass.view.SpecialModeView
/** /**
* Activity to manage database special mode (ie: selection mode) * Activity to manage database special mode (ie: selection mode)
*/ */
@@ -63,8 +66,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
EntrySelectionHelper.removeModesFromIntent(intent) EntrySelectionHelper.removeModesFromIntent(intent)
EntrySelectionHelper.removeInfoFromIntent(intent) EntrySelectionHelper.removeInfoFromIntent(intent)
if (mSpecialMode != SpecialMode.DEFAULT) { if (mSpecialMode != SpecialMode.DEFAULT) {
// To move the app in background backToTheMainAppAndFinish()
moveTaskToBack(true)
} }
} }
} }
@@ -77,8 +79,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
EntrySelectionHelper.removeModesFromIntent(intent) EntrySelectionHelper.removeModesFromIntent(intent)
EntrySelectionHelper.removeInfoFromIntent(intent) EntrySelectionHelper.removeInfoFromIntent(intent)
if (mSpecialMode != SpecialMode.DEFAULT) { if (mSpecialMode != SpecialMode.DEFAULT) {
// To move the app in background backToTheMainAppAndFinish()
moveTaskToBack(true)
} }
} }
} }
@@ -88,11 +89,19 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
// To get the app caller, only for IntentSender // To get the app caller, only for IntentSender
super.onBackPressed() super.onBackPressed()
} else { } else {
// To move the app in background backToTheMainAppAndFinish()
moveTaskToBack(true)
} }
} }
private fun backToTheMainAppAndFinish() {
// To move the app in background and return to the main app
moveTaskToBack(true)
// To remove this instance in the OS app selector
Handler(Looper.getMainLooper()).postDelayed({
finish()
}, 500)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -160,12 +169,17 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
} }
// To hide home button from the regular toolbar in special mode // To hide home button from the regular toolbar in special mode
if (mSpecialMode != SpecialMode.DEFAULT) { if (mSpecialMode != SpecialMode.DEFAULT
&& hideHomeButtonIfModeIsNotDefault()) {
supportActionBar?.setDisplayHomeAsUpEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(false)
supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.setDisplayShowHomeEnabled(false)
} }
} }
open fun hideHomeButtonIfModeIsNotDefault(): Boolean {
return true
}
private fun blockAutofill(searchInfo: SearchInfo?) { private fun blockAutofill(searchInfo: SearchInfo?) {
val webDomain = searchInfo?.webDomain val webDomain = searchInfo?.webDomain
val applicationId = searchInfo?.applicationId val applicationId = searchInfo?.applicationId

View File

@@ -69,8 +69,10 @@ object Stylish {
context.getString(R.string.list_style_name_night) -> context.getString(R.string.list_style_name_light) context.getString(R.string.list_style_name_night) -> context.getString(R.string.list_style_name_light)
context.getString(R.string.list_style_name_black) -> context.getString(R.string.list_style_name_white) context.getString(R.string.list_style_name_black) -> context.getString(R.string.list_style_name_white)
context.getString(R.string.list_style_name_dark) -> context.getString(R.string.list_style_name_clear) context.getString(R.string.list_style_name_dark) -> context.getString(R.string.list_style_name_clear)
context.getString(R.string.list_style_name_simple_night) -> context.getString(R.string.list_style_name_simple)
context.getString(R.string.list_style_name_blue_night) -> context.getString(R.string.list_style_name_blue) context.getString(R.string.list_style_name_blue_night) -> context.getString(R.string.list_style_name_blue)
context.getString(R.string.list_style_name_red_night) -> context.getString(R.string.list_style_name_red) context.getString(R.string.list_style_name_red_night) -> context.getString(R.string.list_style_name_red)
context.getString(R.string.list_style_name_reply_night) -> context.getString(R.string.list_style_name_reply)
context.getString(R.string.list_style_name_purple_dark) -> context.getString(R.string.list_style_name_purple) context.getString(R.string.list_style_name_purple_dark) -> context.getString(R.string.list_style_name_purple)
else -> styleString else -> styleString
} }
@@ -81,8 +83,10 @@ object Stylish {
context.getString(R.string.list_style_name_light) -> context.getString(R.string.list_style_name_night) context.getString(R.string.list_style_name_light) -> context.getString(R.string.list_style_name_night)
context.getString(R.string.list_style_name_white) -> context.getString(R.string.list_style_name_black) context.getString(R.string.list_style_name_white) -> context.getString(R.string.list_style_name_black)
context.getString(R.string.list_style_name_clear) -> context.getString(R.string.list_style_name_dark) context.getString(R.string.list_style_name_clear) -> context.getString(R.string.list_style_name_dark)
context.getString(R.string.list_style_name_simple) -> context.getString(R.string.list_style_name_simple_night)
context.getString(R.string.list_style_name_blue) -> context.getString(R.string.list_style_name_blue_night) context.getString(R.string.list_style_name_blue) -> context.getString(R.string.list_style_name_blue_night)
context.getString(R.string.list_style_name_red) -> context.getString(R.string.list_style_name_red_night) context.getString(R.string.list_style_name_red) -> context.getString(R.string.list_style_name_red_night)
context.getString(R.string.list_style_name_reply) -> context.getString(R.string.list_style_name_reply_night)
context.getString(R.string.list_style_name_purple) -> context.getString(R.string.list_style_name_purple_dark) context.getString(R.string.list_style_name_purple) -> context.getString(R.string.list_style_name_purple_dark)
else -> styleString else -> styleString
} }
@@ -113,10 +117,14 @@ object Stylish {
context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black
context.getString(R.string.list_style_name_clear) -> R.style.KeepassDXStyle_Clear context.getString(R.string.list_style_name_clear) -> R.style.KeepassDXStyle_Clear
context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark
context.getString(R.string.list_style_name_simple) -> R.style.KeepassDXStyle_Simple
context.getString(R.string.list_style_name_simple_night) -> R.style.KeepassDXStyle_Simple_Night
context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue
context.getString(R.string.list_style_name_blue_night) -> R.style.KeepassDXStyle_Blue_Night context.getString(R.string.list_style_name_blue_night) -> R.style.KeepassDXStyle_Blue_Night
context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red
context.getString(R.string.list_style_name_red_night) -> R.style.KeepassDXStyle_Red_Night context.getString(R.string.list_style_name_red_night) -> R.style.KeepassDXStyle_Red_Night
context.getString(R.string.list_style_name_reply) -> R.style.KeepassDXStyle_Reply
context.getString(R.string.list_style_name_reply_night) -> R.style.KeepassDXStyle_Reply_Night
context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple
context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark
else -> R.style.KeepassDXStyle_Light else -> R.style.KeepassDXStyle_Light

View File

@@ -28,7 +28,7 @@ import android.util.Log
import android.view.WindowManager import android.view.WindowManager
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_APPEARANCE_PREFERENCE_CHANGED import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_PREFERENCE_CHANGED
/** /**
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from * Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
@@ -89,8 +89,8 @@ abstract class StylishActivity : AppCompatActivity() {
super.onResume() super.onResume()
if ((customStyle && Stylish.getThemeId(this) != this.themeId) if ((customStyle && Stylish.getThemeId(this) != this.themeId)
|| DATABASE_APPEARANCE_PREFERENCE_CHANGED) { || DATABASE_PREFERENCE_CHANGED) {
DATABASE_APPEARANCE_PREFERENCE_CHANGED = false DATABASE_PREFERENCE_CHANGED = false
Log.d(this.javaClass.name, "Theme change detected, restarting activity") Log.d(this.javaClass.name, "Theme change detected, restarting activity")
recreateActivity() recreateActivity()
} }

View File

@@ -23,11 +23,13 @@ import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
abstract class StylishFragment : Fragment() { abstract class StylishFragment : Fragment() {
@@ -47,27 +49,41 @@ abstract class StylishFragment : Fragment() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val window = requireActivity().window val window = requireActivity().window
val defaultColor = Color.BLACK val defaultColor = Color.BLACK
val windowInset = WindowInsetsControllerCompat(window, window.decorView)
try { try {
val taStatusBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor)) val taStatusBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor))
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
taStatusBarColor?.recycle() taStatusBarColor?.recycle()
} catch (e: Exception) {} } catch (e: Exception) {
Log.e(TAG, "Unable to retrieve theme : status bar color", e)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try { try {
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar)) val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
if (taWindowStatusLight?.getBoolean(0, false) == true) { windowInset.isAppearanceLightStatusBars = taWindowStatusLight
@Suppress("DEPRECATION") ?.getBoolean(0, false) == true
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
taWindowStatusLight?.recycle() taWindowStatusLight?.recycle()
} catch (e: Exception) {} } catch (e: Exception) {
Log.e(TAG, "Unable to retrieve theme : window light status bar", e)
}
} }
try { try {
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor)) val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
taNavigationBarColor?.recycle() taNavigationBarColor?.recycle()
} catch (e: Exception) {} } catch (e: Exception) {
Log.e(TAG, "Unable to retrieve theme : navigation bar color", e)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
try {
val taWindowLightNavigationBar = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightNavigationBar))
windowInset.isAppearanceLightNavigationBars = taWindowLightNavigationBar
?.getBoolean(0, false) == true
taWindowLightNavigationBar?.recycle()
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve theme : navigation light navigation bar", e)
}
}
} }
return super.onCreateView(inflater, container, savedInstanceState) return super.onCreateView(inflater, container, savedInstanceState)
} }
@@ -76,4 +92,8 @@ abstract class StylishFragment : Fragment() {
contextThemed = null contextThemed = null
super.onDetach() super.onDetach()
} }
companion object {
private val TAG = StylishFragment::class.java.simpleName
}
} }

View File

@@ -0,0 +1,150 @@
package com.kunzisoft.keepass.adapters
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.strikeOut
class BreadcrumbAdapter(val context: Context)
: RecyclerView.Adapter<BreadcrumbAdapter.BreadcrumbGroupViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
var iconDrawableFactory: IconDrawableFactory? = null
@SuppressLint("NotifyDataSetChanged")
set(value) {
field = value
notifyDataSetChanged()
}
private var mNodeBreadcrumb: MutableList<Node?> = mutableListOf()
var onItemClickListener: ((item: Node, position: Int)->Unit)? = null
var onLongItemClickListener: ((item: Node, position: Int)->Unit)? = null
private var mShowNumberEntries = false
private var mShowUUID = false
private var mIconColor: Int = 0
init {
mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
mShowUUID = PreferencesUtil.showUUID(context)
// Retrieve the textColor to tint the icon
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
mIconColor = taTextColor.getColor(0, Color.WHITE)
taTextColor.recycle()
}
@SuppressLint("NotifyDataSetChanged")
fun setNode(node: Node?) {
mNodeBreadcrumb.clear()
node?.let {
var currentNode = it
mNodeBreadcrumb.add(0, currentNode)
while (currentNode.containsParent()) {
currentNode.parent?.let { parent ->
currentNode = parent
mNodeBreadcrumb.add(0, currentNode)
}
}
}
notifyDataSetChanged()
}
override fun getItemViewType(position: Int): Int {
return when (position) {
mNodeBreadcrumb.size - 1 -> 0
else -> 1
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BreadcrumbGroupViewHolder {
return BreadcrumbGroupViewHolder(inflater.inflate(
when (viewType) {
0 -> R.layout.item_group
else -> R.layout.item_breadcrumb
}, parent, false)
)
}
override fun onBindViewHolder(holder: BreadcrumbGroupViewHolder, position: Int) {
val node = mNodeBreadcrumb[position]
holder.groupNameView.apply {
text = node?.title ?: ""
strikeOut(node?.isCurrentlyExpires ?: false)
}
holder.itemView.apply {
setOnClickListener {
node?.let {
onItemClickListener?.invoke(it, position)
}
}
setOnLongClickListener {
node?.let {
onLongItemClickListener?.invoke(it, position)
}
true
}
}
if (node?.type == Type.GROUP) {
(node as Group).let { group ->
holder.groupIconView?.let { imageView ->
iconDrawableFactory?.assignDatabaseIcon(
imageView,
group.icon,
mIconColor
)
}
holder.groupNumbersView?.apply {
if (mShowNumberEntries) {
group.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context))
text = group.numberOfChildEntries.toString()
visibility = View.VISIBLE
} else {
visibility = View.GONE
}
}
holder.groupMetaView?.apply {
val meta = group.nodeId.toVisualString()
visibility = if (meta != null
&& !group.isVirtual
&& mShowUUID
) {
text = meta
View.VISIBLE
} else {
View.GONE
}
}
}
}
}
override fun getItemCount(): Int {
return mNodeBreadcrumb.size
}
inner class BreadcrumbGroupViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var groupIconView: ImageView? = itemView.findViewById(R.id.group_icon)
var groupNumbersView: TextView? = itemView.findViewById(R.id.group_numbers)
var groupNameView: TextView = itemView.findViewById(R.id.group_name)
var groupMetaView: TextView? = itemView.findViewById(R.id.group_meta)
}
}

View File

@@ -26,14 +26,13 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback import androidx.recyclerview.widget.SortedListAdapterCallback
import com.google.android.material.progressindicator.CircularProgressIndicator
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
@@ -55,9 +54,9 @@ import java.util.*
* Create node list adapter with contextMenu or not * Create node list adapter with contextMenu or not
* @param context Context to use * @param context Context to use
*/ */
class NodeAdapter (private val context: Context, class NodesAdapter (private val context: Context,
private val database: Database) private val database: Database)
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() { : RecyclerView.Adapter<NodesAdapter.NodeViewHolder>() {
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
private val mNodeSortedListCallback: NodeSortedListCallback private val mNodeSortedListCallback: NodeSortedListCallback
@@ -74,22 +73,29 @@ class NodeAdapter (private val context: Context,
private var mNumberChildrenTextDefaultDimension: Float = 0F private var mNumberChildrenTextDefaultDimension: Float = 0F
private var mIconDefaultDimension: Float = 0F private var mIconDefaultDimension: Float = 0F
private var mShowEntryColors: Boolean = true
private var mShowUserNames: Boolean = true private var mShowUserNames: Boolean = true
private var mShowNumberEntries: Boolean = true private var mShowNumberEntries: Boolean = true
private var mShowOTP: Boolean = false private var mShowOTP: Boolean = false
private var mShowUUID: Boolean = false private var mShowUUID: Boolean = false
private var mEntryFilters = arrayOf<Group.ChildFilter>() private var mEntryFilters = arrayOf<Group.ChildFilter>()
private var mOldVirtualGroup = false
private var mVirtualGroup = false
private var mActionNodesList = LinkedList<Node>() private var mActionNodesList = LinkedList<Node>()
private var mNodeClickCallback: NodeClickCallback? = null private var mNodeClickCallback: NodeClickCallback? = null
private var mClipboardHelper = ClipboardHelper(context) private var mClipboardHelper = ClipboardHelper(context)
@ColorInt @ColorInt
private val mContentSelectionColor: Int private val mTextColorPrimary: Int
@ColorInt @ColorInt
private val mIconGroupColor: Int private val mTextColor: Int
@ColorInt @ColorInt
private val mIconEntryColor: Int private val mTextColorSecondary: Int
@ColorInt
private val mColorAccentLight: Int
@ColorInt
private val mColorOnAccentColor: Int
/** /**
* Determine if the adapter contains or not any element * Determine if the adapter contains or not any element
@@ -106,16 +112,26 @@ class NodeAdapter (private val context: Context,
this.mNodeSortedListCallback = NodeSortedListCallback() this.mNodeSortedListCallback = NodeSortedListCallback()
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback) this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
// Color of content selection
this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
// Retrieve the color to tint the icon // Retrieve the color to tint the icon
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary)) val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK) this.mTextColorPrimary = taTextColorPrimary.getColor(0, Color.BLACK)
taTextColorPrimary.recycle() taTextColorPrimary.recycle()
// In two times to fix bug compilation // To get text color
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
this.mIconEntryColor = taTextColor.getColor(0, Color.BLACK) this.mTextColor = taTextColor.getColor(0, Color.BLACK)
taTextColor.recycle() taTextColor.recycle()
// To get text color secondary
val taTextColorSecondary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorSecondary))
this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK)
taTextColorSecondary.recycle()
// To get background color for selection
val taColorAccentLight = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccentLight))
this.mColorAccentLight = taColorAccentLight.getColor(0, Color.GRAY)
taColorAccentLight.recycle()
// To get text color for selection
val taColorOnAccentColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnAccentColor))
this.mColorOnAccentColor = taColorOnAccentColor.getColor(0, Color.WHITE)
taColorOnAccentColor.recycle()
} }
private fun assignPreferences() { private fun assignPreferences() {
@@ -130,6 +146,7 @@ class NodeAdapter (private val context: Context,
) )
) )
this.mShowEntryColors = PreferencesUtil.showEntryColors(context)
this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context) this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context)
this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context) this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
this.mShowOTP = PreferencesUtil.showOTPToken(context) this.mShowOTP = PreferencesUtil.showOTPToken(context)
@@ -145,6 +162,8 @@ class NodeAdapter (private val context: Context,
* Rebuild the list by clear and build children from the group * Rebuild the list by clear and build children from the group
*/ */
fun rebuildList(group: Group) { fun rebuildList(group: Group) {
mOldVirtualGroup = mVirtualGroup
mVirtualGroup = group.isVirtual
assignPreferences() assignPreferences()
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters)) mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
} }
@@ -155,14 +174,19 @@ class NodeAdapter (private val context: Context,
} }
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean { override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
if (mOldVirtualGroup != mVirtualGroup)
return false
var typeContentTheSame = true var typeContentTheSame = true
if (oldItem is Entry && newItem is Entry) { if (oldItem is Entry && newItem is Entry) {
typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle() typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle()
&& oldItem.username == newItem.username && oldItem.username == newItem.username
&& oldItem.backgroundColor == newItem.backgroundColor
&& oldItem.foregroundColor == newItem.foregroundColor
&& oldItem.getOtpElement() == newItem.getOtpElement() && oldItem.getOtpElement() == newItem.getOtpElement()
&& oldItem.containsAttachment() == newItem.containsAttachment() && oldItem.containsAttachment() == newItem.containsAttachment()
} else if (oldItem is Group && newItem is Group) { } else if (oldItem is Group && newItem is Group) {
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
&& oldItem.notes == newItem.notes
} }
return typeContentTheSame return typeContentTheSame
&& oldItem.nodeId == newItem.nodeId && oldItem.nodeId == newItem.nodeId
@@ -323,23 +347,6 @@ class NodeAdapter (private val context: Context,
isSelected = mActionNodesList.contains(subNode) isSelected = mActionNodesList.contains(subNode)
} }
// Assign image
val iconColor = if (holder.container.isSelected)
mContentSelectionColor
else when (subNode.type) {
Type.GROUP -> mIconGroupColor
Type.ENTRY -> mIconEntryColor
}
holder.imageIdentifier?.setColorFilter(iconColor)
holder.icon.apply {
database.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
// Relative size of the icon
layoutParams?.apply {
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
width = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
}
}
// Assign text // Assign text
holder.text.apply { holder.text.apply {
text = subNode.title text = subNode.title
@@ -348,14 +355,32 @@ class NodeAdapter (private val context: Context,
} }
// Add meta text to show UUID // Add meta text to show UUID
holder.meta.apply { holder.meta.apply {
if (mShowUUID) { val nodeId = subNode.nodeId?.toVisualString()
text = subNode.nodeId.toString() if (mShowUUID && nodeId != null) {
text = nodeId
setTextSize(mTextSizeUnit, mMetaTextDefaultDimension, mPrefSizeMultiplier) setTextSize(mTextSizeUnit, mMetaTextDefaultDimension, mPrefSizeMultiplier)
visibility = View.VISIBLE visibility = View.VISIBLE
} else { } else {
visibility = View.GONE visibility = View.GONE
} }
} }
// Add path to virtual group
if (mVirtualGroup) {
holder.path?.apply {
text = subNode.getPathString()
visibility = View.VISIBLE
}
} else {
holder.path?.visibility = View.GONE
}
// Assign icon colors
var iconColor = if (holder.container.isSelected)
mColorOnAccentColor
else when (subNode.type) {
Type.GROUP -> mTextColorPrimary
Type.ENTRY -> mTextColor
}
// Specific elements for entry // Specific elements for entry
if (subNode.type == Type.ENTRY) { if (subNode.type == Type.ENTRY) {
@@ -398,6 +423,44 @@ class NodeAdapter (private val context: Context,
holder.attachmentIcon?.visibility = holder.attachmentIcon?.visibility =
if (entry.containsAttachment()) View.VISIBLE else View.GONE if (entry.containsAttachment()) View.VISIBLE else View.GONE
// Assign colors
val backgroundColor = if (mShowEntryColors) entry.backgroundColor else null
if (!holder.container.isSelected) {
if (backgroundColor != null) {
holder.container.setBackgroundColor(backgroundColor)
} else {
holder.container.setBackgroundColor(Color.TRANSPARENT)
}
} else {
holder.container.setBackgroundColor(mColorAccentLight)
}
val foregroundColor = if (mShowEntryColors) entry.foregroundColor else null
if (!holder.container.isSelected) {
if (foregroundColor != null) {
holder.text.setTextColor(foregroundColor)
holder.subText?.setTextColor(foregroundColor)
holder.otpToken?.setTextColor(foregroundColor)
holder.otpProgress?.setIndicatorColor(foregroundColor)
holder.attachmentIcon?.setColorFilter(foregroundColor)
holder.meta.setTextColor(foregroundColor)
iconColor = foregroundColor
} else {
holder.text.setTextColor(mTextColor)
holder.subText?.setTextColor(mTextColorSecondary)
holder.otpToken?.setTextColor(mTextColorSecondary)
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
holder.meta.setTextColor(mTextColor)
}
} else {
holder.text.setTextColor(mColorOnAccentColor)
holder.subText?.setTextColor(mColorOnAccentColor)
holder.otpToken?.setTextColor(mColorOnAccentColor)
holder.otpProgress?.setIndicatorColor(mColorOnAccentColor)
holder.attachmentIcon?.setColorFilter(mColorOnAccentColor)
holder.meta.setTextColor(mColorOnAccentColor)
}
database.stopManageEntry(entry) database.stopManageEntry(entry)
} }
@@ -416,6 +479,17 @@ class NodeAdapter (private val context: Context,
} }
} }
// Assign image
holder.imageIdentifier?.setColorFilter(iconColor)
holder.icon.apply {
database.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
// Relative size of the icon
layoutParams?.apply {
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
width = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
}
}
// Assign click // Assign click
holder.container.setOnClickListener { holder.container.setOnClickListener {
mNodeClickCallback?.onNodeClick(database, subNode) mNodeClickCallback?.onNodeClick(database, subNode)
@@ -430,15 +504,16 @@ class NodeAdapter (private val context: Context,
OtpType.HOTP -> { OtpType.HOTP -> {
holder?.otpProgress?.apply { holder?.otpProgress?.apply {
max = 100 max = 100
progress = 100 setProgressCompat(100, true)
} }
} }
OtpType.TOTP -> { OtpType.TOTP -> {
holder?.otpProgress?.apply { holder?.otpProgress?.apply {
max = otpElement.period max = otpElement.period
progress = otpElement.secondsRemaining setProgressCompat(otpElement.secondsRemaining, true)
} }
} }
null -> {}
} }
holder?.otpToken?.apply { holder?.otpToken?.apply {
text = otpElement?.token text = otpElement?.token
@@ -497,8 +572,9 @@ class NodeAdapter (private val context: Context,
var text: TextView = itemView.findViewById(R.id.node_text) var text: TextView = itemView.findViewById(R.id.node_text)
var subText: TextView? = itemView.findViewById(R.id.node_subtext) var subText: TextView? = itemView.findViewById(R.id.node_subtext)
var meta: TextView = itemView.findViewById(R.id.node_meta) var meta: TextView = itemView.findViewById(R.id.node_meta)
var path: TextView? = itemView.findViewById(R.id.node_path)
var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container) var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container)
var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress) var otpProgress: CircularProgressIndicator? = itemView.findViewById(R.id.node_otp_progress)
var otpToken: TextView? = itemView.findViewById(R.id.node_otp_token) var otpToken: TextView? = itemView.findViewById(R.id.node_otp_token)
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer) var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers) var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
@@ -506,6 +582,6 @@ class NodeAdapter (private val context: Context,
} }
companion object { companion object {
private val TAG = NodeAdapter::class.java.name private val TAG = NodesAdapter::class.java.name
} }
} }

View File

@@ -1,180 +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.adapters
import android.content.Context
import android.database.Cursor
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.cursor.EntryCursorKDB
import com.kunzisoft.keepass.database.cursor.EntryCursorKDBX
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.strikeOut
class SearchEntryCursorAdapter(private val context: Context,
private val database: Database)
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
private val cursorInflater: LayoutInflater? = context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
private var mDisplayUsername: Boolean = false
private var mOmitBackup: Boolean = true
private val iconColor: Int
init {
// Get the icon color
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
this.iconColor = taTextColor.getColor(0, Color.WHITE)
taTextColor.recycle()
reInit(context)
}
fun reInit(context: Context) {
this.mDisplayUsername = PreferencesUtil.showUsernamesListEntries(context)
this.mOmitBackup = PreferencesUtil.omitBackup(context)
}
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
val view = cursorInflater!!.inflate(R.layout.item_search_entry, parent, false)
val viewHolder = ViewHolder()
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
viewHolder.textViewSubTitle = view.findViewById(R.id.entry_subtext)
view.tag = viewHolder
return view
}
override fun bindView(view: View, context: Context, cursor: Cursor) {
getEntryFrom(cursor)?.let { currentEntry ->
val viewHolder = view.tag as ViewHolder
// Assign image
viewHolder.imageViewIcon?.let { iconView ->
database.iconDrawableFactory.assignDatabaseIcon(iconView, currentEntry.icon, iconColor)
}
// Assign title
viewHolder.textViewTitle?.apply {
text = currentEntry.getVisualTitle()
strikeOut(currentEntry.isCurrentlyExpires)
}
// Assign subtitle
viewHolder.textViewSubTitle?.apply {
val entryUsername = currentEntry.username
text = if (mDisplayUsername && entryUsername.isNotEmpty()) {
String.format("(%s)", entryUsername)
} else {
""
}
visibility = if (text.isEmpty()) View.GONE else View.VISIBLE
strikeOut(currentEntry.isCurrentlyExpires)
}
}
}
private fun getEntryFrom(cursor: Cursor): Entry? {
return database.createEntry()?.apply {
entryKDB?.let { entryKDB ->
(cursor as EntryCursorKDB).populateEntry(entryKDB,
{ standardIconId ->
database.getStandardIcon(standardIconId)
},
{ customIconId ->
database.getCustomIcon(customIconId)
}
)
}
entryKDBX?.let { entryKDBX ->
(cursor as EntryCursorKDBX).populateEntry(entryKDBX,
{ standardIconId ->
database.getStandardIcon(standardIconId)
},
{ customIconId ->
database.getCustomIcon(customIconId)
}
)
}
}
}
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
return searchEntries(context, constraint.toString())
}
private fun searchEntries(context: Context, query: String): Cursor? {
var cursorKDB: EntryCursorKDB? = null
var cursorKDBX: EntryCursorKDBX? = null
if (database.type == DatabaseKDB.TYPE)
cursorKDB = EntryCursorKDB()
if (database.type == DatabaseKDBX.TYPE)
cursorKDBX = EntryCursorKDBX()
val searchGroup = database.createVirtualGroupFromSearch(query,
mOmitBackup,
SearchHelper.MAX_SEARCH_ENTRY)
if (searchGroup != null) {
// Search in hide entries but not meta-stream
for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) {
database.startManageEntry(entry)
entry.entryKDB?.let {
cursorKDB?.addEntry(it)
}
entry.entryKDBX?.let {
cursorKDBX?.addEntry(it)
}
database.stopManageEntry(entry)
}
}
return cursorKDB ?: cursorKDBX
}
fun getEntryFromPosition(position: Int): Entry? {
var pwEntry: Entry? = null
val cursor = this.cursor
if (cursor.moveToFirst() && cursor.move(position)) {
pwEntry = getEntryFrom(cursor)
}
return pwEntry
}
private class ViewHolder {
var imageViewIcon: ImageView? = null
var textViewTitle: TextView? = null
var textViewSubTitle: TextView? = null
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Tags
class TagsAdapter(context: Context) : RecyclerView.Adapter<TagsAdapter.TagViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var mTags: Tags = Tags()
var onItemClickListener: OnItemClickListener? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TagViewHolder {
val view = inflater.inflate(R.layout.item_tag, parent, false)
return TagViewHolder(view)
}
override fun onBindViewHolder(holder: TagViewHolder, position: Int) {
val field = mTags.get(position)
holder.name.text = field
holder.bind(field, onItemClickListener)
}
override fun getItemCount(): Int {
return mTags.size()
}
fun setTags(tags: Tags) {
mTags.setTags(tags)
notifyDataSetChanged()
}
fun clear() {
mTags.clear()
}
interface OnItemClickListener {
fun onItemClick(item: String)
}
inner class TagViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var name: TextView = itemView.findViewById(R.id.tag_name)
fun bind(item: String, listener: OnItemClickListener?) {
itemView.setOnClickListener { listener?.onItemClick(item) }
}
}
}

View File

@@ -0,0 +1,19 @@
package com.kunzisoft.keepass.adapters
import android.content.Context
import com.kunzisoft.keepass.database.element.Tags
import com.tokenautocomplete.FilteredArrayAdapter
class TagsProposalAdapter(context: Context, proposal: Tags?)
: FilteredArrayAdapter<String>(
context,
android.R.layout.simple_list_item_1,
(proposal ?: Tags()).toList()
) {
override fun keepObject(obj: String, mask: String?): Boolean {
if (mask == null)
return false
return obj.contains(mask, true)
}
}

View File

@@ -9,23 +9,23 @@ import android.widget.BaseAdapter
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.template.Template import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.database.element.template.TemplateField import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.icons.IconDrawableFactory import com.kunzisoft.keepass.icons.IconDrawableFactory
class TemplatesSelectorAdapter(private val context: Context, class TemplatesSelectorAdapter(
private var templates: List<Template>): BaseAdapter() { context: Context,
private var templates: List<Template>): BaseAdapter() {
var iconDrawableFactory: IconDrawableFactory? = null var iconDrawableFactory: IconDrawableFactory? = null
private val inflater: LayoutInflater = LayoutInflater.from(context) private val inflater: LayoutInflater = LayoutInflater.from(context)
private var mIconColor = Color.BLACK private var mTextColor = Color.BLACK
init { init {
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
mIconColor = taIconColor.getColor(0, Color.BLACK) mTextColor = taTextColor.getColor(0, Color.BLACK)
taIconColor.recycle() taTextColor.recycle()
} }
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
@@ -36,6 +36,7 @@ class TemplatesSelectorAdapter(private val context: Context,
if (templateView == null) { if (templateView == null) {
holder = TemplateSelectorViewHolder() holder = TemplateSelectorViewHolder()
templateView = inflater.inflate(R.layout.item_template, parent, false) templateView = inflater.inflate(R.layout.item_template, parent, false)
holder.background = templateView?.findViewById(R.id.template_background)
holder.icon = templateView?.findViewById(R.id.template_image) holder.icon = templateView?.findViewById(R.id.template_image)
holder.name = templateView?.findViewById(R.id.template_name) holder.name = templateView?.findViewById(R.id.template_name)
templateView?.tag = holder templateView?.tag = holder
@@ -43,10 +44,15 @@ class TemplatesSelectorAdapter(private val context: Context,
holder = templateView.tag as TemplateSelectorViewHolder holder = templateView.tag as TemplateSelectorViewHolder
} }
holder.background?.setBackgroundColor(template.backgroundColor ?: Color.TRANSPARENT)
val textColor = template.foregroundColor ?: mTextColor
holder.icon?.let { icon -> holder.icon?.let { icon ->
iconDrawableFactory?.assignDatabaseIcon(icon, template.icon, mIconColor) iconDrawableFactory?.assignDatabaseIcon(icon, template.icon, textColor)
}
holder.name?.apply {
setTextColor(textColor)
text = TemplateField.getLocalizedName(context, template.title)
} }
holder.name?.text = TemplateField.getLocalizedName(context, template.title)
return templateView!! return templateView!!
} }
@@ -64,6 +70,7 @@ class TemplatesSelectorAdapter(private val context: Context,
} }
inner class TemplateSelectorViewHolder { inner class TemplateSelectorViewHolder {
var background: View? = null
var icon: ImageView? = null var icon: ImageView? = null
var name: TextView? = null var name: TextView? = null
} }

View File

@@ -22,7 +22,9 @@ package com.kunzisoft.keepass.app.database
import android.content.* import android.content.*
import android.net.Uri import android.net.Uri
import android.os.IBinder import android.os.IBinder
import android.util.Base64
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.SingletonHolderParameter import com.kunzisoft.keepass.utils.SingletonHolderParameter
@@ -125,15 +127,41 @@ class CipherDatabaseAction(context: Context) {
} }
fun getCipherDatabase(databaseUri: Uri, fun getCipherDatabase(databaseUri: Uri,
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) { cipherDatabaseResultListener: (CipherEncryptDatabase?) -> Unit) {
if (useTempDao) { if (useTempDao) {
serviceActionTask { serviceActionTask {
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri)) var cipherDatabase: CipherEncryptDatabase? = null
mBinder?.getCipherDatabase(databaseUri)?.let { cipherDatabaseEntity ->
cipherDatabase = CipherEncryptDatabase().apply {
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
this.encryptedValue = Base64.decode(
cipherDatabaseEntity.encryptedValue,
Base64.NO_WRAP
)
this.specParameters = Base64.decode(
cipherDatabaseEntity.specParameters,
Base64.NO_WRAP
)
}
}
cipherDatabaseResultListener.invoke(cipherDatabase)
} }
} else { } else {
IOActionTask( IOActionTask(
{ {
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString()) cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())?.let { cipherDatabaseEntity ->
CipherEncryptDatabase().apply {
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
this.encryptedValue = Base64.decode(
cipherDatabaseEntity.encryptedValue,
Base64.NO_WRAP
)
this.specParameters = Base64.decode(
cipherDatabaseEntity.specParameters,
Base64.NO_WRAP
)
}
}
}, },
{ {
cipherDatabaseResultListener.invoke(it) cipherDatabaseResultListener.invoke(it)
@@ -149,18 +177,27 @@ class CipherDatabaseAction(context: Context) {
} }
} }
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity, fun addOrUpdateCipherDatabase(cipherEncryptDatabase: CipherEncryptDatabase,
cipherDatabaseResultListener: (() -> Unit)? = null) { cipherDatabaseResultListener: (() -> Unit)? = null) {
if (useTempDao) { cipherEncryptDatabase.databaseUri?.let { databaseUri ->
// The only case to create service (not needed to get an info)
serviceActionTask(true) { val cipherDatabaseEntity = CipherDatabaseEntity(
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity) databaseUri.toString(),
cipherDatabaseResultListener?.invoke() Base64.encodeToString(cipherEncryptDatabase.encryptedValue, Base64.NO_WRAP),
} Base64.encodeToString(cipherEncryptDatabase.specParameters, Base64.NO_WRAP),
} else { )
IOActionTask(
if (useTempDao) {
// The only case to create service (not needed to get an info)
serviceActionTask(true) {
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
cipherDatabaseResultListener?.invoke()
}
} else {
IOActionTask(
{ {
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri) val cipherDatabaseRetrieve =
cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
// Update values if element not yet in the database // Update values if element not yet in the database
if (cipherDatabaseRetrieve == null) { if (cipherDatabaseRetrieve == null) {
cipherDatabaseDao.add(cipherDatabaseEntity) cipherDatabaseDao.add(cipherDatabaseEntity)
@@ -171,7 +208,8 @@ class CipherDatabaseAction(context: Context) {
{ {
cipherDatabaseResultListener?.invoke() cipherDatabaseResultListener?.invoke()
} }
).execute() ).execute()
}
} }
} }

View File

@@ -4,4 +4,4 @@ import android.app.assist.AssistStructure
import android.view.inputmethod.InlineSuggestionsRequest import android.view.inputmethod.InlineSuggestionsRequest
data class AutofillComponent(val assistStructure: AssistStructure, data class AutofillComponent(val assistStructure: AssistStructure,
val inlineSuggestionsRequest: InlineSuggestionsRequest?) val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)

View File

@@ -34,7 +34,6 @@ import android.service.autofill.InlinePresentation
import android.util.Log import android.util.Log
import android.view.autofill.AutofillManager import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue import android.view.autofill.AutofillValue
import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.RemoteViews import android.widget.RemoteViews
import android.widget.Toast import android.widget.Toast
import android.widget.inline.InlinePresentationSpec import android.widget.inline.InlinePresentationSpec
@@ -63,7 +62,7 @@ import com.kunzisoft.keepass.utils.LOCK_ACTION
object AutofillHelper { object AutofillHelper {
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST" private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? { fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure -> intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
@@ -112,7 +111,7 @@ object AutofillHelper {
database: Database, database: Database,
entryInfo: EntryInfo, entryInfo: EntryInfo,
struct: StructureParser.Result, struct: StructureParser.Result,
inlinePresentation: InlinePresentation?): Dataset? { additionalBuild: ((build: Dataset.Builder) -> Unit)? = null): Dataset? {
val title = makeEntryTitle(entryInfo) val title = makeEntryTitle(entryInfo)
val views = newRemoteViews(context, database, title, entryInfo.icon) val views = newRemoteViews(context, database, title, entryInfo.icon)
val builder = Dataset.Builder(views) val builder = Dataset.Builder(views)
@@ -201,11 +200,7 @@ object AutofillHelper {
} }
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { additionalBuild?.invoke(builder)
inlinePresentation?.let {
builder.setInlinePresentation(it)
}
}
return try { return try {
builder.build() builder.build()
@@ -236,44 +231,51 @@ object AutofillHelper {
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private fun buildInlinePresentationForEntry(context: Context, private fun buildInlinePresentationForEntry(context: Context,
database: Database, database: Database,
inlineSuggestionsRequest: InlineSuggestionsRequest, compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
positionItem: Int, positionItem: Int,
entryInfo: EntryInfo): InlinePresentation? { entryInfo: EntryInfo): InlinePresentation? {
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
if (positionItem <= maxSuggestion - 1 if (positionItem <= maxSuggestion - 1
&& inlinePresentationSpecs.size > positionItem) { && inlinePresentationSpecs.size > positionItem
val inlinePresentationSpec = inlinePresentationSpecs[positionItem] ) {
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
// Make sure that the IME spec claims support for v1 UI template. // Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style val imeStyle = inlinePresentationSpec.style
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1)) if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
return null return null
// Build the content for IME UI // Build the content for IME UI
val pendingIntent = PendingIntent.getActivity(context, val pendingIntent = PendingIntent.getActivity(
0, context,
Intent(context, AutofillSettingsActivity::class.java), 0,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Intent(context, AutofillSettingsActivity::class.java),
PendingIntent.FLAG_IMMUTABLE if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
} else { PendingIntent.FLAG_IMMUTABLE
0 } else {
}) 0
return InlinePresentation( }
)
return InlinePresentation(
InlineSuggestionUi.newContentBuilder(pendingIntent).apply { InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
setContentDescription(context.getString(R.string.autofill_sign_in_prompt)) setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
setTitle(entryInfo.title) setTitle(entryInfo.title)
setSubtitle(entryInfo.username) setSubtitle(entryInfo.username)
setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply { setStartIcon(
setTintBlendMode(BlendMode.DST) Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
}) setTintBlendMode(BlendMode.DST)
})
buildIconFromEntry(context, database, entryInfo)?.let { icon -> buildIconFromEntry(context, database, entryInfo)?.let { icon ->
setEndIcon(icon.apply { setEndIcon(icon.apply {
setTintBlendMode(BlendMode.DST) setTintBlendMode(BlendMode.DST)
}) })
} }
}.build().slice, inlinePresentationSpec, false) }.build().slice, inlinePresentationSpec, false
)
}
} }
return null return null
} }
@@ -303,7 +305,7 @@ object AutofillHelper {
database: Database, database: Database,
entriesInfo: List<EntryInfo>, entriesInfo: List<EntryInfo>,
parseResult: StructureParser.Result, parseResult: StructureParser.Result,
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse? { compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
val responseBuilder = FillResponse.Builder() val responseBuilder = FillResponse.Builder()
// Add Header // Add Header
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -324,7 +326,7 @@ object AutofillHelper {
// Add inline suggestion for new IME and dataset // Add inline suggestion for new IME and dataset
var numberInlineSuggestions = 0 var numberInlineSuggestions = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.let { compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size) numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) { if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) { if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
@@ -336,14 +338,19 @@ object AutofillHelper {
} }
entriesInfo.forEachIndexed { _, entry -> entriesInfo.forEachIndexed { _, entry ->
val inlinePresentation = if (numberInlineSuggestions > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (numberInlineSuggestions > 0
inlineSuggestionsRequest?.let { && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
buildInlinePresentationForEntry(context, database, inlineSuggestionsRequest, numberInlineSuggestions--, entry) && compatInlineSuggestionsRequest != null) {
} responseBuilder.addDataset(buildDataset(context, database, entry, parseResult) { builder ->
buildInlinePresentationForEntry(context, database,
compatInlineSuggestionsRequest, numberInlineSuggestions--, entry
)?.let { inlinePresentation ->
builder.setInlinePresentation(inlinePresentation)
}
})
} else { } else {
null responseBuilder.addDataset(buildDataset(context, database, entry, parseResult))
} }
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult, inlinePresentation))
} }
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) { if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
@@ -355,14 +362,14 @@ object AutofillHelper {
} }
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry) val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context, val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
searchInfo, inlineSuggestionsRequest) searchInfo, compatInlineSuggestionsRequest)
parseResult.allAutofillIds().let { autofillIds -> parseResult.allAutofillIds().let { autofillIds ->
autofillIds.forEach { id -> autofillIds.forEach { id ->
val builder = Dataset.Builder(manualSelectionView) val builder = Dataset.Builder(manualSelectionView)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.let { compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0] val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent) val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
inlinePresentation?.let { inlinePresentation?.let {
@@ -407,11 +414,11 @@ object AutofillHelper {
StructureParser(structure).parse()?.let { result -> StructureParser(structure).parse()?.let { result ->
// New Response // New Response
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val inlineSuggestionsRequest = activity.intent?.getParcelableExtra<InlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST) val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtra<CompatInlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
if (inlineSuggestionsRequest != null) { if (compatInlineSuggestionsRequest != null) {
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show() Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
} }
buildResponse(activity, database, entriesInfo, result, inlineSuggestionsRequest) buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest)
} else { } else {
buildResponse(activity, database, entriesInfo, result, null) buildResponse(activity, database, entriesInfo, result, null)
} }
@@ -464,7 +471,7 @@ object AutofillHelper {
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure) intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) { && PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
autofillComponent.inlineSuggestionsRequest?.let { autofillComponent.compatInlineSuggestionsRequest?.let {
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it) intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
} }
} }

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.autofill
import android.annotation.TargetApi
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.service.autofill.FillRequest
import android.view.inputmethod.InlineSuggestionsRequest
import androidx.annotation.RequiresApi
/**
* Utility class only to prevent java.lang.NoClassDefFoundError for old Android version and new lib compilation
*/
@RequiresApi(Build.VERSION_CODES.O)
class CompatInlineSuggestionsRequest : Parcelable {
@TargetApi(Build.VERSION_CODES.R)
var inlineSuggestionsRequest: InlineSuggestionsRequest? = null
private set
constructor(fillRequest: FillRequest) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
this.inlineSuggestionsRequest = fillRequest.inlineSuggestionsRequest
} else {
this.inlineSuggestionsRequest = null
}
}
@RequiresApi(Build.VERSION_CODES.R)
constructor(inlineSuggestionsRequest: InlineSuggestionsRequest?) {
this.inlineSuggestionsRequest = inlineSuggestionsRequest
}
constructor(parcel: Parcel) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
this.inlineSuggestionsRequest =
parcel.readParcelable(FillRequest::class.java.classLoader)
}
else {
this.inlineSuggestionsRequest = null
}
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
parcel.writeParcelable(inlineSuggestionsRequest, flags)
}
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<CompatInlineSuggestionsRequest> {
override fun createFromParcel(parcel: Parcel): CompatInlineSuggestionsRequest {
return CompatInlineSuggestionsRequest(parcel)
}
override fun newArray(size: Int): Array<CompatInlineSuggestionsRequest?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -109,7 +109,7 @@ class KeeAutofillService : AutofillService() {
searchInfo.webDomain = webDomainWithoutSubDomain searchInfo.webDomain = webDomainWithoutSubDomain
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) { && autofillInlineSuggestionsEnabled) {
request.inlineSuggestionsRequest CompatInlineSuggestionsRequest(request)
} else { } else {
null null
} }
@@ -127,7 +127,7 @@ class KeeAutofillService : AutofillService() {
private fun launchSelection(database: Database?, private fun launchSelection(database: Database?,
searchInfo: SearchInfo, searchInfo: SearchInfo,
parseResult: StructureParser.Result, parseResult: StructureParser.Result,
inlineSuggestionsRequest: InlineSuggestionsRequest?, inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
callback: FillCallback) { callback: FillCallback) {
SearchHelper.checkAutoSearchInfo(this, SearchHelper.checkAutoSearchInfo(this,
database, database,
@@ -155,7 +155,7 @@ class KeeAutofillService : AutofillService() {
private fun showUIForEntrySelection(parseResult: StructureParser.Result, private fun showUIForEntrySelection(parseResult: StructureParser.Result,
database: Database?, database: Database?,
searchInfo: SearchInfo, searchInfo: SearchInfo,
inlineSuggestionsRequest: InlineSuggestionsRequest?, inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
callback: FillCallback) { callback: FillCallback) {
parseResult.allAutofillIds().let { autofillIds -> parseResult.allAutofillIds().let { autofillIds ->
if (autofillIds.isNotEmpty()) { if (autofillIds.isNotEmpty()) {
@@ -249,7 +249,7 @@ class KeeAutofillService : AutofillService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) { && autofillInlineSuggestionsEnabled) {
var inlinePresentation: InlinePresentation? = null var inlinePresentation: InlinePresentation? = null
inlineSuggestionsRequest?.let { inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
if (inlineSuggestionsRequest.maxSuggestionCount > 0 if (inlineSuggestionsRequest.maxSuggestionCount > 0
&& inlinePresentationSpecs.size > 0) { && inlinePresentationSpecs.size > 0) {
@@ -281,8 +281,9 @@ class KeeAutofillService : AutofillService() {
} }
// Build response // Build response
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation) responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
} else {
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
} }
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
callback.onSuccess(responseBuilder.build()) callback.onSuccess(responseBuilder.build())
} }
} }

View File

@@ -377,9 +377,11 @@ class StructureParser(private val structure: AssistStructure) {
when { when {
inputIsVariationType(inputType, inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> { InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
usernameIdCandidate = autofillId if (usernameIdCandidate == null) {
usernameValueCandidate = node.autofillValue usernameIdCandidate = autofillId
Log.d(TAG, "Autofill username candidate android number type: ${showHexInputType(inputType)}") usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate android number type: ${showHexInputType(inputType)}")
}
} }
inputIsVariationType(inputType, inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> { InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {

View File

@@ -40,6 +40,9 @@ import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.database.exception.IODatabaseException import com.kunzisoft.keepass.database.exception.IODatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.model.CipherDecryptDatabase
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.CredentialStorage
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
@@ -60,6 +63,9 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
var databaseFileUri: Uri? = null var databaseFileUri: Uri? = null
private set private set
// TODO Retrieve credential storage from app database
var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT
/** /**
* Manage setting to auto open biometric prompt * Manage setting to auto open biometric prompt
*/ */
@@ -477,6 +483,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} ?: checkUnlockAvailability() } ?: checkUnlockAvailability()
} }
@RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString") Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
@@ -528,16 +535,29 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} }
} }
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) { override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {
databaseFileUri?.let { databaseUri -> databaseFileUri?.let { databaseUri ->
mBuilderListener?.onCredentialEncrypted(databaseUri, encryptedValue, ivSpec) mBuilderListener?.onCredentialEncrypted(
CipherEncryptDatabase().apply {
this.databaseUri = databaseUri
this.credentialStorage = credentialDatabaseStorage
this.encryptedValue = encryptedValue
this.specParameters = ivSpec
}
)
} }
} }
override fun handleDecryptedResult(decryptedValue: String) { override fun handleDecryptedResult(decryptedValue: ByteArray) {
// Load database directly with password retrieve // Load database directly with password retrieve
databaseFileUri?.let { databaseFileUri?.let { databaseUri ->
mBuilderListener?.onCredentialDecrypted(it, decryptedValue) mBuilderListener?.onCredentialDecrypted(
CipherDecryptDatabase().apply {
this.databaseUri = databaseUri
this.credentialStorage = credentialDatabaseStorage
this.decryptedValue = decryptedValue
}
)
} }
} }
@@ -551,6 +571,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key) setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
} }
@RequiresApi(Build.VERSION_CODES.M)
override fun onGenericException(e: Exception) { override fun onGenericException(e: Exception) {
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: "" val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
setAdvancedUnlockedMessageView(errorMessage) setAdvancedUnlockedMessageView(errorMessage)
@@ -580,6 +601,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} }
} }
@RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedMessageView(text: CharSequence) { private fun setAdvancedUnlockedMessageView(text: CharSequence) {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
mAdvancedUnlockInfoView?.message = text mAdvancedUnlockInfoView?.message = text
@@ -617,10 +639,10 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} }
interface BuilderListener { interface BuilderListener {
fun retrieveCredentialForEncryption(): String fun retrieveCredentialForEncryption(): ByteArray
fun conditionToStoreCredential(): Boolean fun conditionToStoreCredential(): Boolean
fun onCredentialEncrypted(databaseUri: Uri, encryptedCredential: String, ivSpec: String) fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase)
fun onCredentialDecrypted(databaseUri: Uri, decryptedCredential: String) fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase)
} }
override fun onPause() { override fun onPause() {

View File

@@ -27,7 +27,6 @@ import android.os.Build
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Base64
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
@@ -214,18 +213,15 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} }
fun encryptData(value: String) { fun encryptData(value: ByteArray) {
if (!isKeyManagerInitialized) { if (!isKeyManagerInitialized) {
return return
} }
try { try {
val encrypted = cipher?.doFinal(value.toByteArray()) val encrypted = cipher?.doFinal(value) ?: byteArrayOf()
val encryptedBase64 = Base64.encodeToString(encrypted, Base64.NO_WRAP)
// passes updated iv spec on to callback so this can be stored for decryption // passes updated iv spec on to callback so this can be stored for decryption
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec -> cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
val ivSpecValue = Base64.encodeToString(spec.iv, Base64.NO_WRAP) advancedUnlockCallback?.handleEncryptedResult(encrypted, spec.iv)
advancedUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to encrypt data", e) Log.e(TAG, "Unable to encrypt data", e)
@@ -233,12 +229,12 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} }
fun initDecryptData(ivSpecValue: String, fun initDecryptData(ivSpecValue: ByteArray,
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) { actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
initDecryptData(ivSpecValue, actionIfCypherInit, true) initDecryptData(ivSpecValue, actionIfCypherInit, true)
} }
private fun initDecryptData(ivSpecValue: String, private fun initDecryptData(ivSpecValue: ByteArray,
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit, actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
firstLaunch: Boolean = true) { firstLaunch: Boolean = true) {
if (!isKeyManagerInitialized) { if (!isKeyManagerInitialized) {
@@ -246,9 +242,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
try { try {
// important to restore spec here that was used for decryption // important to restore spec here that was used for decryption
val iv = Base64.decode(ivSpecValue, Base64.NO_WRAP) val spec = IvParameterSpec(ivSpecValue)
val spec = IvParameterSpec(iv)
getSecretKey()?.let { secretKey -> getSecretKey()?.let { secretKey ->
cipher?.let { cipher -> cipher?.let { cipher ->
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
@@ -284,15 +278,14 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} }
fun decryptData(encryptedValue: String) { fun decryptData(encryptedValue: ByteArray) {
if (!isKeyManagerInitialized) { if (!isKeyManagerInitialized) {
return return
} }
try { try {
// actual decryption here // actual decryption here
val encrypted = Base64.decode(encryptedValue, Base64.NO_WRAP) cipher?.doFinal(encryptedValue)?.let { decrypted ->
cipher?.doFinal(encrypted)?.let { decrypted -> advancedUnlockCallback?.handleDecryptedResult(decrypted)
advancedUnlockCallback?.handleDecryptedResult(String(decrypted))
} }
} catch (badPaddingException: BadPaddingException) { } catch (badPaddingException: BadPaddingException) {
Log.e(TAG, "Unable to decrypt data", badPaddingException) Log.e(TAG, "Unable to decrypt data", badPaddingException)
@@ -367,8 +360,8 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
fun onAuthenticationSucceeded() fun onAuthenticationSucceeded()
fun onAuthenticationFailed() fun onAuthenticationFailed()
fun onAuthenticationError(errorCode: Int, errString: CharSequence) fun onAuthenticationError(errorCode: Int, errString: CharSequence)
fun handleEncryptedResult(encryptedValue: String, ivSpec: String) fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray)
fun handleDecryptedResult(decryptedValue: String) fun handleDecryptedResult(decryptedValue: ByteArray)
} }
companion object { companion object {
@@ -469,9 +462,9 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {} override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {}
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {} override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {}
override fun handleDecryptedResult(decryptedValue: String) {} override fun handleDecryptedResult(decryptedValue: ByteArray) {}
override fun onUnrecoverableKeyException(e: Exception) { override fun onUnrecoverableKeyException(e: Exception) {
advancedCallback.onUnrecoverableKeyException(e) advancedCallback.onUnrecoverableKeyException(e)

View File

@@ -27,7 +27,7 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
open class AssignPasswordInDatabaseRunnable ( open class AssignMainCredentialInDatabaseRunnable (
context: Context, context: Context,
database: Database, database: Database,
protected val mDatabaseUri: Uri, protected val mDatabaseUri: Uri,
@@ -43,7 +43,7 @@ open class AssignPasswordInDatabaseRunnable (
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size) System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri) val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
database.retrieveMasterKey(mMainCredential.masterPassword, uriInputStream) database.assignMasterKey(mMainCredential.masterPassword, uriInputStream)
} catch (e: Exception) { } catch (e: Exception) {
erase(mBackupKey) erase(mBackupKey)
setError(e) setError(e)

View File

@@ -35,7 +35,7 @@ class CreateDatabaseRunnable(context: Context,
private val templateGroupName: String?, private val templateGroupName: String?,
mainCredential: MainCredential, mainCredential: MainCredential,
private val createDatabaseResult: ((Result) -> Unit)?) private val createDatabaseResult: ((Result) -> Unit)?)
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) { : AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
override fun onStartRun() { override fun onStartRun() {
try { try {

View File

@@ -27,12 +27,12 @@ import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
@@ -42,6 +42,7 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
@@ -53,6 +54,7 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MERGE_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
@@ -82,7 +84,6 @@ import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.* import java.util.*
import kotlin.collections.ArrayList
/** /**
* Utility class to connect an activity or a service to the DatabaseTaskNotificationService, * Utility class to connect an activity or a service to the DatabaseTaskNotificationService,
@@ -342,18 +343,27 @@ class DatabaseTaskProvider {
fun startDatabaseLoad(databaseUri: Uri, fun startDatabaseLoad(databaseUri: Uri,
mainCredential: MainCredential, mainCredential: MainCredential,
readOnly: Boolean, readOnly: Boolean,
cipherEntity: CipherDatabaseEntity?, cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean) { fixDuplicateUuid: Boolean) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly) putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity) putParcelable(DatabaseTaskNotificationService.CIPHER_DATABASE_KEY, cipherEncryptDatabase)
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid) putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
} }
, ACTION_DATABASE_LOAD_TASK) , ACTION_DATABASE_LOAD_TASK)
} }
fun startDatabaseMerge(fromDatabaseUri: Uri? = null,
mainCredential: MainCredential? = null) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
}
, ACTION_DATABASE_MERGE_TASK)
}
fun startDatabaseReload(fixDuplicateUuid: Boolean) { fun startDatabaseReload(fixDuplicateUuid: Boolean) {
start(Bundle().apply { start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid) putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
@@ -361,6 +371,19 @@ class DatabaseTaskProvider {
, ACTION_DATABASE_RELOAD_TASK) , ACTION_DATABASE_RELOAD_TASK)
} }
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
if (conditionToAsk) {
AlertDialog.Builder(context)
.setMessage(R.string.warning_database_info_reloaded)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok) { _, _ ->
approved.invoke()
}.create().show()
} else {
approved.invoke()
}
}
fun startDatabaseAssignPassword(databaseUri: Uri, fun startDatabaseAssignPassword(databaseUri: Uri,
mainCredential: MainCredential) { mainCredential: MainCredential) {
@@ -671,9 +694,10 @@ class DatabaseTaskProvider {
/** /**
* Save Database without parameter * Save Database without parameter
*/ */
fun startDatabaseSave(save: Boolean) { fun startDatabaseSave(save: Boolean, saveToUri: Uri? = null) {
start(Bundle().apply { start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
} }
, ACTION_DATABASE_SAVE) , ACTION_DATABASE_SAVE)
} }

View File

@@ -22,12 +22,11 @@ package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -39,7 +38,7 @@ class LoadDatabaseRunnable(private val context: Context,
private val mUri: Uri, private val mUri: Uri,
private val mMainCredential: MainCredential, private val mMainCredential: MainCredential,
private val mReadonly: Boolean, private val mReadonly: Boolean,
private val mCipherEntity: CipherDatabaseEntity?, private val mCipherEncryptDatabase: CipherEncryptDatabase?,
private val mFixDuplicateUUID: Boolean, private val mFixDuplicateUUID: Boolean,
private val progressTaskUpdater: ProgressTaskUpdater?, private val progressTaskUpdater: ProgressTaskUpdater?,
private val mLoadDatabaseResult: ((Result) -> Unit)?) private val mLoadDatabaseResult: ((Result) -> Unit)?)
@@ -60,7 +59,6 @@ class LoadDatabaseRunnable(private val context: Context,
{ memoryWanted -> { memoryWanted ->
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
}, },
LoadedKey.generateNewCipherKey(),
mFixDuplicateUUID, mFixDuplicateUUID,
progressTaskUpdater) progressTaskUpdater)
} }
@@ -77,9 +75,9 @@ class LoadDatabaseRunnable(private val context: Context,
} }
// Register the biometric // Register the biometric
mCipherEntity?.let { cipherDatabaseEntity -> mCipherEncryptDatabase?.let { cipherDatabase ->
CipherDatabaseAction.getInstance(context) CipherDatabaseAction.getInstance(context)
.addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called .addOrUpdateCipherDatabase(cipherDatabase) // return value not called
} }
// Register the current time to init the lock timer // Register the current time to init the lock timer

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.action
import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
class MergeDatabaseRunnable(private val context: Context,
private val mDatabase: Database,
private val mDatabaseToMergeUri: Uri?,
private val mDatabaseToMergeMainCredential: MainCredential?,
private val progressTaskUpdater: ProgressTaskUpdater?,
private val mLoadDatabaseResult: ((Result) -> Unit)?)
: ActionRunnable() {
override fun onStartRun() {
mDatabase.wasReloaded = true
}
override fun onActionRun() {
try {
mDatabase.mergeData(mDatabaseToMergeUri,
mDatabaseToMergeMainCredential,
context.contentResolver,
{ memoryWanted ->
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
},
progressTaskUpdater
)
} catch (e: LoadDatabaseException) {
setError(e)
}
if (result.isSuccess) {
// Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context)
}
}
override fun onFinishRun() {
mLoadDatabaseResult?.invoke(result)
}
}

View File

@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.Context
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -35,23 +34,18 @@ class ReloadDatabaseRunnable(private val context: Context,
private val mLoadDatabaseResult: ((Result) -> Unit)?) private val mLoadDatabaseResult: ((Result) -> Unit)?)
: ActionRunnable() { : ActionRunnable() {
private var tempCipherKey: LoadedKey? = null
override fun onStartRun() { override fun onStartRun() {
tempCipherKey = mDatabase.binaryCache.loadedCipherKey
// Clear before we load // Clear before we load
mDatabase.clear(UriUtil.getBinaryDir(context)) mDatabase.clearIndexesAndBinaries(UriUtil.getBinaryDir(context))
mDatabase.wasReloaded = true mDatabase.wasReloaded = true
} }
override fun onActionRun() { override fun onActionRun() {
try { try {
mDatabase.reloadData(context.contentResolver, mDatabase.reloadData(context.contentResolver,
UriUtil.getBinaryDir(context),
{ memoryWanted -> { memoryWanted ->
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
}, },
tempCipherKey ?: LoadedKey.generateNewCipherKey(),
progressTaskUpdater) progressTaskUpdater)
} catch (e: LoadDatabaseException) { } catch (e: LoadDatabaseException) {
setError(e) setError(e)
@@ -61,7 +55,6 @@ class ReloadDatabaseRunnable(private val context: Context,
// Register the current time to init the lock timer // Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context) PreferencesUtil.saveCurrentTime(context)
} else { } else {
tempCipherKey = null
mDatabase.clearAndClose(context) mDatabase.clearAndClose(context)
} }
} }

View File

@@ -20,13 +20,15 @@
package com.kunzisoft.keepass.database.action package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DatabaseException import com.kunzisoft.keepass.database.exception.DatabaseException
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
open class SaveDatabaseRunnable(protected var context: Context, open class SaveDatabaseRunnable(protected var context: Context,
protected var database: Database, protected var database: Database,
private var saveDatabase: Boolean) private var saveDatabase: Boolean,
private var databaseCopyUri: Uri? = null)
: ActionRunnable() { : ActionRunnable() {
var mAfterSaveDatabase: ((Result) -> Unit)? = null var mAfterSaveDatabase: ((Result) -> Unit)? = null
@@ -34,9 +36,10 @@ open class SaveDatabaseRunnable(protected var context: Context,
override fun onStartRun() {} override fun onStartRun() {}
override fun onActionRun() { override fun onActionRun() {
database.checkVersion()
if (saveDatabase && result.isSuccess) { if (saveDatabase && result.isSuccess) {
try { try {
database.saveData(context.contentResolver) database.saveData(databaseCopyUri, context.contentResolver)
} catch (e: DatabaseException) { } catch (e: DatabaseException) {
setError(e) setError(e)
} }

View File

@@ -36,7 +36,11 @@ abstract class ActionNodeDatabaseRunnable(
abstract fun nodeAction() abstract fun nodeAction()
override fun onStartRun() { override fun onStartRun() {
nodeAction() try {
nodeAction()
} catch (e: Exception) {
setError(e)
}
super.onStartRun() super.onStartRun()
} }

View File

@@ -40,7 +40,7 @@ class DeleteNodesRunnable(context: Context,
foreachNode@ for(nodeToDelete in mNodesToDelete) { foreachNode@ for(nodeToDelete in mNodesToDelete) {
mOldParent = nodeToDelete.parent mOldParent = nodeToDelete.parent
mOldParent?.touch(modified = false, touchParents = true) nodeToDelete.touch(modified = true, touchParents = true)
when (nodeToDelete.type) { when (nodeToDelete.type) {
Type.GROUP -> { Type.GROUP -> {
@@ -50,9 +50,9 @@ class DeleteNodesRunnable(context: Context,
// Remove Node from parent // Remove Node from parent
mCanRecycle = database.canRecycle(groupToDelete) mCanRecycle = database.canRecycle(groupToDelete)
if (mCanRecycle) { if (mCanRecycle) {
groupToDelete.touch(modified = false, touchParents = true)
database.recycle(groupToDelete, context.resources) database.recycle(groupToDelete, context.resources)
groupToDelete.setPreviousParentGroup(mOldParent) groupToDelete.setPreviousParentGroup(mOldParent)
groupToDelete.touch(modified = true, touchParents = true)
} else { } else {
database.deleteGroup(groupToDelete) database.deleteGroup(groupToDelete)
} }
@@ -64,9 +64,9 @@ class DeleteNodesRunnable(context: Context,
// Remove Node from parent // Remove Node from parent
mCanRecycle = database.canRecycle(entryToDelete) mCanRecycle = database.canRecycle(entryToDelete)
if (mCanRecycle) { if (mCanRecycle) {
entryToDelete.touch(modified = false, touchParents = true)
database.recycle(entryToDelete, context.resources) database.recycle(entryToDelete, context.resources)
entryToDelete.setPreviousParentGroup(mOldParent) entryToDelete.setPreviousParentGroup(mOldParent)
entryToDelete.touch(modified = true, touchParents = true)
} else { } else {
database.deleteEntry(entryToDelete) database.deleteEntry(entryToDelete)
} }

View File

@@ -43,6 +43,7 @@ class MoveNodesRunnable constructor(
foreachNode@ for(nodeToMove in mNodesToMove) { foreachNode@ for(nodeToMove in mNodesToMove) {
// Move node in new parent // Move node in new parent
mOldParent = nodeToMove.parent mOldParent = nodeToMove.parent
nodeToMove.touch(modified = true, touchParents = true)
when (nodeToMove.type) { when (nodeToMove.type) {
Type.GROUP -> { Type.GROUP -> {
@@ -52,9 +53,9 @@ class MoveNodesRunnable constructor(
// and if not in the current group // and if not in the current group
&& groupToMove != mNewParent && groupToMove != mNewParent
&& !mNewParent.isContainedIn(groupToMove)) { && !mNewParent.isContainedIn(groupToMove)) {
groupToMove.touch(modified = true, touchParents = true)
database.moveGroupTo(groupToMove, mNewParent) database.moveGroupTo(groupToMove, mNewParent)
groupToMove.setPreviousParentGroup(mOldParent) groupToMove.setPreviousParentGroup(mOldParent)
groupToMove.touch(modified = true, touchParents = true)
} else { } else {
// Only finish thread // Only finish thread
setError(MoveGroupDatabaseException()) setError(MoveGroupDatabaseException())
@@ -67,9 +68,9 @@ class MoveNodesRunnable constructor(
if (mOldParent != mNewParent if (mOldParent != mNewParent
// and root can contains entry // and root can contains entry
&& (mNewParent != database.rootGroup || database.rootCanContainsEntry())) { && (mNewParent != database.rootGroup || database.rootCanContainsEntry())) {
entryToMove.touch(modified = true, touchParents = true)
database.moveEntryTo(entryToMove, mNewParent) database.moveEntryTo(entryToMove, mNewParent)
entryToMove.setPreviousParentGroup(mOldParent) entryToMove.setPreviousParentGroup(mOldParent)
entryToMove.touch(modified = true, touchParents = true)
} else { } else {
// Only finish thread // Only finish thread
setError(MoveEntryDatabaseException()) setError(MoveEntryDatabaseException())

View File

@@ -42,6 +42,9 @@ class UpdateGroupRunnable constructor(
// Update group with new values // Update group with new values
mNewGroup.touch(modified = true, touchParents = true) mNewGroup.touch(modified = true, touchParents = true)
if (database.rootGroup == mOldGroup) {
database.rootGroup = mNewGroup
}
// Only change data in index // Only change data in index
database.updateGroup(mNewGroup) database.updateGroup(mNewGroup)
} }
@@ -50,6 +53,9 @@ class UpdateGroupRunnable constructor(
override fun nodeFinish(): ActionNodesValues { override fun nodeFinish(): ActionNodesValues {
if (!result.isSuccess) { if (!result.isSuccess) {
// If we fail to save, back out changes to global structure // If we fail to save, back out changes to global structure
if (database.rootGroup == mNewGroup) {
database.rootGroup = mOldGroup
}
database.updateGroup(mOldGroup) database.updateGroup(mOldGroup)
} }

View File

@@ -1,89 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.cursor
import android.database.MatrixCursor
import android.provider.BaseColumns
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeId
import java.util.*
abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>> : MatrixCursor(arrayOf(
_ID,
COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS,
COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS,
COLUMN_INDEX_TITLE,
COLUMN_INDEX_ICON_STANDARD,
COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS,
COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS,
COLUMN_INDEX_USERNAME,
COLUMN_INDEX_PASSWORD,
COLUMN_INDEX_URL,
COLUMN_INDEX_NOTES,
COLUMN_INDEX_EXPIRY_TIME,
COLUMN_INDEX_EXPIRES
)) {
protected var entryId: Long = 0
abstract fun addEntry(entry: PwEntryV)
abstract fun getPwNodeId(): NodeId<EntryId>
open fun populateEntry(pwEntry: PwEntryV,
retrieveStandardIcon: (Int) -> IconImageStandard,
retrieveCustomIcon: (UUID) -> IconImageCustom) {
pwEntry.nodeId = getPwNodeId()
pwEntry.title = getString(getColumnIndex(COLUMN_INDEX_TITLE))
val iconStandard = retrieveStandardIcon.invoke(getInt(getColumnIndex(COLUMN_INDEX_ICON_STANDARD)))
val iconCustom = retrieveCustomIcon.invoke(UUID(getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)),
getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS))))
pwEntry.icon = IconImage(iconStandard, iconCustom)
pwEntry.username = getString(getColumnIndex(COLUMN_INDEX_USERNAME))
pwEntry.password = getString(getColumnIndex(COLUMN_INDEX_PASSWORD))
pwEntry.url = getString(getColumnIndex(COLUMN_INDEX_URL))
pwEntry.notes = getString(getColumnIndex(COLUMN_INDEX_NOTES))
pwEntry.expiryTime = DateInstant(getString(getColumnIndex(COLUMN_INDEX_EXPIRY_TIME)))
pwEntry.expires = getString(getColumnIndex(COLUMN_INDEX_EXPIRES))
.lowercase(Locale.ENGLISH) != "false"
}
companion object {
const val _ID = BaseColumns._ID
const val COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS = "UUID_most_significant_bits"
const val COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS = "UUID_least_significant_bits"
const val COLUMN_INDEX_TITLE = "title"
const val COLUMN_INDEX_ICON_STANDARD = "icon_standard"
const val COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS = "icon_custom_UUID_most_significant_bits"
const val COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS = "icon_custom_UUID_least_significant_bits"
const val COLUMN_INDEX_USERNAME = "username"
const val COLUMN_INDEX_PASSWORD = "password"
const val COLUMN_INDEX_URL = "URL"
const val COLUMN_INDEX_NOTES = "notes"
const val COLUMN_INDEX_EXPIRY_TIME = "expiry_time"
const val COLUMN_INDEX_EXPIRES = "expires"
}
}

View File

@@ -1,45 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.cursor
import com.kunzisoft.keepass.database.element.entry.EntryKDB
class EntryCursorKDB : EntryCursorUUID<EntryKDB>() {
override fun addEntry(entry: EntryKDB) {
addRow(arrayOf(
entryId,
entry.id.mostSignificantBits,
entry.id.leastSignificantBits,
entry.title,
entry.icon.standard.id,
entry.icon.custom.uuid.mostSignificantBits,
entry.icon.custom.uuid.leastSignificantBits,
entry.username,
entry.password,
entry.url,
entry.notes,
entry.expiryTime,
entry.expires
))
entryId++
}
}

View File

@@ -1,73 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.cursor
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import java.util.*
class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
private val extraFieldCursor: ExtraFieldCursor = ExtraFieldCursor()
override fun addEntry(entry: EntryKDBX) {
addRow(arrayOf(
entryId,
entry.id.mostSignificantBits,
entry.id.leastSignificantBits,
entry.title,
entry.icon.standard.id,
entry.icon.custom.uuid.mostSignificantBits,
entry.icon.custom.uuid.leastSignificantBits,
entry.username,
entry.password,
entry.url,
entry.notes,
entry.expiryTime,
entry.expires
))
entry.doForEachDecodedCustomField { field ->
extraFieldCursor.addExtraField(entryId, field)
}
entryId++
}
override fun populateEntry(pwEntry: EntryKDBX,
retrieveStandardIcon: (Int) -> IconImageStandard,
retrieveCustomIcon: (UUID) -> IconImageCustom) {
super.populateEntry(pwEntry, retrieveStandardIcon, retrieveCustomIcon)
// Retrieve extra fields
if (extraFieldCursor.moveToFirst()) {
while (!extraFieldCursor.isAfterLast) {
// Add a new extra field only if entryId is the one we want
if (extraFieldCursor.getLong(extraFieldCursor
.getColumnIndex(ExtraFieldCursor.FOREIGN_KEY_ENTRY_ID))
== getLong(getColumnIndex(_ID))) {
extraFieldCursor.populateExtraFieldInEntry(pwEntry)
}
extraFieldCursor.moveToNext()
}
}
}
}

View File

@@ -1,34 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.cursor
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import java.util.*
abstract class EntryCursorUUID<EntryV: EntryVersioned<*, UUID, *, *>>: EntryCursor<UUID, EntryV>() {
override fun getPwNodeId(): NodeId<UUID> {
return NodeIdUUID(
UUID(getLong(getColumnIndex(COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)),
getLong(getColumnIndex(COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS))))
}
}

View File

@@ -1,62 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.cursor
import android.database.MatrixCursor
import android.provider.BaseColumns
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.security.ProtectedString
class ExtraFieldCursor : MatrixCursor(arrayOf(
_ID,
FOREIGN_KEY_ENTRY_ID,
COLUMN_LABEL,
COLUMN_PROTECTION,
COLUMN_VALUE
)) {
private var fieldId: Long = 0
@Synchronized
fun addExtraField(entryId: Long, field: Field) {
addRow(arrayOf(fieldId,
entryId,
field.name,
if (field.protectedValue.isProtected) 1 else 0,
field.protectedValue.toString()))
fieldId++
}
fun populateExtraFieldInEntry(pwEntry: EntryKDBX) {
pwEntry.putField(getString(getColumnIndex(COLUMN_LABEL)),
ProtectedString(getInt(getColumnIndex(COLUMN_PROTECTION)) > 0,
getString(getColumnIndex(COLUMN_VALUE))))
}
companion object {
const val _ID = BaseColumns._ID
const val FOREIGN_KEY_ENTRY_ID = "entry_id"
const val COLUMN_LABEL = "label"
const val COLUMN_PROTECTION = "protection"
const val COLUMN_VALUE = "value"
}
}

View File

@@ -1,3 +1,22 @@
/*
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element package com.kunzisoft.keepass.database.element
import android.os.Parcel import android.os.Parcel
@@ -17,7 +36,10 @@ class CustomData : Parcelable {
} }
constructor(parcel: Parcel) { constructor(parcel: Parcel) {
ParcelableUtil.readStringParcelableMap(parcel, CustomDataItem::class.java) mCustomDataItems.clear()
mCustomDataItems.putAll(ParcelableUtil
.readStringParcelableMap(parcel, CustomDataItem::class.java)
)
} }
fun get(key: String): CustomDataItem? { fun get(key: String): CustomDataItem? {
@@ -46,6 +68,10 @@ class CustomData : Parcelable {
} }
} }
override fun toString(): String {
return mCustomDataItems.toString()
}
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
ParcelableUtil.writeStringParcelableMap(parcel, flags, mCustomDataItems) ParcelableUtil.writeStringParcelableMap(parcel, flags, mCustomDataItems)
} }

View File

@@ -21,6 +21,10 @@ class CustomDataItem : Parcelable {
this.lastModificationTime = lastModificationTime this.lastModificationTime = lastModificationTime
} }
override fun toString(): String {
return value
}
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(key) parcel.writeString(key)
parcel.writeString(value) parcel.writeString(value)

View File

@@ -28,29 +28,18 @@ import java.util.*
class DeletedObject : Parcelable { class DeletedObject : Parcelable {
var uuid: UUID = DatabaseVersioned.UUID_ZERO var uuid: UUID = DatabaseVersioned.UUID_ZERO
private var mDeletionTime: DateInstant? = null var deletionTime: DateInstant = DateInstant()
constructor() constructor()
constructor(uuid: UUID, deletionTime: DateInstant = DateInstant()) { constructor(uuid: UUID, deletionTime: DateInstant = DateInstant()) {
this.uuid = uuid this.uuid = uuid
this.mDeletionTime = deletionTime this.deletionTime = deletionTime
} }
constructor(parcel: Parcel) { constructor(parcel: Parcel) {
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
mDeletionTime = parcel.readParcelable(DateInstant::class.java.classLoader) deletionTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: deletionTime
}
fun getDeletionTime(): DateInstant {
if (mDeletionTime == null) {
mDeletionTime = DateInstant(System.currentTimeMillis())
}
return mDeletionTime!!
}
fun setDeletionTime(deletionTime: DateInstant) {
this.mDeletionTime = deletionTime
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@@ -69,7 +58,7 @@ class DeletedObject : Parcelable {
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(ParcelUuid(uuid), flags) parcel.writeParcelable(ParcelUuid(uuid), flags)
parcel.writeParcelable(mDeletionTime, flags) parcel.writeParcelable(deletionTime, flags)
} }
override fun describeContents(): Int { override fun describeContents(): Int {

View File

@@ -19,11 +19,14 @@
*/ */
package com.kunzisoft.keepass.database.element package com.kunzisoft.keepass.database.element
import android.graphics.Color
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.database.element.binary.AttachmentPool import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.AutoType
import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
@@ -238,6 +241,54 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryKDBX?.notes = value entryKDBX?.notes = value
} }
var backgroundColor: Int?
get() {
var colorInt: Int? = null
entryKDBX?.backgroundColor?.let {
try {
colorInt = Color.parseColor(it)
} catch (e: Exception) {}
}
return colorInt
}
set(value) {
entryKDBX?.backgroundColor = if (value == null) {
""
} else {
ChromaUtil.getFormattedColorString(value, false)
}
}
var foregroundColor: Int?
get() {
var colorInt: Int? = null
entryKDBX?.foregroundColor?.let {
try {
colorInt = Color.parseColor(it)
} catch (e: Exception) {}
}
return colorInt
}
set(value) {
entryKDBX?.foregroundColor = if (value == null) {
""
} else {
ChromaUtil.getFormattedColorString(value, false)
}
}
var customData: CustomData
get() = entryKDBX?.customData ?: CustomData()
set(value) {
entryKDBX?.customData = value
}
var autoType: AutoType
get() = entryKDBX?.autoType ?: AutoType()
set(value) {
entryKDBX?.autoType = value
}
private fun isTan(): Boolean { private fun isTan(): Boolean {
return title == PMS_TAN_ENTRY && username.isNotEmpty() return title == PMS_TAN_ENTRY && username.isNotEmpty()
} }
@@ -419,6 +470,11 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryInfo.expiryTime = expiryTime entryInfo.expiryTime = expiryTime
entryInfo.url = url entryInfo.url = url
entryInfo.notes = notes entryInfo.notes = notes
entryInfo.tags = tags
entryInfo.backgroundColor = backgroundColor
entryInfo.foregroundColor = foregroundColor
entryInfo.customData = customData
entryInfo.autoType = autoType
entryInfo.customFields = getExtraFields().toMutableList() entryInfo.customFields = getExtraFields().toMutableList()
// Add otpElement to generate token // Add otpElement to generate token
entryInfo.otpModel = getOtpElement()?.otpModel entryInfo.otpModel = getOtpElement()?.otpModel
@@ -453,6 +509,11 @@ class Entry : Node, EntryVersionedInterface<Group> {
expiryTime = newEntryInfo.expiryTime expiryTime = newEntryInfo.expiryTime
url = newEntryInfo.url url = newEntryInfo.url
notes = newEntryInfo.notes notes = newEntryInfo.notes
tags = newEntryInfo.tags
backgroundColor = newEntryInfo.backgroundColor
foregroundColor = newEntryInfo.foregroundColor
customData = newEntryInfo.customData
autoType = newEntryInfo.autoType
addExtraFields(newEntryInfo.customFields) addExtraFields(newEntryInfo.customFields)
database?.attachmentPool?.let { binaryPool -> database?.attachmentPool?.let { binaryPool ->
newEntryInfo.attachments.forEach { attachment -> newEntryInfo.attachments.forEach { attachment ->

View File

@@ -262,6 +262,12 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
} }
} }
var customData: CustomData
get() = groupKDBX?.customData ?: CustomData()
set(value) {
groupKDBX?.customData = value
}
override fun getChildGroups(): List<Group> { override fun getChildGroups(): List<Group> {
return groupKDB?.getChildGroups()?.map { return groupKDB?.getChildGroups()?.map {
Group(it) Group(it)
@@ -308,8 +314,9 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
val withoutMetaStream = filters.contains(ChildFilter.META_STREAM) val withoutMetaStream = filters.contains(ChildFilter.META_STREAM)
val showExpiredEntries = !filters.contains(ChildFilter.EXPIRED) val showExpiredEntries = !filters.contains(ChildFilter.EXPIRED)
// TODO Change KDB parser to remove meta entries
return groupKDB?.getChildEntries()?.filter { return groupKDB?.getChildEntries()?.filter {
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream)) (!withoutMetaStream || (withoutMetaStream && !it.isMetaStream()))
&& (!it.isCurrentlyExpires or showExpiredEntries) && (!it.isCurrentlyExpires or showExpiredEntries)
}?.map { }?.map {
Entry(it) Entry(it)
@@ -433,13 +440,36 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupKDBX?.nodeId = id groupKDBX?.nodeId = id
} }
fun setEnableAutoType(enableAutoType: Boolean?) { var searchable: Boolean?
groupKDBX?.enableAutoType = enableAutoType get() = groupKDBX?.enableSearching
set(value) {
groupKDBX?.enableSearching = value
}
fun isSearchable(): Boolean {
val searchableGroup = searchable
if (searchableGroup == null) {
val parenGroup = parent
if (parenGroup == null)
return true
else
return parenGroup.isSearchable()
} else {
return searchableGroup
}
} }
fun setEnableSearching(enableSearching: Boolean?) { var enableAutoType: Boolean?
groupKDBX?.enableSearching = enableSearching get() = groupKDBX?.enableAutoType
} set(value) {
groupKDBX?.enableAutoType = value
}
var defaultAutoTypeSequence: String
get() = groupKDBX?.defaultAutoTypeSequence ?: ""
set(value) {
groupKDBX?.defaultAutoTypeSequence = value
}
fun setExpanded(expanded: Boolean) { fun setExpanded(expanded: Boolean) {
groupKDBX?.isExpanded = expanded groupKDBX?.isExpanded = expanded
@@ -453,6 +483,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
fun getGroupInfo(): GroupInfo { fun getGroupInfo(): GroupInfo {
val groupInfo = GroupInfo() val groupInfo = GroupInfo()
groupInfo.id = groupKDBX?.nodeId?.id
groupInfo.title = title groupInfo.title = title
groupInfo.icon = icon groupInfo.icon = icon
groupInfo.creationTime = creationTime groupInfo.creationTime = creationTime
@@ -460,6 +491,11 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupInfo.expires = expires groupInfo.expires = expires
groupInfo.expiryTime = expiryTime groupInfo.expiryTime = expiryTime
groupInfo.notes = notes groupInfo.notes = notes
groupInfo.searchable = searchable
groupInfo.enableAutoType = enableAutoType
groupInfo.defaultAutoTypeSequence = defaultAutoTypeSequence
groupInfo.tags = tags
groupInfo.customData = customData
return groupInfo return groupInfo
} }
@@ -472,6 +508,11 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
expires = groupInfo.expires expires = groupInfo.expires
expiryTime = groupInfo.expiryTime expiryTime = groupInfo.expiryTime
notes = groupInfo.notes notes = groupInfo.notes
searchable = groupInfo.searchable
enableAutoType = groupInfo.enableAutoType
defaultAutoTypeSequence = groupInfo.defaultAutoTypeSequence
tags = groupInfo.tags
customData = groupInfo.customData
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

View File

@@ -2,15 +2,19 @@ package com.kunzisoft.keepass.database.element
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
class Tags: Parcelable { class Tags: Parcelable {
private val mTags = ArrayList<String>() private val mTags = mutableListOf<String>()
constructor() constructor()
constructor(values: String): this() { constructor(values: String): this() {
mTags.addAll(values.split(';')) mTags.addAll(values
.split(DELIMITER, DELIMITER1)
.filter { it.removeSpaceChars().isNotEmpty() }
)
} }
constructor(parcel: Parcel) : this() { constructor(parcel: Parcel) : this() {
@@ -25,15 +29,55 @@ class Tags: Parcelable {
return 0 return 0
} }
fun setTags(tags: Tags) {
mTags.clear()
mTags.addAll(tags.mTags)
}
fun get(position: Int): String {
return mTags[position]
}
fun put(tag: String) {
if (tag.removeSpaceChars().isNotEmpty() && !mTags.contains(tag))
mTags.add(tag)
}
fun put(tags: Tags) {
tags.mTags.forEach {
put(it)
}
}
fun isEmpty(): Boolean { fun isEmpty(): Boolean {
return mTags.isEmpty() return mTags.isEmpty()
} }
fun isNotEmpty(): Boolean {
return !isEmpty()
}
fun size(): Int {
return mTags.size
}
fun clear() {
mTags.clear()
}
fun toList(): List<String> {
return mTags
}
override fun toString(): String { override fun toString(): String {
return mTags.joinToString(";") return mTags.joinToString(DELIMITER.toString())
} }
companion object CREATOR : Parcelable.Creator<Tags> { companion object CREATOR : Parcelable.Creator<Tags> {
const val DELIMITER= ','
const val DELIMITER1= ';'
val DELIMITERS = listOf(',', ';')
override fun createFromParcel(parcel: Parcel): Tags { override fun createFromParcel(parcel: Parcel): Tags {
return Tags(parcel) return Tags(parcel)
} }

View File

@@ -19,7 +19,7 @@
*/ */
package com.kunzisoft.keepass.database.element.binary package com.kunzisoft.keepass.database.element.binary
class AttachmentPool(binaryCache: BinaryCache) : BinaryPool<Int>(binaryCache) { class AttachmentPool : BinaryPool<Int>() {
/** /**
* Utility method to find an unused key in the pool * Utility method to find an unused key in the pool

View File

@@ -180,12 +180,16 @@ abstract class BinaryData : Parcelable {
companion object { companion object {
private val TAG = BinaryData::class.java.name private val TAG = BinaryData::class.java.name
private const val MAX_BINARY_BYTE = 10485760 // 10 MB
fun canMemoryBeAllocatedInRAM(context: Context, memoryWanted: Long): Boolean { fun canMemoryBeAllocatedInRAM(context: Context, memoryWanted: Long): Boolean {
if (memoryWanted > MAX_BINARY_BYTE)
return false
val memoryInfo = ActivityManager.MemoryInfo() val memoryInfo = ActivityManager.MemoryInfo()
(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(memoryInfo) (context.getSystemService(Context.ACTIVITY_SERVICE)
as? ActivityManager?)?.getMemoryInfo(memoryInfo)
val availableMemory = memoryInfo.availMem val availableMemory = memoryInfo.availMem
return availableMemory > memoryWanted * 3 return availableMemory > (memoryWanted * 5)
} }
} }

View File

@@ -23,7 +23,7 @@ import android.util.Log
import java.io.IOException import java.io.IOException
import kotlin.math.abs import kotlin.math.abs
abstract class BinaryPool<T>(private val mBinaryCache: BinaryCache) { abstract class BinaryPool<T> {
protected val pool = LinkedHashMap<T, BinaryData>() protected val pool = LinkedHashMap<T, BinaryData>()
@@ -225,9 +225,6 @@ abstract class BinaryPool<T>(private val mBinaryCache: BinaryCache) {
@Throws(IOException::class) @Throws(IOException::class)
fun clear() { fun clear() {
doForEachBinary { _, binary ->
binary.clear(mBinaryCache)
}
pool.clear() pool.clear()
} }

View File

@@ -4,19 +4,16 @@ import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import java.util.* import java.util.*
class CustomIconPool(private val binaryCache: BinaryCache) : BinaryPool<UUID>(binaryCache) { class CustomIconPool : BinaryPool<UUID>() {
private val customIcons = HashMap<UUID, IconImageCustom>() private val customIcons = HashMap<UUID, IconImageCustom>()
fun put(key: UUID? = null, fun put(key: UUID? = null,
name: String, name: String,
lastModificationTime: DateInstant?, lastModificationTime: DateInstant?,
smallSize: Boolean, builder: (uniqueBinaryId: String) -> BinaryData,
result: (IconImageCustom, BinaryData?) -> Unit) { result: (IconImageCustom, BinaryData?) -> Unit) {
val keyBinary = super.put(key) { uniqueBinaryId -> val keyBinary = super.put(key, builder)
// Create a byte array for better performance with small data
binaryCache.getBinaryData(uniqueBinaryId, smallSize)
}
val uuid = keyBinary.keys.first() val uuid = keyBinary.keys.first()
val customIcon = IconImageCustom(uuid, name, lastModificationTime) val customIcon = IconImageCustom(uuid, name, lastModificationTime)
customIcons[uuid] = customIcon customIcons[uuid] = customIcon

View File

@@ -34,23 +34,44 @@ import com.kunzisoft.keepass.database.element.node.NodeVersioned
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.util.* import java.util.*
import kotlin.collections.ArrayList
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() { class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
private var kdfListV3: MutableList<KdfEngine> = ArrayList() override var encryptionAlgorithm: EncryptionAlgorithm = EncryptionAlgorithm.AESRijndael
override val availableEncryptionAlgorithms: List<EncryptionAlgorithm> = listOf(
EncryptionAlgorithm.AESRijndael,
EncryptionAlgorithm.Twofish
)
override var kdfEngine: KdfEngine?
get() = kdfAvailableList[0]
set(value) {
value?.let {
numberKeyEncryptionRounds = value.defaultKeyRounds
}
}
override val kdfAvailableList: List<KdfEngine> = listOf(
KdfFactory.aesKdf
)
override val passwordEncoding: String
get() = "ISO-8859-1"
override var numberKeyEncryptionRounds = 300L
override val version: String override val version: String
get() = "V1" get() = "V1"
init { override val defaultFileExtension: String
kdfListV3.add(KdfFactory.aesKdf) get() = ".kdb"
}
private fun getGroupById(groupId: Int): GroupKDB? { init {
if (groupId == -1) // New manual root because KDB contains multiple root groups (here available with getRootGroups())
return null rootGroup = createGroup().apply {
return getGroupById(NodeIdInt(groupId)) icon.standard = getStandardIcon(IconImageStandard.DATABASE_ID)
}
} }
val backupGroup: GroupKDB? val backupGroup: GroupKDB?
@@ -63,33 +84,9 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return listOf(BACKUP_FOLDER_TITLE) return listOf(BACKUP_FOLDER_TITLE)
} }
override val kdfEngine: KdfEngine var defaultUserName: String = ""
get() = kdfListV3[0]
override val kdfAvailableList: List<KdfEngine> var color: Int? = null
get() = kdfListV3
override val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
get() {
val list = ArrayList<EncryptionAlgorithm>()
list.add(EncryptionAlgorithm.AESRijndael)
list.add(EncryptionAlgorithm.Twofish)
return list
}
val rootGroups: List<GroupKDB>
get() {
return rootGroup?.getChildGroups() ?: ArrayList()
}
override val passwordEncoding: String
get() = "ISO-8859-1"
override var numberKeyEncryptionRounds = 300L
init {
algorithm = EncryptionAlgorithm.AESRijndael
}
/** /**
* Generates an unused random tree id * Generates an unused random tree id
@@ -215,29 +212,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return true return true
} }
fun recycle(group: GroupKDB) { fun buildNewBinaryAttachment(): BinaryData {
removeGroupFrom(group, group.parent)
addGroupTo(group, backupGroup)
group.afterAssignNewParent()
}
fun recycle(entry: EntryKDB) {
removeEntryFrom(entry, entry.parent)
addEntryTo(entry, backupGroup)
entry.afterAssignNewParent()
}
fun undoRecycle(group: GroupKDB, origParent: GroupKDB) {
removeGroupFrom(group, backupGroup)
addGroupTo(group, origParent)
}
fun undoRecycle(entry: EntryKDB, origParent: GroupKDB) {
removeEntryFrom(entry, backupGroup)
addEntryTo(entry, origParent)
}
fun buildNewAttachment(): BinaryData {
// Generate an unique new file // Generate an unique new file
return attachmentPool.put { uniqueBinaryId -> return attachmentPool.put { uniqueBinaryId ->
binaryCache.getBinaryData(uniqueBinaryId, false) binaryCache.getBinaryData(uniqueBinaryId, false)

View File

@@ -25,16 +25,16 @@ import android.util.Log
import com.kunzisoft.encrypt.HashManager import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.action.node.NodeHandler import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.crypto.AesEngine
import com.kunzisoft.keepass.database.crypto.CipherEngine
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.VariantDictionary import com.kunzisoft.keepass.database.crypto.VariantDictionary
import com.kunzisoft.keepass.database.crypto.kdf.AesKdf
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
import com.kunzisoft.keepass.database.element.CustomData import com.kunzisoft.keepass.database.element.CustomData
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.Tags
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
@@ -42,12 +42,13 @@ import com.kunzisoft.keepass.database.element.entry.FieldReferencesEngine
import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.node.NodeVersioned import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.template.Template import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible
import com.kunzisoft.keepass.database.exception.UnknownKDF
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_31 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_31
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
@@ -66,6 +67,7 @@ import javax.crypto.Mac
import javax.xml.XMLConstants import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException import javax.xml.parsers.ParserConfigurationException
import kotlin.collections.HashSet
import kotlin.math.min import kotlin.math.min
@@ -73,27 +75,70 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
var hmacKey: ByteArray? = null var hmacKey: ByteArray? = null
private set private set
var cipherUuid = EncryptionAlgorithm.AESRijndael.uuid
private var dataEngine: CipherEngine = AesEngine() override var encryptionAlgorithm: EncryptionAlgorithm = EncryptionAlgorithm.AESRijndael
var compressionAlgorithm = CompressionAlgorithm.GZip
fun setEncryptionAlgorithmFromUUID(uuid: UUID) {
encryptionAlgorithm = EncryptionAlgorithm.getFrom(uuid)
}
override val availableEncryptionAlgorithms: List<EncryptionAlgorithm> = listOf(
EncryptionAlgorithm.AESRijndael,
EncryptionAlgorithm.Twofish,
EncryptionAlgorithm.ChaCha20
)
var kdfParameters: KdfParameters? = null var kdfParameters: KdfParameters? = null
private var kdfList: MutableList<KdfEngine> = ArrayList()
private var numKeyEncRounds: Long = 0 override var kdfEngine: KdfEngine?
var publicCustomData = VariantDictionary() get() = getKdfEngineFromParameters(kdfParameters)
set(value) {
value?.let {
if (kdfParameters?.uuid != value.defaultParameters.uuid)
kdfParameters = value.defaultParameters
numberKeyEncryptionRounds = value.defaultKeyRounds
memoryUsage = value.defaultMemoryUsage
parallelism = value.defaultParallelism
}
}
private fun getKdfEngineFromParameters(kdfParameters: KdfParameters?): KdfEngine? {
if (kdfParameters == null) {
return null
}
for (engine in kdfAvailableList) {
if (engine.uuid == kdfParameters.uuid) {
return engine
}
}
return null
}
fun randomizeKdfParameters() {
kdfParameters?.let {
kdfEngine?.randomize(it)
}
}
override val kdfAvailableList: List<KdfEngine> = listOf(
KdfFactory.aesKdf,
KdfFactory.argon2dKdf,
KdfFactory.argon2idKdf
)
var compressionAlgorithm = CompressionAlgorithm.GZip
private val mFieldReferenceEngine = FieldReferencesEngine(this) private val mFieldReferenceEngine = FieldReferencesEngine(this)
private val mTemplateEngine = TemplateEngineCompatible(this) private val mTemplateEngine = TemplateEngineCompatible(this)
var kdbxVersion = UnsignedInt(0) var kdbxVersion = UnsignedInt(0)
var name = "" var name = ""
var nameChanged = DateInstant() var nameChanged = DateInstant()
// TODO change setting date
var settingsChanged = DateInstant()
var description = "" var description = ""
var descriptionChanged = DateInstant() var descriptionChanged = DateInstant()
var defaultUserName = "" var defaultUserName = ""
var defaultUserNameChanged = DateInstant() var defaultUserNameChanged = DateInstant()
var settingsChanged = DateInstant()
// TODO last change date
var keyLastChanged = DateInstant() var keyLastChanged = DateInstant()
var keyChangeRecDays: Long = -1 var keyChangeRecDays: Long = -1
var keyChangeForceDays: Long = 1 var keyChangeForceDays: Long = 1
@@ -115,16 +160,13 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
var lastSelectedGroupUUID = UUID_ZERO var lastSelectedGroupUUID = UUID_ZERO
var lastTopVisibleGroupUUID = UUID_ZERO var lastTopVisibleGroupUUID = UUID_ZERO
var memoryProtection = MemoryProtectionConfig() var memoryProtection = MemoryProtectionConfig()
val deletedObjects = ArrayList<DeletedObject>() val deletedObjects = HashSet<DeletedObject>()
var publicCustomData = VariantDictionary()
val customData = CustomData() val customData = CustomData()
var localizedAppName = "KeePassDX" val tagPool = Tags()
init { var localizedAppName = "KeePassDX"
kdfList.add(KdfFactory.aesKdf)
kdfList.add(KdfFactory.argon2dKdf)
kdfList.add(KdfFactory.argon2idKdf)
}
constructor() constructor()
@@ -159,38 +201,75 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return "V2 - KDBX$kdbxStringVersion" return "V2 - KDBX$kdbxStringVersion"
} }
override val kdfEngine: KdfEngine? override val defaultFileExtension: String
get() = try { get() = ".kdbx"
getEngineKDBX4(kdfParameters)
} catch (unknownKDF: UnknownKDF) {
Log.i(TAG, "Unable to retrieve KDF engine", unknownKDF)
null
}
override val kdfAvailableList: List<KdfEngine> private open class NodeOperationHandler<T: NodeKDBXInterface> : NodeHandler<T>() {
get() = kdfList var containsCustomData = false
override fun operate(node: T): Boolean {
@Throws(UnknownKDF::class) if (node.customData.isNotEmpty()) {
fun getEngineKDBX4(kdfParameters: KdfParameters?): KdfEngine { containsCustomData = true
val unknownKDFException = UnknownKDF()
if (kdfParameters == null) {
throw unknownKDFException
}
for (engine in kdfList) {
if (engine.uuid == kdfParameters.uuid) {
return engine
} }
return true
} }
throw unknownKDFException
} }
val availableCompressionAlgorithms: List<CompressionAlgorithm> private inner class EntryOperationHandler: NodeOperationHandler<EntryKDBX>() {
get() { var passwordQualityEstimationDisabled = false
val list = ArrayList<CompressionAlgorithm>() override fun operate(node: EntryKDBX): Boolean {
list.add(CompressionAlgorithm.None) if (!node.qualityCheck) {
list.add(CompressionAlgorithm.GZip) passwordQualityEstimationDisabled = true
return list }
return super.operate(node)
} }
}
private inner class GroupOperationHandler: NodeOperationHandler<GroupKDBX>() {
var containsTags = false
override fun operate(node: GroupKDBX): Boolean {
if (node.tags.isNotEmpty())
containsTags = true
return super.operate(node)
}
}
fun getMinKdbxVersion(): UnsignedInt {
val entryHandler = EntryOperationHandler()
val groupHandler = GroupOperationHandler()
rootGroup?.doForEachChildAndForIt(entryHandler, groupHandler)
// https://keepass.info/help/kb/kdbx_4.1.html
val containsGroupWithTag = groupHandler.containsTags
val containsEntryWithPasswordQualityEstimationDisabled = entryHandler.passwordQualityEstimationDisabled
val containsCustomIconWithNameOrLastModificationTime = iconsManager.containsCustomIconWithNameOrLastModificationTime()
val containsHeaderCustomDataWithLastModificationTime = customData.containsItemWithLastModificationTime()
// https://keepass.info/help/kb/kdbx_4.html
// If AES is not use, it's at least 4.0
val keyDerivationFunction = kdfEngine
val kdfIsNotAes = keyDerivationFunction != null && keyDerivationFunction.uuid != AesKdf.CIPHER_UUID
val containsHeaderCustomData = customData.isNotEmpty()
val containsNodeCustomData = entryHandler.containsCustomData || groupHandler.containsCustomData
// Check each condition to determine version
return if (containsGroupWithTag
|| containsEntryWithPasswordQualityEstimationDisabled
|| containsCustomIconWithNameOrLastModificationTime
|| containsHeaderCustomDataWithLastModificationTime) {
FILE_VERSION_41
} else if (kdfIsNotAes
|| containsHeaderCustomData
|| containsNodeCustomData) {
FILE_VERSION_40
} else {
FILE_VERSION_31
}
}
val availableCompressionAlgorithms: List<CompressionAlgorithm> = listOf(
CompressionAlgorithm.None,
CompressionAlgorithm.GZip
)
fun changeBinaryCompression(oldCompression: CompressionAlgorithm, fun changeBinaryCompression(oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm) { newCompression: CompressionAlgorithm) {
@@ -245,18 +324,10 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} }
} }
override val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
get() {
val list = ArrayList<EncryptionAlgorithm>()
list.add(EncryptionAlgorithm.AESRijndael)
list.add(EncryptionAlgorithm.Twofish)
list.add(EncryptionAlgorithm.ChaCha20)
return list
}
override var numberKeyEncryptionRounds: Long override var numberKeyEncryptionRounds: Long
get() { get() {
val kdfEngine = kdfEngine val kdfEngine = kdfEngine
var numKeyEncRounds: Long = 0
if (kdfEngine != null && kdfParameters != null) if (kdfEngine != null && kdfParameters != null)
numKeyEncRounds = kdfEngine.getKeyRounds(kdfParameters!!) numKeyEncRounds = kdfEngine.getKeyRounds(kdfParameters!!)
return numKeyEncRounds return numKeyEncRounds
@@ -265,7 +336,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
val kdfEngine = kdfEngine val kdfEngine = kdfEngine
if (kdfEngine != null && kdfParameters != null) if (kdfEngine != null && kdfParameters != null)
kdfEngine.setKeyRounds(kdfParameters!!, rounds) kdfEngine.setKeyRounds(kdfParameters!!, rounds)
numKeyEncRounds = rounds
} }
var memoryUsage: Long var memoryUsage: Long
@@ -305,7 +375,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
// Retrieve recycle bin in index // Retrieve recycle bin in index
val recycleBin: GroupKDBX? val recycleBin: GroupKDBX?
get() = if (recycleBinUUID == UUID_ZERO) null else getGroupByUUID(recycleBinUUID) get() = getGroupByUUID(recycleBinUUID)
val lastSelectedGroup: GroupKDBX? val lastSelectedGroup: GroupKDBX?
get() = getGroupByUUID(lastSelectedGroupUUID) get() = getGroupByUUID(lastSelectedGroupUUID)
@@ -313,17 +383,14 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
val lastTopVisibleGroup: GroupKDBX? val lastTopVisibleGroup: GroupKDBX?
get() = getGroupByUUID(lastTopVisibleGroupUUID) get() = getGroupByUUID(lastTopVisibleGroupUUID)
fun setDataEngine(dataEngine: CipherEngine) {
this.dataEngine = dataEngine
}
override fun getStandardIcon(iconId: Int): IconImageStandard { override fun getStandardIcon(iconId: Int): IconImageStandard {
return this.iconsManager.getIcon(iconId) return this.iconsManager.getIcon(iconId)
} }
fun buildNewCustomIcon(customIconId: UUID? = null, fun buildNewCustomIcon(customIconId: UUID? = null,
result: (IconImageCustom, BinaryData?) -> Unit) { result: (IconImageCustom, BinaryData?) -> Unit) {
iconsManager.buildNewCustomIcon(customIconId, result) // Create a binary file for a brand new custom icon
addCustomIcon(customIconId, "", null, false, result)
} }
fun addCustomIcon(customIconId: UUID? = null, fun addCustomIcon(customIconId: UUID? = null,
@@ -331,14 +398,21 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
lastModificationTime: DateInstant?, lastModificationTime: DateInstant?,
smallSize: Boolean, smallSize: Boolean,
result: (IconImageCustom, BinaryData?) -> Unit) { result: (IconImageCustom, BinaryData?) -> Unit) {
iconsManager.addCustomIcon(customIconId, name, lastModificationTime, smallSize, result) iconsManager.addCustomIcon(customIconId, name, lastModificationTime, { uniqueBinaryId ->
// Create a byte array for better performance with small data
binaryCache.getBinaryData(uniqueBinaryId, smallSize)
}, result)
}
fun removeCustomIcon(iconUuid: UUID) {
iconsManager.removeCustomIcon(iconUuid, binaryCache)
} }
fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean { fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean {
return iconsManager.isCustomIconBinaryDuplicate(binary) return iconsManager.isCustomIconBinaryDuplicate(binary)
} }
fun getCustomIcon(iconUuid: UUID): IconImageCustom { fun getCustomIcon(iconUuid: UUID): IconImageCustom? {
return this.iconsManager.getIcon(iconUuid) return this.iconsManager.getIcon(iconUuid)
} }
@@ -355,7 +429,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
val templatesGroup = firstGroupWithValidName val templatesGroup = firstGroupWithValidName
?: mTemplateEngine.createNewTemplatesGroup(templatesGroupName) ?: mTemplateEngine.createNewTemplatesGroup(templatesGroupName)
entryTemplatesGroup = templatesGroup.id entryTemplatesGroup = templatesGroup.id
entryTemplatesGroupChanged = templatesGroup.lastModificationTime
} else { } else {
removeTemplatesGroup() removeTemplatesGroup()
} }
@@ -363,7 +436,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
fun removeTemplatesGroup() { fun removeTemplatesGroup() {
entryTemplatesGroup = UUID_ZERO entryTemplatesGroup = UUID_ZERO
entryTemplatesGroupChanged = DateInstant()
mTemplateEngine.clearCache() mTemplateEngine.clearCache()
} }
@@ -414,37 +486,37 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} }
fun getEntryByTitle(title: String, recursionLevel: Int): EntryKDBX? { fun getEntryByTitle(title: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry -> return findEntry { entry ->
entry.decodeTitleKey(recursionLevel).equals(title, true) entry.decodeTitleKey(recursionLevel).equals(title, true)
} }
} }
fun getEntryByUsername(username: String, recursionLevel: Int): EntryKDBX? { fun getEntryByUsername(username: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry -> return findEntry { entry ->
entry.decodeUsernameKey(recursionLevel).equals(username, true) entry.decodeUsernameKey(recursionLevel).equals(username, true)
} }
} }
fun getEntryByURL(url: String, recursionLevel: Int): EntryKDBX? { fun getEntryByURL(url: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry -> return findEntry { entry ->
entry.decodeUrlKey(recursionLevel).equals(url, true) entry.decodeUrlKey(recursionLevel).equals(url, true)
} }
} }
fun getEntryByPassword(password: String, recursionLevel: Int): EntryKDBX? { fun getEntryByPassword(password: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry -> return findEntry { entry ->
entry.decodePasswordKey(recursionLevel).equals(password, true) entry.decodePasswordKey(recursionLevel).equals(password, true)
} }
} }
fun getEntryByNotes(notes: String, recursionLevel: Int): EntryKDBX? { fun getEntryByNotes(notes: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry -> return findEntry { entry ->
entry.decodeNotesKey(recursionLevel).equals(notes, true) entry.decodeNotesKey(recursionLevel).equals(notes, true)
} }
} }
fun getEntryByCustomData(customDataValue: String): EntryKDBX? { fun getEntryByCustomData(customDataValue: String): EntryKDBX? {
return entryIndexes.values.find { entry -> return findEntry { entry ->
entry.customData.containsItemWithValue(customDataValue) entry.customData.containsItemWithValue(customDataValue)
} }
} }
@@ -476,7 +548,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
fun makeFinalKey(masterSeed: ByteArray) { fun makeFinalKey(masterSeed: ByteArray) {
kdfParameters?.let { keyDerivationFunctionParameters -> kdfParameters?.let { keyDerivationFunctionParameters ->
val kdfEngine = getEngineKDBX4(keyDerivationFunctionParameters) val kdfEngine = getKdfEngineFromParameters(keyDerivationFunctionParameters)
?: throw IOException("Unknown key derivation function")
var transformedMasterKey = kdfEngine.transform(masterKey, keyDerivationFunctionParameters) var transformedMasterKey = kdfEngine.transform(masterKey, keyDerivationFunctionParameters)
if (transformedMasterKey.size != 32) { if (transformedMasterKey.size != 32) {
@@ -486,7 +559,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
val cmpKey = ByteArray(65) val cmpKey = ByteArray(65)
System.arraycopy(masterSeed, 0, cmpKey, 0, 32) System.arraycopy(masterSeed, 0, cmpKey, 0, 32)
System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32) System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32)
finalKey = resizeKey(cmpKey, dataEngine.keyLength()) finalKey = resizeKey(cmpKey, encryptionAlgorithm.cipherEngine.keyLength())
val messageDigest: MessageDigest val messageDigest: MessageDigest
try { try {
@@ -724,14 +797,13 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
firstGroupWithValidName firstGroupWithValidName
} }
recycleBinUUID = recycleBinGroup.id recycleBinUUID = recycleBinGroup.id
recycleBinChanged = recycleBinGroup.lastModificationTime recycleBinChanged = DateInstant()
} }
} }
fun removeRecycleBin() { fun removeRecycleBin() {
if (recycleBin != null) { if (recycleBin != null) {
recycleBinUUID = UUID_ZERO recycleBinUUID = UUID_ZERO
recycleBinChanged = DateInstant()
} }
} }
@@ -753,67 +825,44 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return false return false
} }
fun recycle(group: GroupKDBX, resources: Resources) { fun getDeletedObject(nodeId: NodeId<UUID>): DeletedObject? {
ensureRecycleBinExists(resources) return deletedObjects.find { it.uuid == nodeId.id }
removeGroupFrom(group, group.parent)
addGroupTo(group, recycleBin)
group.afterAssignNewParent()
}
fun recycle(entry: EntryKDBX, resources: Resources) {
ensureRecycleBinExists(resources)
removeEntryFrom(entry, entry.parent)
addEntryTo(entry, recycleBin)
entry.afterAssignNewParent()
}
fun undoRecycle(group: GroupKDBX, origParent: GroupKDBX) {
removeGroupFrom(group, recycleBin)
addGroupTo(group, origParent)
}
fun undoRecycle(entry: EntryKDBX, origParent: GroupKDBX) {
removeEntryFrom(entry, recycleBin)
addEntryTo(entry, origParent)
}
fun getDeletedObjects(): List<DeletedObject> {
return deletedObjects
} }
fun addDeletedObject(deletedObject: DeletedObject) { fun addDeletedObject(deletedObject: DeletedObject) {
this.deletedObjects.add(deletedObject) this.deletedObjects.add(deletedObject)
} }
fun addDeletedObject(objectId: UUID) {
addDeletedObject(DeletedObject(objectId))
}
override fun addEntryTo(newEntry: EntryKDBX, parent: GroupKDBX?) { override fun addEntryTo(newEntry: EntryKDBX, parent: GroupKDBX?) {
super.addEntryTo(newEntry, parent) super.addEntryTo(newEntry, parent)
tagPool.put(newEntry.tags)
mFieldReferenceEngine.clear() mFieldReferenceEngine.clear()
} }
override fun updateEntry(entry: EntryKDBX) { override fun updateEntry(entry: EntryKDBX) {
super.updateEntry(entry) super.updateEntry(entry)
tagPool.put(entry.tags)
mFieldReferenceEngine.clear() mFieldReferenceEngine.clear()
} }
override fun removeEntryFrom(entryToRemove: EntryKDBX, parent: GroupKDBX?) { override fun removeEntryFrom(entryToRemove: EntryKDBX, parent: GroupKDBX?) {
super.removeEntryFrom(entryToRemove, parent) super.removeEntryFrom(entryToRemove, parent)
deletedObjects.add(DeletedObject(entryToRemove.id)) // Do not remove tags from pool, it's only in temp memory
mFieldReferenceEngine.clear() mFieldReferenceEngine.clear()
} }
override fun undoDeleteEntryFrom(entry: EntryKDBX, origParent: GroupKDBX?) {
super.undoDeleteEntryFrom(entry, origParent)
deletedObjects.remove(DeletedObject(entry.id))
}
fun containsPublicCustomData(): Boolean { fun containsPublicCustomData(): Boolean {
return publicCustomData.size() > 0 return publicCustomData.size() > 0
} }
fun buildNewAttachment(smallSize: Boolean, fun buildNewBinaryAttachment(smallSize: Boolean,
compression: Boolean, compression: Boolean,
protection: Boolean, protection: Boolean,
binaryPoolId: Int? = null): BinaryData { binaryPoolId: Int? = null): BinaryData {
return attachmentPool.put(binaryPoolId) { uniqueBinaryId -> return attachmentPool.put(binaryPoolId) { uniqueBinaryId ->
binaryCache.getBinaryData(uniqueBinaryId, smallSize, compression, protection) binaryCache.getBinaryData(uniqueBinaryId, smallSize, compression, protection)
}.binary }.binary
@@ -830,6 +879,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} }
private fun removeUnlinkedAttachments(binaries: List<BinaryData>, clear: Boolean) { private fun removeUnlinkedAttachments(binaries: List<BinaryData>, clear: Boolean) {
// TODO check in icon pool
// Build binaries to remove with all binaries known // Build binaries to remove with all binaries known
val binariesToRemove = ArrayList<BinaryData>() val binariesToRemove = ArrayList<BinaryData>()
if (binaries.isEmpty()) { if (binaries.isEmpty()) {
@@ -866,11 +916,10 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return super.validatePasswordEncoding(password, containsKeyFile) return super.validatePasswordEncoding(password, containsKeyFile)
} }
override fun clearCache() { override fun clearIndexes() {
try { try {
super.clearCache() super.clearIndexes()
mFieldReferenceEngine.clear() mFieldReferenceEngine.clear()
attachmentPool.clear()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to clear cache", e) Log.e(TAG, "Unable to clear cache", e)
} }

View File

@@ -19,8 +19,10 @@
*/ */
package com.kunzisoft.keepass.database.element.database package com.kunzisoft.keepass.database.element.database
import android.util.Log
import com.kunzisoft.encrypt.HashManager import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.binary.AttachmentPool import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.binary.BinaryCache import com.kunzisoft.keepass.database.element.binary.BinaryCache
import com.kunzisoft.keepass.database.element.entry.EntryVersioned import com.kunzisoft.keepass.database.element.entry.EntryVersioned
@@ -44,51 +46,43 @@ abstract class DatabaseVersioned<
Entry : EntryVersioned<GroupId, EntryId, Group, Entry> Entry : EntryVersioned<GroupId, EntryId, Group, Entry>
> { > {
// Algorithm used to encrypt the database // Algorithm used to encrypt the database
protected var algorithm: EncryptionAlgorithm? = null abstract var encryptionAlgorithm: EncryptionAlgorithm
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
abstract val kdfEngine: com.kunzisoft.keepass.database.crypto.kdf.KdfEngine? abstract var kdfEngine: KdfEngine?
abstract val kdfAvailableList: List<KdfEngine>
abstract var numberKeyEncryptionRounds: Long
abstract val kdfAvailableList: List<com.kunzisoft.keepass.database.crypto.kdf.KdfEngine> protected abstract val passwordEncoding: String
var masterKey = ByteArray(32) var masterKey = ByteArray(32)
var finalKey: ByteArray? = null var finalKey: ByteArray? = null
protected set protected set
abstract val version: String
abstract val defaultFileExtension: String
/** /**
* To manage binaries in faster way * To manage binaries in faster way
* Cipher key generated when the database is loaded, and destroyed when the database is closed * Cipher key generated when the database is loaded, and destroyed when the database is closed
* Can be used to temporarily store database elements * Can be used to temporarily store database elements
*/ */
var binaryCache = BinaryCache() var binaryCache = BinaryCache()
val iconsManager = IconsManager(binaryCache) var iconsManager = IconsManager()
var attachmentPool = AttachmentPool(binaryCache) var attachmentPool = AttachmentPool()
var changeDuplicateId = false var changeDuplicateId = false
private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>() private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>()
protected var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>() private var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
abstract val version: String
protected abstract val passwordEncoding: String
abstract var numberKeyEncryptionRounds: Long
var encryptionAlgorithm: EncryptionAlgorithm
get() {
return algorithm ?: EncryptionAlgorithm.AESRijndael
}
set(algorithm) {
this.algorithm = algorithm
}
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
var rootGroup: Group? = null var rootGroup: Group? = null
set(value) { set(value) {
field = value field = value
value?.let { value?.let {
removeGroupIndex(it)
addGroupIndex(it) addGroupIndex(it)
} }
} }
@@ -198,12 +192,6 @@ abstract class DatabaseVersioned<
* ------------------------------------- * -------------------------------------
*/ */
fun doForEachGroupInIndex(action: (Group) -> Unit) {
for (group in groupIndexes) {
action.invoke(group.value)
}
}
/** /**
* Determine if an id number is already in use * Determine if an id number is already in use
* *
@@ -219,14 +207,7 @@ abstract class DatabaseVersioned<
return groupIndexes.values return groupIndexes.values
} }
fun setGroupIndexes(groupList: List<Group>) { open fun getGroupById(id: NodeId<GroupId>): Group? {
this.groupIndexes.clear()
for (currentGroup in groupList) {
this.groupIndexes[currentGroup.nodeId] = currentGroup
}
}
fun getGroupById(id: NodeId<GroupId>): Group? {
return this.groupIndexes[id] return this.groupIndexes[id]
} }
@@ -250,16 +231,6 @@ abstract class DatabaseVersioned<
this.groupIndexes.remove(group.nodeId) this.groupIndexes.remove(group.nodeId)
} }
fun numberOfGroups(): Int {
return groupIndexes.size
}
fun doForEachEntryInIndex(action: (Entry) -> Unit) {
for (entry in entryIndexes) {
action.invoke(entry.value)
}
}
fun isEntryIdUsed(id: NodeId<EntryId>): Boolean { fun isEntryIdUsed(id: NodeId<EntryId>): Boolean {
return entryIndexes.containsKey(id) return entryIndexes.containsKey(id)
} }
@@ -272,6 +243,10 @@ abstract class DatabaseVersioned<
return this.entryIndexes[id] return this.entryIndexes[id]
} }
fun findEntry(predicate: (Entry) -> Boolean): Entry? {
return this.entryIndexes.values.find(predicate)
}
fun addEntryIndex(entry: Entry) { fun addEntryIndex(entry: Entry) {
val entryId = entry.nodeId val entryId = entry.nodeId
if (entryIndexes.containsKey(entryId)) { if (entryIndexes.containsKey(entryId)) {
@@ -292,11 +267,7 @@ abstract class DatabaseVersioned<
this.entryIndexes.remove(entry.nodeId) this.entryIndexes.remove(entry.nodeId)
} }
fun numberOfEntries(): Int { open fun clearIndexes() {
return entryIndexes.size
}
open fun clearCache() {
this.groupIndexes.clear() this.groupIndexes.clear()
this.entryIndexes.clear() this.entryIndexes.clear()
} }
@@ -326,7 +297,7 @@ abstract class DatabaseVersioned<
} }
} }
fun removeGroupFrom(groupToRemove: Group, parent: Group?) { open fun removeGroupFrom(groupToRemove: Group, parent: Group?) {
// Remove tree from parent tree // Remove tree from parent tree
parent?.removeChildGroup(groupToRemove) parent?.removeChildGroup(groupToRemove)
removeGroupIndex(groupToRemove) removeGroupIndex(groupToRemove)
@@ -353,23 +324,39 @@ abstract class DatabaseVersioned<
removeEntryIndex(entryToRemove) removeEntryIndex(entryToRemove)
} }
// TODO Delete group
fun undoDeleteGroupFrom(group: Group, origParent: Group?) {
addGroupTo(group, origParent)
}
open fun undoDeleteEntryFrom(entry: Entry, origParent: Group?) {
addEntryTo(entry, origParent)
}
abstract fun isInRecycleBin(group: Group): Boolean abstract fun isInRecycleBin(group: Group): Boolean
fun isGroupSearchable(group: Group?, omitBackup: Boolean): Boolean { fun clearIconsCache() {
if (group == null) iconsManager.doForEachCustomIcon { _, binary ->
return false try {
if (omitBackup && isInRecycleBin(group)) binary.clear(binaryCache)
return false } catch (e: Exception) {
return true Log.e(TAG, "Unable to clear icon binary cache", e)
}
}
iconsManager.clear()
}
fun clearAttachmentsCache() {
attachmentPool.doForEachBinary { _, binary ->
try {
binary.clear(binaryCache)
} catch (e: Exception) {
Log.e(TAG, "Unable to clear attachment binary cache", e)
}
}
attachmentPool.clear()
}
fun clearBinaries() {
binaryCache.clear()
}
fun clearAll() {
clearIndexes()
clearIconsCache()
clearAttachmentsCache()
clearBinaries()
} }
companion object { companion object {

View File

@@ -25,6 +25,7 @@ import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.binary.AttachmentPool import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
@@ -60,18 +61,43 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
private var binaryDataId: Int? = null private var binaryDataId: Int? = null
// Determine if this is a MetaStream entry // Determine if this is a MetaStream entry
val isMetaStream: Boolean fun isMetaStream(): Boolean {
get() { if (notes.isEmpty()) return false
if (notes.isEmpty()) return false if (binaryDescription != PMS_ID_BINDESC) return false
if (binaryDescription != PMS_ID_BINDESC) return false if (title.isEmpty()) return false
if (title.isEmpty()) return false if (title != PMS_ID_TITLE) return false
if (title != PMS_ID_TITLE) return false if (username.isEmpty()) return false
if (username.isEmpty()) return false if (username != PMS_ID_USER) return false
if (username != PMS_ID_USER) return false if (url.isEmpty()) return false
if (url.isEmpty()) return false if (url != PMS_ID_URL) return false
if (url != PMS_ID_URL) return false return icon.standard.id == KEY_ID
return icon.standard.id == KEY_ID }
}
fun isMetaStreamDefaultUsername(): Boolean {
return isMetaStream() && notes == PMS_STREAM_DEFAULTUSER
}
private fun setMetaStream() {
binaryDescription = PMS_ID_BINDESC
title = PMS_ID_TITLE
username = PMS_ID_USER
url = PMS_ID_URL
icon.standard = IconImageStandard(KEY_ID)
}
fun setMetaStreamDefaultUsername() {
notes = PMS_STREAM_DEFAULTUSER
setMetaStream()
}
fun isMetaStreamDatabaseColor(): Boolean {
return isMetaStream() && notes == PMS_STREAM_DBCOLOR
}
fun setMetaStreamDatabaseColor() {
notes = PMS_STREAM_DBCOLOR
setMetaStream()
}
override fun initNodeId(): NodeId<UUID> { override fun initNodeId(): NodeId<UUID> {
return NodeIdUUID() return NodeIdUUID()
@@ -113,8 +139,9 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
dest.writeInt(binaryDataId ?: -1) dest.writeInt(binaryDataId ?: -1)
} }
fun updateWith(source: EntryKDB) { fun updateWith(source: EntryKDB,
super.updateWith(source) updateParents: Boolean = true) {
super.updateWith(source, updateParents)
title = source.title title = source.title
username = source.username username = source.username
password = source.password password = source.password
@@ -184,6 +211,13 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
private const val PMS_ID_USER = "SYSTEM" private const val PMS_ID_USER = "SYSTEM"
private const val PMS_ID_URL = "$" private const val PMS_ID_URL = "$"
const val PMS_STREAM_SIMPLESTATE = "Simple UI State"
const val PMS_STREAM_DEFAULTUSER = "Default User Name"
const val PMS_STREAM_SEARCHHISTORYITEM = "Search History Item"
const val PMS_STREAM_CUSTOMKVP = "Custom KVP"
const val PMS_STREAM_DBCOLOR = "Database Color"
const val PMS_STREAM_KPXICON2 = "KPX_CUSTOM_ICONS_2"
@JvmField @JvmField
val CREATOR: Parcelable.Creator<EntryKDB> = object : Parcelable.Creator<EntryKDB> { val CREATOR: Parcelable.Creator<EntryKDB> = object : Parcelable.Creator<EntryKDB> {
override fun createFromParcel(parcel: Parcel): EntryKDB { override fun createFromParcel(parcel: Parcel): EntryKDB {

View File

@@ -110,8 +110,10 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
* Update with deep copy of each entry element * Update with deep copy of each entry element
* @param source * @param source
*/ */
fun updateWith(source: EntryKDBX, copyHistory: Boolean = true) { fun updateWith(source: EntryKDBX,
super.updateWith(source) copyHistory: Boolean = true,
updateParents: Boolean = true) {
super.updateWith(source, updateParents)
usageCount = source.usageCount usageCount = source.usageCount
locationChanged = DateInstant(source.locationChanged) locationChanged = DateInstant(source.locationChanged)
customData = CustomData(source.customData) customData = CustomData(source.customData)

View File

@@ -53,8 +53,9 @@ class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
dest.writeInt(groupFlags) dest.writeInt(groupFlags)
} }
fun updateWith(source: GroupKDB) { fun updateWith(source: GroupKDB,
super.updateWith(source) updateParents: Boolean = true) {
super.updateWith(source, updateParents)
groupFlags = source.groupFlags groupFlags = source.groupFlags
} }

View File

@@ -41,9 +41,9 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
override var customData = CustomData() override var customData = CustomData()
var notes = "" var notes = ""
var isExpanded = true var isExpanded = true
var defaultAutoTypeSequence = ""
var enableAutoType: Boolean? = null
var enableSearching: Boolean? = null var enableSearching: Boolean? = null
var enableAutoType: Boolean? = null
var defaultAutoTypeSequence: String = ""
var lastTopVisibleEntry: UUID = DatabaseVersioned.UUID_ZERO var lastTopVisibleEntry: UUID = DatabaseVersioned.UUID_ZERO
override var tags = Tags() override var tags = Tags()
override var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO override var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO
@@ -69,11 +69,11 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
customData = parcel.readParcelable(CustomData::class.java.classLoader) ?: CustomData() customData = parcel.readParcelable(CustomData::class.java.classLoader) ?: CustomData()
notes = parcel.readString() ?: notes notes = parcel.readString() ?: notes
isExpanded = parcel.readByte().toInt() != 0 isExpanded = parcel.readByte().toInt() != 0
defaultAutoTypeSequence = parcel.readString() ?: defaultAutoTypeSequence
val isAutoTypeEnabled = parcel.readInt()
enableAutoType = if (isAutoTypeEnabled == -1) null else isAutoTypeEnabled == 1
val isSearchingEnabled = parcel.readInt() val isSearchingEnabled = parcel.readInt()
enableSearching = if (isSearchingEnabled == -1) null else isSearchingEnabled == 1 enableSearching = if (isSearchingEnabled == -1) null else isSearchingEnabled == 1
val isAutoTypeEnabled = parcel.readInt()
enableAutoType = if (isAutoTypeEnabled == -1) null else isAutoTypeEnabled == 1
defaultAutoTypeSequence = parcel.readString() ?: defaultAutoTypeSequence
lastTopVisibleEntry = parcel.readSerializable() as UUID lastTopVisibleEntry = parcel.readSerializable() as UUID
tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags
previousParentGroup = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO previousParentGroup = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
@@ -94,25 +94,26 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
dest.writeParcelable(customData, flags) dest.writeParcelable(customData, flags)
dest.writeString(notes) dest.writeString(notes)
dest.writeByte((if (isExpanded) 1 else 0).toByte()) dest.writeByte((if (isExpanded) 1 else 0).toByte())
dest.writeString(defaultAutoTypeSequence)
dest.writeInt(if (enableAutoType == null) -1 else if (enableAutoType!!) 1 else 0)
dest.writeInt(if (enableSearching == null) -1 else if (enableSearching!!) 1 else 0) dest.writeInt(if (enableSearching == null) -1 else if (enableSearching!!) 1 else 0)
dest.writeInt(if (enableAutoType == null) -1 else if (enableAutoType!!) 1 else 0)
dest.writeString(defaultAutoTypeSequence)
dest.writeSerializable(lastTopVisibleEntry) dest.writeSerializable(lastTopVisibleEntry)
dest.writeParcelable(tags, flags) dest.writeParcelable(tags, flags)
dest.writeParcelable(ParcelUuid(previousParentGroup), flags) dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
} }
fun updateWith(source: GroupKDBX) { fun updateWith(source: GroupKDBX,
super.updateWith(source) updateParents: Boolean = true) {
super.updateWith(source, updateParents)
usageCount = source.usageCount usageCount = source.usageCount
locationChanged = DateInstant(source.locationChanged) locationChanged = DateInstant(source.locationChanged)
// Add all custom elements in map // Add all custom elements in map
customData = CustomData(source.customData) customData = CustomData(source.customData)
notes = source.notes notes = source.notes
isExpanded = source.isExpanded isExpanded = source.isExpanded
defaultAutoTypeSequence = source.defaultAutoTypeSequence
enableAutoType = source.enableAutoType
enableSearching = source.enableSearching enableSearching = source.enableSearching
enableAutoType = source.enableAutoType
defaultAutoTypeSequence = source.defaultAutoTypeSequence
lastTopVisibleEntry = source.lastTopVisibleEntry lastTopVisibleEntry = source.lastTopVisibleEntry
tags = source.tags tags = source.tags
previousParentGroup = source.previousParentGroup previousParentGroup = source.previousParentGroup

View File

@@ -51,12 +51,15 @@ abstract class GroupVersioned
dest.writeString(titleGroup) dest.writeString(titleGroup)
} }
protected fun updateWith(source: GroupVersioned<GroupId, EntryId, Group, Entry>) { protected fun updateWith(source: GroupVersioned<GroupId, EntryId, Group, Entry>,
super.updateWith(source) updateParents: Boolean = true) {
super.updateWith(source, updateParents)
titleGroup = source.titleGroup titleGroup = source.titleGroup
removeChildren() if (updateParents) {
childGroups.addAll(source.childGroups) removeChildren()
childEntries.addAll(source.childEntries) childGroups.addAll(source.childGroups)
childEntries.addAll(source.childEntries)
}
} }
override var title: String override var title: String

View File

@@ -81,6 +81,7 @@ class IconImageStandard : IconImageDraw {
const val CREDIT_CARD_ID = 37 const val CREDIT_CARD_ID = 37
const val TRASH_ID = 43 const val TRASH_ID = 43
const val FOLDER_ID = 48 const val FOLDER_ID = 48
const val DATABASE_ID = 50
const val LIST_ID = 57 const val LIST_ID = 57
const val BUILD_ID = 59 const val BUILD_ID = 59
const val STAR_ID = 61 const val STAR_ID = 61

View File

@@ -28,12 +28,12 @@ import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.K
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
import java.util.* import java.util.*
class IconsManager(binaryCache: BinaryCache) { class IconsManager {
private val standardCache = List(NB_ICONS) { private val standardCache = List(NB_ICONS) {
IconImageStandard(it) IconImageStandard(it)
} }
private val customCache = CustomIconPool(binaryCache) private val customCache = CustomIconPool()
fun getIcon(iconId: Int): IconImageStandard { fun getIcon(iconId: Int): IconImageStandard {
val searchIconId = if (IconImageStandard.isCorrectIconId(iconId)) iconId else KEY_ID val searchIconId = if (IconImageStandard.isCorrectIconId(iconId)) iconId else KEY_ID
@@ -50,29 +50,23 @@ class IconsManager(binaryCache: BinaryCache) {
* Custom * Custom
*/ */
fun buildNewCustomIcon(key: UUID? = null,
result: (IconImageCustom, BinaryData?) -> Unit) {
// Create a binary file for a brand new custom icon
addCustomIcon(key, "", null, false, result)
}
fun addCustomIcon(key: UUID? = null, fun addCustomIcon(key: UUID? = null,
name: String, name: String,
lastModificationTime: DateInstant?, lastModificationTime: DateInstant?,
smallSize: Boolean, builder: (uniqueBinaryId: String) -> BinaryData,
result: (IconImageCustom, BinaryData?) -> Unit) { result: (IconImageCustom, BinaryData?) -> Unit) {
customCache.put(key, name, lastModificationTime, smallSize, result) customCache.put(key, name, lastModificationTime, builder, result)
} }
fun getIcon(iconUuid: UUID): IconImageCustom { fun getIcon(iconUuid: UUID): IconImageCustom? {
return customCache.getCustomIcon(iconUuid) ?: IconImageCustom(iconUuid) return customCache.getCustomIcon(iconUuid)
} }
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean { fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
return customCache.isBinaryDuplicate(binaryData) return customCache.isBinaryDuplicate(binaryData)
} }
fun removeCustomIcon(binaryCache: BinaryCache, iconUuid: UUID) { fun removeCustomIcon(iconUuid: UUID, binaryCache: BinaryCache) {
val binary = customCache[iconUuid] val binary = customCache[iconUuid]
customCache.remove(iconUuid) customCache.remove(iconUuid)
try { try {
@@ -99,12 +93,8 @@ class IconsManager(binaryCache: BinaryCache) {
/** /**
* Clear the cache of icons * Clear the cache of icons
*/ */
fun clearCache() { fun clear() {
try { customCache.clear()
customCache.clear()
} catch(e: Exception) {
Log.e(TAG, "Unable to clear cache", e)
}
} }
companion object { companion object {

View File

@@ -32,6 +32,19 @@ interface Node: NodeVersionedInterface<Group> {
fun removeParent() { fun removeParent() {
parent = null parent = null
} }
fun getPathString(): String {
val pathNodes = mutableListOf<Node>()
var currentNode = this
pathNodes.add(0, currentNode)
while (currentNode.containsParent()) {
currentNode.parent?.let { parent ->
currentNode = parent
pathNodes.add(0, currentNode)
}
}
return pathNodes.joinToString("/") { it.title }
}
} }
/** /**

View File

@@ -44,4 +44,6 @@ abstract class NodeId<Id> : Parcelable {
override fun hashCode(): Int { override fun hashCode(): Int {
return id?.hashCode() ?: 0 return id?.hashCode() ?: 0
} }
abstract fun toVisualString(): String?
} }

View File

@@ -63,6 +63,10 @@ class NodeIdInt : NodeId<Int> {
return id.toString() return id.toString()
} }
override fun toVisualString(): String? {
return null
}
companion object { companion object {
@JvmField @JvmField
val CREATOR: Parcelable.Creator<NodeIdInt> = object : Parcelable.Creator<NodeIdInt> { val CREATOR: Parcelable.Creator<NodeIdInt> = object : Parcelable.Creator<NodeIdInt> {

View File

@@ -64,6 +64,10 @@ class NodeIdUUID : NodeId<UUID> {
return UuidUtil.toHexString(id) ?: id.toString() return UuidUtil.toHexString(id) ?: id.toString()
} }
override fun toVisualString(): String {
return toString()
}
companion object { companion object {
@JvmField @JvmField
val CREATOR: Parcelable.Creator<NodeIdUUID> = object : Parcelable.Creator<NodeIdUUID> { val CREATOR: Parcelable.Creator<NodeIdUUID> = object : Parcelable.Creator<NodeIdUUID> {

View File

@@ -68,9 +68,12 @@ abstract class NodeVersioned<IdType, Parent : GroupVersionedInterface<Parent, En
return 0 return 0
} }
protected fun updateWith(source: NodeVersioned<IdType, Parent, Entry>) { protected fun updateWith(source: NodeVersioned<IdType, Parent, Entry>,
updateParents: Boolean = true) {
this.nodeId = copyNodeId(source.nodeId) this.nodeId = copyNodeId(source.nodeId)
this.parent = source.parent if (updateParents) {
this.parent = source.parent
}
this.icon = source.icon this.icon = source.icon
this.creationTime = DateInstant(source.creationTime) this.creationTime = DateInstant(source.creationTime)
this.lastModificationTime = DateInstant(source.lastModificationTime) this.lastModificationTime = DateInstant(source.lastModificationTime)

View File

@@ -23,10 +23,8 @@ import android.os.ParcelUuid
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.collections.LinkedHashMap
class Template : Parcelable { class Template : Parcelable {
@@ -34,6 +32,8 @@ class Template : Parcelable {
var uuid: UUID = DatabaseVersioned.UUID_ZERO var uuid: UUID = DatabaseVersioned.UUID_ZERO
var title = "" var title = ""
var icon = IconImage() var icon = IconImage()
var backgroundColor: Int? = null
var foregroundColor: Int? = null
var sections: MutableList<TemplateSection> = ArrayList() var sections: MutableList<TemplateSection> = ArrayList()
private set private set
@@ -41,7 +41,8 @@ class Template : Parcelable {
title: String, title: String,
icon: IconImage, icon: IconImage,
section: TemplateSection, section: TemplateSection,
version: Int = 1): this(uuid, title, icon, ArrayList<TemplateSection>().apply { version: Int = 1)
: this(uuid, title, icon, ArrayList<TemplateSection>().apply {
add(section) add(section)
}, version) }, version)
@@ -49,11 +50,22 @@ class Template : Parcelable {
title: String, title: String,
icon: IconImage, icon: IconImage,
sections: List<TemplateSection>, sections: List<TemplateSection>,
version: Int = 1)
: this(uuid, title, icon, null, null, sections, version)
constructor(uuid: UUID,
title: String,
icon: IconImage,
backgroundColor: Int?,
foregroundColor: Int?,
sections: List<TemplateSection>,
version: Int = 1) { version: Int = 1) {
this.version = version this.version = version
this.uuid = uuid this.uuid = uuid
this.title = title this.title = title
this.icon = icon this.icon = icon
this.backgroundColor = backgroundColor
this.foregroundColor = foregroundColor
this.sections.clear() this.sections.clear()
this.sections.addAll(sections) this.sections.addAll(sections)
} }
@@ -63,6 +75,8 @@ class Template : Parcelable {
this.uuid = template.uuid this.uuid = template.uuid
this.title = template.title this.title = template.title
this.icon = template.icon this.icon = template.icon
this.backgroundColor = template.backgroundColor
this.foregroundColor = template.foregroundColor
this.sections.clear() this.sections.clear()
this.sections.addAll(template.sections) this.sections.addAll(template.sections)
} }
@@ -72,6 +86,8 @@ class Template : Parcelable {
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: uuid uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: uuid
title = parcel.readString() ?: title title = parcel.readString() ?: title
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
backgroundColor = parcel.readInt()
foregroundColor = parcel.readInt()
parcel.readList(sections, TemplateSection::class.java.classLoader) parcel.readList(sections, TemplateSection::class.java.classLoader)
} }
@@ -80,6 +96,8 @@ class Template : Parcelable {
parcel.writeParcelable(ParcelUuid(uuid), flags) parcel.writeParcelable(ParcelUuid(uuid), flags)
parcel.writeString(title) parcel.writeString(title)
parcel.writeParcelable(icon, flags) parcel.writeParcelable(icon, flags)
parcel.writeInt(backgroundColor ?: -1)
parcel.writeInt(foregroundColor ?: -1)
parcel.writeList(sections) parcel.writeList(sections)
} }

View File

@@ -1,3 +1,21 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*/
package com.kunzisoft.keepass.database.element.template package com.kunzisoft.keepass.database.element.template
enum class TemplateAttributeAction { enum class TemplateAttributeAction {

View File

@@ -1,3 +1,21 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*/
package com.kunzisoft.keepass.database.element.template package com.kunzisoft.keepass.database.element.template
import android.os.Parcel import android.os.Parcel

View File

@@ -1,9 +1,26 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*/
package com.kunzisoft.keepass.database.element.template package com.kunzisoft.keepass.database.element.template
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import java.util.* import java.util.*
import kotlin.collections.LinkedHashMap
class TemplateBuilder { class TemplateBuilder {

View File

@@ -1,6 +1,24 @@
package com.kunzisoft.keepass.database.element.template package com.kunzisoft.keepass.database.element.template
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*/
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Color
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Field import com.kunzisoft.keepass.database.element.Field
@@ -26,7 +44,7 @@ abstract class TemplateEngine(private val mDatabase: DatabaseKDBX) {
if (templateGroup != null) { if (templateGroup != null) {
templates.add(Template.STANDARD) templates.add(Template.STANDARD)
templateGroup.getChildEntries().forEach { templateEntry -> templateGroup.getChildEntries().forEach { templateEntry ->
getTemplateFromTemplateEntry(templateEntry)?.let { getTemplateFromTemplateEntry(templateEntry).let {
mCacheTemplates[templateEntry.id] = it mCacheTemplates[templateEntry.id] = it
templates.add(it) templates.add(it)
} }
@@ -70,7 +88,7 @@ abstract class TemplateEngine(private val mDatabase: DatabaseKDBX) {
return mCacheTemplates[uuid] return mCacheTemplates[uuid]
else { else {
mDatabase.getEntryById(uuid)?.let { templateEntry -> mDatabase.getEntryById(uuid)?.let { templateEntry ->
getTemplateFromTemplateEntry(templateEntry)?.let { newTemplate -> getTemplateFromTemplateEntry(templateEntry).let { newTemplate ->
mCacheTemplates[uuid] = newTemplate mCacheTemplates[uuid] = newTemplate
return newTemplate return newTemplate
} }
@@ -134,7 +152,7 @@ abstract class TemplateEngine(private val mDatabase: DatabaseKDBX) {
return TemplateSection(sectionAttributes) return TemplateSection(sectionAttributes)
} }
private fun getTemplateFromTemplateEntry(templateEntry: EntryKDBX): Template? { private fun getTemplateFromTemplateEntry(templateEntry: EntryKDBX): Template {
val templateEntryDecoded = decodeTemplateEntry(templateEntry) val templateEntryDecoded = decodeTemplateEntry(templateEntry)
val templateSections = mutableListOf<TemplateSection>() val templateSections = mutableListOf<TemplateSection>()
@@ -149,7 +167,28 @@ abstract class TemplateEngine(private val mDatabase: DatabaseKDBX) {
} }
templateSections.add(buildTemplateSectionFromFields(sectionFields)) templateSections.add(buildTemplateSectionFromFields(sectionFields))
return Template(templateEntry.id, templateEntry.title, templateEntry.icon, templateSections, getVersion()) var backgroundColor: Int? = null
templateEntry.backgroundColor.let {
try {
backgroundColor = Color.parseColor(it)
} catch (e: Exception) {}
}
var foregroundColor: Int? = null
templateEntry.foregroundColor.let {
try {
foregroundColor = Color.parseColor(it)
} catch (e: Exception) {}
}
return Template(
templateEntry.id,
templateEntry.title,
templateEntry.icon,
backgroundColor,
foregroundColor,
templateSections,
getVersion()
)
} }
companion object { companion object {

View File

@@ -1,3 +1,21 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*/
package com.kunzisoft.keepass.database.element.template package com.kunzisoft.keepass.database.element.template
import android.util.Log import android.util.Log
@@ -257,6 +275,9 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
entryCopy.putField(field) entryCopy.putField(field)
} }
} }
// Add colors
entryCopy.foregroundColor = templateEntry.foregroundColor
entryCopy.backgroundColor = templateEntry.backgroundColor
return entryCopy return entryCopy
} }
@@ -359,6 +380,9 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
} }
} }
} }
// Add colors
entryCopy.foregroundColor = templateEntry.foregroundColor
entryCopy.backgroundColor = templateEntry.backgroundColor
return entryCopy return entryCopy
} }

View File

@@ -1,3 +1,21 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*/
package com.kunzisoft.keepass.database.element.template package com.kunzisoft.keepass.database.element.template
import android.content.Context import android.content.Context
@@ -85,7 +103,7 @@ object TemplateField {
LABEL_SSID.equals(name, true) -> context.getString(R.string.ssid) LABEL_SSID.equals(name, true) -> context.getString(R.string.ssid)
LABEL_TYPE.equals(name, true) -> context.getString(R.string.type) LABEL_TYPE.equals(name, true) -> context.getString(R.string.type)
LABEL_CRYPTOCURRENCY.equals(name, true) -> context.getString(R.string.cryptocurrency) LABEL_CRYPTOCURRENCY.equals(name, true) -> context.getString(R.string.cryptocurrency)
LABEL_TOKEN.equals(name, true) -> context.getString(R.string.token) LABEL_TOKEN.equals(name, false) -> context.getString(R.string.token)
LABEL_PUBLIC_KEY.equals(name, true) -> context.getString(R.string.public_key) LABEL_PUBLIC_KEY.equals(name, true) -> context.getString(R.string.public_key)
LABEL_PRIVATE_KEY.equals(name, true) -> context.getString(R.string.private_key) LABEL_PRIVATE_KEY.equals(name, true) -> context.getString(R.string.private_key)
LABEL_SEED.equals(name, true) -> context.getString(R.string.seed) LABEL_SEED.equals(name, true) -> context.getString(R.string.seed)

View File

@@ -1,3 +1,21 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*/
package com.kunzisoft.keepass.database.element.template package com.kunzisoft.keepass.database.element.template
import android.os.Parcel import android.os.Parcel

View File

@@ -46,6 +46,7 @@ open class LoadDatabaseException : DatabaseException {
@StringRes @StringRes
override var errorId: Int = R.string.error_load_database override var errorId: Int = R.string.error_load_database
constructor() : super() constructor() : super()
constructor(string: String) : super(string)
constructor(throwable: Throwable) : super(throwable) constructor(throwable: Throwable) : super(throwable)
} }
@@ -53,6 +54,7 @@ class FileNotFoundDatabaseException : LoadDatabaseException {
@StringRes @StringRes
override var errorId: Int = R.string.file_not_found_content override var errorId: Int = R.string.file_not_found_content
constructor() : super() constructor() : super()
constructor(string: String) : super(string)
constructor(exception: Throwable) : super(exception) constructor(exception: Throwable) : super(exception)
} }
@@ -76,6 +78,7 @@ class IODatabaseException : LoadDatabaseException {
@StringRes @StringRes
override var errorId: Int = R.string.error_load_database override var errorId: Int = R.string.error_load_database
constructor() : super() constructor() : super()
constructor(string: String) : super(string)
constructor(exception: Throwable) : super(exception) constructor(exception: Throwable) : super(exception)
} }

View File

@@ -19,8 +19,6 @@
*/ */
package com.kunzisoft.keepass.database.file package com.kunzisoft.keepass.database.file
import com.kunzisoft.keepass.utils.UnsignedInt
abstract class DatabaseHeader { abstract class DatabaseHeader {
/** /**
@@ -33,8 +31,4 @@ abstract class DatabaseHeader {
*/ */
var encryptionIV = ByteArray(16) var encryptionIV = ByteArray(16)
companion object {
val PWM_DBSIG_1 = UnsignedInt(-0x655d26fd)
}
} }

View File

@@ -34,7 +34,7 @@ class DatabaseHeaderKDB : DatabaseHeader() {
*/ */
var transformSeed = ByteArray(32) var transformSeed = ByteArray(32)
var signature1 = UnsignedInt(0) // = PWM_DBSIG_1 var signature1 = UnsignedInt(0) // = DBSIG_1
var signature2 = UnsignedInt(0) // = DBSIG_2 var signature2 = UnsignedInt(0) // = DBSIG_2
var flags= UnsignedInt(0) var flags= UnsignedInt(0)
var version= UnsignedInt(0) var version= UnsignedInt(0)
@@ -84,9 +84,9 @@ class DatabaseHeaderKDB : DatabaseHeader() {
companion object { companion object {
// DB sig from KeePass 1.03 // DB sig from KeePass 1.03
val DBSIG_2 = UnsignedInt(-0x4ab4049b) val DBSIG_1 = UnsignedInt(-0x655d26fd) // 0x9AA2D903
// DB sig from KeePass 1.03 val DBSIG_2 = UnsignedInt(-0x4ab4049b) // 0xB54BFB65
val DBVER_DW = UnsignedInt(0x00030003) val DBVER_DW = UnsignedInt(0x00030004)
val FLAG_SHA2 = UnsignedInt(1) val FLAG_SHA2 = UnsignedInt(1)
val FLAG_RIJNDAEL = UnsignedInt(2) val FLAG_RIJNDAEL = UnsignedInt(2)
@@ -97,7 +97,7 @@ class DatabaseHeaderKDB : DatabaseHeader() {
const val BUF_SIZE = 124 const val BUF_SIZE = 124
fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean { fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean {
return sig1.toKotlinInt() == PWM_DBSIG_1.toKotlinInt() && sig2.toKotlinInt() == DBSIG_2.toKotlinInt() return sig1.toKotlinInt() == DBSIG_1.toKotlinInt() && sig2.toKotlinInt() == DBSIG_2.toKotlinInt()
} }
fun compatibleHeaders(one: UnsignedInt, two: UnsignedInt): Boolean { fun compatibleHeaders(one: UnsignedInt, two: UnsignedInt): Boolean {

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.database.file package com.kunzisoft.keepass.database.file
import com.kunzisoft.encrypt.HashManager import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
import com.kunzisoft.keepass.database.crypto.VariantDictionary import com.kunzisoft.keepass.database.crypto.VariantDictionary
import com.kunzisoft.keepass.database.crypto.kdf.AesKdf import com.kunzisoft.keepass.database.crypto.kdf.AesKdf
@@ -28,9 +27,6 @@ import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.exception.VersionDatabaseException import com.kunzisoft.keepass.database.exception.VersionDatabaseException
import com.kunzisoft.keepass.stream.CopyInputStream import com.kunzisoft.keepass.stream.CopyInputStream
import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.utils.*
@@ -87,71 +83,10 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
inner class HeaderAndHash(var header: ByteArray, var hash: ByteArray) inner class HeaderAndHash(var header: ByteArray, var hash: ByteArray)
init { init {
this.version = getMinKdbxVersion(databaseV4) // Only for writing this.version = databaseV4.getMinKdbxVersion()
this.masterSeed = ByteArray(32) this.masterSeed = ByteArray(32)
} }
private open class NodeOperationHandler<T: NodeKDBXInterface> : NodeHandler<T>() {
var containsCustomData = false
override fun operate(node: T): Boolean {
if (node.customData.isNotEmpty()) {
containsCustomData = true
}
return true
}
}
private inner class EntryOperationHandler: NodeOperationHandler<EntryKDBX>() {
var passwordQualityEstimationDisabled = false
override fun operate(node: EntryKDBX): Boolean {
if (!node.qualityCheck) {
passwordQualityEstimationDisabled = true
}
return super.operate(node)
}
}
private inner class GroupOperationHandler: NodeOperationHandler<GroupKDBX>() {
var containsTags = false
override fun operate(node: GroupKDBX): Boolean {
if (!node.tags.isEmpty())
containsTags = true
return super.operate(node)
}
}
private fun getMinKdbxVersion(databaseKDBX: DatabaseKDBX): UnsignedInt {
val entryHandler = EntryOperationHandler()
val groupHandler = GroupOperationHandler()
databaseKDBX.rootGroup?.doForEachChildAndForIt(entryHandler, groupHandler)
// https://keepass.info/help/kb/kdbx_4.1.html
val containsGroupWithTag = groupHandler.containsTags
val containsEntryWithPasswordQualityEstimationDisabled = entryHandler.passwordQualityEstimationDisabled
val containsCustomIconWithNameOrLastModificationTime = databaseKDBX.iconsManager.containsCustomIconWithNameOrLastModificationTime()
val containsHeaderCustomDataWithLastModificationTime = databaseKDBX.customData.containsItemWithLastModificationTime()
// https://keepass.info/help/kb/kdbx_4.html
// If AES is not use, it's at least 4.0
val kdfIsNotAes = databaseKDBX.kdfParameters?.uuid != AesKdf.CIPHER_UUID
val containsHeaderCustomData = databaseKDBX.customData.isNotEmpty()
val containsNodeCustomData = entryHandler.containsCustomData || groupHandler.containsCustomData
// Check each condition to determine version
return if (containsGroupWithTag
|| containsEntryWithPasswordQualityEstimationDisabled
|| containsCustomIconWithNameOrLastModificationTime
|| containsHeaderCustomDataWithLastModificationTime) {
FILE_VERSION_41
} else if (kdfIsNotAes
|| containsHeaderCustomData
|| containsNodeCustomData) {
FILE_VERSION_40
} else {
FILE_VERSION_31
}
}
/** Assumes the input stream is at the beginning of the .kdbx file /** Assumes the input stream is at the beginning of the .kdbx file
* @param inputStream * @param inputStream
* @throws IOException * @throws IOException
@@ -256,8 +191,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
if (pbId == null || pbId.size != 16) { if (pbId == null || pbId.size != 16) {
throw IOException("Invalid cipher ID.") throw IOException("Invalid cipher ID.")
} }
databaseV4.setEncryptionAlgorithmFromUUID(bytes16ToUuid(pbId))
databaseV4.cipherUuid = bytes16ToUuid(pbId)
} }
private fun setTransformRound(roundsByte: ByteArray) { private fun setTransformRound(roundsByte: ByteArray) {
@@ -311,8 +245,9 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
companion object { companion object {
val DBSIG_PRE2 = UnsignedInt(-0x4ab4049a) val DBSIG_1 = UnsignedInt(-0x655d26fd) // 0x9AA2D903
val DBSIG_2 = UnsignedInt(-0x4ab40499) val DBSIG_PRE2 = UnsignedInt(-0x4ab4049a) // 0xB54BFB66
val DBSIG_2 = UnsignedInt(-0x4ab40499) // 0xB54BFB67
private val FILE_VERSION_CRITICAL_MASK = UnsignedInt(-0x10000) private val FILE_VERSION_CRITICAL_MASK = UnsignedInt(-0x10000)
val FILE_VERSION_31 = UnsignedInt(0x00030001) val FILE_VERSION_31 = UnsignedInt(0x00030001)
@@ -335,7 +270,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
} }
fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean { fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean {
return sig1 == PWM_DBSIG_1 && (sig2 == DBSIG_PRE2 || sig2 == DBSIG_2) return sig1 == DBSIG_1 && (sig2 == DBSIG_PRE2 || sig2 == DBSIG_2)
} }
} }
} }

View File

@@ -21,16 +21,12 @@ package com.kunzisoft.keepass.database.file.input
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import java.io.File
import java.io.InputStream import java.io.InputStream
abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>> abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>> (protected var mDatabase: D) {
(protected val cacheDirectory: File,
protected val isRAMSufficient: (memoryWanted: Long) -> Boolean) {
private var startTimeKey = System.currentTimeMillis() private var startTimeKey = System.currentTimeMillis()
private var startTimeContent = System.currentTimeMillis() private var startTimeContent = System.currentTimeMillis()
@@ -47,19 +43,8 @@ abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>>
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream, abstract fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyfileInputStream: InputStream?,
loadedCipherKey: LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): D assignMasterKey: (() -> Unit)): D
@Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
loadedCipherKey: LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): D
protected fun startKeyTimer(progressTaskUpdater: ProgressTaskUpdater?) { protected fun startKeyTimer(progressTaskUpdater: ProgressTaskUpdater?) {
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)

View File

@@ -20,17 +20,16 @@
package com.kunzisoft.keepass.database.file.input package com.kunzisoft.keepass.database.file.input
import android.graphics.Color
import com.kunzisoft.encrypt.HashManager import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.element.database.DatabaseKDB import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.exception.* import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeader
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.utils.*
@@ -40,48 +39,18 @@ import java.security.MessageDigest
import java.util.* import java.util.*
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.CipherInputStream import javax.crypto.CipherInputStream
import kotlin.collections.HashMap
/** /**
* Load a KDB database file. * Load a KDB database file.
*/ */
class DatabaseInputKDB(cacheDirectory: File, class DatabaseInputKDB(database: DatabaseKDB)
isRAMSufficient: (memoryWanted: Long) -> Boolean) : DatabaseInput<DatabaseKDB>(database) {
: DatabaseInput<DatabaseKDB>(cacheDirectory, isRAMSufficient) {
private lateinit var mDatabase: DatabaseKDB
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyfileInputStream: InputStream?,
loadedCipherKey: LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB { assignMasterKey: (() -> Unit)): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
mDatabase.retrieveMasterKey(password, keyfileInputStream)
}
}
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
loadedCipherKey: LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
mDatabase.masterKey = masterKey
}
}
@Throws(LoadDatabaseException::class)
private fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean,
assignMasterKey: (() -> Unit)? = null): DatabaseKDB {
try { try {
startKeyTimer(progressTaskUpdater) startKeyTimer(progressTaskUpdater)
@@ -98,7 +67,7 @@ class DatabaseInputKDB(cacheDirectory: File,
if (fileSize != (contentSize + DatabaseHeaderKDB.BUF_SIZE)) if (fileSize != (contentSize + DatabaseHeaderKDB.BUF_SIZE))
throw IOException("Header corrupted") throw IOException("Header corrupted")
if (header.signature1 != DatabaseHeader.PWM_DBSIG_1 if (header.signature1 != DatabaseHeaderKDB.DBSIG_1
|| header.signature2 != DatabaseHeaderKDB.DBSIG_2) { || header.signature2 != DatabaseHeaderKDB.DBSIG_2) {
throw SignatureDatabaseException() throw SignatureDatabaseException()
} }
@@ -107,11 +76,7 @@ class DatabaseInputKDB(cacheDirectory: File,
throw VersionDatabaseException() throw VersionDatabaseException()
} }
mDatabase = DatabaseKDB() assignMasterKey.invoke()
mDatabase.binaryCache.cacheDirectory = cacheDirectory
mDatabase.changeDuplicateId = fixDuplicateUUID
assignMasterKey?.invoke()
// Select algorithm // Select algorithm
when { when {
@@ -153,10 +118,6 @@ class DatabaseInputKDB(cacheDirectory: File,
) )
) )
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
val newRoot = mDatabase.createGroup()
mDatabase.rootGroup = newRoot
// Import all nodes // Import all nodes
val groupLevelList = HashMap<GroupKDB, Int>() val groupLevelList = HashMap<GroupKDB, Int>()
var newGroup: GroupKDB? = null var newGroup: GroupKDB? = null
@@ -285,7 +246,7 @@ class DatabaseInputKDB(cacheDirectory: File,
0x000E -> { 0x000E -> {
newEntry?.let { entry -> newEntry?.let { entry ->
if (fieldSize > 0) { if (fieldSize > 0) {
val binaryData = mDatabase.buildNewAttachment() val binaryData = mDatabase.buildNewBinaryAttachment()
entry.putBinary(binaryData, mDatabase.attachmentPool) entry.putBinary(binaryData, mDatabase.attachmentPool)
BufferedOutputStream(binaryData.getOutputDataStream(mDatabase.binaryCache)).use { outputStream -> BufferedOutputStream(binaryData.getOutputDataStream(mDatabase.binaryCache)).use { outputStream ->
cipherInputStream.readBytes(fieldSize) { buffer -> cipherInputStream.readBytes(fieldSize) { buffer ->
@@ -303,7 +264,34 @@ class DatabaseInputKDB(cacheDirectory: File,
newGroup = null newGroup = null
} }
newEntry?.let { entry -> newEntry?.let { entry ->
mDatabase.addEntryIndex(entry) // Parse meta info
when {
entry.isMetaStreamDefaultUsername() -> {
var defaultUser = ""
entry.getBinary(mDatabase.attachmentPool)
?.getInputDataStream(mDatabase.binaryCache)?.use {
defaultUser = String(it.readBytes())
}
mDatabase.defaultUserName = defaultUser
}
entry.isMetaStreamDatabaseColor() -> {
var color: Int? = null
entry.getBinary(mDatabase.attachmentPool)
?.getInputDataStream(mDatabase.binaryCache)?.use {
val reverseColor = UnsignedInt(it.readBytes4ToUInt()).toKotlinInt()
color = Color.rgb(
Color.blue(reverseColor),
Color.green(reverseColor),
Color.red(reverseColor)
)
}
mDatabase.color = color
}
// TODO manager other meta stream
else -> {
mDatabase.addEntryIndex(entry)
}
}
currentEntryNumber++ currentEntryNumber++
newEntry = null newEntry = null
} }
@@ -323,16 +311,16 @@ class DatabaseInputKDB(cacheDirectory: File,
stopContentTimer() stopContentTimer()
} catch (e: LoadDatabaseException) { } catch (e: LoadDatabaseException) {
mDatabase.clearCache() mDatabase.clearAll()
throw e throw e
} catch (e: IOException) { } catch (e: IOException) {
mDatabase.clearCache() mDatabase.clearAll()
throw IODatabaseException(e) throw IODatabaseException(e)
} catch (e: OutOfMemoryError) { } catch (e: OutOfMemoryError) {
mDatabase.clearCache() mDatabase.clearAll()
throw NoMemoryDatabaseException(e) throw NoMemoryDatabaseException(e)
} catch (e: Exception) { } catch (e: Exception) {
mDatabase.clearCache() mDatabase.clearAll()
throw LoadDatabaseException(e) throw LoadDatabaseException(e)
} }

View File

@@ -24,17 +24,16 @@ import android.util.Log
import com.kunzisoft.encrypt.StreamCipher import com.kunzisoft.encrypt.StreamCipher
import com.kunzisoft.keepass.database.crypto.CipherEngine import com.kunzisoft.keepass.database.crypto.CipherEngine
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.HmacBlock import com.kunzisoft.keepass.database.crypto.HmacBlock
import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
@@ -50,7 +49,6 @@ import com.kunzisoft.keepass.utils.*
import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory import org.xmlpull.v1.XmlPullParserFactory
import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.UnsupportedEncodingException import java.io.UnsupportedEncodingException
@@ -63,12 +61,10 @@ import javax.crypto.CipherInputStream
import javax.crypto.Mac import javax.crypto.Mac
import kotlin.math.min import kotlin.math.min
class DatabaseInputKDBX(cacheDirectory: File, class DatabaseInputKDBX(database: DatabaseKDBX)
isRAMSufficient: (memoryWanted: Long) -> Boolean) : DatabaseInput<DatabaseKDBX>(database) {
: DatabaseInput<DatabaseKDBX>(cacheDirectory, isRAMSufficient) {
private var randomStream: StreamCipher? = null private var randomStream: StreamCipher? = null
private lateinit var mDatabase: DatabaseKDBX
private var hashOfHeader: ByteArray? = null private var hashOfHeader: ByteArray? = null
@@ -97,42 +93,18 @@ class DatabaseInputKDBX(cacheDirectory: File,
private var entryCustomDataKey: String? = null private var entryCustomDataKey: String? = null
private var entryCustomDataValue: String? = null private var entryCustomDataValue: String? = null
@Throws(LoadDatabaseException::class) private var isRAMSufficient: (memoryWanted: Long) -> Boolean = {true}
override fun openDatabase(databaseInputStream: InputStream,
password: String?, fun setMethodToCheckIfRAMIsSufficient(method: (memoryWanted: Long) -> Boolean) {
keyfileInputStream: InputStream?, this.isRAMSufficient = method
loadedCipherKey: LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
mDatabase.retrieveMasterKey(password, keyfileInputStream)
}
} }
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
loadedCipherKey: LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDBX { assignMasterKey: (() -> Unit)): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
mDatabase.masterKey = masterKey
}
}
@Throws(LoadDatabaseException::class)
private fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean,
assignMasterKey: (() -> Unit)? = null): DatabaseKDBX {
try { try {
startKeyTimer(progressTaskUpdater) startKeyTimer(progressTaskUpdater)
mDatabase = DatabaseKDBX()
mDatabase.binaryCache.cacheDirectory = cacheDirectory
mDatabase.changeDuplicateId = fixDuplicateUUID
val header = DatabaseHeaderKDBX(mDatabase) val header = DatabaseHeaderKDBX(mDatabase)
@@ -142,19 +114,16 @@ class DatabaseInputKDBX(cacheDirectory: File,
hashOfHeader = headerAndHash.hash hashOfHeader = headerAndHash.hash
val pbHeader = headerAndHash.header val pbHeader = headerAndHash.header
assignMasterKey?.invoke() assignMasterKey.invoke()
mDatabase.makeFinalKey(header.masterSeed) mDatabase.makeFinalKey(header.masterSeed)
stopKeyTimer() stopKeyTimer()
startContentTimer(progressTaskUpdater) startContentTimer(progressTaskUpdater)
val engine: CipherEngine
val cipher: Cipher val cipher: Cipher
try { try {
engine = EncryptionAlgorithm.getFrom(mDatabase.cipherUuid).cipherEngine val engine: CipherEngine = mDatabase.encryptionAlgorithm.cipherEngine
engine.forcePaddingCompatibility = true engine.forcePaddingCompatibility = true
mDatabase.setDataEngine(engine)
mDatabase.encryptionAlgorithm = engine.getEncryptionAlgorithm()
cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV) cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV)
engine.forcePaddingCompatibility = false engine.forcePaddingCompatibility = false
} catch (e: Exception) { } catch (e: Exception) {
@@ -288,7 +257,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
val byteLength = size - 1 val byteLength = size - 1
// No compression at this level // No compression at this level
val protectedBinary = mDatabase.buildNewAttachment( val protectedBinary = mDatabase.buildNewBinaryAttachment(
isRAMSufficient.invoke(byteLength.toLong()), false, protectedFlag) isRAMSufficient.invoke(byteLength.toLong()), false, protectedFlag)
protectedBinary.getOutputDataStream(mDatabase.binaryCache).use { outputStream -> protectedBinary.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
dataInputStream.readBytes(byteLength) { buffer -> dataInputStream.readBytes(byteLength) { buffer ->
@@ -524,7 +493,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
ctxGroup?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt()) ctxGroup?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
ctxGroup?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp)) val iconUUID = readUuid(xpp)
ctxGroup?.icon?.custom = mDatabase.getCustomIcon(iconUUID) ?: IconImageCustom(iconUUID)
} else if (name.equals(DatabaseKDBXXML.ElemTags, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemTags, ignoreCase = true)) {
ctxGroup?.tags = readTags(xpp) ctxGroup?.tags = readTags(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemPreviousParentGroup, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemPreviousParentGroup, ignoreCase = true)) {
@@ -583,7 +553,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
ctxEntry?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt()) ctxEntry?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
ctxEntry?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp)) val iconUUID = readUuid(xpp)
ctxEntry?.icon?.custom = mDatabase.getCustomIcon(iconUUID) ?: IconImageCustom(iconUUID)
} else if (name.equals(DatabaseKDBXXML.ElemFgColor, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemFgColor, ignoreCase = true)) {
ctxEntry?.foregroundColor = readString(xpp) ctxEntry?.foregroundColor = readString(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemBgColor, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemBgColor, ignoreCase = true)) {
@@ -704,7 +675,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
KdbContext.DeletedObject -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) { KdbContext.DeletedObject -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
ctxDeletedObject?.uuid = readUuid(xpp) ctxDeletedObject?.uuid = readUuid(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) {
ctxDeletedObject?.setDeletionTime(readDateInstant(xpp)) ctxDeletedObject?.deletionTime = readDateInstant(xpp)
} else { } else {
readUnknown(xpp) readUnknown(xpp)
} }
@@ -885,7 +856,9 @@ class DatabaseInputKDBX(cacheDirectory: File,
@Throws(IOException::class, XmlPullParserException::class) @Throws(IOException::class, XmlPullParserException::class)
private fun readTags(xpp: XmlPullParser): Tags { private fun readTags(xpp: XmlPullParser): Tags {
return Tags(readString(xpp)) val tags = Tags(readString(xpp))
mDatabase.tagPool.put(tags)
return tags
} }
@Throws(XmlPullParserException::class, IOException::class) @Throws(XmlPullParserException::class, IOException::class)
@@ -1009,7 +982,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
var binaryRetrieve = mDatabase.attachmentPool[id] var binaryRetrieve = mDatabase.attachmentPool[id]
// Create empty binary if not retrieved in pool // Create empty binary if not retrieved in pool
if (binaryRetrieve == null) { if (binaryRetrieve == null) {
binaryRetrieve = mDatabase.buildNewAttachment( binaryRetrieve = mDatabase.buildNewBinaryAttachment(
smallSize = false, smallSize = false,
compression = false, compression = false,
protection = false, protection = false,
@@ -1049,7 +1022,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
return null return null
// Build the new binary and compress // Build the new binary and compress
val binaryAttachment = mDatabase.buildNewAttachment( val binaryAttachment = mDatabase.buildNewBinaryAttachment(
isRAMSufficient.invoke(base64.length.toLong()), compressed, protected, binaryId) isRAMSufficient.invoke(base64.length.toLong()), compressed, protected, binaryId)
try { try {
binaryAttachment.getOutputDataStream(mDatabase.binaryCache).use { outputStream -> binaryAttachment.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->

View File

@@ -25,7 +25,6 @@ import com.kunzisoft.keepass.database.crypto.VariantDictionary
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.file.DatabaseHeader
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.stream.MacOutputStream import com.kunzisoft.keepass.stream.MacOutputStream
@@ -68,11 +67,11 @@ constructor(private val databaseKDBX: DatabaseKDBX,
@Throws(IOException::class) @Throws(IOException::class)
fun output() { fun output() {
mos.write4BytesUInt(DatabaseHeader.PWM_DBSIG_1) mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_1)
mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_2) mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_2)
mos.write4BytesUInt(header.version) mos.write4BytesUInt(header.version)
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CipherID, uuidTo16Bytes(databaseKDBX.cipherUuid)) writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CipherID, uuidTo16Bytes(databaseKDBX.encryptionAlgorithm.uuid))
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CompressionFlags, uIntTo4Bytes(DatabaseHeaderKDBX.getFlagFromCompression(databaseKDBX.compressionAlgorithm))) writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CompressionFlags, uIntTo4Bytes(DatabaseHeaderKDBX.getFlagFromCompression(databaseKDBX.compressionAlgorithm)))
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.MasterSeed, header.masterSeed) writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.MasterSeed, header.masterSeed)

View File

@@ -19,9 +19,11 @@
*/ */
package com.kunzisoft.keepass.database.file.output package com.kunzisoft.keepass.database.file.output
import android.graphics.Color
import com.kunzisoft.encrypt.HashManager import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDB import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.file.DatabaseHeader import com.kunzisoft.keepass.database.file.DatabaseHeader
@@ -34,7 +36,6 @@ import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
import java.security.* import java.security.*
import java.util.*
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.CipherOutputStream import javax.crypto.CipherOutputStream
@@ -44,6 +45,9 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
private var headerHashBlock: ByteArray? = null private var headerHashBlock: ByteArray? = null
private var mGroupList = mutableListOf<GroupKDB>()
private var mEntryList = mutableListOf<EntryKDB>()
@Throws(DatabaseOutputException::class) @Throws(DatabaseOutputException::class)
fun getFinalKey(header: DatabaseHeader): ByteArray? { fun getFinalKey(header: DatabaseHeader): ByteArray? {
try { try {
@@ -61,7 +65,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
// and remove any orphaned nodes that are no longer part of the tree hierarchy // and remove any orphaned nodes that are no longer part of the tree hierarchy
// also remove the virtual root not present in kdb // also remove the virtual root not present in kdb
val rootGroup = mDatabaseKDB.rootGroup val rootGroup = mDatabaseKDB.rootGroup
sortGroupsForOutput() sortNodesForOutput()
val header = outputHeader(mOutputStream) val header = outputHeader(mOutputStream)
@@ -91,6 +95,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
} finally { } finally {
// Add again the virtual root group for better management // Add again the virtual root group for better management
mDatabaseKDB.rootGroup = rootGroup mDatabaseKDB.rootGroup = rootGroup
clearParser()
} }
} }
@@ -105,7 +110,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDB { override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDB {
// Build header // Build header
val header = DatabaseHeaderKDB() val header = DatabaseHeaderKDB()
header.signature1 = DatabaseHeader.PWM_DBSIG_1 header.signature1 = DatabaseHeaderKDB.DBSIG_1
header.signature2 = DatabaseHeaderKDB.DBSIG_2 header.signature2 = DatabaseHeaderKDB.DBSIG_2
header.flags = DatabaseHeaderKDB.FLAG_SHA2 header.flags = DatabaseHeaderKDB.FLAG_SHA2
@@ -120,8 +125,9 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
} }
header.version = DatabaseHeaderKDB.DBVER_DW header.version = DatabaseHeaderKDB.DBVER_DW
header.numGroups = UnsignedInt(mDatabaseKDB.numberOfGroups()) // To remove root
header.numEntries = UnsignedInt(mDatabaseKDB.numberOfEntries()) header.numGroups = UnsignedInt(mGroupList.size)
header.numEntries = UnsignedInt(mEntryList.size)
header.numKeyEncRounds = UnsignedInt.fromKotlinLong(mDatabaseKDB.numberKeyEncryptionRounds) header.numKeyEncRounds = UnsignedInt.fromKotlinLong(mDatabaseKDB.numberKeyEncryptionRounds)
setIVs(header) setIVs(header)
@@ -194,31 +200,89 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
} }
// Groups // Groups
mDatabaseKDB.doForEachGroupInIndex { group -> mGroupList.forEach { group ->
GroupOutputKDB(group, outputStream).output() if (group != mDatabaseKDB.rootGroup) {
GroupOutputKDB(group, outputStream).output()
}
} }
// Entries // Entries
mDatabaseKDB.doForEachEntryInIndex { entry -> mEntryList.forEach { entry ->
EntryOutputKDB(mDatabaseKDB, entry, outputStream).output() EntryOutputKDB(mDatabaseKDB, entry, outputStream).output()
} }
} }
private fun sortGroupsForOutput() { private fun clearParser() {
val groupList = ArrayList<GroupKDB>() mGroupList.clear()
// Rebuild list according to sorting order removing any orphaned groups mEntryList.clear()
for (rootGroup in mDatabaseKDB.rootGroups) {
sortGroup(rootGroup, groupList)
}
mDatabaseKDB.setGroupIndexes(groupList)
} }
private fun sortGroup(group: GroupKDB, groupList: MutableList<GroupKDB>) { private fun sortNodesForOutput() {
clearParser()
// Rebuild list according to sorting order removing any orphaned groups
// Do not keep root
mDatabaseKDB.rootGroup?.getChildGroups()?.let { rootSubGroups ->
for (rootGroup in rootSubGroups) {
sortGroup(rootGroup)
}
}
}
private fun sortGroup(group: GroupKDB) {
// Add current tree // Add current tree
groupList.add(group) mGroupList.add(group)
for (childEntry in group.getChildEntries()) {
if (!childEntry.isMetaStreamDefaultUsername()
&& !childEntry.isMetaStreamDatabaseColor()) {
mEntryList.add(childEntry)
}
}
// Add MetaStream
if (mDatabaseKDB.defaultUserName.isNotEmpty()) {
val metaEntry = EntryKDB().apply {
setMetaStreamDefaultUsername()
setDefaultUsername(this)
}
mDatabaseKDB.addEntryTo(metaEntry, group)
mEntryList.add(metaEntry)
}
if (mDatabaseKDB.color != null) {
val metaEntry = EntryKDB().apply {
setMetaStreamDatabaseColor()
setDatabaseColor(this)
}
mDatabaseKDB.addEntryTo(metaEntry, group)
mEntryList.add(metaEntry)
}
// Recurse over children // Recurse over children
for (childGroup in group.getChildGroups()) { for (childGroup in group.getChildGroups()) {
sortGroup(childGroup, groupList) sortGroup(childGroup)
}
}
private fun setDefaultUsername(entryKDB: EntryKDB) {
val binaryData = mDatabaseKDB.buildNewBinaryAttachment()
entryKDB.putBinary(binaryData, mDatabaseKDB.attachmentPool)
BufferedOutputStream(binaryData.getOutputDataStream(mDatabaseKDB.binaryCache)).use { outputStream ->
outputStream.write(mDatabaseKDB.defaultUserName.toByteArray())
}
}
private fun setDatabaseColor(entryKDB: EntryKDB) {
val binaryData = mDatabaseKDB.buildNewBinaryAttachment()
entryKDB.putBinary(binaryData, mDatabaseKDB.attachmentPool)
BufferedOutputStream(binaryData.getOutputDataStream(mDatabaseKDB.binaryCache)).use { outputStream ->
var reversColor = Color.BLACK
mDatabaseKDB.color?.let {
reversColor = Color.rgb(
Color.blue(it),
Color.green(it),
Color.red(it)
)
}
outputStream.write4BytesUInt(UnsignedInt(reversColor))
} }
} }

View File

@@ -24,9 +24,7 @@ import android.util.Log
import android.util.Xml import android.util.Xml
import com.kunzisoft.encrypt.StreamCipher import com.kunzisoft.encrypt.StreamCipher
import com.kunzisoft.keepass.database.action.node.NodeHandler import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.crypto.CipherEngine
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
@@ -39,7 +37,6 @@ import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.exception.UnknownKDF
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
@@ -48,11 +45,9 @@ import com.kunzisoft.keepass.database.file.DateKDBXUtil
import com.kunzisoft.keepass.stream.HashedBlockOutputStream import com.kunzisoft.keepass.stream.HashedBlockOutputStream
import com.kunzisoft.keepass.stream.HmacBlockOutputStream import com.kunzisoft.keepass.stream.HmacBlockOutputStream
import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.utils.*
import org.joda.time.DateTime
import org.xmlpull.v1.XmlSerializer import org.xmlpull.v1.XmlSerializer
import java.io.IOException import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom import java.security.SecureRandom
import java.util.* import java.util.*
import java.util.zip.GZIPOutputStream import java.util.zip.GZIPOutputStream
@@ -70,18 +65,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
private var header: DatabaseHeaderKDBX? = null private var header: DatabaseHeaderKDBX? = null
private var hashOfHeader: ByteArray? = null private var hashOfHeader: ByteArray? = null
private var headerHmac: ByteArray? = null private var headerHmac: ByteArray? = null
private var engine: CipherEngine? = null
@Throws(DatabaseOutputException::class) @Throws(DatabaseOutputException::class)
override fun output() { override fun output() {
try { try {
try {
engine = EncryptionAlgorithm.getFrom(mDatabaseKDBX.cipherUuid).cipherEngine
} catch (e: NoSuchAlgorithmException) {
throw DatabaseOutputException("No such cipher", e)
}
header = outputHeader(mOutputStream) header = outputHeader(mOutputStream)
val osPlain: OutputStream = if (header!!.version.isBefore(FILE_VERSION_40)) { val osPlain: OutputStream = if (header!!.version.isBefore(FILE_VERSION_40)) {
@@ -241,6 +229,9 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
writeString(DatabaseKDBXXML.ElemHeaderHash, String(Base64.encode(hashOfHeader!!, BASE_64_FLAG))) writeString(DatabaseKDBXXML.ElemHeaderHash, String(Base64.encode(hashOfHeader!!, BASE_64_FLAG)))
} }
if (!header!!.version.isBefore(FILE_VERSION_40)) {
writeDateInstant(DatabaseKDBXXML.ElemSettingsChanged, mDatabaseKDBX.settingsChanged)
}
writeString(DatabaseKDBXXML.ElemDbName, mDatabaseKDBX.name, true) writeString(DatabaseKDBXXML.ElemDbName, mDatabaseKDBX.name, true)
writeDateInstant(DatabaseKDBXXML.ElemDbNameChanged, mDatabaseKDBX.nameChanged) writeDateInstant(DatabaseKDBXXML.ElemDbNameChanged, mDatabaseKDBX.nameChanged)
writeString(DatabaseKDBXXML.ElemDbDesc, mDatabaseKDBX.description, true) writeString(DatabaseKDBXXML.ElemDbDesc, mDatabaseKDBX.description, true)
@@ -280,7 +271,10 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
private fun attachStreamEncryptor(header: DatabaseHeaderKDBX, os: OutputStream): CipherOutputStream { private fun attachStreamEncryptor(header: DatabaseHeaderKDBX, os: OutputStream): CipherOutputStream {
val cipher: Cipher val cipher: Cipher
try { try {
cipher = engine!!.getCipher(Cipher.ENCRYPT_MODE, mDatabaseKDBX.finalKey!!, header.encryptionIV) cipher = mDatabaseKDBX
.encryptionAlgorithm
.cipherEngine
.getCipher(Cipher.ENCRYPT_MODE, mDatabaseKDBX.finalKey!!, header.encryptionIV)
} catch (e: Exception) { } catch (e: Exception) {
throw DatabaseOutputException("Invalid algorithm.", e) throw DatabaseOutputException("Invalid algorithm.", e)
} }
@@ -293,7 +287,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
val random = super.setIVs(header) val random = super.setIVs(header)
random.nextBytes(header.masterSeed) random.nextBytes(header.masterSeed)
val ivLength = engine!!.ivLength() val ivLength = mDatabaseKDBX.encryptionAlgorithm.cipherEngine.ivLength()
if (ivLength != header.encryptionIV.size) { if (ivLength != header.encryptionIV.size) {
header.encryptionIV = ByteArray(ivLength) header.encryptionIV = ByteArray(ivLength)
} }
@@ -303,12 +297,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
mDatabaseKDBX.kdfParameters = KdfFactory.aesKdf.defaultParameters mDatabaseKDBX.kdfParameters = KdfFactory.aesKdf.defaultParameters
} }
try { mDatabaseKDBX.randomizeKdfParameters()
val kdf = mDatabaseKDBX.getEngineKDBX4(mDatabaseKDBX.kdfParameters)
kdf.randomize(mDatabaseKDBX.kdfParameters!!)
} catch (unknownKDF: UnknownKDF) {
Log.e(TAG, "Unable to retrieve header", unknownKDF)
}
if (header.version.isBefore(FILE_VERSION_40)) { if (header.version.isBefore(FILE_VERSION_40)) {
header.innerRandomStream = CrsAlgorithm.Salsa20 header.innerRandomStream = CrsAlgorithm.Salsa20
@@ -592,7 +581,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject) xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject)
writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid) writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid)
writeDateInstant(DatabaseKDBXXML.ElemDeletionTime, value.getDeletionTime()) writeDateInstant(DatabaseKDBXXML.ElemDeletionTime, value.deletionTime)
xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject) xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject)
} }
@@ -618,7 +607,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
} }
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeDeletedObjects(value: List<DeletedObject>) { private fun writeDeletedObjects(value: Collection<DeletedObject>) {
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObjects) xml.startTag(null, DatabaseKDBXXML.ElemDeletedObjects)
for (pdo in value) { for (pdo in value) {
@@ -674,7 +663,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeTags(tags: Tags) { private fun writeTags(tags: Tags) {
if (!tags.isEmpty()) { if (tags.isNotEmpty()) {
writeString(DatabaseKDBXXML.ElemTags, tags.toString()) writeString(DatabaseKDBXXML.ElemTags, tags.toString())
} }
} }

View File

@@ -0,0 +1,504 @@
/*
* Copyright 2022 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.merge
import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.CustomData
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.utils.readAllBytes
import java.io.IOException
import java.util.*
class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
var isRAMSufficient: (memoryWanted: Long) -> Boolean = {true}
/**
* Merge a KDB database in a KDBX database, by default all data are copied from the KDB
*/
fun merge(databaseToMerge: DatabaseKDB) {
val rootGroup = database.rootGroup
val rootGroupId = rootGroup?.nodeId
val rootGroupToMerge = databaseToMerge.rootGroup
val rootGroupIdToMerge = rootGroupToMerge?.nodeId
if (rootGroupId == null || rootGroupIdToMerge == null) {
throw IOException("Database is not open")
}
// Replace the UUID of the KDB root group to init seed
databaseToMerge.removeGroupIndex(rootGroupToMerge)
rootGroupToMerge.nodeId = NodeIdInt(0)
databaseToMerge.addGroupIndex(rootGroupToMerge)
// Merge children
rootGroupToMerge.doForEachChild(
object : NodeHandler<EntryKDB>() {
override fun operate(node: EntryKDB): Boolean {
mergeEntry(rootGroup.nodeId, node, databaseToMerge)
return true
}
},
object : NodeHandler<GroupKDB>() {
override fun operate(node: GroupKDB): Boolean {
mergeGroup(rootGroup.nodeId, node, databaseToMerge)
return true
}
}
)
}
/**
* Utility method to transform KDB id nodes in KDBX id nodes
*/
private fun getNodeIdUUIDFrom(seed: NodeId<UUID>, intId: NodeId<Int>): NodeId<UUID> {
val seedUUID = seed.id
val idInt = intId.id
return NodeIdUUID(UUID(seedUUID.mostSignificantBits, seedUUID.leastSignificantBits + idInt))
}
/**
* Utility method to merge a KDB entry
*/
private fun mergeEntry(seed: NodeId<UUID>, nodeToMerge: EntryKDB, databaseToMerge: DatabaseKDB) {
val entryId: NodeId<UUID> = nodeToMerge.nodeId
val entry = database.getEntryById(entryId)
databaseToMerge.getEntryById(entryId)?.let { srcEntryToMerge ->
// Do not merge meta stream elements
if (!srcEntryToMerge.isMetaStream()) {
// Retrieve parent in current database
var parentEntryToMerge: GroupKDBX? = null
srcEntryToMerge.parent?.nodeId?.let {
val parentGroupIdToMerge = getNodeIdUUIDFrom(seed, it)
parentEntryToMerge = database.getGroupById(parentGroupIdToMerge)
}
// Copy attachment
var newAttachment: Attachment? = null
srcEntryToMerge.getAttachment(databaseToMerge.attachmentPool)?.let { attachment ->
val binarySize = attachment.binaryData.getSize()
val binaryData = database.buildNewBinaryAttachment(
isRAMSufficient.invoke(binarySize),
attachment.binaryData.isCompressed,
attachment.binaryData.isProtected
)
attachment.binaryData.getInputDataStream(databaseToMerge.binaryCache)
.use { inputStream ->
binaryData.getOutputDataStream(database.binaryCache)
.use { outputStream ->
inputStream.readAllBytes { buffer ->
outputStream.write(buffer)
}
}
}
newAttachment = Attachment(attachment.name, binaryData)
}
// Create new entry format
val entryToMerge = EntryKDBX().apply {
this.nodeId = srcEntryToMerge.nodeId
this.icon = srcEntryToMerge.icon
this.creationTime = DateInstant(srcEntryToMerge.creationTime)
this.lastModificationTime = DateInstant(srcEntryToMerge.lastModificationTime)
this.lastAccessTime = DateInstant(srcEntryToMerge.lastAccessTime)
this.expiryTime = DateInstant(srcEntryToMerge.expiryTime)
this.expires = srcEntryToMerge.expires
this.title = srcEntryToMerge.title
this.username = srcEntryToMerge.username
this.password = srcEntryToMerge.password
this.url = srcEntryToMerge.url
this.notes = srcEntryToMerge.notes
newAttachment?.let {
this.putAttachment(it, database.attachmentPool)
}
}
if (entry != null) {
entry.updateWith(entryToMerge, false)
} else if (parentEntryToMerge != null) {
database.addEntryTo(entryToMerge, parentEntryToMerge)
}
}
}
}
/**
* Utility method to merge a KDB group
*/
private fun mergeGroup(seed: NodeId<UUID>, nodeToMerge: GroupKDB, databaseToMerge: DatabaseKDB) {
val groupId: NodeId<Int> = nodeToMerge.nodeId
val group = database.getGroupById(getNodeIdUUIDFrom(seed, groupId))
databaseToMerge.getGroupById(groupId)?.let { srcGroupToMerge ->
// Retrieve parent in current database
var parentGroupToMerge: GroupKDBX? = null
srcGroupToMerge.parent?.nodeId?.let {
val parentGroupIdToMerge = getNodeIdUUIDFrom(seed, it)
parentGroupToMerge = database.getGroupById(parentGroupIdToMerge)
}
val groupToMerge = GroupKDBX().apply {
this.nodeId = getNodeIdUUIDFrom(seed, srcGroupToMerge.nodeId)
this.icon = srcGroupToMerge.icon
this.creationTime = DateInstant(srcGroupToMerge.creationTime)
this.lastModificationTime = DateInstant(srcGroupToMerge.lastModificationTime)
this.lastAccessTime = DateInstant(srcGroupToMerge.lastAccessTime)
this.expiryTime = DateInstant(srcGroupToMerge.expiryTime)
this.expires = srcGroupToMerge.expires
this.title = srcGroupToMerge.title
}
if (group != null) {
group.updateWith(groupToMerge, false)
} else if (parentGroupToMerge != null) {
database.addGroupTo(groupToMerge, parentGroupToMerge)
}
}
}
/**
* Merge a KDB> database in a KDBX database,
* Try to take into account the modification date of each element
* To make a merge as accurate as possible
*/
fun merge(databaseToMerge: DatabaseKDBX) {
// Merge settings
if (database.nameChanged.date.before(databaseToMerge.nameChanged.date)) {
database.name = databaseToMerge.name
database.nameChanged = databaseToMerge.nameChanged
}
if (database.descriptionChanged.date.before(databaseToMerge.descriptionChanged.date)) {
database.description = databaseToMerge.description
database.descriptionChanged = databaseToMerge.descriptionChanged
}
if (database.defaultUserNameChanged.date.before(databaseToMerge.defaultUserNameChanged.date)) {
database.defaultUserName = databaseToMerge.defaultUserName
database.defaultUserNameChanged = databaseToMerge.defaultUserNameChanged
}
if (database.keyLastChanged.date.before(databaseToMerge.keyLastChanged.date)) {
database.keyChangeRecDays = databaseToMerge.keyChangeRecDays
database.keyChangeForceDays = databaseToMerge.keyChangeForceDays
database.isKeyChangeForceOnce = databaseToMerge.isKeyChangeForceOnce
database.keyLastChanged = databaseToMerge.keyLastChanged
}
if (database.recycleBinChanged.date.before(databaseToMerge.recycleBinChanged.date)) {
database.isRecycleBinEnabled = databaseToMerge.isRecycleBinEnabled
database.recycleBinUUID = databaseToMerge.recycleBinUUID
database.recycleBinChanged = databaseToMerge.recycleBinChanged
}
if (database.entryTemplatesGroupChanged.date.before(databaseToMerge.entryTemplatesGroupChanged.date)) {
database.entryTemplatesGroup = databaseToMerge.entryTemplatesGroup
database.entryTemplatesGroupChanged = databaseToMerge.entryTemplatesGroupChanged
}
if (database.settingsChanged.date.before(databaseToMerge.settingsChanged.date)) {
database.color = databaseToMerge.color
database.compressionAlgorithm = databaseToMerge.compressionAlgorithm
database.historyMaxItems = databaseToMerge.historyMaxItems
database.historyMaxSize = databaseToMerge.historyMaxSize
database.encryptionAlgorithm = databaseToMerge.encryptionAlgorithm
database.kdfEngine = databaseToMerge.kdfEngine
database.numberKeyEncryptionRounds = databaseToMerge.numberKeyEncryptionRounds
database.memoryUsage = databaseToMerge.memoryUsage
database.parallelism = databaseToMerge.parallelism
database.settingsChanged = databaseToMerge.settingsChanged
}
val rootGroup = database.rootGroup
val rootGroupId = rootGroup?.nodeId
val rootGroupToMerge = databaseToMerge.rootGroup
val rootGroupIdToMerge = rootGroupToMerge?.nodeId
if (rootGroupId == null || rootGroupIdToMerge == null) {
throw IOException("Database is not open")
}
// UUID of the root group to merge is unknown
if (database.getGroupById(rootGroupIdToMerge) == null) {
// Change it to copy children database root
databaseToMerge.removeGroupIndex(rootGroupToMerge)
rootGroupToMerge.nodeId = rootGroupId
databaseToMerge.addGroupIndex(rootGroupToMerge)
}
// Merge root group
if (rootGroup.lastModificationTime.date
.before(rootGroupToMerge.lastModificationTime.date)) {
rootGroup.updateWith(rootGroupToMerge, updateParents = false)
}
// Merge children
rootGroupToMerge.doForEachChild(
object : NodeHandler<EntryKDBX>() {
override fun operate(node: EntryKDBX): Boolean {
mergeEntry(node, databaseToMerge)
return true
}
},
object : NodeHandler<GroupKDBX>() {
override fun operate(node: GroupKDBX): Boolean {
mergeGroup(node, databaseToMerge)
return true
}
}
)
// Merge custom data in database header
mergeCustomData(database.customData, databaseToMerge.customData)
// Merge icons
databaseToMerge.iconsManager.doForEachCustomIcon { iconImageCustom, binaryData ->
val customIconUuid = iconImageCustom.uuid
// If custom icon not present, add it
val customIcon = database.iconsManager.getIcon(customIconUuid)
if (customIcon == null) {
database.addCustomIcon(
customIconUuid,
iconImageCustom.name,
iconImageCustom.lastModificationTime,
false
) { _, newBinaryData ->
binaryData.getInputDataStream(databaseToMerge.binaryCache).use { inputStream ->
newBinaryData?.getOutputDataStream(database.binaryCache).use { outputStream ->
inputStream.readAllBytes { buffer ->
outputStream?.write(buffer)
}
}
}
}
} else {
val customIconModification = customIcon.lastModificationTime
val customIconToMerge = databaseToMerge.iconsManager.getIcon(customIconUuid)
val customIconModificationToMerge = customIconToMerge?.lastModificationTime
if (customIconModification != null && customIconModificationToMerge != null) {
if (customIconModification.date.before(customIconModificationToMerge.date)) {
customIcon.updateWith(customIconToMerge)
}
} else if (customIconModificationToMerge != null) {
customIcon.updateWith(customIconToMerge)
}
}
}
// Manage deleted objects
databaseToMerge.deletedObjects.forEach { deletedObject ->
val deletedObjectId = deletedObject.uuid
val databaseEntry = database.getEntryById(deletedObjectId)
val databaseGroup = database.getGroupById(deletedObjectId)
val databaseIcon = database.iconsManager.getIcon(deletedObjectId)
val databaseIconModificationTime = databaseIcon?.lastModificationTime
if (databaseEntry != null
&& deletedObject.deletionTime.date
.after(databaseEntry.lastModificationTime.date)) {
database.removeEntryFrom(databaseEntry, databaseEntry.parent)
}
if (databaseGroup != null
&& deletedObject.deletionTime.date
.after(databaseGroup.lastModificationTime.date)) {
database.removeGroupFrom(databaseGroup, databaseGroup.parent)
}
if (databaseIcon != null
&& (
databaseIconModificationTime == null
|| (deletedObject.deletionTime.date.after(databaseIconModificationTime.date))
)
) {
database.removeCustomIcon(deletedObjectId)
}
// Attachments are removed and optimized during the database save
}
}
/**
* Merge [customDataToMerge] in [customData]
*/
private fun mergeCustomData(customData: CustomData, customDataToMerge: CustomData) {
customDataToMerge.doForEachItems { customDataItemToMerge ->
val customDataItem = customData.get(customDataItemToMerge.key)
if (customDataItem == null) {
customData.put(customDataItemToMerge)
} else {
val customDataItemModification = customDataItem.lastModificationTime
val customDataItemToMergeModification = customDataItemToMerge.lastModificationTime
if (customDataItemModification != null && customDataItemToMergeModification != null) {
if (customDataItemModification.date
.before(customDataItemToMergeModification.date)) {
customData.put(customDataItemToMerge)
}
} else {
customData.put(customDataItemToMerge)
}
}
}
}
/**
* Utility method to merge a KDBX entry
*/
private fun mergeEntry(nodeToMerge: EntryKDBX, databaseToMerge: DatabaseKDBX) {
val entryId = nodeToMerge.nodeId
val entry = database.getEntryById(entryId)
val deletedObject = database.getDeletedObject(entryId)
databaseToMerge.getEntryById(entryId)?.let { srcEntryToMerge ->
// Retrieve parent in current database
var parentEntryToMerge: GroupKDBX? = null
srcEntryToMerge.parent?.nodeId?.let {
parentEntryToMerge = database.getGroupById(it)
}
val entryToMerge = EntryKDBX().apply {
updateWith(srcEntryToMerge, copyHistory = true, updateParents = false)
}
// Copy attachments in main pool
val newAttachments = mutableListOf<Attachment>()
entryToMerge.getAttachments(databaseToMerge.attachmentPool).forEach { attachment ->
val binarySize = attachment.binaryData.getSize()
val binaryData = database.buildNewBinaryAttachment(
isRAMSufficient.invoke(binarySize),
attachment.binaryData.isCompressed,
attachment.binaryData.isProtected
)
attachment.binaryData.getInputDataStream(databaseToMerge.binaryCache).use { inputStream ->
binaryData.getOutputDataStream(database.binaryCache).use { outputStream ->
inputStream.readAllBytes { buffer ->
outputStream.write(buffer)
}
}
}
newAttachments.add(Attachment(attachment.name, binaryData))
}
entryToMerge.removeAttachments()
newAttachments.forEach { newAttachment ->
entryToMerge.putAttachment(newAttachment, database.attachmentPool)
}
if (entry == null) {
// If it's a deleted object, but another instance was updated
// If entry parent to add exists and in current database
if ((deletedObject == null
|| deletedObject.deletionTime.date
.before(entryToMerge.lastModificationTime.date))
&& parentEntryToMerge != null) {
database.addEntryTo(entryToMerge, parentEntryToMerge)
}
} else {
// Merge independently custom data
mergeCustomData(entry.customData, entryToMerge.customData)
// Merge by modification time
if (entry.lastModificationTime.date
.before(entryToMerge.lastModificationTime.date)
) {
addHistory(entry, entryToMerge)
if (parentEntryToMerge == entry.parent) {
entry.updateWith(entryToMerge, copyHistory = true, updateParents = false)
} else {
// Update entry with databaseEntryToMerge and merge history
database.removeEntryFrom(entry, entry.parent)
if (parentEntryToMerge != null) {
database.addEntryTo(entryToMerge, parentEntryToMerge)
}
}
} else if (entry.lastModificationTime.date
.after(entryToMerge.lastModificationTime.date)
) {
addHistory(entryToMerge, entry)
}
}
}
}
/**
* Utility method to merge an history from an [entryA] to an [entryB],
* [entryB] is modified
*/
private fun addHistory(entryA: EntryKDBX, entryB: EntryKDBX) {
// Keep entry as history if already not present
entryA.history.forEach { history ->
// If history not present
if (!entryB.history.any {
it.lastModificationTime == history.lastModificationTime
}) {
entryB.addEntryToHistory(history)
}
}
// Last entry not present
if (entryB.history.find {
it.lastModificationTime == entryA.lastModificationTime
} == null) {
val history = EntryKDBX().apply {
updateWith(entryA, copyHistory = false, updateParents = false)
parent = null
}
entryB.addEntryToHistory(history)
}
}
/**
* Utility method to merge a KDBX group
*/
private fun mergeGroup(nodeToMerge: GroupKDBX, databaseToMerge: DatabaseKDBX) {
val groupId = nodeToMerge.nodeId
val group = database.getGroupById(groupId)
val deletedObject = database.getDeletedObject(groupId)
databaseToMerge.getGroupById(groupId)?.let { srcGroupToMerge ->
// Retrieve parent in current database
var parentGroupToMerge: GroupKDBX? = null
srcGroupToMerge.parent?.nodeId?.let {
parentGroupToMerge = database.getGroupById(it)
}
val groupToMerge = GroupKDBX().apply {
updateWith(srcGroupToMerge, updateParents = false)
}
if (group == null) {
// If group parent to add exists and in current database
if ((deletedObject == null
|| deletedObject.deletionTime.date
.before(groupToMerge.lastModificationTime.date))
&& parentGroupToMerge != null) {
database.addGroupTo(groupToMerge, parentGroupToMerge)
}
} else {
// Merge independently custom data
mergeCustomData(group.customData, groupToMerge.customData)
// Merge by modification time
if (group.lastModificationTime.date
.before(groupToMerge.lastModificationTime.date)
) {
if (parentGroupToMerge == group.parent) {
group.updateWith(groupToMerge, false)
} else {
database.removeGroupFrom(group, group.parent)
if (parentGroupToMerge != null) {
database.addGroupTo(groupToMerge, parentGroupToMerge)
}
}
}
}
}
}
}

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