Compare commits

..

193 Commits
2.8.2 ... 2.8.4

Author SHA1 Message Date
J-Jamet
b2e29ac4bd Merge branch 'release/2.8.4' 2020-09-18 14:05:24 +02:00
J-Jamet
7df6309a68 Update CHANGELOG 2020-09-18 13:57:05 +02:00
J-Jamet
a203ad9f64 Replace strong tag 2020-09-18 13:48:56 +02:00
J-Jamet
44d6e09337 Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into develop 2020-09-18 13:44:27 +02:00
J-Jamet
7c59ec019a Add credentials information and fix small error issue in keyfile 2020-09-18 13:35:32 +02:00
J-Jamet
b457f64ec8 Fix passwordEncodingDialogFragment leak 2020-09-18 13:12:25 +02:00
J-Jamet
e152e59a61 Fix dialog leak 2020-09-18 12:55:04 +02:00
J-Jamet
2c620ad69a Add dialog for empty keyfile #679 2020-09-18 12:24:43 +02:00
J-Jamet
c08b405fc2 Remove unused dialog 2020-09-18 12:09:22 +02:00
J-Jamet
585e39c591 Open database with empty key file #679 2020-09-18 11:35:57 +02:00
J-Jamet
60e857dba9 Fix title in setting after orientation change 2020-09-17 19:33:06 +02:00
J-Jamet
9a4aa2b08f Clear listeners during fragment onDetach() 2020-09-17 19:24:39 +02:00
J-Jamet
2900b08b70 Fix OTP title and username #707 2020-09-17 19:10:26 +02:00
J-Jamet
b0936563a2 Fix upload attachment multiple times 2020-09-17 18:09:37 +02:00
J-Jamet
3d327b245a Update CHANGELOG 2020-09-17 17:39:58 +02:00
J-Jamet
9a14de3448 Add corrupted attachment icon #691 2020-09-17 17:38:29 +02:00
HARADA Hiroyuki
59f83545cf Translated using Weblate (Japanese)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-09-17 16:24:03 +02:00
J-Jamet
d9da1ef085 Fix opening database with bad attachment #691 2020-09-17 15:59:48 +02:00
Vachan
279a27f740 Translated using Weblate (Malayalam)
Currently translated at 80.5% (364 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2020-09-17 09:53:01 +02:00
HARADA Hiroyuki
6ba822ee48 Translated using Weblate (Japanese)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-09-16 19:27:58 +02:00
J-Jamet
e4445949c8 Update CHANGELOG 2020-09-16 11:39:48 +02:00
J-Jamet
8178ff583d Merge branch 'feature/Fragment_Edit_Entry' into develop #686 2020-09-16 11:36:32 +02:00
J-Jamet
0e0e19a802 Delete extra field animation 2020-09-16 11:34:01 +02:00
J-Jamet
1e2e7d841f Fix OTP generation 2020-09-15 23:22:49 +02:00
J-Jamet
263c2f00eb Fix OTP generation 2020-09-15 22:48:35 +02:00
J-Jamet
39c0b57652 Fix expire date 2020-09-15 22:42:24 +02:00
J-Jamet
677baf549c Remove unused classes 2020-09-15 22:42:08 +02:00
J-Jamet
848478c28b Remove unused code 2020-09-15 22:02:35 +02:00
J-Jamet
98ad33a589 Fix concurrent modification 2020-09-15 21:59:16 +02:00
J-Jamet
3f33733f40 Fix expiry time 2020-09-15 21:50:55 +02:00
J-Jamet
cdc4ae4fb3 Fix date instant 2020-09-15 21:35:47 +02:00
J-Jamet
3edfa8a6ce Fix entry edit education 2020-09-15 21:25:07 +02:00
J-Jamet
6e99b667af Refactor edit fragment code 2020-09-15 20:16:40 +02:00
J-Jamet
23c8735568 Try to fix leak 2020-09-15 20:12:44 +02:00
J-Jamet
f39065044f Remove unused code 2020-09-15 19:50:37 +02:00
J-Jamet
7d2ae47b0f Fix last focus 2020-09-15 19:49:20 +02:00
J-Jamet
7247de6908 Refactor fragment edit entry, using EntryInfo 2020-09-15 19:04:20 +02:00
Zidan Pragata
4c2aef8504 Translated using Weblate (Javanese)
Currently translated at 5.9% (27 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/jv/
2020-09-15 17:36:14 +02:00
Zidan Pragata
ac8717cf1f Translated using Weblate (Indonesian)
Currently translated at 40.2% (182 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2020-09-15 17:36:13 +02:00
Stephan Paternotte
7761928064 Translated using Weblate (Dutch)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2020-09-15 17:36:13 +02:00
HARADA Hiroyuki
198047406b Translated using Weblate (Japanese)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-09-15 17:36:12 +02:00
J-Jamet
1d57309db9 EntryEditContentsFragment as EntryEditFragment 2020-09-15 16:56:51 +02:00
J-Jamet
07af9f36b2 Move temp entry in fragment 2020-09-15 16:50:04 +02:00
J-Jamet
d157fea9be Add fragment in edit entry 2020-09-15 14:04:48 +02:00
Zidan Pragata
7692caf622 Added translation using Weblate (Javanese) 2020-09-14 13:50:26 +02:00
J-Jamet
dc65a8823f Update CHANGELOG 2020-09-14 13:01:01 +02:00
J-Jamet
2210932fdf Fix app crash when unlocking database V1 without backup folder #692 2020-09-14 12:56:38 +02:00
J-Jamet
0cb1bf4b7f Show error when upload error 2020-09-14 12:28:02 +02:00
J-Jamet
44d175dd40 Remove unlinked data as database preference #684 2020-09-14 12:13:26 +02:00
J-Jamet
097feb6437 Move unlinked attachment callback in fragment 2020-09-14 11:17:33 +02:00
J-Jamet
250c7e5f20 Change string 2020-09-13 16:29:35 +02:00
J-Jamet
83740f77bb Fix attachment service listeners 2020-09-13 16:18:46 +02:00
J-Jamet
8267e13d22 Fix uploading attachment with same name 2020-09-13 15:47:05 +02:00
HARADA Hiroyuki
a7e9396f35 Translated using Weblate (Japanese)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-09-13 15:36:11 +02:00
J-Jamet
0f6c8601d2 Remove temp attachment if not used 2020-09-13 13:38:33 +02:00
J-Jamet
66e68092d5 Fix incomplete attachment deletion #684 2020-09-13 13:11:08 +02:00
J-Jamet
09616a594c Fix commit attachment layout 2020-09-13 10:36:51 +02:00
J-Jamet
7ae58ea7ea Change consumme by consume 2020-09-13 10:25:01 +02:00
Oğuz Ersen
9f1f85a3c4 Translated using Weblate (Turkish)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-09-11 23:44:02 +02:00
Vachan
9992cdfce0 Translated using Weblate (Malayalam)
Currently translated at 71.9% (325 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2020-09-11 12:36:11 +02:00
Milo Ivir
f5fa08ce94 Translated using Weblate (Croatian)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-09-11 12:36:10 +02:00
Andrew
67f70f7f85 Translated using Weblate (Russian)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-09-11 12:36:10 +02:00
Éfrit
840bd56a3d Translated using Weblate (French)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-09-11 12:36:10 +02:00
HARADA Hiroyuki
c1e2d31cfd Translated using Weblate (Japanese)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-09-09 18:05:42 +02:00
HARADA Hiroyuki
c35322d44e Translated using Weblate (Japanese)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-09-06 19:24:01 +02:00
Dhruvan Ganesh
9867e46c3f Translated using Weblate (Tamil)
Currently translated at 2.8% (13 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ta/
2020-09-06 14:36:08 +02:00
WaldiS
946a120038 Translated using Weblate (Polish)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-09-06 14:36:08 +02:00
Dhruvan Ganesh
2cd1a17aa2 Added translation using Weblate (Tamil) 2020-09-05 13:58:58 +02:00
J-Jamet
6e29fe2932 Fix education hint freeze #685 2020-09-04 10:55:16 +02:00
J-Jamet
e6c6bf6613 Rename version to 2.8.4 2020-09-04 09:57:23 +02:00
Eric
72c53169df Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-09-03 14:57:25 +02:00
ihor_ck
b78cce7b4f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-09-03 14:57:25 +02:00
solokot
c40499ec31 Translated using Weblate (Russian)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-09-03 14:57:25 +02:00
HARADA Hiroyuki
ee158e517e Translated using Weblate (Japanese)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-09-03 14:57:24 +02:00
J-Jamet
70aebcf9aa Upgrade to 2.9 2020-09-02 13:46:02 +02:00
J-Jamet
b15870d441 Merge tag '2.8.3' into develop
2.8.3
2020-09-02 12:29:43 +02:00
J-Jamet
fe7074736a Merge branch 'release/2.8.3' 2020-09-02 12:29:36 +02:00
J-Jamet
69c523ffad Image button at 48dp 2020-09-02 12:21:18 +02:00
J-Jamet
6aeefdf43d Merge branch 'translations' into develop 2020-09-01 20:27:41 +02:00
J-Jamet
5f4c8be3d3 Replace strong tags 2020-09-01 20:27:19 +02:00
J-Jamet
3092e4c557 Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations
# Conflicts:
#	app/src/main/res/values-cs/strings.xml
#	app/src/main/res/values-da/strings.xml
#	app/src/main/res/values-de/strings.xml
#	app/src/main/res/values-hr/strings.xml
#	app/src/main/res/values-ja/strings.xml
#	app/src/main/res/values-pl/strings.xml
#	app/src/main/res/values-ru/strings.xml
#	app/src/main/res/values-tr/strings.xml
#	app/src/main/res/values-uk/strings.xml
#	app/src/main/res/values-zh-rCN/strings.xml
#	app/src/main/res/values-zh-rTW/strings.xml
2020-09-01 20:19:46 +02:00
J-Jamet
ea119068da Remove full file path setting 2020-09-01 19:19:15 +02:00
J-Jamet
14371ecf94 Educational hint for attachment 2020-09-01 19:12:25 +02:00
James Alison
45b0fcfe15 Translated using Weblate (Persian)
Currently translated at 57.3% (257 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fa/
2020-09-01 18:49:37 +02:00
behnam ghafari
00aa5f5586 Translated using Weblate (Persian)
Currently translated at 57.3% (257 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fa/
2020-09-01 18:49:37 +02:00
J-Jamet
79fd53fd4c Edit custom fields #675 2020-09-01 18:45:52 +02:00
J-Jamet
357ee3daf0 Fix styles color in Kitkat 2020-09-01 13:22:41 +02:00
J-Jamet
c2f7897f10 Information icon as home button 2020-09-01 12:43:29 +02:00
J-Jamet
75455c0c48 Change file modification info #667 2020-09-01 12:14:37 +02:00
J-Jamet
a394bb9f8e Remove modification date when not available 2020-09-01 12:00:08 +02:00
J-Jamet
2f5a846493 Change bottom bar color 2020-09-01 00:30:18 +02:00
J-Jamet
90376b361d Fix populate OTP 2020-09-01 00:30:03 +02:00
J-Jamet
229cf6bf5f Change purple background cardview 2020-09-01 00:19:00 +02:00
J-Jamet
bc46737353 Fix tab selection 2020-08-31 23:29:00 +02:00
J-Jamet
1db2243a2e Upgrade CHANGELOG 2020-08-31 22:16:28 +02:00
J-Jamet
7cf836b3cb Refactor backup methods for KDB database 2020-08-31 22:14:04 +02:00
J-Jamet
f7bbd295d6 Fix backup group 2020-08-31 21:45:39 +02:00
behnam ghafari
62dbd95b48 Added translation using Weblate (Persian) 2020-08-31 20:01:41 +02:00
HARADA Hiroyuki
df07e9c719 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-31 19:46:13 +02:00
J-Jamet
9aaf72726e Fix deletion for database V1 #394 2020-08-31 17:18:37 +02:00
J-Jamet
5289927619 Removes max lines in notes #676 2020-08-31 16:54:26 +02:00
J-Jamet
fa8c686f75 Revert EditTextVisibility #660 2020-08-31 16:35:02 +02:00
J-Jamet
df5f28b7c4 Smooth scroll when adding element and fix #660 2020-08-31 16:05:22 +02:00
HARADA Hiroyuki
280d8368fa Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-31 15:22:27 +02:00
HARADA Hiroyuki
80dbff1f21 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-31 15:11:27 +02:00
J-Jamet
7ee68a8481 Change item attachment focus 2020-08-31 13:35:38 +02:00
J-Jamet
ac8dd42c45 Change small code 2020-08-31 13:35:18 +02:00
J-Jamet
eed2148b2a Remove unused import 2020-08-28 21:49:15 +02:00
J-Jamet
dc5345b6d3 Fix database alias #670 2020-08-28 21:49:00 +02:00
J-Jamet
221af0b5bb Fix attachment history with the same name 2020-08-28 13:48:07 +02:00
J-Jamet
a10ccc1eb0 Second pass to fix attachment deleted in history 2020-08-28 13:12:54 +02:00
J-Jamet
b72d858480 First pass to fix attachment deleted in history 2020-08-28 11:08:30 +02:00
J-Jamet
60412cc90b Update CHANGELOG 2020-08-28 10:43:54 +02:00
J-Jamet
512ac87dc9 Fix attachment icon color 2020-08-28 10:38:48 +02:00
J-Jamet
d97020d1c5 Merge branch 'wiomoc-patch-1' into develop 2020-08-28 10:22:12 +02:00
Zidan Pragata
f82cb617ba Translated using Weblate (Indonesian)
Currently translated at 39.2% (176 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2020-08-28 09:00:49 +02:00
Small Ku
f79281a1a0 Translated using Weblate (Chinese (Traditional))
Currently translated at 55.5% (249 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2020-08-28 09:00:45 +02:00
HARADA Hiroyuki
7be1dbb78b Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-28 09:00:44 +02:00
C. Rüdinger
f875787799 Translated using Weblate (German)
Currently translated at 99.7% (447 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-08-28 09:00:44 +02:00
jan madsen
f82c208556 Translated using Weblate (Danish)
Currently translated at 94.8% (425 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-08-28 09:00:43 +02:00
Christoph Walcher
f2150e3d85 Early return in readHeaderField
In the old version the outer loop won't terminate if `EndOfHeader` is a zero sized field.
2020-08-28 03:09:23 +02:00
J-Jamet
ecc198e8a0 Fix toolbar appearance #669 2020-08-27 20:39:03 +02:00
J-Jamet
949bc58a80 Merge branch 'feature/File_Attachment' into develop #189 2020-08-27 19:14:51 +02:00
J-Jamet
7d79fff16f Disable compression by entry attachment in DatabaseV4 2020-08-27 19:04:58 +02:00
J-Jamet
3aeb678292 Fix binaries indexes in DatabaseV4 2020-08-27 18:45:56 +02:00
Zidan Pragata
160ac41bed Added translation using Weblate (Indonesian) 2020-08-27 16:41:50 +02:00
J-Jamet
9dfbcbe89c Not compress by default for database v4 2020-08-27 14:21:36 +02:00
J-Jamet
30da529348 Fix corruption in header 2020-08-27 13:57:34 +02:00
J-Jamet
dd8d114711 Change save binaries compression for database 3.1 & 4 2020-08-27 13:49:48 +02:00
J-Jamet
2191a4a848 Allow swipe notification when download completed 2020-08-27 10:27:45 +02:00
J-Jamet
8b9ea8d988 Fix remove oldest attachments 2020-08-27 10:16:37 +02:00
J-Jamet
46dda8567d Remove warnings and better gzip view implementation 2020-08-26 23:43:49 +02:00
J-Jamet
6953da4d9a Change containsAttachment method 2020-08-26 23:35:16 +02:00
J-Jamet
e987d6647e Fix header binary compression 2020-08-26 23:25:13 +02:00
J-Jamet
359d85727e Remove oldest attachments files when deleted from entries 2020-08-26 21:25:51 +02:00
J-Jamet
a994bf9dd8 Change string to Gzip 2020-08-26 20:35:45 +02:00
J-Jamet
14c4e095f6 Remove attachment path view 2020-08-26 20:33:13 +02:00
J-Jamet
59dce0e56f Restore unknown compression 2020-08-26 20:27:13 +02:00
J-Jamet
1f54a893a7 Move classes 2020-08-26 19:28:26 +02:00
J-Jamet
9489f1ee3d Rename removeUnlinkedAttachments 2020-08-26 19:25:30 +02:00
J-Jamet
dc3d720e8d Binary files as time 2020-08-26 19:20:37 +02:00
J-Jamet
efe30b598b Write only attachments in header
Remove unlinked attachments
Simpler compression
2020-08-26 18:51:25 +02:00
J-Jamet
42515bfb2d Encapsulate consume attachment action 2020-08-26 11:44:56 +02:00
J-Jamet
acb3657d95 Better compression - decompression implementation 2020-08-26 11:43:35 +02:00
J-Jamet
e7159c9d36 Try to fix decompression 2020-08-26 10:39:56 +02:00
J-Jamet
f3fdca368b Fix compression after download and upload attachment 2020-08-25 20:07:41 +02:00
J-Jamet
4ea3e08a45 Refactor EntryAttachment to Attachment 2020-08-25 19:28:41 +02:00
J-Jamet
1eebc72b21 Encapsulate binary methods 2020-08-25 19:20:58 +02:00
J-Jamet
9a91be7e36 Add attachment icon in entry list 2020-08-25 18:13:44 +02:00
J-Jamet
48476f9b88 Add attachments 2020-08-25 17:51:01 +02:00
J-Jamet
68c991eb9b Ask to replace file 2020-08-25 17:48:17 +02:00
J-Jamet
b51c77b01b Ask for big files 2020-08-25 17:19:58 +02:00
J-Jamet
57105db554 Allow only one attachment for Database KDB 2020-08-25 12:52:41 +02:00
J-Jamet
4bd3bdaddf Fix concurrent exception and upload the same file 2020-08-25 12:39:17 +02:00
J-Jamet
9cf59b8d73 Refactoring code 2020-08-25 12:27:43 +02:00
J-Jamet
a793b0bb42 Remove not compatible elements below KitKat 2020-08-25 12:00:17 +02:00
J-Jamet
4d9e2e1471 Upload progression 2020-08-25 11:37:11 +02:00
J-Jamet
1719887e55 Fix lock button after download or upload 2020-08-24 22:52:12 +02:00
J-Jamet
2be00aca9d Merge branch 'develop' into feature/File_Attachment 2020-08-24 22:33:05 +02:00
J-Jamet
6fd05c5ad7 Rotate arrow drawable 2020-08-24 22:32:40 +02:00
J-Jamet
65e404374f Change upload icon 2020-08-24 22:32:05 +02:00
J-Jamet
0b78731bb3 Strings for upload notification 2020-08-24 22:20:58 +02:00
J-Jamet
65d318ed88 Fix many upload issues 2020-08-24 21:54:21 +02:00
Milo Ivir
1bfcea55a9 Translated using Weblate (Croatian)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-08-24 16:06:20 +02:00
Eric
6780eb004d Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-08-24 16:06:20 +02:00
ihor_ck
0e12b2d021 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-08-24 16:06:20 +02:00
Andrew
1b356f87ec Translated using Weblate (Russian)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-08-24 16:06:20 +02:00
solokot
bf24d0bae1 Translated using Weblate (Russian)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-08-24 16:06:19 +02:00
WaldiS
97c831d4bb Translated using Weblate (Polish)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-08-24 16:06:19 +02:00
HARADA Hiroyuki
e3e48ffa6d Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-24 16:06:19 +02:00
zeritti
b678416122 Translated using Weblate (Czech)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-08-24 16:06:19 +02:00
J-Jamet
df722925fa Upload attachments #189 2020-08-24 13:03:59 +02:00
J-Jamet
4cbc0d9806 Merge branch 'develop' into feature/File_Attachment 2020-08-23 12:49:44 +02:00
J-Jamet
b0f3711b4e Remove previous allow lock code 2020-08-23 12:49:12 +02:00
J-Jamet
14ec6579b2 Merge branch 'develop' into feature/File_Attachment 2020-08-23 12:41:56 +02:00
J-Jamet
30bf039473 Fix last item visibility 2020-08-23 12:26:10 +02:00
Oğuz Ersen
33bea317b0 Translated using Weblate (Turkish)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-08-22 05:31:03 +02:00
Andrew
c6814dc05e Translated using Weblate (Russian)
Currently translated at 97.9% (439 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-08-22 05:31:03 +02:00
HARADA Hiroyuki
16808069ec Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-22 05:31:02 +02:00
J-Jamet
e813974e29 Fix file info issue 2020-08-21 20:17:05 +02:00
J-Jamet
7e0010b536 Images buttons width 36dp 2020-08-21 20:02:32 +02:00
J-Jamet
93171adcb3 Smaller download icon 2020-08-21 19:48:16 +02:00
J-Jamet
9501cc76a4 Notes and URL as EntryField 2020-08-21 19:43:54 +02:00
J-Jamet
5d7db046ac Encapsulate username and OTP as EntryField 2020-08-21 18:35:54 +02:00
J-Jamet
46c259bc3e Fix field delete button position 2020-08-21 18:24:41 +02:00
J-Jamet
3bacff91d3 Visibility button for each field, password as EntryField view 2020-08-21 17:31:15 +02:00
J-Jamet
bd79d483d2 Upgrade to 2.8.3 2020-08-21 16:16:35 +02:00
Hosted Weblate
16e31f4881 Merge branch 'origin/master' into Weblate. 2020-08-21 16:14:09 +02:00
ssantos
afa23c393d Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2020-08-21 16:14:08 +02:00
HARADA Hiroyuki
54d23cb781 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-21 16:14:08 +02:00
random r
c757e410e9 Translated using Weblate (Italian)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-08-21 16:14:08 +02:00
J-Jamet
39dd25567d Merge tag '2.8.2' into develop
2.8.2
2020-08-21 16:04:09 +02:00
J-Jamet
b3c0494618 Better file attachment download implementation 2020-08-06 16:59:15 +02:00
155 changed files with 4651 additions and 2404 deletions

View File

@@ -1,3 +1,18 @@
KeePassDX(2.8.4)
* Fix incomplete attachment deletion #684
* Fix opening database v1 without backup folder #692
* Fix ANR during first entry education #685
* Entry edition as fragment and manual views to fix focus #686
* Fix opening database with corrupted attachment #691
* Manage empty keyfile #679
KeePassDX(2.8.3)
* Upload attachments
* Visibility button for each hidden field
* Fix read header file
* Fix deletion in KDB database
* Fix minor issues
KeePassDX(2.8.2)
* Fix themes / new UI
* Fix multiples notifications

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass"
minSdkVersion 14
targetSdkVersion 29
versionCode = 38
versionName = "2.8.2"
versionCode = 40
versionName = "2.8.4"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"

View File

@@ -39,14 +39,15 @@ import com.google.android.material.appbar.CollapsingToolbarLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
@@ -86,7 +87,7 @@ class EntryActivity : LockingActivity() {
private var mShowPassword: Boolean = false
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAttachmentsToDownload: HashMap<Int, EntryAttachment> = HashMap()
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
private var clipboardHelper: ClipboardHelper? = null
private var mFirstLaunchOfActivity: Boolean = false
@@ -212,8 +213,8 @@ class EntryActivity : LockingActivity() {
mAttachmentFileBinderManager?.apply {
registerProgressTask()
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
override fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment) {
entryContentsView?.updateAttachmentDownloadProgress(attachment)
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
entryContentsView?.putAttachment(entryAttachmentState)
}
}
}
@@ -240,14 +241,13 @@ class EntryActivity : LockingActivity() {
toolbar?.title = entryTitle
// Assign basic fields
entryContentsView?.assignUserName(entry.username)
entryContentsView?.assignUserNameCopyListener(View.OnClickListener {
entryContentsView?.assignUserName(entry.username) {
database.startManageEntry(entry)
clipboardHelper?.timeoutCopyToClipboard(entry.username,
getString(R.string.copy_field,
getString(R.string.copy_field,
getString(R.string.entry_user_name)))
database.stopManageEntry(entry)
})
}
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
@@ -274,23 +274,25 @@ class EntryActivity : LockingActivity() {
}
}
entryContentsView?.assignPassword(entry.password, allowCopyPasswordAndProtectedFields)
if (allowCopyPasswordAndProtectedFields) {
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
View.OnClickListener {
database.startManageEntry(entry)
clipboardHelper?.timeoutCopyToClipboard(entry.password,
getString(R.string.copy_field,
getString(R.string.copy_field,
getString(R.string.entry_password)))
database.stopManageEntry(entry)
})
}
} else {
// If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
entryContentsView?.assignPasswordCopyListener(showWarningClipboardDialogOnClickListener)
showWarningClipboardDialogOnClickListener
} else {
entryContentsView?.assignPasswordCopyListener(null)
null
}
}
entryContentsView?.assignPassword(entry.password,
allowCopyPasswordAndProtectedFields,
onPasswordCopyClickListener)
//Assign OTP field
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
@@ -304,20 +306,22 @@ class EntryActivity : LockingActivity() {
})
entryContentsView?.assignURL(entry.url)
entryContentsView?.assignComment(entry.notes)
entryContentsView?.assignNotes(entry.notes)
// Assign custom fields
if (entry.allowCustomFields()) {
if (mDatabase?.allowEntryCustomFields() == true) {
entryContentsView?.clearExtraFields()
for ((label, value) in entry.customFields) {
entry.getExtraFields().forEach { field ->
val label = field.name
val value = field.protectedValue
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
if (allowCopyProtectedField) {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, View.OnClickListener {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
clipboardHelper?.timeoutCopyToClipboard(
value.toString(),
getString(R.string.copy_field, label)
)
})
}
} else {
// If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
@@ -328,18 +332,13 @@ class EntryActivity : LockingActivity() {
}
}
}
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
// Manage attachments
entryContentsView?.assignAttachments(entry.getAttachments()) { attachmentItem ->
when (attachmentItem.downloadState) {
AttachmentState.NULL, AttachmentState.ERROR, AttachmentState.COMPLETE -> {
createDocument(this, attachmentItem.name)?.let { requestCode ->
mAttachmentsToDownload[requestCode] = attachmentItem
}
}
else -> {
// TODO Stop download
mDatabase?.binaryPool?.let { binaryPool ->
entryContentsView?.assignAttachments(entry.getAttachments(binaryPool).toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
createDocument(this, attachmentItem.name)?.let { requestCode ->
mAttachmentsToDownload[requestCode] = attachmentItem
}
}
}
@@ -393,16 +392,6 @@ class EntryActivity : LockingActivity() {
}
}
private fun changeShowPasswordIcon(togglePassword: MenuItem?) {
if (mShowPassword) {
togglePassword?.setTitle(R.string.menu_hide_password)
togglePassword?.setIcon(R.drawable.ic_visibility_off_white_24dp)
} else {
togglePassword?.setTitle(R.string.menu_showpass)
togglePassword?.setIcon(R.drawable.ic_visibility_white_24dp)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
@@ -418,15 +407,6 @@ class EntryActivity : LockingActivity() {
menu.findItem(R.id.menu_edit)?.isVisible = false
}
val togglePassword = menu.findItem(R.id.menu_toggle_pass)
entryContentsView?.let {
if (it.isPasswordPresent || it.atLeastOneFieldProtectedPresent()) {
changeShowPasswordIcon(togglePassword)
} else {
togglePassword?.isVisible = false
}
}
val gotoUrl = menu.findItem(R.id.menu_goto_url)
gotoUrl?.apply {
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
@@ -449,28 +429,31 @@ class EntryActivity : LockingActivity() {
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
menu: Menu) {
val entryCopyEducationPerformed = entryContentsView?.isUserNamePresent == true
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
val entryCopyEducationPerformed = entryFieldCopyView != null
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
findViewById(R.id.entry_user_name_action_image),
entryFieldCopyView,
{
clipboardHelper?.timeoutCopyToClipboard(mEntry!!.username,
getString(R.string.copy_field,
getString(R.string.entry_user_name)))
val appNameString = getString(R.string.app_name)
clipboardHelper?.timeoutCopyToClipboard(appNameString,
getString(R.string.copy_field, appNameString))
},
{
performedNextEducation(entryActivityEducation, menu)
})
if (!entryCopyEducationPerformed) {
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
// entryEditEducationPerformed
toolbar?.findViewById<View>(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
toolbar!!.findViewById(R.id.menu_edit),
{
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
},
{
performedNextEducation(entryActivityEducation, menu)
})
menuEditView != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
menuEditView,
{
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
},
{
performedNextEducation(entryActivityEducation, menu)
}
)
}
}
@@ -480,12 +463,6 @@ class EntryActivity : LockingActivity() {
MenuUtil.onContributionItemSelected(this)
return true
}
R.id.menu_toggle_pass -> {
mShowPassword = !mShowPassword
changeShowPasswordIcon(item)
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
return true
}
R.id.menu_edit -> {
mEntry?.let {
EntryEditActivity.launch(this@EntryActivity, it)

View File

@@ -22,6 +22,8 @@ import android.app.Activity
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.util.Log
@@ -33,35 +35,40 @@ import android.widget.TimePicker
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.model.FocusedEditField
import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.view.EntryEditContentsView
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.showActionError
import com.kunzisoft.keepass.view.updateLockPaddingLeft
import org.joda.time.DateTime
import java.util.*
import kotlin.collections.ArrayList
class EntryEditActivity : LockingActivity(),
IconPickerDialogFragment.IconPickerListener,
@@ -69,26 +76,30 @@ class EntryEditActivity : LockingActivity(),
GeneratePasswordDialogFragment.GeneratePasswordListener,
SetOTPDialogFragment.CreateOtpListener,
DatePickerDialog.OnDateSetListener,
TimePickerDialog.OnTimeSetListener {
TimePickerDialog.OnTimeSetListener,
FileTooBigDialogFragment.ActionChooseListener,
ReplaceFileDialogFragment.ActionChooseListener {
private var mDatabase: Database? = null
// Refs of an entry and group in database, are not modifiable
private var mEntry: Entry? = null
private var mParent: Group? = null
// New or copy of mEntry in the database to be modifiable
private var mNewEntry: Entry? = null
private var mIsNew: Boolean = false
// Views
private var coordinatorLayout: CoordinatorLayout? = null
private var scrollView: NestedScrollView? = null
private var entryEditContentsView: EntryEditContentsView? = null
private var entryEditFragment: EntryEditFragment? = null
private var entryEditAddToolBar: Toolbar? = null
private var validateButton: View? = null
private var lockView: View? = null
private var mFocusedEditExtraField: FocusedEditField? = null
// To manage attachments
private var mSelectFileHelper: SelectFileHelper? = null
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAllowMultipleAttachments: Boolean = false
private var mTempAttachments = ArrayList<Attachment>()
// Education
private var entryEditActivityEducation: EntryEditActivityEducation? = null
@@ -108,22 +119,6 @@ class EntryEditActivity : LockingActivity(),
scrollView = findViewById(R.id.entry_edit_scroll)
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
entryEditContentsView = findViewById(R.id.entry_edit_contents)
entryEditContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
entryEditContentsView?.onDateClickListener = View.OnClickListener {
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
val dateTime = DateTime(expiresDate)
val defaultYear = dateTime.year
val defaultMonth = dateTime.monthOfYear-1
val defaultDay = dateTime.dayOfMonth
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
.show(supportFragmentManager, "DatePickerFragment")
}
}
entryEditContentsView?.entryPasswordGeneratorView?.setOnClickListener {
openPasswordGenerator()
}
lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener {
lockAndExit()
@@ -138,6 +133,8 @@ class EntryEditActivity : LockingActivity(),
// Likely the app has been killed exit the activity
mDatabase = Database.getInstance()
var tempEntryInfo: EntryInfo? = null
// Entry is retrieve, it's an entry to update
intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let {
mIsNew = false
@@ -153,84 +150,91 @@ class EntryEditActivity : LockingActivity(),
entry.parent = mParent
}
}
// Create the new entry from the current one
if (savedInstanceState?.containsKey(KEY_NEW_ENTRY) != true) {
mEntry?.let { entry ->
// Create a copy to modify
mNewEntry = Entry(entry).also { newEntry ->
// WARNING Remove the parent to keep memory with parcelable
newEntry.removeParent()
}
}
}
tempEntryInfo = mEntry?.getEntryInfo(mDatabase, true)
}
// Parent is retrieve, it's a new entry to create
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let {
mIsNew = true
// Create an empty new entry
if (savedInstanceState?.containsKey(KEY_NEW_ENTRY) != true) {
mNewEntry = mDatabase?.createEntry()
}
mParent = mDatabase?.getGroupById(it)
// Add the default icon from parent if not a folder
val parentIcon = mParent?.icon
tempEntryInfo = mDatabase?.createEntry()?.getEntryInfo(mDatabase, true)
// Set default icon
if (parentIcon != null
&& parentIcon.iconId != IconImage.UNKNOWN_ID
&& parentIcon.iconId != IconImageStandard.FOLDER) {
temporarilySaveAndShowSelectedIcon(parentIcon)
} else {
mDatabase?.drawFactory?.let { iconFactory ->
entryEditContentsView?.setDefaultIcon(iconFactory)
tempEntryInfo?.icon = parentIcon
}
// Set default username
tempEntryInfo?.username = mDatabase?.defaultUsername ?: ""
}
// Build fragment to manage entry modification
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
if (entryEditFragment == null) {
entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo)
}
supportFragmentManager.beginTransaction()
.replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG)
.commit()
entryEditFragment?.apply {
drawFactory = mDatabase?.drawFactory
setOnDateClickListener = View.OnClickListener {
expiryTime.date.let { expiresDate ->
val dateTime = DateTime(expiresDate)
val defaultYear = dateTime.year
val defaultMonth = dateTime.monthOfYear-1
val defaultDay = dateTime.dayOfMonth
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
.show(supportFragmentManager, "DatePickerFragment")
}
}
setOnPasswordGeneratorClickListener = View.OnClickListener {
openPasswordGenerator()
}
// Add listener to the icon
setOnIconViewClickListener = View.OnClickListener {
IconPickerDialogFragment.launch(this@EntryEditActivity)
}
setOnRemoveAttachment = { attachment ->
mAttachmentFileBinderManager?.removeBinaryAttachment(attachment)
removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD))
}
setOnEditCustomField = { field ->
editCustomField(field)
}
}
// Retrieve the new entry after an orientation change
if (savedInstanceState?.containsKey(KEY_NEW_ENTRY) == true) {
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY)
// Retrieve temp attachments in case of deletion
if (savedInstanceState?.containsKey(TEMP_ATTACHMENTS) == true) {
mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments
}
if (savedInstanceState?.containsKey(EXTRA_FIELD_FOCUSED_ENTRY) == true) {
mFocusedEditExtraField = savedInstanceState.getParcelable(EXTRA_FIELD_FOCUSED_ENTRY)
}
// Close the activity if entry or parent can't be retrieve
if (mNewEntry == null || mParent == null) {
finish()
return
}
populateViewsWithEntry(mNewEntry!!)
// Assign title
title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry)
// Add listener to the icon
entryEditContentsView?.setOnIconViewClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) }
// Bottom Bar
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
entryEditAddToolBar?.apply {
menuInflater.inflate(R.menu.entry_edit, menu)
menu.findItem(R.id.menu_add_field).apply {
val allowLock = PreferencesUtil.showLockDatabaseButton(context)
isEnabled = allowLock
isVisible = allowLock
}
menu.findItem(R.id.menu_add_field).apply {
val allowCustomField = mNewEntry?.allowCustomFields() == true
val allowCustomField = mDatabase?.allowEntryCustomFields() == true
isEnabled = allowCustomField
isVisible = allowCustomField
}
// Attachment not compatible below KitKat
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
menu.findItem(R.id.menu_add_attachment).isVisible = false
}
menu.findItem(R.id.menu_add_otp).apply {
val allowOTP = mDatabase?.allowOTP == true
isEnabled = allowOTP
isVisible = allowOTP
// OTP not compatible below KitKat
isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
}
setOnMenuItemClickListener { item ->
@@ -239,6 +243,10 @@ class EntryEditActivity : LockingActivity(),
addNewCustomField()
true
}
R.id.menu_add_attachment -> {
addNewAttachment(item)
true
}
R.id.menu_add_otp -> {
setupOTP()
true
@@ -248,6 +256,10 @@ class EntryEditActivity : LockingActivity(),
}
}
// To retrieve attachment
mSelectFileHelper = SelectFileHelper(this)
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
// Save button
validateButton = findViewById(R.id.entry_edit_validate)
validateButton?.setOnClickListener { saveEntry() }
@@ -260,8 +272,22 @@ class EntryEditActivity : LockingActivity(),
when (actionTask) {
ACTION_DATABASE_CREATE_ENTRY_TASK,
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
if (result.isSuccess)
finish()
try {
if (result.isSuccess) {
var newNodes: List<Node> = ArrayList()
result.data?.getBundle(DatabaseTaskNotificationService.NEW_NODES_KEY)?.let { newNodesBundle ->
mDatabase?.let { database ->
newNodes = DatabaseTaskNotificationService.getListNodesFromBundle(database, newNodesBundle)
}
}
if (newNodes.size == 1) {
mEntry = newNodes[0] as Entry?
finish()
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve entry after database action", e)
}
}
}
coordinatorLayout?.showActionError(result)
@@ -279,69 +305,55 @@ class EntryEditActivity : LockingActivity(),
// Padding if lock button visible
entryEditAddToolBar?.updateLockPaddingLeft()
}
private fun populateViewsWithEntry(newEntry: Entry) {
// Don't start the field reference manager, we want to see the raw ref
mDatabase?.stopManageEntry(newEntry)
// Set info in temp parameters
temporarilySaveAndShowSelectedIcon(newEntry.icon)
// Set info in view
entryEditContentsView?.apply {
title = newEntry.title
username = if (mIsNew && newEntry.username.isEmpty())
mDatabase?.defaultUsername ?: ""
else
newEntry.username
url = newEntry.url
password = newEntry.password
expires = newEntry.expires
if (expires)
expiresDate = newEntry.expiryTime
notes = newEntry.notes
assignExtraFields(newEntry.customFields.mapTo(ArrayList()) {
Field(it.key, it.value)
}, mFocusedEditExtraField)
assignAttachments(newEntry.getAttachments()) { attachment ->
newEntry.removeAttachment(attachment)
mAllowMultipleAttachments = mDatabase?.allowMultipleAttachments == true
mAttachmentFileBinderManager?.apply {
registerProgressTask()
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
when (entryAttachmentState.downloadState) {
AttachmentState.START -> {
entryEditFragment?.apply {
// When only one attachment is allowed
if (!mAllowMultipleAttachments) {
clearAttachments()
}
putAttachment(entryAttachmentState)
// Scroll to the attachment position
getAttachmentViewPosition(entryAttachmentState) {
scrollView?.smoothScrollTo(0, it.toInt())
}
}
}
AttachmentState.IN_PROGRESS -> {
entryEditFragment?.putAttachment(entryAttachmentState)
}
AttachmentState.COMPLETE -> {
entryEditFragment?.apply {
putAttachment(entryAttachmentState)
// Scroll to the attachment position
getAttachmentViewPosition(entryAttachmentState) {
scrollView?.smoothScrollTo(0, it.toInt())
}
}
}
AttachmentState.ERROR -> {
entryEditFragment?.removeAttachment(entryAttachmentState)
coordinatorLayout?.let {
Snackbar.make(it, R.string.error_file_not_create, Snackbar.LENGTH_LONG).asError().show()
}
}
else -> {}
}
}
}
}
}
private fun populateEntryWithViews(newEntry: Entry) {
override fun onPause() {
mAttachmentFileBinderManager?.unregisterProgressTask()
mDatabase?.startManageEntry(newEntry)
newEntry.apply {
// Build info from view
entryEditContentsView?.let { entryView ->
removeAllFields()
title = entryView.title
username = entryView.username
url = entryView.url
password = entryView.password
expires = entryView.expires
if (entryView.expires) {
expiryTime = entryView.expiresDate
}
notes = entryView.notes
entryView.getExtraField().forEach { customField ->
putExtraField(customField.name, customField.protectedValue)
}
mFocusedEditExtraField = entryView.getExtraFieldFocused()
}
}
mDatabase?.stopManageEntry(newEntry)
}
private fun temporarilySaveAndShowSelectedIcon(icon: IconImage) {
mNewEntry?.icon = icon
mDatabase?.drawFactory?.let { iconDrawFactory ->
entryEditContentsView?.setIcon(iconDrawFactory, icon)
}
super.onPause()
}
/**
@@ -358,16 +370,92 @@ class EntryEditActivity : LockingActivity(),
EntryCustomFieldDialogFragment.getInstance().show(supportFragmentManager, "customFieldDialog")
}
override fun onNewCustomFieldApproved(label: String, protection: Boolean) {
entryEditContentsView?.putExtraField(Field(label, ProtectedString(protection)))
private fun editCustomField(field: Field) {
EntryCustomFieldDialogFragment.getInstance(field).show(supportFragmentManager, "customFieldDialog")
}
override fun onNewCustomFieldCanceled(label: String, protection: Boolean) {}
override fun onNewCustomFieldApproved(newField: Field) {
entryEditFragment?.apply {
putExtraField(newField)
}
}
override fun onEditCustomFieldApproved(oldField: Field, newField: Field) {
entryEditFragment?.replaceExtraField(oldField, newField)
}
override fun onDeleteCustomFieldApproved(oldField: Field) {
entryEditFragment?.removeExtraField(oldField)
}
/**
* Add a new attachment
*/
private fun addNewAttachment(item: MenuItem) {
mSelectFileHelper?.selectFileOnClickViewListener?.onMenuItemClick(item)
}
override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) {
if (attachmentToUploadUri != null && fileName != null) {
buildNewAttachment(attachmentToUploadUri, fileName)
}
}
override fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?) {
startUploadAttachment(attachmentToUploadUri, attachment)
}
private fun startUploadAttachment(attachmentToUploadUri: Uri?, attachment: Attachment?) {
if (attachmentToUploadUri != null && attachment != null) {
// Start uploading in service
mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, attachment)
// Add in temp list
mTempAttachments.add(attachment)
}
}
private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) {
val compression = mDatabase?.compressionForNewEntry() ?: false
mDatabase?.buildNewBinary(applicationContext.filesDir, false, compression)?.let { binaryAttachment ->
val entryAttachment = Attachment(fileName, binaryAttachment)
// Ask to replace the current attachment
if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) ||
entryEditFragment?.containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD)) == true) {
ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment)
.show(supportFragmentManager, "replacementFileFragment")
} else {
startUploadAttachment(attachmentToUploadUri, entryAttachment)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
uri?.let { attachmentToUploadUri ->
// TODO Async to get the name
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
documentFile.name?.let { fileName ->
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
.show(supportFragmentManager, "fileTooBigFragment")
} else {
buildNewAttachment(attachmentToUploadUri, fileName)
}
}
}
}
}
}
/**
* Set up OTP (HOTP or TOTP) and add it as extra field
*/
private fun setupOTP() {
// Retrieve the current otpElement if exists
// and open the dialog to set up the OTP
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
SetOTPDialogFragment.build(entryEditFragment?.getEntryInfo()?.otpModel)
.show(supportFragmentManager, "addOTPDialog")
}
@@ -375,18 +463,30 @@ class EntryEditActivity : LockingActivity(),
* Saves the new entry or update an existing entry in the database
*/
private fun saveEntry() {
// Launch a validation and show the error if present
if (entryEditContentsView?.isValid() == true) {
// Clone the entry
mNewEntry?.let { newEntry ->
// Get the temp entry
entryEditFragment?.getEntryInfo()?.let { newEntryInfo ->
// WARNING Add the parent previously deleted
newEntry.parent = mEntry?.parent
if (mIsNew) {
// Create new one
mDatabase?.createEntry()
} else {
// Create a clone
Entry(mEntry!!)
}?.let { newEntry ->
newEntry.setEntryInfo(mDatabase, newEntryInfo)
// Build info
newEntry.lastAccessTime = DateInstant()
newEntry.lastModificationTime = DateInstant()
populateEntryWithViews(newEntry)
// Delete temp attachment if not used
mTempAttachments.forEach {
mDatabase?.binaryPool?.let { binaryPool ->
if (!newEntry.getAttachments(binaryPool).contains(it)) {
mDatabase?.removeAttachmentIfNotUsed(it)
}
}
}
// Open a progress dialog and save entry
if (mIsNew) {
@@ -419,30 +519,23 @@ class EntryEditActivity : LockingActivity(),
menu.findItem(R.id.menu_save_database)?.isVisible = false
MenuUtil.contributionMenuInflater(inflater, menu)
entryEditActivityEducation?.let {
Handler().post { performedNextEducation(it) }
}
return true
}
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
val passwordGeneratorView: View? = entryEditContentsView?.entryPasswordGeneratorView
val generatePasswordEducationPerformed = passwordGeneratorView != null
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
passwordGeneratorView,
{
openPasswordGenerator()
},
{
performedNextEducation(entryEditActivityEducation)
}
)
if (!generatePasswordEducationPerformed) {
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
entryEditActivityEducation?.let {
Handler().post { performedNextEducation(it) }
}
return super.onPrepareOptionsMenu(menu)
}
fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
if (entryEditFragment?.generatePasswordEducationPerformed(entryEditActivityEducation) != true) {
val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field)
val addNewFieldEducationPerformed = mNewEntry != null
&& mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
val addNewFieldEducationPerformed = mDatabase?.allowEntryCustomFields() == true
&& addNewFieldView != null
&& addNewFieldView.isVisible
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
addNewFieldView,
{
@@ -453,13 +546,29 @@ class EntryEditActivity : LockingActivity(),
}
)
if (!addNewFieldEducationPerformed) {
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
setupOtpView != null && setupOtpView.visibility == View.VISIBLE
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
setupOtpView,
val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment)
val addAttachmentEducationPerformed = attachmentView != null
&& attachmentView.isVisible
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
attachmentView,
{
setupOTP()
})
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(attachmentView)
},
{
performedNextEducation(entryEditActivityEducation)
}
)
if (!addAttachmentEducationPerformed) {
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
setupOtpView != null
&& setupOtpView.isVisible
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
setupOtpView,
{
setupOTP()
}
)
}
}
}
}
@@ -482,16 +591,28 @@ class EntryEditActivity : LockingActivity(),
}
override fun onOtpCreated(otpElement: OtpElement) {
var titleOTP: String? = null
var usernameOTP: String? = null
// Build a temp entry to get title and username (by ref)
entryEditFragment?.getEntryInfo()?.let { entryInfo ->
val entryTemp = mDatabase?.createEntry()
entryTemp?.setEntryInfo(mDatabase, entryInfo)
mDatabase?.startManageEntry(entryTemp)
titleOTP = entryTemp?.title
usernameOTP = entryTemp?.username
mDatabase?.stopManageEntry(mEntry)
}
// Update the otp field with otpauth:// url
val otpField = OtpEntryFields.buildOtpField(otpElement,
mEntry?.title, mEntry?.username)
entryEditContentsView?.putExtraField(otpField)
mEntry?.putExtraField(otpField.name, otpField.protectedValue)
val otpField = OtpEntryFields.buildOtpField(otpElement, titleOTP, usernameOTP)
mEntry?.putExtraField(Field(otpField.name, otpField.protectedValue))
entryEditFragment?.apply {
putExtraField(otpField)
}
}
override fun iconPicked(bundle: Bundle) {
IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon ->
temporarilySaveAndShowSelectedIcon(icon)
entryEditFragment?.icon = icon
}
}
@@ -499,9 +620,9 @@ class EntryEditActivity : LockingActivity(),
// To fix android 4.4 issue
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
if (datePicker?.isShown == true) {
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
entryEditFragment?.expiryTime?.date?.let { expiresDate ->
// Save the date
entryEditContentsView?.expiresDate =
entryEditFragment?.expiryTime =
DateInstant(DateTime(expiresDate)
.withYear(year)
.withMonthOfYear(month + 1)
@@ -518,9 +639,9 @@ class EntryEditActivity : LockingActivity(),
}
override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) {
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
entryEditFragment?.expiryTime?.date?.let { expiresDate ->
// Save the date
entryEditContentsView?.expiresDate =
entryEditFragment?.expiryTime =
DateInstant(DateTime(expiresDate)
.withHourOfDay(hours)
.withMinuteOfHour(minutes)
@@ -529,21 +650,15 @@ class EntryEditActivity : LockingActivity(),
}
override fun onSaveInstanceState(outState: Bundle) {
mNewEntry?.let {
populateEntryWithViews(it)
outState.putParcelable(KEY_NEW_ENTRY, it)
}
mFocusedEditExtraField?.let {
outState.putParcelable(EXTRA_FIELD_FOCUSED_ENTRY, it)
}
outState.putParcelableArrayList(TEMP_ATTACHMENTS, mTempAttachments)
super.onSaveInstanceState(outState)
}
override fun acceptPassword(bundle: Bundle) {
bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID)?.let {
entryEditContentsView?.password = it
entryEditFragment?.password = it
}
entryEditActivityEducation?.let {
@@ -567,10 +682,10 @@ class EntryEditActivity : LockingActivity(),
override fun finish() {
// Assign entry callback as a result in all case
try {
mNewEntry?.let {
mEntry?.let { entry ->
val bundle = Bundle()
val intentEntry = Intent()
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, mNewEntry)
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry)
intentEntry.putExtras(bundle)
if (mIsNew) {
setResult(ADD_ENTRY_RESULT_CODE, intentEntry)
@@ -594,8 +709,7 @@ class EntryEditActivity : LockingActivity(),
const val KEY_PARENT = "parent"
// SaveInstanceState
const val KEY_NEW_ENTRY = "new_entry"
const val EXTRA_FIELD_FOCUSED_ENTRY = "EXTRA_FIELD_FOCUSED_ENTRY"
const val TEMP_ATTACHMENTS = "TEMP_ATTACHMENTS"
// Keys for callback
const val ADD_ENTRY_RESULT_CODE = 31
@@ -603,6 +717,8 @@ class EntryEditActivity : LockingActivity(),
const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
const val ENTRY_EDIT_FRAGMENT_TAG = "ENTRY_EDIT_FRAGMENT_TAG"
/**
* Launch EntryEditActivity to update an existing entry
*

View File

@@ -0,0 +1,535 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities
import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.applyFontVisibility
import com.kunzisoft.keepass.view.collapse
import com.kunzisoft.keepass.view.expand
class EntryEditFragment: StylishFragment() {
private lateinit var entryTitleLayoutView: TextInputLayout
private lateinit var entryTitleView: EditText
private lateinit var entryIconView: ImageView
private lateinit var entryUserNameView: EditText
private lateinit var entryUrlView: EditText
private lateinit var entryPasswordLayoutView: TextInputLayout
private lateinit var entryPasswordView: EditText
private lateinit var entryPasswordGeneratorView: View
private lateinit var entryExpiresCheckBox: CompoundButton
private lateinit var entryExpiresTextView: TextView
private lateinit var entryNotesView: EditText
private lateinit var extraFieldsContainerView: View
private lateinit var extraFieldsListView: ViewGroup
private lateinit var attachmentsContainerView: View
private lateinit var attachmentsListView: RecyclerView
private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter
private var fontInVisibility: Boolean = false
private var iconColor: Int = 0
private var expiresInstant: DateInstant = DateInstant.IN_ONE_MONTH
var drawFactory: IconDrawableFactory? = null
var setOnDateClickListener: View.OnClickListener? = null
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
var setOnIconViewClickListener: View.OnClickListener? = null
var setOnEditCustomField: ((Field) -> Unit)? = null
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
// Elements to modify the current entry
private var mEntryInfo = EntryInfo()
private var mLastFocusedEditField: FocusedEditField? = null
private var mExtraViewToRequestFocus: EditText? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
val rootView = inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_entry_edit_contents, container, false)
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
entryTitleLayoutView = rootView.findViewById(R.id.entry_edit_container_title)
entryTitleView = rootView.findViewById(R.id.entry_edit_title)
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
entryIconView.setOnClickListener {
setOnIconViewClickListener?.onClick(it)
}
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
entryUrlView = rootView.findViewById(R.id.entry_edit_url)
entryPasswordLayoutView = rootView.findViewById(R.id.entry_edit_container_password)
entryPasswordView = rootView.findViewById(R.id.entry_edit_password)
entryPasswordGeneratorView = rootView.findViewById(R.id.entry_edit_password_generator_button)
entryPasswordGeneratorView.setOnClickListener {
setOnPasswordGeneratorClickListener?.onClick(it)
}
entryExpiresCheckBox = rootView.findViewById(R.id.entry_edit_expires_checkbox)
entryExpiresTextView = rootView.findViewById(R.id.entry_edit_expires_text)
entryExpiresTextView.setOnClickListener {
if (entryExpiresCheckBox.isChecked)
setOnDateClickListener?.onClick(it)
}
entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
extraFieldsContainerView = rootView.findViewById(R.id.extra_fields_container)
extraFieldsListView = rootView.findViewById(R.id.extra_fields_list)
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
if (previousSize > 0 && newSize == 0) {
attachmentsContainerView.collapse(true)
} else if (previousSize == 0 && newSize == 1) {
attachmentsContainerView.expand(true)
}
}
attachmentsListView.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adapter = attachmentsAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
assignExpiresDateText()
}
// Retrieve the textColor to tint the icon
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
taIconColor?.recycle()
// Retrieve the new entry after an orientation change
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
else if (savedInstanceState?.containsKey(KEY_TEMP_ENTRY_INFO) == true) {
mEntryInfo = savedInstanceState.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
}
if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) {
mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD) ?: mLastFocusedEditField
}
populateViewsWithEntry()
return rootView
}
override fun onDetach() {
super.onDetach()
drawFactory = null
setOnDateClickListener = null
setOnPasswordGeneratorClickListener = null
setOnIconViewClickListener = null
setOnRemoveAttachment = null
setOnEditCustomField = null
}
fun getEntryInfo(): EntryInfo? {
populateEntryWithViews()
return mEntryInfo
}
fun generatePasswordEducationPerformed(entryEditActivityEducation: EntryEditActivityEducation): Boolean {
return entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
entryPasswordGeneratorView,
{
GeneratePasswordDialogFragment().show(parentFragmentManager, "PasswordGeneratorFragment")
},
{
try {
(activity as? EntryEditActivity?)?.performedNextEducation(entryEditActivityEducation)
} catch (ignore: Exception) {}
}
)
}
private fun populateViewsWithEntry() {
// Set info in view
icon = mEntryInfo.icon
title = mEntryInfo.title
username = mEntryInfo.username
url = mEntryInfo.url
password = mEntryInfo.password
expires = mEntryInfo.expires
expiryTime = mEntryInfo.expiryTime
notes = mEntryInfo.notes
assignExtraFields(mEntryInfo.customFields) { fields ->
setOnEditCustomField?.invoke(fields)
}
assignAttachments(mEntryInfo.attachments, StreamDirection.UPLOAD) { attachment ->
setOnRemoveAttachment?.invoke(attachment)
}
}
private fun populateEntryWithViews() {
// Icon already populate
mEntryInfo.title = title
mEntryInfo.username = username
mEntryInfo.url = url
mEntryInfo.password = password
mEntryInfo.expires = expires
mEntryInfo.expiryTime = expiryTime
mEntryInfo.notes = notes
mEntryInfo.customFields = getExtraFields()
mEntryInfo.otpModel = OtpEntryFields.parseFields { key ->
getExtraFields().firstOrNull { it.name == key }?.protectedValue?.toString()
}?.otpModel
mEntryInfo.attachments = getAttachments()
}
var title: String
get() {
return entryTitleView.text.toString()
}
set(value) {
entryTitleView.setText(value)
if (fontInVisibility)
entryTitleView.applyFontVisibility()
}
var icon: IconImage
get() {
return mEntryInfo.icon
}
set(value) {
mEntryInfo.icon = value
drawFactory?.let { drawFactory ->
entryIconView.assignDatabaseIcon(drawFactory, value, iconColor)
}
}
var username: String
get() {
return entryUserNameView.text.toString()
}
set(value) {
entryUserNameView.setText(value)
if (fontInVisibility)
entryUserNameView.applyFontVisibility()
}
var url: String
get() {
return entryUrlView.text.toString()
}
set(value) {
entryUrlView.setText(value)
if (fontInVisibility)
entryUrlView.applyFontVisibility()
}
var password: String
get() {
return entryPasswordView.text.toString()
}
set(value) {
entryPasswordView.setText(value)
if (fontInVisibility) {
entryPasswordView.applyFontVisibility()
}
}
private fun assignExpiresDateText() {
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
entryExpiresTextView.setOnClickListener(setOnDateClickListener)
expiresInstant.getDateTimeString(resources)
} else {
entryExpiresTextView.setOnClickListener(null)
resources.getString(R.string.never)
}
if (fontInVisibility)
entryExpiresTextView.applyFontVisibility()
}
var expires: Boolean
get() {
return entryExpiresCheckBox.isChecked
}
set(value) {
if (!value) {
expiresInstant = DateInstant.IN_ONE_MONTH
}
entryExpiresCheckBox.isChecked = value
assignExpiresDateText()
}
var expiryTime: DateInstant
get() {
return if (expires)
expiresInstant
else
DateInstant.NEVER_EXPIRE
}
set(value) {
if (expires)
expiresInstant = value
assignExpiresDateText()
}
var notes: String
get() {
return entryNotesView.text.toString()
}
set(value) {
entryNotesView.setText(value)
if (fontInVisibility)
entryNotesView.applyFontVisibility()
}
/* -------------
* Extra Fields
* -------------
*/
private var mExtraFieldsList: MutableList<Field> = ArrayList()
private var mOnEditButtonClickListener: ((item: Field)->Unit)? = null
private fun buildViewFromField(extraField: Field): View? {
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
val itemView: View? = inflater?.inflate(R.layout.item_entry_edit_extra_field, extraFieldsListView, false)
itemView?.id = View.NO_ID
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
extraFieldValueContainer?.isPasswordVisibilityToggleEnabled = extraField.protectedValue.isProtected
extraFieldValueContainer?.hint = extraField.name
extraFieldValueContainer?.id = View.NO_ID
val extraFieldValue: TextInputEditText? = itemView?.findViewById(R.id.entry_extra_field_value)
extraFieldValue?.apply {
if (extraField.protectedValue.isProtected) {
inputType = extraFieldValue.inputType or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
}
setText(extraField.protectedValue.toString())
if (fontInVisibility)
applyFontVisibility()
}
extraFieldValue?.id = View.NO_ID
extraFieldValue?.tag = "FIELD_VALUE_TAG"
if (mLastFocusedEditField?.field == extraField) {
mExtraViewToRequestFocus = extraFieldValue
}
val extraFieldEditButton: View? = itemView?.findViewById(R.id.entry_extra_field_edit)
extraFieldEditButton?.setOnClickListener {
mOnEditButtonClickListener?.invoke(extraField)
}
extraFieldEditButton?.id = View.NO_ID
return itemView
}
fun getExtraFields(): List<Field> {
mLastFocusedEditField = null
for (index in 0 until extraFieldsListView.childCount) {
val extraFieldValue: EditText = extraFieldsListView.getChildAt(index)
.findViewWithTag("FIELD_VALUE_TAG")
val extraField = mExtraFieldsList[index]
extraField.protectedValue.stringValue = extraFieldValue.text?.toString() ?: ""
if (extraFieldValue.isFocused) {
mLastFocusedEditField = FocusedEditField().apply {
field = extraField
cursorSelectionStart = extraFieldValue.selectionStart
cursorSelectionEnd = extraFieldValue.selectionEnd
}
}
}
return mExtraFieldsList
}
/**
* Remove all children and add new views for each field
*/
fun assignExtraFields(fields: List<Field>,
onEditButtonClickListener: ((item: Field)->Unit)?) {
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
// Reinit focused field
mExtraFieldsList.clear()
mExtraFieldsList.addAll(fields)
extraFieldsListView.removeAllViews()
fields.forEach {
extraFieldsListView.addView(buildViewFromField(it))
}
// Request last focus
mLastFocusedEditField?.let { focusField ->
mExtraViewToRequestFocus?.apply {
requestFocus()
setSelection(focusField.cursorSelectionStart,
focusField.cursorSelectionEnd)
}
}
mLastFocusedEditField = null
mOnEditButtonClickListener = onEditButtonClickListener
}
/**
* Update an extra field or create a new one if doesn't exists
*/
fun putExtraField(extraField: Field) {
extraFieldsContainerView.visibility = View.VISIBLE
val oldField = mExtraFieldsList.firstOrNull { it.name == extraField.name }
oldField?.let {
val index = mExtraFieldsList.indexOf(oldField)
mExtraFieldsList.removeAt(index)
mExtraFieldsList.add(index, extraField)
extraFieldsListView.removeViewAt(index)
val newView = buildViewFromField(extraField)
extraFieldsListView.addView(newView, index)
newView?.requestFocus()
} ?: kotlin.run {
mExtraFieldsList.add(extraField)
val newView = buildViewFromField(extraField)
extraFieldsListView.addView(newView)
newView?.requestFocus()
}
}
fun replaceExtraField(oldExtraField: Field, newExtraField: Field) {
extraFieldsContainerView.visibility = View.VISIBLE
val index = mExtraFieldsList.indexOf(oldExtraField)
mExtraFieldsList.removeAt(index)
mExtraFieldsList.add(index, newExtraField)
extraFieldsListView.removeViewAt(index)
extraFieldsListView.addView(buildViewFromField(newExtraField), index)
}
fun removeExtraField(oldExtraField: Field) {
val previousSize = mExtraFieldsList.size
val index = mExtraFieldsList.indexOf(oldExtraField)
extraFieldsListView.getChildAt(index)?.let {
it.collapse(true) {
mExtraFieldsList.removeAt(index)
extraFieldsListView.removeViewAt(index)
val newSize = mExtraFieldsList.size
if (previousSize > 0 && newSize == 0) {
extraFieldsContainerView.collapse(true)
} else if (previousSize == 0 && newSize == 1) {
extraFieldsContainerView.expand(true)
}
}
}
}
/* -------------
* Attachments
* -------------
*/
fun getAttachments(): List<Attachment> {
return attachmentsAdapter.itemsList.map { it.attachment }
}
fun assignAttachments(attachments: List<Attachment>,
streamDirection: StreamDirection,
onDeleteItem: (attachment: Attachment)->Unit) {
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
attachmentsAdapter.onDeleteButtonClickListener = { item ->
onDeleteItem.invoke(item.attachment)
}
}
fun containsAttachment(): Boolean {
return !attachmentsAdapter.isEmpty()
}
fun containsAttachment(attachment: EntryAttachmentState): Boolean {
return attachmentsAdapter.contains(attachment)
}
fun putAttachment(attachment: EntryAttachmentState) {
attachmentsContainerView.visibility = View.VISIBLE
attachmentsAdapter.putItem(attachment)
}
fun removeAttachment(attachment: EntryAttachmentState) {
attachmentsAdapter.removeItem(attachment)
}
fun clearAttachments() {
attachmentsAdapter.clear()
}
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
attachmentsListView.postDelayed({
position.invoke(attachmentsContainerView.y
+ attachmentsListView.y
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
?: 0F)
)
}, 250)
}
override fun onSaveInstanceState(outState: Bundle) {
populateEntryWithViews()
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
super.onSaveInstanceState(outState)
}
companion object {
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
return EntryEditFragment().apply {
arguments = Bundle().apply {
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
}
}
}
}
}

View File

@@ -45,7 +45,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
@@ -69,7 +69,6 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
// Views
private var coordinatorLayout: CoordinatorLayout? = null
private var fileManagerExplanationButton: View? = null
private var createDatabaseButtonView: View? = null
private var openDatabaseButtonView: View? = null
@@ -82,7 +81,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
private var mDatabaseFileUri: Uri? = null
private var mOpenFileHelper: OpenFileHelper? = null
private var mSelectFileHelper: SelectFileHelper? = null
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
@@ -98,20 +97,15 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
toolbar.title = ""
setSupportActionBar(toolbar)
fileManagerExplanationButton = findViewById(R.id.file_manager_explanation_button)
fileManagerExplanationButton?.setOnClickListener {
UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
}
// Create database button
createDatabaseButtonView = findViewById(R.id.create_database_button)
createDatabaseButtonView?.setOnClickListener { createNewFile() }
// Open database button
mOpenFileHelper = OpenFileHelper(this)
mSelectFileHelper = SelectFileHelper(this)
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
openDatabaseButtonView?.apply {
mOpenFileHelper?.openFileOnClickViewListener?.let {
mSelectFileHelper?.selectFileOnClickViewListener?.let {
setOnClickListener(it)
setOnLongClickListener(it)
}
@@ -389,7 +383,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
if (uri != null) {
launchPasswordActivityWithPath(uri)
}
@@ -445,7 +439,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
openDatabaseButtonView!!,
{tapTargetView ->
tapTargetView?.let {
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
}
},
{}
@@ -454,6 +448,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
}
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
}

View File

@@ -97,6 +97,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}
}
override fun onDetach() {
nodeClickListener = null
onScrollListener = null
super.onDetach()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@@ -44,8 +44,8 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
@@ -64,7 +64,9 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.asError
@@ -92,7 +94,7 @@ open class PasswordActivity : SpecialModeActivity() {
private var mDatabaseKeyFileUri: Uri? = null
private var mRememberKeyFile: Boolean = false
private var mOpenFileHelper: OpenFileHelper? = null
private var mSelectFileHelper: SelectFileHelper? = null
private var mPermissionAsked = false
private var readOnly: Boolean = false
@@ -136,9 +138,9 @@ open class PasswordActivity : SpecialModeActivity() {
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
keyFileSelectionView?.apply {
mOpenFileHelper?.openFileOnClickViewListener?.let {
mSelectFileHelper?.selectFileOnClickViewListener?.let {
setOnClickListener(it)
setOnLongClickListener(it)
}
@@ -747,7 +749,7 @@ open class PasswordActivity : SpecialModeActivity() {
}
var keyFileResult = false
mOpenFileHelper?.let {
mSelectFileHelper?.let {
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
if (uri != null) {

View File

@@ -25,16 +25,19 @@ import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import com.google.android.material.textfield.TextInputLayout
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.text.Editable
import android.text.SpannableStringBuilder
import android.text.TextWatcher
import android.view.View
import android.widget.CompoundButton
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView
class AssignMasterKeyDialogFragment : DialogFragment() {
@@ -56,7 +59,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
private var mListener: AssignPasswordDialogListener? = null
private var mOpenFileHelper: OpenFileHelper? = null
private var mSelectFileHelper: SelectFileHelper? = null
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
private var mNoKeyConfirmationDialog: AlertDialog? = null
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
private val passwordTextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
@@ -85,6 +92,17 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
}
}
override fun onDetach() {
mListener = null
mEmptyPasswordConfirmationDialog?.dismiss()
mEmptyPasswordConfirmationDialog = null
mNoKeyConfirmationDialog?.dismiss()
mNoKeyConfirmationDialog = null
mEmptyKeyFileConfirmationDialog?.dismiss()
mEmptyKeyFileConfirmationDialog = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
@@ -99,11 +117,15 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
rootView = inflater.inflate(R.layout.fragment_set_password, null)
builder.setView(rootView)
.setTitle(R.string.assign_master_key)
// Add action buttons
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
val credentialsInfo: ImageView? = rootView?.findViewById(R.id.credentials_information)
credentialsInfo?.setOnClickListener {
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
}
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
passwordView = rootView?.findViewById(R.id.pass_password)
@@ -113,10 +135,10 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
mOpenFileHelper = OpenFileHelper(this)
mSelectFileHelper = SelectFileHelper(this)
keyFileSelectionView?.apply {
setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
setOnLongClickListener(mOpenFileHelper?.openFileOnClickViewListener)
setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
}
val dialog = builder.create()
@@ -129,7 +151,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
mMasterPassword = ""
mKeyFile = null
var error = verifyPassword() || verifyFile()
var error = verifyPassword() || verifyKeyFile()
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
error = true
if (allowNoMasterKey)
@@ -199,7 +221,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
return error
}
private fun verifyFile(): Boolean {
private fun verifyKeyFile(): Boolean {
var error = false
if (keyFileCheckBox != null
&& keyFileCheckBox!!.isChecked) {
@@ -219,7 +241,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
val builder = AlertDialog.Builder(it)
builder.setMessage(R.string.warning_empty_password)
.setPositiveButton(android.R.string.ok) { _, _ ->
if (!verifyFile()) {
if (!verifyKeyFile()) {
mListener?.onAssignKeyDialogPositiveClick(
passwordCheckBox!!.isChecked, mMasterPassword,
keyFileCheckBox!!.isChecked, mKeyFile)
@@ -227,7 +249,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
}
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
builder.create().show()
mEmptyPasswordConfirmationDialog = builder.create()
mEmptyPasswordConfirmationDialog?.show()
}
}
@@ -242,18 +265,44 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
this@AssignMasterKeyDialogFragment.dismiss()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
builder.create().show()
mNoKeyConfirmationDialog = builder.create()
mNoKeyConfirmationDialog?.show()
}
}
private fun showEmptyKeyFileConfirmationDialog() {
activity?.let {
val builder = AlertDialog.Builder(it)
builder.setMessage(SpannableStringBuilder().apply {
append(getString(R.string.warning_empty_keyfile))
append("\n\n")
append(getString(R.string.warning_empty_keyfile_explanation))
append("\n\n")
append(getString(R.string.warning_sure_add_file))
})
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ ->
keyFileCheckBox?.isChecked = false
keyFileSelectionView?.uri = null
}
mEmptyKeyFileConfirmationDialog = builder.create()
mEmptyKeyFileConfirmationDialog?.show()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
uri?.let { pathUri ->
keyFileCheckBox?.isChecked = true
keyFileSelectionView?.uri = pathUri
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
keyFileSelectionView?.error = null
keyFileCheckBox?.isChecked = true
keyFileSelectionView?.uri = pathUri
if (lengthFile <= 0L) {
showEmptyKeyFileConfirmationDialog()
}
}
}
}
}

View File

@@ -24,6 +24,11 @@ class DatePickerFragment : DialogFragment() {
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// Create a new instance of DatePickerDialog and return it
return context?.let {

View File

@@ -46,6 +46,11 @@ class DeleteNodesDialogFragment : DialogFragment() {
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
arguments?.apply {

View File

@@ -22,24 +22,31 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.view.View
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
import android.view.inputmethod.EditorInfo
import android.widget.Button
import android.widget.CompoundButton
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.Field
class EntryCustomFieldDialogFragment: DialogFragment() {
private var oldField: Field? = null
private var entryCustomFieldListener: EntryCustomFieldListener? = null
private var customFieldLabelContainer: TextInputLayout? = null
private var customFieldLabel: TextView? = null
private var customFieldDeleteButton: ImageView? = null
private var customFieldProtectionButton: CompoundButton? = null
override fun onAttach(context: Context) {
@@ -53,22 +60,37 @@ class EntryCustomFieldDialogFragment: DialogFragment() {
}
}
override fun onDetach() {
entryCustomFieldListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_entry_new_field, null)
customFieldLabelContainer = root?.findViewById(R.id.entry_custom_field_label_container)
customFieldLabel = root?.findViewById(R.id.entry_custom_field_label)
customFieldDeleteButton = root?.findViewById(R.id.entry_custom_field_delete)
customFieldProtectionButton = root?.findViewById(R.id.entry_custom_field_protection)
oldField = arguments?.getParcelable(KEY_FIELD)
oldField?.let { oldCustomField ->
customFieldLabel?.text = oldCustomField.name
customFieldProtectionButton?.isChecked = oldCustomField.protectedValue.isProtected
customFieldDeleteButton?.visibility = View.VISIBLE
customFieldDeleteButton?.setOnClickListener {
entryCustomFieldListener?.onDeleteCustomFieldApproved(oldCustomField)
(dialog as AlertDialog?)?.dismiss()
}
} ?: run {
customFieldDeleteButton?.visibility = View.GONE
}
val builder = AlertDialog.Builder(activity)
builder.setView(root)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel) { _, _ ->
entryCustomFieldListener?.onNewCustomFieldCanceled(
customFieldLabel?.text.toString(),
customFieldProtectionButton?.isChecked == true
)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
val dialogCreated = builder.create()
customFieldLabel?.requestFocus()
@@ -102,10 +124,19 @@ class EntryCustomFieldDialogFragment: DialogFragment() {
private fun approveIfValid() {
if (isValid()) {
entryCustomFieldListener?.onNewCustomFieldApproved(
customFieldLabel?.text.toString(),
customFieldProtectionButton?.isChecked == true
)
oldField?.let {
// New property with old value
entryCustomFieldListener?.onEditCustomFieldApproved(it,
Field(customFieldLabel?.text?.toString() ?: "",
ProtectedString(customFieldProtectionButton?.isChecked == true,
it.protectedValue.stringValue))
)
} ?: run {
entryCustomFieldListener?.onNewCustomFieldApproved(
Field(customFieldLabel?.text?.toString() ?: "",
ProtectedString(customFieldProtectionButton?.isChecked == true))
)
}
(dialog as AlertDialog?)?.dismiss()
}
}
@@ -127,13 +158,25 @@ class EntryCustomFieldDialogFragment: DialogFragment() {
}
interface EntryCustomFieldListener {
fun onNewCustomFieldApproved(label: String, protection: Boolean)
fun onNewCustomFieldCanceled(label: String, protection: Boolean)
fun onNewCustomFieldApproved(newField: Field)
fun onEditCustomFieldApproved(oldField: Field, newField: Field)
fun onDeleteCustomFieldApproved(oldField: Field)
}
companion object {
private const val KEY_FIELD = "KEY_FIELD"
fun getInstance(): EntryCustomFieldDialogFragment {
return EntryCustomFieldDialogFragment()
}
fun getInstance(field: Field): EntryCustomFieldDialogFragment {
return EntryCustomFieldDialogFragment().apply {
arguments = Bundle().apply {
putParcelable(KEY_FIELD, field)
}
}
}
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.text.SpannableStringBuilder
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
/**
* Custom Dialog to confirm big file to upload
*/
class FileTooBigDialogFragment : DialogFragment() {
private var mActionChooseListener: ActionChooseListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
// Verify that the host activity implements the callback interface
try {
mActionChooseListener = context as ActionChooseListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + ActionChooseListener::class.java.name)
}
}
override fun onDetach() {
mActionChooseListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity)
builder.setMessage(SpannableStringBuilder().apply {
append(getString(R.string.warning_file_too_big))
append("\n\n")
append(getString(R.string.warning_sure_add_file))
})
builder.setPositiveButton(android.R.string.yes) { _, _ ->
mActionChooseListener?.onValidateUploadFileTooBig(
arguments?.getParcelable(KEY_FILE_URI),
arguments?.getString(KEY_FILE_NAME))
}
builder.setNegativeButton(android.R.string.no) { _, _ ->
dismiss()
}
// Create the AlertDialog object and return it
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
interface ActionChooseListener {
fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?)
}
companion object {
const val MAX_WARNING_BINARY_FILE = 5242880
private const val KEY_FILE_URI = "KEY_FILE_URI"
private const val KEY_FILE_NAME = "KEY_FILE_NAME"
fun build(attachmentToUploadUri: Uri,
fileName: String): FileTooBigDialogFragment {
val fragment = FileTooBigDialogFragment()
fragment.arguments = Bundle().apply {
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
putString(KEY_FILE_NAME, fileName)
}
return fragment
}
}
}

View File

@@ -64,6 +64,11 @@ class GeneratePasswordDialogFragment : DialogFragment() {
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)

View File

@@ -73,7 +73,11 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
throw ClassCastException(context.toString()
+ " must implement " + GroupEditDialogFragment::class.java.name)
}
}
override fun onDetach() {
editGroupListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

View File

@@ -34,6 +34,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.ImageViewCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.icons.IconPack
@@ -56,6 +57,11 @@ class IconPickerDialogFragment : DialogFragment() {
}
}
override fun onDetach() {
iconPickerListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
@@ -132,7 +138,7 @@ class IconPickerDialogFragment : DialogFragment() {
return bundle.getParcelable(KEY_ICON_STANDARD)
}
fun launch(activity: AppCompatActivity) {
fun launch(activity: FragmentActivity) {
// Create an instance of the dialog fragment and show it
val dialog = IconPickerDialogFragment()
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")

View File

@@ -21,20 +21,51 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.AlertDialog
import android.app.Dialog
import android.content.DialogInterface
import android.content.Context
import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
class PasswordEncodingDialogFragment : DialogFragment() {
var positiveButtonClickListener: DialogInterface.OnClickListener? = null
private var mListener: Listener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as Listener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + Listener::class.java.name)
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
val masterPasswordChecked: Boolean = savedInstanceState?.getBoolean(MASTER_PASSWORD_CHECKED_KEY) ?: false
val masterPassword: String? = savedInstanceState?.getString(MASTER_PASSWORD_KEY)
val keyFileChecked: Boolean = savedInstanceState?.getBoolean(KEY_FILE_CHECKED_KEY) ?: false
val keyFile: Uri? = savedInstanceState?.getParcelable(KEY_FILE_URI_KEY)
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener)
builder.setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.onPasswordEncodingValidateListener(
databaseUri,
masterPasswordChecked,
masterPassword,
keyFileChecked,
keyFile
)
}
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
return builder.create()
@@ -42,5 +73,36 @@ class PasswordEncodingDialogFragment : DialogFragment() {
return super.onCreateDialog(savedInstanceState)
}
interface Listener {
fun onPasswordEncodingValidateListener(databaseUri: Uri?,
masterPasswordChecked: Boolean,
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?)
}
companion object {
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
private const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
private const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
private const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
private const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY"
fun getInstance(databaseUri: Uri,
masterPasswordChecked: Boolean,
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?): SortDialogFragment {
val fragment = SortDialogFragment()
fragment.arguments = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri)
putBoolean(MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
putString(MASTER_PASSWORD_KEY, masterPassword)
putBoolean(KEY_FILE_CHECKED_KEY, keyFileChecked)
putParcelable(KEY_FILE_URI_KEY, keyFile)
}
return fragment
}
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.text.SpannableStringBuilder
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Attachment
/**
* Custom Dialog to confirm big file to upload
*/
class ReplaceFileDialogFragment : DialogFragment() {
private var mActionChooseListener: ActionChooseListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
// Verify that the host activity implements the callback interface
try {
mActionChooseListener = context as ActionChooseListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + ActionChooseListener::class.java.name)
}
}
override fun onDetach() {
mActionChooseListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity)
builder.setMessage(SpannableStringBuilder().apply {
append(getString(R.string.warning_replace_file))
append("\n\n")
append(getString(R.string.warning_sure_add_file))
})
builder.setPositiveButton(android.R.string.yes) { _, _ ->
mActionChooseListener?.onValidateReplaceFile(
arguments?.getParcelable(KEY_FILE_URI),
arguments?.getParcelable(KEY_ENTRY_ATTACHMENT))
}
builder.setNegativeButton(android.R.string.no) { _, _ ->
dismiss()
}
// Create the AlertDialog object and return it
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
interface ActionChooseListener {
fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?)
}
companion object {
private const val KEY_FILE_URI = "KEY_FILE_URI"
private const val KEY_ENTRY_ATTACHMENT = "KEY_ENTRY_ATTACHMENT"
fun build(attachmentToUploadUri: Uri,
attachment: Attachment): ReplaceFileDialogFragment {
val fragment = ReplaceFileDialogFragment()
fragment.arguments = Bundle().apply {
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
putParcelable(KEY_ENTRY_ATTACHMENT, attachment)
}
return fragment
}
}
}

View File

@@ -107,6 +107,11 @@ class SetOTPDialogFragment : DialogFragment() {
}
}
override fun onDetach() {
mCreateOTPElementListener = null
super.onDetach()
}
@SuppressLint("ClickableViewAccessibility")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

View File

@@ -54,6 +54,11 @@ class SortDialogFragment : DialogFragment() {
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)

View File

@@ -25,6 +25,11 @@ class TimePickerFragment : DialogFragment() {
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// Create a new instance of DatePickerDialog and return it
return context?.let {

View File

@@ -28,19 +28,20 @@ import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.util.Log
import android.view.MenuItem
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
import com.kunzisoft.keepass.utils.UriUtil
class OpenFileHelper {
class SelectFileHelper {
private var activity: Activity? = null
private var fragment: Fragment? = null
val openFileOnClickViewListener: OpenFileOnClickViewListener
get() = OpenFileOnClickViewListener()
val selectFileOnClickViewListener: SelectFileOnClickViewListener
get() = SelectFileOnClickViewListener()
constructor(context: Activity) {
this.activity = context
@@ -52,7 +53,10 @@ class OpenFileHelper {
this.fragment = context
}
inner class OpenFileOnClickViewListener : View.OnClickListener, View.OnLongClickListener {
inner class SelectFileOnClickViewListener :
View.OnClickListener,
View.OnLongClickListener,
MenuItem.OnMenuItemClickListener {
private fun onAbstractClick(longClick: Boolean = false) {
try {
@@ -85,6 +89,11 @@ class OpenFileHelper {
onAbstractClick(true)
return true
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
onAbstractClick()
return true
}
}
@SuppressLint("InlinedApi")

View File

@@ -32,6 +32,18 @@ abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val contex
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
}
open fun isEmpty(): Boolean {
return itemsList.isEmpty()
}
open fun contains(item: Item): Boolean {
return itemsList.contains(item)
}
open fun indexOf(item: Item): Int {
return itemsList.indexOf(item)
}
open fun putItem(item: Item) {
val previousSize = itemsList.size
if (itemsList.contains(item)) {
@@ -46,13 +58,42 @@ abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val contex
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
}
fun onBindDeleteButton(holder: T, deleteButton: View, item: Item, position: Int) {
/**
* Only replace [oldItem] by [newItem] if [oldItem] exists
*/
open fun replaceItem(oldItem: Item, newItem: Item) {
if (itemsList.contains(oldItem)) {
val index = itemsList.indexOf(oldItem)
itemsList.removeAt(index)
itemsList.add(index, newItem)
notifyItemChanged(index)
}
}
/**
* Only remove [item] if doesn't exists
*/
open fun removeItem(item: Item) {
if (itemsList.contains(item)) {
mItemToRemove = item
notifyItemChanged(itemsList.indexOf(item))
}
}
protected fun performDeletion(holder: T, item: Item): Boolean {
val effectivelyDeletionPerformed = mItemToRemove == item
if (effectivelyDeletionPerformed) {
holder.itemView.collapse(true) {
deleteItem(item)
}
}
return effectivelyDeletionPerformed
}
protected fun onBindDeleteButton(holder: T, deleteButton: View, item: Item, position: Int) {
deleteButton.apply {
visibility = View.VISIBLE
if (mItemToRemove == item) {
holder.itemView.collapse(true) {
deleteItem(item)
}
if (performDeletion(holder, item)) {
setOnClickListener(null)
} else {
setOnClickListener {

View File

@@ -20,39 +20,66 @@
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.content.res.TypedArray
import android.graphics.Color
import android.text.format.Formatter
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
class EntryAttachmentsItemsAdapter(context: Context, private val editable: Boolean)
: AnimatedItemsAdapter<EntryAttachment, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
var onItemClickListener: ((item: EntryAttachment)->Unit)? = null
class EntryAttachmentsItemsAdapter(context: Context)
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
private val mDatabase = Database.getInstance()
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
private var mTitleColor: Int
init {
// Get the primary text color of the theme
val typedValue = TypedValue()
context.theme.resolveAttribute(android.R.attr.textColorPrimary, typedValue, true)
val typedArray: TypedArray = context.obtainStyledAttributes(typedValue.data, intArrayOf(
android.R.attr.textColor))
mTitleColor = typedArray.getColor(0, -1)
typedArray.recycle()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder {
return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false))
}
override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) {
val entryAttachment = itemsList[position]
val entryAttachmentState = itemsList[position]
holder.itemView.visibility = View.VISIBLE
holder.binaryFileTitle.text = entryAttachment.name
holder.binaryFileBroken.apply {
setColorFilter(Color.RED)
visibility = if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
View.VISIBLE
} else {
View.GONE
}
}
holder.binaryFileTitle.text = entryAttachmentState.attachment.name
if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
holder.binaryFileTitle.setTextColor(Color.RED)
} else {
holder.binaryFileTitle.setTextColor(mTitleColor)
}
holder.binaryFileSize.text = Formatter.formatFileSize(context,
entryAttachment.binaryAttachment.length())
entryAttachmentState.attachment.binaryAttachment.length())
holder.binaryFileCompression.apply {
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip
|| entryAttachment.binaryAttachment.isCompressed == true) {
if (entryAttachmentState.attachment.binaryAttachment.isCompressed) {
text = CompressionAlgorithm.GZip.getName(context.resources)
visibility = View.VISIBLE
} else {
@@ -60,42 +87,61 @@ class EntryAttachmentsItemsAdapter(context: Context, private val editable: Boole
visibility = View.GONE
}
}
if (editable) {
holder.binaryFileProgressContainer.visibility = View.GONE
holder.binaryFileDeleteButton.apply {
visibility = View.VISIBLE
onBindDeleteButton(holder, this, entryAttachment, position)
}
} else {
holder.binaryFileProgressContainer.visibility = View.VISIBLE
holder.binaryFileDeleteButton.visibility = View.GONE
holder.binaryFileProgress.apply {
visibility = when (entryAttachment.downloadState) {
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
when (entryAttachmentState.streamDirection) {
StreamDirection.UPLOAD -> {
holder.binaryFileProgressIcon.isActivated = true
when (entryAttachmentState.downloadState) {
AttachmentState.START,
AttachmentState.IN_PROGRESS -> {
holder.binaryFileProgressContainer.visibility = View.VISIBLE
holder.binaryFileProgress.apply {
visibility = View.VISIBLE
progress = entryAttachmentState.downloadProgression
}
holder.binaryFileDeleteButton.apply {
visibility = View.GONE
setOnClickListener(null)
}
}
AttachmentState.NULL,
AttachmentState.ERROR,
AttachmentState.COMPLETE -> {
holder.binaryFileProgressContainer.visibility = View.GONE
holder.binaryFileProgress.visibility = View.GONE
holder.binaryFileDeleteButton.apply {
visibility = View.VISIBLE
onBindDeleteButton(holder, this, entryAttachmentState, position)
}
}
}
progress = entryAttachment.downloadProgression
holder.itemView.setOnClickListener(null)
}
holder.itemView.setOnClickListener {
onItemClickListener?.invoke(entryAttachment)
StreamDirection.DOWNLOAD -> {
holder.binaryFileProgressIcon.isActivated = false
holder.binaryFileProgressContainer.visibility = View.VISIBLE
holder.binaryFileDeleteButton.visibility = View.GONE
holder.binaryFileProgress.apply {
visibility = when (entryAttachmentState.downloadState) {
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
}
progress = entryAttachmentState.downloadProgression
}
holder.itemView.setOnClickListener {
onItemClickListener?.invoke(entryAttachmentState)
}
}
}
}
fun updateProgress(entryAttachment: EntryAttachment) {
val indexEntryAttachment = itemsList.indexOfLast { current -> current.name == entryAttachment.name }
if (indexEntryAttachment != -1) {
itemsList[indexEntryAttachment] = entryAttachment
notifyItemChanged(indexEntryAttachment)
}
}
inner class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var binaryFileBroken: ImageView = itemView.findViewById(R.id.item_attachment_broken)
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
var binaryFileProgressContainer: View = itemView.findViewById(R.id.item_attachment_progress_container)
var binaryFileProgressIcon: ImageView = itemView.findViewById(R.id.item_attachment_icon)
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
var binaryFileDeleteButton: View = itemView.findViewById(R.id.item_attachment_delete_button)
}

View File

@@ -1,186 +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.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.core.widget.doOnTextChanged
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.model.FocusedEditField
import com.kunzisoft.keepass.view.EditTextSelectable
import com.kunzisoft.keepass.view.applyFontVisibility
class EntryExtraFieldsItemsAdapter(context: Context)
: AnimatedItemsAdapter<Field, EntryExtraFieldsItemsAdapter.EntryExtraFieldViewHolder>(context) {
var applyFontVisibility = false
set(value) {
field = value
notifyDataSetChanged()
}
private var mValueViewInputType: Int = 0
private var mLastFocusedEditField = FocusedEditField()
private var mLastFocusedTimestamp: Long = 0L
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryExtraFieldViewHolder {
val view = EntryExtraFieldViewHolder(
inflater.inflate(R.layout.item_entry_edit_extra_field, parent, false)
)
mValueViewInputType = view.extraFieldValue.inputType
return view
}
override fun onBindViewHolder(holder: EntryExtraFieldViewHolder, position: Int) {
val extraField = itemsList[position]
holder.itemView.visibility = View.VISIBLE
if (extraField.protectedValue.isProtected) {
holder.extraFieldValueContainer.isPasswordVisibilityToggleEnabled = true
holder.extraFieldValue.inputType = EditorInfo.TYPE_TEXT_VARIATION_PASSWORD or mValueViewInputType
} else {
holder.extraFieldValueContainer.isPasswordVisibilityToggleEnabled = false
holder.extraFieldValue.inputType = mValueViewInputType
}
holder.extraFieldValueContainer.hint = extraField.name
holder.extraFieldValue.apply {
setText(extraField.protectedValue.toString())
// To Fix focus in RecyclerView
setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
setFocusField(extraField, selectionStart, selectionEnd)
} else {
// request focus on last text focused
if (focusedTimestampNotExpired()) {
requestFocusField(this, extraField, false)
} else {
removeFocusField(extraField)
}
}
}
addOnSelectionChangedListener(object: EditTextSelectable.OnSelectionChangedListener {
override fun onSelectionChanged(start: Int, end: Int) {
mLastFocusedEditField.apply {
cursorSelectionStart = start
cursorSelectionEnd = end
}
}
})
requestFocusField(this, extraField, true)
doOnTextChanged { text, _, _, _ ->
extraField.protectedValue.stringValue = text.toString()
}
if (applyFontVisibility)
applyFontVisibility()
}
holder.extraFieldDeleteButton.apply {
onBindDeleteButton(holder, this, extraField, position)
}
}
fun assignItems(items: List<Field>, focusedEditField: FocusedEditField?) {
focusedEditField?.let {
setFocusField(it, true)
}
super.assignItems(items)
}
override fun putItem(item: Field) {
setFocusField(mLastFocusedEditField.apply {
field = item
cursorSelectionStart = -1
cursorSelectionEnd = -1
}, true)
super.putItem(item)
}
private fun setFocusField(field: Field,
selectionStart: Int,
selectionEnd: Int,
force: Boolean = false) {
mLastFocusedEditField.apply {
this.field = field
this.cursorSelectionStart = selectionStart
this.cursorSelectionEnd = selectionEnd
}
setFocusField(mLastFocusedEditField, force)
}
private fun setFocusField(field: FocusedEditField, force: Boolean = false) {
mLastFocusedEditField = field
mLastFocusedTimestamp = if (force) 0L else System.currentTimeMillis()
}
private fun removeFocusField(field: Field? = null) {
if (field == null || mLastFocusedEditField.field == field) {
mLastFocusedEditField.destroy()
}
}
private fun requestFocusField(editText: EditText, field: Field, setSelection: Boolean) {
if (field == mLastFocusedEditField.field) {
editText.apply {
post {
if (setSelection) {
setEditTextSelection(editText)
}
requestFocus()
}
}
}
}
private fun setEditTextSelection(editText: EditText) {
try {
var newCursorPositionStart = mLastFocusedEditField.cursorSelectionStart
var newCursorPositionEnd = mLastFocusedEditField.cursorSelectionEnd
// Cursor at end if 0 or less
if (newCursorPositionStart < 0 || newCursorPositionEnd < 0) {
newCursorPositionStart = (editText.text?:"").length
newCursorPositionEnd = newCursorPositionStart
}
editText.setSelection(newCursorPositionStart, newCursorPositionEnd)
} catch (ignoredException: Exception) {}
}
private fun focusedTimestampNotExpired(): Boolean {
return mLastFocusedTimestamp == 0L || (mLastFocusedTimestamp + FOCUS_TIMESTAMP) > System.currentTimeMillis()
}
fun getFocusedField(): FocusedEditField {
return mLastFocusedEditField
}
class EntryExtraFieldViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var extraFieldValueContainer: TextInputLayout = itemView.findViewById(R.id.entry_extra_field_value_container)
var extraFieldValue: EditTextSelectable = itemView.findViewById(R.id.entry_extra_field_value)
var extraFieldDeleteButton: View = itemView.findViewById(R.id.entry_extra_field_delete)
}
companion object {
// time to focus element when a keyboard appears
private const val FOCUS_TIMESTAMP = 400L
}
}

View File

@@ -101,9 +101,9 @@ class FileDatabaseHistoryAdapter(context: Context)
// Modification
databaseFile.databaseLastModified?.let {
holder.fileModification.text = it
holder.fileModification.visibility = View.VISIBLE
holder.fileModificationContainer.visibility = View.VISIBLE
} ?: run {
holder.fileModification.visibility = View.GONE
holder.fileModificationContainer.visibility = View.GONE
}
// Size
@@ -234,7 +234,7 @@ class FileDatabaseHistoryAdapter(context: Context)
this.saveAliasListener = listener
}
inner class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)
@@ -250,6 +250,7 @@ class FileDatabaseHistoryAdapter(context: Context)
var fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
var filePath: TextView = itemView.findViewById(R.id.file_path)
var fileModificationContainer: ViewGroup = itemView.findViewById(R.id.file_modification_container)
var fileModification: TextView = itemView.findViewById(R.id.file_modification)
var fileSize: TextView = itemView.findViewById(R.id.file_size)
}

View File

@@ -338,6 +338,9 @@ class NodeAdapter (private val context: Context)
}
}
holder.attachmentIcon?.visibility =
if (entry.containsAttachment()) View.VISIBLE else View.GONE
mDatabase.stopManageEntry(entry)
}
@@ -391,6 +394,7 @@ class NodeAdapter (private val context: Context)
var text: TextView = itemView.findViewById(R.id.node_text)
var subText: TextView = itemView.findViewById(R.id.node_subtext)
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
}
companion object {

View File

@@ -119,15 +119,19 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
IOActionTask(
{
databaseFileToAddOrUpdate.databaseUri?.let { databaseUri ->
// Try to get info in database first
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
// Complete alias if not exists
val fileDatabaseHistory = FileDatabaseHistoryEntity(
databaseUri.toString(),
databaseFileToAddOrUpdate.databaseAlias ?: "",
databaseFileToAddOrUpdate.databaseAlias
?: fileDatabaseHistoryRetrieve?.databaseAlias
?: "",
databaseFileToAddOrUpdate.keyFileUri?.toString(),
System.currentTimeMillis()
)
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(fileDatabaseHistory.databaseUri)
// Update values if history element not yet in the database
if (fileDatabaseHistoryRetrieve == null) {
databaseFileHistoryDao.add(fileDatabaseHistory)

View File

@@ -23,7 +23,6 @@ import android.content.*
import android.content.Context.BIND_ABOVE_CLIENT
import android.content.Context.BIND_NOT_FOREGROUND
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import androidx.fragment.app.FragmentActivity
@@ -46,6 +45,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
@@ -463,6 +463,13 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
}
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
}
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
newMaxHistoryItems: Int,
save: Boolean) {

View File

@@ -0,0 +1,40 @@
/*
* 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.action
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
class RemoveUnlinkedDataDatabaseRunnable (
context: Context,
database: Database,
saveDatabase: Boolean)
: SaveDatabaseRunnable(context, database, saveDatabase) {
override fun onActionRun() {
try {
database.removeUnlinkedAttachments()
} catch (e: Exception) {
setError(e)
}
super.onActionRun()
}
}

View File

@@ -34,7 +34,7 @@ class DeleteEntryHistoryDatabaseRunnable (
override fun onStartRun() {
try {
mainEntry.removeEntryFromHistory(entryHistoryPosition)
database.removeEntryHistory(mainEntry, entryHistoryPosition)
} catch (e: Exception) {
setError(e)
}

View File

@@ -64,6 +64,10 @@ class DeleteNodesRunnable(context: Context,
} else {
database.deleteEntry(currentNode)
}
// Remove the oldest attachments
currentNode.getAttachments(database.binaryPool).forEach {
database.removeAttachmentIfNotUsed(it)
}
}
}
}

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.database.action.node
import android.content.Context
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.Node
@@ -40,16 +41,34 @@ class UpdateEntryRunnable constructor(
// WARNING : Re attribute parent removed in entry edit activity to save memory
mNewEntry.addParentFrom(mOldEntry)
// Build oldest attachments
val oldEntryAttachments = mOldEntry.getAttachments(database.binaryPool, true)
val newEntryAttachments = mNewEntry.getAttachments(database.binaryPool, true)
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
// Not use equals because only check name
newEntryAttachments.forEach { newAttachment ->
oldEntryAttachments.forEach { oldAttachment ->
if (oldAttachment.name == newAttachment.name
&& oldAttachment.binaryAttachment == newAttachment.binaryAttachment)
attachmentsToRemove.remove(oldAttachment)
}
}
// Update entry with new values
mOldEntry.updateWith(mNewEntry)
mNewEntry.touch(modified = true, touchParents = true)
// Create an entry history (an entry history don't have history)
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
database.removeOldestEntryHistory(mOldEntry)
database.removeOldestEntryHistory(mOldEntry, database.binaryPool)
// Only change data in index
database.updateEntry(mOldEntry)
// Remove oldest attachments
attachmentsToRemove.forEach {
database.removeAttachmentIfNotUsed(it)
}
}
override fun nodeFinish(): ActionNodesValues {

View File

@@ -0,0 +1,69 @@
/*
* 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.element
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
data class Attachment(var name: String,
var binaryAttachment: BinaryAttachment) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString() ?: "",
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(name)
parcel.writeParcelable(binaryAttachment, flags)
}
override fun describeContents(): Int {
return 0
}
override fun toString(): String {
return "$name at $binaryAttachment"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Attachment) return false
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
return name.hashCode()
}
companion object CREATOR : Parcelable.Creator<Attachment> {
override fun createFromParcel(parcel: Parcel): Attachment {
return Attachment(parcel)
}
override fun newArray(size: Int): Array<Attachment?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -25,9 +25,7 @@ import android.net.Uri
import android.util.Log
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.*
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdInt
@@ -52,6 +50,7 @@ import com.kunzisoft.keepass.utils.SingletonHolder
import com.kunzisoft.keepass.utils.UriUtil
import java.io.*
import java.util.*
import kotlin.collections.ArrayList
class Database {
@@ -157,6 +156,17 @@ class Database {
}
}
fun compressionForNewEntry(): Boolean {
if (mDatabaseKDB != null)
return false
// Default compression not necessary if stored in header
mDatabaseKDBX?.let {
return it.compressionAlgorithm == CompressionAlgorithm.GZip
&& it.kdbxVersion.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()
}
return false
}
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm) {
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
@@ -268,14 +278,14 @@ class Database {
}
/**
* Determine if RecycleBin is available or not for this version of database
* @return true if RecycleBin available
* Determine if a configurable RecycleBin is available or not for this version of database
* @return true if a configurable RecycleBin available
*/
val allowRecycleBin: Boolean
val allowConfigurableRecycleBin: Boolean
get() = mDatabaseKDBX != null
var isRecycleBinEnabled: Boolean
// TODO #394 isRecycleBinEnabled mDatabaseKDB
// Backup is always enabled in KDB database
get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false
set(value) {
mDatabaseKDBX?.isRecycleBinEnabled = value
@@ -293,12 +303,12 @@ class Database {
}
fun ensureRecycleBinExists(resources: Resources) {
mDatabaseKDB?.ensureRecycleBinExists()
mDatabaseKDB?.ensureBackupExists()
mDatabaseKDBX?.ensureRecycleBinExists(resources)
}
fun removeRecycleBin() {
// TODO #394 delete backup mDatabaseKDB?.removeRecycleBin()
// Don't allow remove backup in KDB
mDatabaseKDBX?.removeRecycleBin()
}
@@ -428,6 +438,38 @@ class Database {
}, omitBackup, max)
}
val binaryPool: BinaryPool
get() {
return mDatabaseKDBX?.binaryPool ?: BinaryPool()
}
val allowMultipleAttachments: Boolean
get() {
if (mDatabaseKDB != null)
return false
if (mDatabaseKDBX != null)
return true
return false
}
fun buildNewBinary(cacheDirectory: File,
enableProtection: Boolean = false,
compressed: Boolean = false): BinaryAttachment? {
return mDatabaseKDB?.buildNewBinary(cacheDirectory)
?: mDatabaseKDBX?.buildNewBinary(cacheDirectory, enableProtection, compressed)
}
fun removeAttachmentIfNotUsed(attachment: Attachment) {
// No need in KDB database because unique attachment by entry
// Don't clear to fix upload multiple times
mDatabaseKDBX?.removeUnlinkedAttachment(attachment.binaryAttachment, false)
}
fun removeUnlinkedAttachments() {
// No check in database KDB because unique attachment by entry
mDatabaseKDBX?.removeUnlinkedAttachments(true)
}
@Throws(DatabaseOutputException::class)
fun saveData(contentResolver: ContentResolver) {
try {
@@ -473,7 +515,7 @@ class Database {
} else {
var outputStream: OutputStream? = null
try {
outputStream = contentResolver.openOutputStream(uri)
outputStream = contentResolver.openOutputStream(uri, "rwt")
outputStream?.let { definedOutputStream ->
val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
@@ -718,7 +760,7 @@ class Database {
fun canRecycle(entry: Entry): Boolean {
var canRecycle: Boolean? = null
entry.entryKDB?.let {
canRecycle = mDatabaseKDB?.canRecycle()
canRecycle = mDatabaseKDB?.canRecycle(it)
}
entry.entryKDBX?.let {
canRecycle = mDatabaseKDBX?.canRecycle(it)
@@ -729,7 +771,7 @@ class Database {
fun canRecycle(group: Group): Boolean {
var canRecycle: Boolean? = null
group.groupKDB?.let {
canRecycle = mDatabaseKDB?.canRecycle()
canRecycle = mDatabaseKDB?.canRecycle(it)
}
group.groupKDBX?.let {
canRecycle = mDatabaseKDBX?.canRecycle(it)
@@ -781,18 +823,25 @@ class Database {
}
}
fun startManageEntry(entry: Entry) {
fun startManageEntry(entry: Entry?) {
mDatabaseKDBX?.let {
entry.startToManageFieldReferences(it)
entry?.startToManageFieldReferences(it)
}
}
fun stopManageEntry(entry: Entry) {
fun stopManageEntry(entry: Entry?) {
mDatabaseKDBX?.let {
entry.stopToManageFieldReferences()
entry?.stopToManageFieldReferences()
}
}
/**
* @return true if database allows custom field
*/
fun allowEntryCustomFields(): Boolean {
return mDatabaseKDBX != null
}
/**
* Remove oldest history for each entry if more than max items or max memory
*/
@@ -800,7 +849,7 @@ class Database {
rootGroup?.doForEachChildAndForIt(
object : NodeHandler<Entry>() {
override fun operate(node: Entry): Boolean {
removeOldestEntryHistory(node)
removeOldestEntryHistory(node, binaryPool)
return true
}
},
@@ -808,34 +857,19 @@ class Database {
override fun operate(node: Group): Boolean {
return true
}
})
}
fun removeEachEntryHistory() {
rootGroup?.doForEachChildAndForIt(
object : NodeHandler<Entry>() {
override fun operate(node: Entry): Boolean {
node.removeAllHistory()
return true
}
},
object : NodeHandler<Group>() {
override fun operate(node: Group): Boolean {
return true
}
})
}
)
}
/**
* Remove oldest history if more than max items or max memory
*/
fun removeOldestEntryHistory(entry: Entry) {
fun removeOldestEntryHistory(entry: Entry, binaryPool: BinaryPool) {
mDatabaseKDBX?.let {
val maxItems = historyMaxItems
if (maxItems >= 0) {
while (entry.getHistory().size > maxItems) {
entry.removeOldestEntryFromHistory()
removeOldestEntryHistory(entry)
}
}
@@ -844,11 +878,10 @@ class Database {
while (true) {
var historySize: Long = 0
for (entryHistory in entry.getHistory()) {
historySize += entryHistory.getSize()
historySize += entryHistory.getSize(binaryPool)
}
if (historySize > maxSize) {
entry.removeOldestEntryFromHistory()
removeOldestEntryHistory(entry)
} else {
break
}
@@ -857,6 +890,22 @@ class Database {
}
}
private fun removeOldestEntryHistory(entry: Entry) {
entry.removeOldestEntryFromHistory()?.let {
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
removeAttachmentIfNotUsed(attachmentToRemove)
}
}
}
fun removeEntryHistory(entry: Entry, entryHistoryPosition: Int) {
entry.removeEntryFromHistory(entryHistoryPosition)?.let {
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
removeAttachmentIfNotUsed(attachmentToRemove)
}
}
}
companion object : SingletonHolder<Database>(::Database) {
private val TAG = Database::class.java.name

View File

@@ -23,6 +23,8 @@ import android.content.res.Resources
import android.os.Parcel
import android.os.Parcelable
import androidx.core.os.ConfigurationCompat
import org.joda.time.Duration
import org.joda.time.Instant
import java.text.SimpleDateFormat
import java.util.*
@@ -95,6 +97,7 @@ class DateInstant : Parcelable {
companion object {
val NEVER_EXPIRE = neverExpire
val IN_ONE_MONTH = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
private val dateFormat = SimpleDateFormat.getDateTimeInstance()
private val neverExpire: DateInstant

View File

@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.database.element
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.BinaryPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
@@ -32,8 +33,6 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.otp.OtpElement
@@ -284,73 +283,88 @@ class Entry : Node, EntryVersionedInterface<Group> {
}
/**
* Retrieve custom fields to show, key is the label, value is the value of field (protected or not)
* Retrieve extra fields to show, key is the label, value is the value of field (protected or not)
* @return Map of label/value
*/
val customFields: HashMap<String, ProtectedString>
get() = entryKDBX?.customFields ?: HashMap()
/**
* To redefine if version of entry allow custom field,
* @return true if entry allows custom field
*/
fun allowCustomFields(): Boolean {
return entryKDBX?.allowCustomFields() ?: false
}
fun removeAllFields() {
entryKDBX?.removeAllFields()
fun getExtraFields(): List<Field> {
val extraFields = ArrayList<Field>()
entryKDBX?.let {
for (field in it.customFields) {
extraFields.add(Field(field.key, field.value))
}
}
return extraFields
}
/**
* Update or add an extra field to the list (standard or custom)
* @param label Label of field, must be unique
* @param value Value of field
*/
fun putExtraField(label: String, value: ProtectedString) {
entryKDBX?.putExtraField(label, value)
fun putExtraField(field: Field) {
entryKDBX?.putExtraField(field.name, field.protectedValue)
}
fun getOtpElement(): OtpElement? {
return OtpEntryFields.parseFields { key ->
customFields[key]?.toString()
private fun addExtraFields(fields: List<Field>) {
fields.forEach {
putExtraField(it)
}
}
fun startToManageFieldReferences(db: DatabaseKDBX) {
entryKDBX?.startToManageFieldReferences(db)
private fun removeAllFields() {
entryKDBX?.removeAllFields()
}
fun getOtpElement(): OtpElement? {
entryKDBX?.let {
return OtpEntryFields.parseFields { key ->
it.customFields[key]?.toString()
}
}
return null
}
fun startToManageFieldReferences(database: DatabaseKDBX) {
entryKDBX?.startToManageFieldReferences(database)
}
fun stopToManageFieldReferences() {
entryKDBX?.stopToManageFieldReferences()
}
fun getAttachments(): ArrayList<EntryAttachment> {
val attachments = ArrayList<EntryAttachment>()
entryKDB?.binaryData?.let { binaryKDB ->
attachments.add(EntryAttachment(entryKDB?.binaryDescription ?: "", binaryKDB))
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> {
val attachments = ArrayList<Attachment>()
entryKDB?.getAttachment()?.let {
attachments.add(it)
}
entryKDBX?.binaries?.let { binariesKDBX ->
for ((key, value) in binariesKDBX) {
attachments.add(EntryAttachment(key, value))
}
entryKDBX?.getAttachments(binaryPool, inHistory)?.let {
attachments.addAll(it)
}
return attachments
}
fun removeAttachment(attachment: EntryAttachment) {
entryKDB?.apply {
if (binaryDescription == attachment.name
&& binaryData == attachment.binaryAttachment) {
binaryDescription = ""
binaryData = null
}
}
fun containsAttachment(): Boolean {
return entryKDB?.containsAttachment() == true
|| entryKDBX?.containsAttachment() == true
}
entryKDBX?.removeProtectedBinary(attachment.name)
private fun addAttachments(binaryPool: BinaryPool, attachments: List<Attachment>) {
attachments.forEach {
putAttachment(it, binaryPool)
}
}
private fun removeAttachment(attachment: Attachment) {
entryKDB?.removeAttachment(attachment)
entryKDBX?.removeAttachment(attachment)
}
private fun removeAllAttachments() {
entryKDB?.removeAttachment()
entryKDBX?.removeAttachments()
}
private fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
entryKDB?.putAttachment(attachment)
entryKDBX?.putAttachment(attachment, binaryPool)
}
fun getHistory(): ArrayList<Entry> {
@@ -368,20 +382,22 @@ class Entry : Node, EntryVersionedInterface<Group> {
}
}
fun removeEntryFromHistory(position: Int) {
entryKDBX?.removeEntryFromHistory(position)
fun removeEntryFromHistory(position: Int): Entry? {
entryKDBX?.removeEntryFromHistory(position)?.let {
return Entry(it)
}
return null
}
fun removeAllHistory() {
entryKDBX?.removeAllHistory()
fun removeOldestEntryFromHistory(): Entry? {
entryKDBX?.removeOldestEntryFromHistory()?.let {
return Entry(it)
}
return null
}
fun removeOldestEntryFromHistory() {
entryKDBX?.removeOldestEntryFromHistory()
}
fun getSize(): Long {
return entryKDBX?.size ?: 0L
fun getSize(binaryPool: BinaryPool): Long {
return entryKDBX?.getSize(binaryPool) ?: 0L
}
fun containsCustomData(): Boolean {
@@ -404,26 +420,54 @@ class Entry : Node, EntryVersionedInterface<Group> {
database?.stopManageEntry(this)
else
database?.startManageEntry(this)
entryInfo.id = nodeId.toString()
entryInfo.title = title
entryInfo.icon = icon
entryInfo.username = username
entryInfo.password = password
entryInfo.expires = expires
entryInfo.expiryTime = expiryTime
entryInfo.url = url
entryInfo.notes = notes
for (entry in customFields.entries) {
entryInfo.customFields.add(
Field(entry.key, entry.value))
}
entryInfo.customFields = getExtraFields()
// Add otpElement to generate token
entryInfo.otpModel = getOtpElement()?.otpModel
// Replace parameter fields by generated OTP fields
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
if (!raw) {
// Replace parameter fields by generated OTP fields
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
}
database?.binaryPool?.let { binaryPool ->
entryInfo.attachments = getAttachments(binaryPool)
}
if (!raw)
database?.stopManageEntry(this)
return entryInfo
}
fun setEntryInfo(database: Database?, newEntryInfo: EntryInfo) {
database?.startManageEntry(this)
removeAllFields()
removeAllAttachments()
// NodeId stay as is
title = newEntryInfo.title
icon = newEntryInfo.icon
username = newEntryInfo.username
password = newEntryInfo.password
expires = newEntryInfo.expires
expiryTime = newEntryInfo.expiryTime
url = newEntryInfo.url
notes = newEntryInfo.notes
addExtraFields(newEntryInfo.customFields)
database?.binaryPool?.let { binaryPool ->
addAttachments(binaryPool, newEntryInfo.attachments)
}
database?.stopManageEntry(this)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

View File

@@ -17,10 +17,8 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element.security
package com.kunzisoft.keepass.database.element.database
import android.content.ContentResolver
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.stream.readBytes
@@ -30,10 +28,11 @@ import java.util.zip.GZIPOutputStream
class BinaryAttachment : Parcelable {
var isCompressed: Boolean? = null
var isCompressed: Boolean = false
private set
var isProtected: Boolean = false
private set
var isCorrupted: Boolean = false
private var dataFile: File? = null
fun length(): Long {
@@ -45,13 +44,9 @@ class BinaryAttachment : Parcelable {
/**
* Empty protected binary
*/
constructor() {
this.isCompressed = null
this.isProtected = false
this.dataFile = null
}
constructor()
constructor(dataFile: File, enableProtection: Boolean = false, compressed: Boolean? = null) {
constructor(dataFile: File, enableProtection: Boolean = false, compressed: Boolean = false) {
this.isCompressed = compressed
this.isProtected = enableProtection
this.dataFile = dataFile
@@ -59,8 +54,9 @@ class BinaryAttachment : Parcelable {
private constructor(parcel: Parcel) {
val compressedByte = parcel.readByte().toInt()
isCompressed = if (compressedByte == 2) null else compressedByte != 0
isCompressed = compressedByte != 0
isProtected = parcel.readByte().toInt() != 0
isCorrupted = parcel.readByte().toInt() != 0
parcel.readString()?.let {
dataFile = File(it)
}
@@ -74,32 +70,51 @@ class BinaryAttachment : Parcelable {
}
}
@Throws(IOException::class)
fun getUnGzipInputDataStream(): InputStream {
return if (isCompressed)
GZIPInputStream(getInputDataStream())
else
getInputDataStream()
}
@Throws(IOException::class)
fun getOutputDataStream(): OutputStream {
return when {
dataFile != null -> FileOutputStream(dataFile!!)
else -> throw IOException("Unable to write in an unknown file")
}
}
@Throws(IOException::class)
fun getGzipOutputDataStream(): OutputStream {
return if (isCompressed) {
GZIPOutputStream(getOutputDataStream())
} else {
getOutputDataStream()
}
}
@Throws(IOException::class)
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
dataFile?.let { concreteDataFile ->
// To compress, create a new binary with file
if (isCompressed != true) {
if (!isCompressed) {
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
var outputStream: GZIPOutputStream? = null
var inputStream: InputStream? = null
try {
outputStream = GZIPOutputStream(FileOutputStream(fileBinaryCompress))
inputStream = getInputDataStream()
inputStream.readBytes(bufferSize) { buffer ->
outputStream.write(buffer)
}
} finally {
inputStream?.close()
outputStream?.close()
// Remove unGzip file
if (concreteDataFile.delete()) {
if (fileBinaryCompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = true
GZIPOutputStream(FileOutputStream(fileBinaryCompress)).use { outputStream ->
getInputDataStream().use { inputStream ->
inputStream.readBytes(bufferSize) { buffer ->
outputStream.write(buffer)
}
}
}
// Remove unGzip file
if (concreteDataFile.delete()) {
if (fileBinaryCompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = true
}
}
}
}
}
@@ -107,52 +122,20 @@ class BinaryAttachment : Parcelable {
@Throws(IOException::class)
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
dataFile?.let { concreteDataFile ->
if (isCompressed != false) {
if (isCompressed) {
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
var outputStream: FileOutputStream? = null
var inputStream: GZIPInputStream? = null
try {
outputStream = FileOutputStream(fileBinaryDecompress)
inputStream = GZIPInputStream(getInputDataStream())
inputStream.readBytes(bufferSize) { buffer ->
outputStream.write(buffer)
}
} finally {
inputStream?.close()
outputStream?.close()
// Remove gzip file
if (concreteDataFile.delete()) {
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = false
FileOutputStream(fileBinaryDecompress).use { outputStream ->
getUnGzipInputDataStream().use { inputStream ->
inputStream.readBytes(bufferSize) { buffer ->
outputStream.write(buffer)
}
}
}
}
}
}
fun download(createdFileUri: Uri,
contentResolver: ContentResolver,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
update: ((percent: Int)->Unit)? = null) {
var dataDownloaded = 0
contentResolver.openOutputStream(createdFileUri).use { outputStream ->
outputStream?.let { fileOutputStream ->
if (isCompressed == true) {
GZIPInputStream(getInputDataStream())
} else {
getInputDataStream()
}.use { inputStream ->
inputStream.readBytes(bufferSize) { buffer ->
fileOutputStream.write(buffer)
dataDownloaded += buffer.size
try {
val percentDownload = (100 * dataDownloaded / length()).toInt()
update?.invoke(percentDownload)
} catch (e: Exception) {}
// Remove gzip file
if (concreteDataFile.delete()) {
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = false
}
}
}
@@ -179,25 +162,32 @@ class BinaryAttachment : Parcelable {
return isCompressed == other.isCompressed
&& isProtected == other.isProtected
&& isCorrupted == other.isCorrupted
&& sameData
}
override fun hashCode(): Int {
var result = 0
result = 31 * result + if (isCompressed == null) 2 else if (isCompressed!!) 1 else 0
result = 31 * result + if (isCompressed) 1 else 0
result = 31 * result + if (isProtected) 1 else 0
result = 31 * result + if (isCorrupted) 1 else 0
result = 31 * result + dataFile!!.hashCode()
return result
}
override fun toString(): String {
return dataFile.toString()
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeByte((if (isCompressed == null) 2 else if (isCompressed!!) 1 else 0).toByte())
dest.writeByte((if (isCompressed) 1 else 0).toByte())
dest.writeByte((if (isProtected) 1 else 0).toByte())
dest.writeByte((if (isCorrupted) 1 else 0).toByte())
dest.writeString(dataFile?.absolutePath)
}

View File

@@ -19,52 +19,126 @@
*/
package com.kunzisoft.keepass.database.element.database
import android.util.SparseArray
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import java.io.IOException
class BinaryPool {
private val pool = SparseArray<BinaryAttachment>()
private val pool = LinkedHashMap<Int, BinaryAttachment>()
/**
* To get a binary by the pool key (ref attribute in entry)
*/
operator fun get(key: Int): BinaryAttachment? {
return pool[key]
}
fun put(key: Int, value: BinaryAttachment) {
pool.put(key, value)
/**
* To linked a binary with a pool key, if the pool key doesn't exists, create an unused one
*/
fun put(key: Int?, value: BinaryAttachment) {
if (key == null)
put(value)
else
pool[key] = value
}
fun doForEachBinary(action: (key: Int, binary: BinaryAttachment) -> Unit) {
for (i in 0 until pool.size()) {
action.invoke(i, pool.get(pool.keyAt(i)))
/**
* To put a [binaryAttachment] in the pool,
* if already exists, replace the current one,
* else add it with a new key
*/
fun put(binaryAttachment: BinaryAttachment): Int {
var key = findKey(binaryAttachment)
if (key == null) {
key = findUnusedKey()
}
pool[key] = binaryAttachment
return key
}
/**
* Remove a binary from the pool, the file is not deleted
*/
@Throws(IOException::class)
fun clear() {
doForEachBinary { _, binary ->
binary.clear()
fun remove(binaryAttachment: BinaryAttachment) {
findKey(binaryAttachment)?.let {
pool.remove(it)
}
pool.clear()
// Don't clear attachment here because a file can be used in many BinaryAttachment
}
fun add(fileBinary: BinaryAttachment) {
if (findKey(fileBinary) == null) {
pool.put(findUnusedKey(), fileBinary)
}
}
fun findUnusedKey(): Int {
var unusedKey = pool.size()
while (get(unusedKey) != null)
/**
* Utility method to find an unused key in the pool
*/
private fun findUnusedKey(): Int {
var unusedKey = 0
while (pool[unusedKey] != null)
unusedKey++
return unusedKey
}
fun findKey(pb: BinaryAttachment): Int? {
for (i in 0 until pool.size()) {
if (pool.get(pool.keyAt(i)) == pb) return i
/**
* Return key of [binaryAttachmentToRetrieve] or null if not found
*/
private fun findKey(binaryAttachmentToRetrieve: BinaryAttachment): Int? {
val contains = pool.containsValue(binaryAttachmentToRetrieve)
return if (!contains)
null
else {
for ((key, binary) in pool) {
if (binary == binaryAttachmentToRetrieve) {
return key
}
}
return null
}
return null
}
/**
* Utility method to order binaries and solve index problem in database v4
*/
private fun orderedBinaries(): List<KeyBinary> {
val keyBinaryList = ArrayList<KeyBinary>()
for ((key, binary) in pool) {
keyBinaryList.add(KeyBinary(key, binary))
}
return keyBinaryList
}
/**
* To register a binary with a ref corresponding to an ordered index
*/
fun getBinaryIndexFromKey(key: Int): Int? {
val index = orderedBinaries().indexOfFirst { it.key == key }
return if (index < 0)
null
else
index
}
/**
* Different from doForEach, provide an ordered index to each binary
*/
fun doForEachOrderedBinary(action: (index: Int, keyBinary: KeyBinary) -> Unit) {
orderedBinaries().forEachIndexed(action)
}
/**
* To do an action on each binary in the pool
*/
fun doForEachBinary(action: (binary: BinaryAttachment) -> Unit) {
pool.values.forEach { action.invoke(it) }
}
@Throws(IOException::class)
fun clear() {
doForEachBinary {
it.clear()
}
pool.clear()
}
/**
* Utility data class to order binaries
*/
data class KeyBinary(val key: Int, val binary: BinaryAttachment)
}

View File

@@ -26,8 +26,10 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.stream.NullOutputStream
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.security.DigestOutputStream
@@ -38,7 +40,7 @@ import kotlin.collections.ArrayList
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
var backupGroupId: Int = BACKUP_FOLDER_UNDEFINED_ID
private var backupGroupId: Int = BACKUP_FOLDER_UNDEFINED_ID
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
@@ -57,7 +59,12 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
// Retrieve backup group in index
val backupGroup: GroupKDB?
get() = if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID) null else getGroupById(backupGroupId)
get() {
return if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
null
else
getGroupById(backupGroupId)
}
override val kdfEngine: KdfEngine?
get() = kdfListV3[0]
@@ -177,6 +184,9 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
override fun isInRecycleBin(group: GroupKDB): Boolean {
var currentGroup: GroupKDB? = group
if (backupGroup == null)
return false
if (currentGroup == backupGroup)
return true
@@ -192,10 +202,10 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
}
/**
* Ensure that the recycle bin tree exists, if enabled and create it
* Ensure that the backup tree exists if enabled, and create it
* if it doesn't exist
*/
fun ensureRecycleBinExists() {
fun ensureBackupExists() {
rootGroups.forEach { currentGroup ->
if (currentGroup.level == 0
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
@@ -219,21 +229,25 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
* @param node Node to remove
* @return true if node can be recycle, false elsewhere
*/
// TODO #394 Backup KDB
// fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
fun canRecycle(): Boolean {
fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
if (backupGroup == null)
ensureBackupExists()
if (node == backupGroup)
return false
backupGroup?.let {
if (node.isContainedIn(it))
return false
}
return true
}
fun recycle(group: GroupKDB) {
ensureRecycleBinExists()
removeGroupFrom(group, group.parent)
addGroupTo(group, backupGroup)
group.afterAssignNewParent()
}
fun recycle(entry: EntryKDB) {
ensureRecycleBinExists()
removeEntryFrom(entry, entry.parent)
addEntryTo(entry, backupGroup)
entry.afterAssignNewParent()
@@ -249,6 +263,12 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
addEntryTo(entry, origParent)
}
fun buildNewBinary(cacheDirectory: File): BinaryAttachment {
// Generate an unique new file with timestamp
val fileInCache = File(cacheDirectory, System.currentTimeMillis().toString())
return BinaryAttachment(fileInCache)
}
companion object {
val TYPE = DatabaseKDB::class.java

View File

@@ -29,6 +29,8 @@ import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
@@ -46,6 +48,7 @@ import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.VariantDictionary
import org.w3c.dom.Node
import org.w3c.dom.Text
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.security.MessageDigest
@@ -173,33 +176,51 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
fun changeBinaryCompression(oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm) {
binaryPool.doForEachBinary { key, binary ->
try {
when (oldCompression) {
CompressionAlgorithm.None -> {
when (newCompression) {
CompressionAlgorithm.None -> {
}
CompressionAlgorithm.GZip -> {
// To compress, create a new binary with file
binary.compress(BUFFER_SIZE_BYTES)
}
}
}
when (oldCompression) {
CompressionAlgorithm.None -> {
when (newCompression) {
CompressionAlgorithm.None -> {}
CompressionAlgorithm.GZip -> {
when (newCompression) {
CompressionAlgorithm.None -> {
// To decompress, create a new binary with file
binary.decompress(BUFFER_SIZE_BYTES)
}
CompressionAlgorithm.GZip -> {
}
// Only in databaseV3.1, in databaseV4 the header is zipped during the save
if (kdbxVersion.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) {
compressAllBinaries()
}
}
}
}
CompressionAlgorithm.GZip -> {
// In databaseV4 the header is zipped during the save, so not necessary here
if (kdbxVersion.toKotlinLong() >= FILE_VERSION_32_4.toKotlinLong()) {
decompressAllBinaries()
} else {
when (newCompression) {
CompressionAlgorithm.None -> {
decompressAllBinaries()
}
CompressionAlgorithm.GZip -> {}
}
}
}
}
}
private fun compressAllBinaries() {
binaryPool.doForEachBinary { binary ->
try {
// To compress, create a new binary with file
binary.compress(BUFFER_SIZE_BYTES)
} catch (e: Exception) {
Log.e(TAG, "Unable to change compression for $key")
Log.e(TAG, "Unable to compress $binary", e)
}
}
}
private fun decompressAllBinaries() {
binaryPool.doForEachBinary { binary ->
try {
binary.decompress(BUFFER_SIZE_BYTES)
} catch (e: Exception) {
Log.e(TAG, "Unable to decompress $binary", e)
}
}
}
@@ -536,6 +557,59 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return publicCustomData.size() > 0
}
fun buildNewBinary(cacheDirectory: File,
protection: Boolean,
compression: Boolean,
binaryPoolId: Int? = null): BinaryAttachment {
// New file with current time
val fileInCache = File(cacheDirectory, System.currentTimeMillis().toString())
val binaryAttachment = BinaryAttachment(fileInCache, protection, compression)
// add attachment to pool
binaryPool.put(binaryPoolId, binaryAttachment)
return binaryAttachment
}
fun removeUnlinkedAttachment(binary: BinaryAttachment, clear: Boolean) {
val listBinaries = ArrayList<BinaryAttachment>()
listBinaries.add(binary)
removeUnlinkedAttachments(listBinaries, clear)
}
fun removeUnlinkedAttachments(clear: Boolean) {
removeUnlinkedAttachments(emptyList(), clear)
}
private fun removeUnlinkedAttachments(binaries: List<BinaryAttachment>, clear: Boolean) {
// Build binaries to remove with all binaries known
val binariesToRemove = ArrayList<BinaryAttachment>()
if (binaries.isEmpty()) {
binaryPool.doForEachBinary { binary ->
binariesToRemove.add(binary)
}
} else {
binariesToRemove.addAll(binaries)
}
// Remove binaries from the list
rootGroup?.doForEachChild(object : NodeHandler<EntryKDBX>() {
override fun operate(node: EntryKDBX): Boolean {
node.getAttachments(binaryPool, true).forEach {
binariesToRemove.remove(it.binaryAttachment)
}
return binariesToRemove.isNotEmpty()
}
}, null)
// Effective removing
binariesToRemove.forEach {
try {
binaryPool.remove(it)
if (clear)
it.clear()
} catch (e: Exception) {
Log.w(TAG, "Unable to clean binaries", e)
}
}
}
override fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
if (password == null)
return true
@@ -564,7 +638,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
private const val KeyElementName = "Key"
private const val KeyDataElementName = "Data"
const val BASE_64_FLAG = Base64.NO_WRAP
const val BASE_64_FLAG = Base64.NO_PADDING or Base64.NO_WRAP
const val BUFFER_SIZE_BYTES = 3 * 128
}

View File

@@ -27,7 +27,6 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.KeyFileEmptyDatabaseException
import java.io.*
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
@@ -136,7 +135,6 @@ abstract class DatabaseVersioned<
}
when (keyData.size.toLong()) {
0L -> throw KeyFileEmptyDatabaseException()
32L -> return keyData
64L -> try {
return hexStringToByteArray(String(keyData))

View File

@@ -26,8 +26,10 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBInterface
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.Attachment
import java.util.*
import kotlin.collections.ArrayList
/**
* Structure containing information about one entry.
@@ -135,6 +137,29 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
override val type: Type
get() = Type.ENTRY
fun getAttachment(): Attachment? {
val binary = binaryData
return if (binary != null)
Attachment(binaryDescription, binary)
else null
}
fun containsAttachment(): Boolean {
return binaryData != null
}
fun putAttachment(attachment: Attachment) {
this.binaryDescription = attachment.name
this.binaryData = attachment.binaryAttachment
}
fun removeAttachment(attachment: Attachment? = null) {
if (attachment == null || this.binaryDescription == attachment.name) {
this.binaryDescription = ""
this.binaryData = null
}
}
companion object {
/** Size of byte buffer needed to hold this struct. */

View File

@@ -21,7 +21,9 @@ package com.kunzisoft.keepass.database.element.entry
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.database.BinaryPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImage
@@ -31,11 +33,11 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.utils.ParcelableUtil
import com.kunzisoft.keepass.utils.UnsignedLong
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.LinkedHashMap
class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
@@ -60,8 +62,9 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
}
var iconCustom = IconImageCustom.UNKNOWN_ICON
private var customData = LinkedHashMap<String, String>()
// TODO Private
var fields = LinkedHashMap<String, ProtectedString>()
var binaries = LinkedHashMap<String, BinaryAttachment>()
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
var foregroundColor = ""
var backgroundColor = ""
var overrideURL = ""
@@ -70,36 +73,32 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
var additional = ""
var tags = ""
val size: Long
get() {
var size = FIXED_LENGTH_SIZE
fun getSize(binaryPool: BinaryPool): Long {
var size = FIXED_LENGTH_SIZE
for (entry in fields.entries) {
size += entry.key.length.toLong()
size += entry.value.length().toLong()
}
for ((key, value) in binaries) {
size += key.length.toLong()
size += value.length()
}
size += autoType.defaultSequence.length.toLong()
for ((key, value) in autoType.entrySet()) {
size += key.length.toLong()
size += value.length.toLong()
}
for (entry in history) {
size += entry.size
}
size += overrideURL.length.toLong()
size += tags.length.toLong()
return size
for (entry in fields.entries) {
size += entry.key.length.toLong()
size += entry.value.length().toLong()
}
size += getAttachmentsSize(binaryPool)
size += autoType.defaultSequence.length.toLong()
for ((key, value) in autoType.entrySet()) {
size += key.length.toLong()
size += value.length.toLong()
}
for (entry in history) {
size += entry.getSize(binaryPool)
}
size += overrideURL.length.toLong()
size += tags.length.toLong()
return size
}
override var expires: Boolean = false
constructor() : super()
@@ -110,7 +109,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
customData = ParcelableUtil.readStringParcelableMap(parcel)
fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java)
binaries = ParcelableUtil.readStringParcelableMap(parcel, BinaryAttachment::class.java)
binaries = ParcelableUtil.readStringIntMap(parcel)
foregroundColor = parcel.readString() ?: foregroundColor
backgroundColor = parcel.readString() ?: backgroundColor
overrideURL = parcel.readString() ?: overrideURL
@@ -128,7 +127,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
dest.writeParcelable(locationChanged, flags)
ParcelableUtil.writeStringParcelableMap(dest, customData)
ParcelableUtil.writeStringParcelableMap(dest, flags, fields)
ParcelableUtil.writeStringParcelableMap(dest, flags, binaries)
ParcelableUtil.writeStringIntMap(dest, binaries)
dest.writeString(foregroundColor)
dest.writeString(backgroundColor)
dest.writeString(overrideURL)
@@ -167,8 +166,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
tags = source.tags
}
fun startToManageFieldReferences(db: DatabaseKDBX) {
this.mDatabase = db
fun startToManageFieldReferences(database: DatabaseKDBX) {
this.mDatabase = database
this.mDecodeRef = true
}
@@ -272,10 +271,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
return field
}
fun allowCustomFields(): Boolean {
return true
}
fun removeAllFields() {
fields.clear()
}
@@ -284,16 +279,47 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
fields[label] = value
}
fun putProtectedBinary(key: String, value: BinaryAttachment) {
binaries[key] = value
/**
* It's a list because history labels can be defined multiple times
*/
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> {
val entryAttachmentList = ArrayList<Attachment>()
for ((label, poolId) in binaries) {
binaryPool[poolId]?.let { binary ->
entryAttachmentList.add(Attachment(label, binary))
}
}
if (inHistory) {
history.forEach {
entryAttachmentList.addAll(it.getAttachments(binaryPool, false))
}
}
return entryAttachmentList
}
fun removeProtectedBinary(name: String) {
binaries.remove(name)
fun containsAttachment(): Boolean {
return binaries.isNotEmpty()
}
fun sizeOfHistory(): Int {
return history.size
fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
binaries[attachment.name] = binaryPool.put(attachment.binaryAttachment)
}
fun removeAttachment(attachment: Attachment) {
binaries.remove(attachment.name)
}
fun removeAttachments() {
binaries.clear()
}
private fun getAttachmentsSize(binaryPool: BinaryPool): Long {
var size = 0L
for ((label, poolId) in binaries) {
size += label.length.toLong()
size += binaryPool[poolId]?.length() ?: 0
}
return size
}
override fun putCustomData(key: String, value: String) {
@@ -308,15 +334,11 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
history.add(entry)
}
fun removeEntryFromHistory(position: Int) {
history.removeAt(position)
fun removeEntryFromHistory(position: Int): EntryKDBX? {
return history.removeAt(position)
}
fun removeAllHistory() {
history.clear()
}
fun removeOldestEntryFromHistory() {
fun removeOldestEntryFromHistory(): EntryKDBX? {
var min: Date? = null
var index = -1
@@ -329,9 +351,9 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
}
}
if (index != -1) {
return if (index != -1) {
history.removeAt(index)
}
} else null
}
override fun touch(modified: Boolean, touchParents: Boolean) {

View File

@@ -116,13 +116,6 @@ class InvalidCredentialsDatabaseException : LoadDatabaseException {
constructor(exception: Throwable) : super(exception)
}
class KeyFileEmptyDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.keyfile_is_empty
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class NoMemoryDatabaseException: LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_out_of_memory

View File

@@ -192,10 +192,11 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
}
}
if (fieldID == PwDbHeaderV4Fields.EndOfHeader)
return true
if (fieldData != null)
when (fieldID) {
PwDbHeaderV4Fields.EndOfHeader -> return true
PwDbHeaderV4Fields.CipherID -> setCipher(fieldData)
PwDbHeaderV4Fields.CompressionFlags -> setCompressionFlags(fieldData)

View File

@@ -27,14 +27,12 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeader
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.stream.*
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import org.joda.time.Instant
import java.io.*
import java.security.*
import java.util.*
@@ -282,11 +280,9 @@ class DatabaseInputKDB(cacheDirectory: File,
0x000E -> {
newEntry?.let { entry ->
if (fieldSize > 0) {
// Generate an unique new file with timestamp
val binaryFile = File(cacheDirectory,
Instant.now().millis.toString())
entry.binaryData = BinaryAttachment(binaryFile)
BufferedOutputStream(FileOutputStream(binaryFile)).use { outputStream ->
val binaryAttachment = mDatabaseToOpen.buildNewBinary(cacheDirectory)
entry.binaryData = binaryAttachment
BufferedOutputStream(binaryAttachment.getOutputDataStream()).use { outputStream ->
cipherInputStream.readBytes(fieldSize,
DatabaseKDB.BUFFER_SIZE_BYTES) { buffer ->
outputStream.write(buffer)

View File

@@ -20,12 +20,14 @@
package com.kunzisoft.keepass.database.file.input
import android.util.Base64
import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.crypto.StreamCipherFactory
import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
@@ -35,7 +37,7 @@ 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.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
@@ -49,12 +51,14 @@ import org.bouncycastle.crypto.StreamCipher
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory
import java.io.*
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.UnsupportedEncodingException
import java.nio.charset.Charset
import java.text.ParseException
import java.util.*
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import kotlin.math.min
@@ -68,9 +72,6 @@ class DatabaseInputKDBX(cacheDirectory: File,
private var hashOfHeader: ByteArray? = null
private val unusedCacheFileName: String
get() = mDatabase.binaryPool.findUnusedKey().toString()
private var readNextNode = true
private val ctxGroups = Stack<GroupKDBX>()
private var ctxGroup: GroupKDBX? = null
@@ -233,8 +234,10 @@ class DatabaseInputKDBX(cacheDirectory: File,
var data = ByteArray(0)
if (size > 0) {
if (fieldId != DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
if (fieldId != DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) {
// TODO OOM here
data = dataInputStream.readBytes(size)
}
}
var result = true
@@ -249,18 +252,16 @@ class DatabaseInputKDBX(cacheDirectory: File,
header.innerRandomStreamKey = data
}
DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary -> {
val flag = dataInputStream.readBytes(1)[0].toInt() != 0
val protectedFlag = flag && DatabaseHeaderKDBX.KdbxBinaryFlags.Protected.toInt() != DatabaseHeaderKDBX.KdbxBinaryFlags.None.toInt()
val byteLength = size - 1
// Read in a file
val file = File(cacheDirectory, unusedCacheFileName)
FileOutputStream(file).use { outputStream ->
val protectedFlag = dataInputStream.readBytes(1)[0].toInt() != 0
val byteLength = size - 1
// No compression at this level
val protectedBinary = mDatabase.buildNewBinary(cacheDirectory, protectedFlag, false)
protectedBinary.getOutputDataStream().use { outputStream ->
dataInputStream.readBytes(byteLength, DatabaseKDBX.BUFFER_SIZE_BYTES) { buffer ->
outputStream.write(buffer)
}
}
val protectedBinary = BinaryAttachment(file, protectedFlag)
mDatabase.binaryPool.add(protectedBinary)
}
}
@@ -443,14 +444,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
}
KdbContext.Binaries -> if (name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
val key = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrId)
if (key != null) {
val pbData = readBinary(xpp)
val id = Integer.parseInt(key)
mDatabase.binaryPool.put(id, pbData!!)
} else {
readUnknown(xpp)
}
readBinary(xpp)
} else {
readUnknown(xpp)
}
@@ -749,8 +743,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
if (entryInHistory) {
ctxEntry = ctxHistoryBase
return KdbContext.EntryHistory
}
else if (ctxEntry != null) {
} else if (ctxEntry != null) {
// Add entry to the index only when close the XML element
mDatabase.addEntryIndex(ctxEntry!!)
}
@@ -766,8 +759,9 @@ class DatabaseInputKDBX(cacheDirectory: File,
return KdbContext.Entry
} else if (ctx == KdbContext.EntryBinary && name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
if (ctxBinaryName != null && ctxBinaryValue != null)
ctxEntry?.putProtectedBinary(ctxBinaryName!!, ctxBinaryValue!!)
if (ctxBinaryName != null && ctxBinaryValue != null) {
ctxEntry?.putAttachment(Attachment(ctxBinaryName!!, ctxBinaryValue!!), mDatabase.binaryPool)
}
ctxBinaryName = null
ctxBinaryValue = null
@@ -885,9 +879,14 @@ class DatabaseInputKDBX(cacheDirectory: File,
if (encoded.isEmpty()) {
return DatabaseVersioned.UUID_ZERO
}
val buf = Base64.decode(encoded, BASE_64_FLAG)
return bytes16ToUuid(buf)
return try {
val buf = Base64.decode(encoded, BASE_64_FLAG)
bytes16ToUuid(buf)
} catch (e: Exception) {
Log.e(TAG, "Unable to read base 64 UUID, create a random one", e)
UUID.randomUUID()
}
}
@Throws(IOException::class, XmlPullParserException::class)
@@ -947,50 +946,63 @@ class DatabaseInputKDBX(cacheDirectory: File,
// Reference Id to a binary already present in binary pool
val ref = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrRef)
if (ref != null) {
xpp.next() // Consume end tag
// New id to a binary
val key = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrId)
val id = Integer.parseInt(ref)
return mDatabase.binaryPool[id]
}
// New binary to retrieve
else {
var compressed = false
var protected = false
if (xpp.attributeCount > 0) {
val compress = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrCompressed)
if (compress != null) {
compressed = compress.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
}
val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
if (protect != null) {
protected = protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
}
return when {
ref != null -> {
xpp.next() // Consume end tag
val id = Integer.parseInt(ref)
// A ref is not necessarily an index in Database V3.1
mDatabase.binaryPool[id]
}
val base64 = readString(xpp)
if (base64.isEmpty())
return BinaryAttachment()
val data = Base64.decode(base64, BASE_64_FLAG)
val file = File(cacheDirectory, unusedCacheFileName)
return FileOutputStream(file).use { outputStream ->
// Force compression in this specific case
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip
&& !compressed) {
GZIPOutputStream(outputStream).write(data)
BinaryAttachment(file, protected, true)
} else {
outputStream.write(data)
BinaryAttachment(file, protected, compressed)
}
key != null -> {
createBinary(key.toIntOrNull(), xpp)
}
else -> {
// New binary to retrieve
createBinary(null, xpp)
}
}
}
@Throws(IOException::class, XmlPullParserException::class)
private fun createBinary(binaryId: Int?, xpp: XmlPullParser): BinaryAttachment? {
var compressed = false
var protected = false
if (xpp.attributeCount > 0) {
val compress = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrCompressed)
if (compress != null) {
compressed = compress.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
}
val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
if (protect != null) {
protected = protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
}
}
val base64 = readString(xpp)
if (base64.isEmpty())
return null
// Build the new binary and compress
val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory, protected, compressed, binaryId)
try {
binaryAttachment.getOutputDataStream().use { outputStream ->
outputStream.write(Base64.decode(base64, BASE_64_FLAG))
}
} catch (e: Exception) {
Log.e(TAG, "Unable to read base 64 attachment", e)
binaryAttachment.isCorrupted = true
binaryAttachment.getOutputDataStream().use { outputStream ->
outputStream.write(base64.toByteArray())
}
}
return binaryAttachment
}
@Throws(IOException::class, XmlPullParserException::class)
private fun readString(xpp: XmlPullParser): String {
val buf = readProtectedBase64String(xpp)
@@ -1045,6 +1057,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
companion object {
private val TAG = DatabaseInputKDBX::class.java.name
private val DEFAULT_HISTORY_DAYS = UnsignedInt(365)
@Throws(XmlPullParserException::class)

View File

@@ -47,18 +47,25 @@ class DatabaseInnerHeaderOutputKDBX(private val database: DatabaseKDBX,
dataOutputStream.writeInt(streamKeySize)
dataOutputStream.write(header.innerRandomStreamKey)
database.binaryPool.doForEachBinary { _, protectedBinary ->
database.binaryPool.doForEachOrderedBinary { _, keyBinary ->
val protectedBinary = keyBinary.binary
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
if (protectedBinary.isProtected) {
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
}
// Force decompression to add binary in header
protectedBinary.decompress()
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary.toInt())
dataOutputStream.writeInt(protectedBinary.length().toInt() + 1) // TODO verify
dataOutputStream.writeInt(protectedBinary.length().toInt() + 1)
dataOutputStream.write(flag.toInt())
protectedBinary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
dataOutputStream.write(buffer)
// if was compressed in cache, uncompress it
protectedBinary.getInputDataStream().use { inputStream ->
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
dataOutputStream.write(buffer)
}
}
}

View File

@@ -38,7 +38,7 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
@@ -55,7 +55,6 @@ import java.io.OutputStream
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import java.util.*
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
@@ -422,7 +421,6 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
private fun writeBinary(binary : BinaryAttachment) {
val binaryLength = binary.length()
if (binaryLength > 0) {
if (binary.isProtected) {
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
@@ -433,21 +431,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.text(charArray, 0, charArray.size)
}
} else {
// Force binary compression from database (compression was harmonized during import)
if (mDatabaseKDBX.compressionAlgorithm === CompressionAlgorithm.GZip) {
if (binary.isCompressed) {
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
}
// Force decompression in this specific case
val binaryInputStream = if (mDatabaseKDBX.compressionAlgorithm == CompressionAlgorithm.None
&& binary.isCompressed == true) {
GZIPInputStream(binary.getInputDataStream())
} else {
binary.getInputDataStream()
}
// Write the XML
binaryInputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
binary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray()
xml.text(charArray, 0, charArray.size)
}
@@ -459,10 +447,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
private fun writeMetaBinaries() {
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
mDatabaseKDBX.binaryPool.doForEachBinary { key, binary ->
// Use indexes because necessarily in DatabaseV4 (binary header ref is the order)
mDatabaseKDBX.binaryPool.doForEachOrderedBinary { index, keyBinary ->
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
xml.attribute(null, DatabaseKDBXXML.AttrId, key.toString())
writeBinary(binary)
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
writeBinary(keyBinary.binary)
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
}
@@ -559,23 +548,22 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeEntryBinaries(binaries: Map<String, BinaryAttachment>) {
for ((key, binary) in binaries) {
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
xml.startTag(null, DatabaseKDBXXML.ElemKey)
xml.text(safeXmlString(key))
xml.endTag(null, DatabaseKDBXXML.ElemKey)
private fun writeEntryBinaries(binaries: LinkedHashMap<String, Int>) {
for ((label, poolId) in binaries) {
// Retrieve the right index with the poolId, don't use ref because of header in DatabaseV4
mDatabaseKDBX.binaryPool.getBinaryIndexFromKey(poolId)?.toString()?.let { indexString ->
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
xml.startTag(null, DatabaseKDBXXML.ElemKey)
xml.text(safeXmlString(label))
xml.endTag(null, DatabaseKDBXXML.ElemKey)
xml.startTag(null, DatabaseKDBXXML.ElemValue)
val ref = mDatabaseKDBX.binaryPool.findKey(binary)
if (ref != null) {
xml.attribute(null, DatabaseKDBXXML.AttrRef, ref.toString())
} else {
writeBinary(binary)
xml.startTag(null, DatabaseKDBXXML.ElemValue)
// Use only pool data in Meta to save binaries
xml.attribute(null, DatabaseKDBXXML.AttrRef, indexString)
xml.endTag(null, DatabaseKDBXXML.ElemValue)
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
}
xml.endTag(null, DatabaseKDBXXML.ElemValue)
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
}
}

View File

@@ -95,9 +95,9 @@ open class Education(val activity: Activity) {
R.string.education_entry_edit_key,
R.string.education_password_generator_key,
R.string.education_entry_new_field_key,
R.string.education_add_attachment_key,
R.string.education_setup_OTP_key)
/**
* Get preferences bundle for education
*/
@@ -272,6 +272,18 @@ open class Education(val activity: Activity) {
context.resources.getBoolean(R.bool.education_entry_new_field_default))
}
/**
* Determines whether the explanatory view of the new attachment button in an entry has already been displayed.
*
* @param context The context to open the SharedPreferences
* @return boolean value of education_add_attachment_key key
*/
fun isEducationAddAttachmentPerformed(context: Context): Boolean {
val prefs = getEducationSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.education_add_attachment_key),
context.resources.getBoolean(R.bool.education_add_attachment_default))
}
/**
* Determines whether the explanatory view to setup OTP has already been displayed.
*

View File

@@ -29,6 +29,10 @@ import com.kunzisoft.keepass.R
class EntryEditActivityEducation(activity: Activity)
: Education(activity) {
/**
* Check and display learning views
* Displays the explanation for the password generator
*/
fun checkAndPerformedGeneratePasswordEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
@@ -56,7 +60,7 @@ class EntryEditActivityEducation(activity: Activity)
/**
* Check and display learning views
* Displays the explanation for the icon selection, the password generator and for a new field
* Displays the explanation to create a new field
*/
fun checkAndPerformedEntryNewFieldEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
@@ -83,6 +87,35 @@ class EntryEditActivityEducation(activity: Activity)
R.string.education_entry_new_field_key)
}
/**
* Check and display learning views
* Displays the explanation for to upload attachment
*/
fun checkAndPerformedAttachmentEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationAddAttachmentPerformed(activity),
TapTarget.forView(educationView,
activity.getString(R.string.education_add_attachment_title),
activity.getString(R.string.education_add_attachment_summary))
.textColorInt(Color.WHITE)
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {
override fun onTargetClick(view: TapTargetView) {
super.onTargetClick(view)
onEducationViewClick?.invoke(view)
}
override fun onOuterCircleClick(view: TapTargetView?) {
super.onOuterCircleClick(view)
view?.dismiss(false)
onOuterViewClick?.invoke(view)
}
},
R.string.education_add_attachment_key)
}
/**
* Check and display learning views
* Displays the explanation to setup OTP

View File

@@ -21,24 +21,25 @@ package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.utils.readEnum
import com.kunzisoft.keepass.utils.writeEnum
data class EntryAttachment(var name: String,
var binaryAttachment: BinaryAttachment,
var downloadState: AttachmentState = AttachmentState.NULL,
var downloadProgression: Int = 0) : Parcelable {
data class EntryAttachmentState(var attachment: Attachment,
var streamDirection: StreamDirection,
var downloadState: AttachmentState = AttachmentState.NULL,
var downloadProgression: Int = 0) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString() ?: "",
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment(),
parcel.readParcelable(Attachment::class.java.classLoader) ?: Attachment("", BinaryAttachment()),
parcel.readEnum<StreamDirection>() ?: StreamDirection.DOWNLOAD,
parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL,
parcel.readInt())
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(name)
parcel.writeParcelable(binaryAttachment, flags)
parcel.writeParcelable(attachment, flags)
parcel.writeEnum(streamDirection)
parcel.writeEnum(downloadState)
parcel.writeInt(downloadProgression)
}
@@ -49,26 +50,23 @@ data class EntryAttachment(var name: String,
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is EntryAttachment) return false
if (other !is EntryAttachmentState) return false
if (name != other.name) return false
if (binaryAttachment != other.binaryAttachment) return false
if (attachment != other.attachment) return false
return true
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + binaryAttachment.hashCode()
return result
return attachment.hashCode()
}
companion object CREATOR : Parcelable.Creator<EntryAttachment> {
override fun createFromParcel(parcel: Parcel): EntryAttachment {
return EntryAttachment(parcel)
companion object CREATOR : Parcelable.Creator<EntryAttachmentState> {
override fun createFromParcel(parcel: Parcel): EntryAttachmentState {
return EntryAttachmentState(parcel)
}
override fun newArray(size: Int): Array<EntryAttachment?> {
override fun newArray(size: Int): Array<EntryAttachmentState?> {
return arrayOfNulls(size)
}
}

View File

@@ -21,7 +21,10 @@ package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import java.util.*
@@ -30,12 +33,15 @@ class EntryInfo : Parcelable {
var id: String = ""
var title: String = ""
var icon: IconImage? = null
var icon: IconImage = IconImageStandard()
var username: String = ""
var password: String = ""
var expires: Boolean = false
var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH
var url: String = ""
var notes: String = ""
var customFields: MutableList<Field> = ArrayList()
var customFields: List<Field> = ArrayList()
var attachments: List<Attachment> = ArrayList()
var otpModel: OtpModel? = null
constructor()
@@ -43,12 +49,15 @@ class EntryInfo : Parcelable {
private constructor(parcel: Parcel) {
id = parcel.readString() ?: id
title = parcel.readString() ?: title
icon = parcel.readParcelable(IconImage::class.java.classLoader)
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
username = parcel.readString() ?: username
password = parcel.readString() ?: password
expires = parcel.readInt() != 0
expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime
url = parcel.readString() ?: url
notes = parcel.readString() ?: notes
parcel.readList(customFields as List<Field>, Field::class.java.classLoader)
parcel.readList(customFields, Field::class.java.classLoader)
parcel.readList(attachments, Attachment::class.java.classLoader)
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
}
@@ -62,9 +71,12 @@ class EntryInfo : Parcelable {
parcel.writeParcelable(icon, flags)
parcel.writeString(username)
parcel.writeString(password)
parcel.writeInt(if (expires) 1 else 0)
parcel.writeParcelable(expiryTime, flags)
parcel.writeString(url)
parcel.writeString(notes)
parcel.writeArray(customFields.toTypedArray())
parcel.writeArray(attachments.toTypedArray())
parcel.writeParcelable(otpModel, flags)
}

View File

@@ -0,0 +1,5 @@
package com.kunzisoft.keepass.model
enum class StreamDirection {
UPLOAD, DOWNLOAD
}

View File

@@ -28,17 +28,24 @@ import android.os.IBinder
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.stream.readBytes
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.UriUtil
import kotlinx.coroutines.*
import java.io.BufferedInputStream
import java.util.*
import kotlin.collections.HashMap
import java.util.concurrent.CopyOnWriteArrayList
class AttachmentFileNotificationService: LockNotificationService() {
override val notificationId: Int = 10000
private val attachmentNotificationList = CopyOnWriteArrayList<AttachmentNotification>()
private var mActionTaskBinder = ActionTaskBinder()
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
@@ -51,33 +58,25 @@ class AttachmentFileNotificationService: LockNotificationService() {
fun addActionTaskListener(actionTaskListener: ActionTaskListener) {
mActionTaskListeners.add(actionTaskListener)
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
entry.value.attachmentTask?.onUpdate = { uri, attachment, notificationIdAttach ->
newNotification(uri, attachment, notificationIdAttach)
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentProgress(entry.key, attachment)
}
}
}
})
}
fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
entry.value.attachmentTask?.onUpdate = null
}
})
mActionTaskListeners.remove(actionTaskListener)
}
}
private val attachmentFileActionListener = object: AttachmentFileAction.AttachmentFileActionListener {
override fun onUpdate(attachmentNotification: AttachmentNotification) {
newNotification(attachmentNotification)
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentAction(attachmentNotification.uri,
attachmentNotification.entryAttachmentState)
}
}
}
interface ActionTaskListener {
fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment)
fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState)
}
override fun onBind(intent: Intent): IBinder? {
@@ -87,49 +86,36 @@ class AttachmentFileNotificationService: LockNotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val downloadFileUri: Uri? = if (intent?.hasExtra(DOWNLOAD_FILE_URI_KEY) == true) {
intent.getParcelableExtra(DOWNLOAD_FILE_URI_KEY)
val downloadFileUri: Uri? = if (intent?.hasExtra(FILE_URI_KEY) == true) {
intent.getParcelableExtra(FILE_URI_KEY)
} else null
when(intent?.action) {
ACTION_ATTACHMENT_FILE_START_UPLOAD -> {
actionUploadOrDownload(downloadFileUri,
intent,
StreamDirection.UPLOAD)
}
ACTION_ATTACHMENT_FILE_START_DOWNLOAD -> {
if (downloadFileUri != null
&& intent.hasExtra(ATTACHMENT_KEY)) {
val nextNotificationId = (downloadFileUris.values.maxByOrNull { it.notificationId }
?.notificationId ?: notificationId) + 1
try {
intent.getParcelableExtra<EntryAttachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
val attachmentNotification = AttachmentNotification(nextNotificationId, entryAttachment)
downloadFileUris[downloadFileUri] = attachmentNotification
mainScope.launch {
AttachmentFileActionClass(downloadFileUri,
attachmentNotification,
contentResolver).apply {
onUpdate = { uri, attachment, notificationIdAttach ->
newNotification(uri, attachment, notificationIdAttach)
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentProgress(downloadFileUri, attachment)
}
}
}.executeAction()
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to download $downloadFileUri", e)
actionUploadOrDownload(downloadFileUri,
intent,
StreamDirection.DOWNLOAD)
}
ACTION_ATTACHMENT_REMOVE -> {
intent.getParcelableExtra<Attachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
attachmentNotificationList.firstOrNull { it.entryAttachmentState.attachment == entryAttachment }?.let { elementToRemove ->
attachmentNotificationList.remove(elementToRemove)
}
}
}
else -> {
if (downloadFileUri != null) {
downloadFileUris[downloadFileUri]?.notificationId?.let {
notificationManager?.cancel(it)
downloadFileUris.remove(downloadFileUri)
attachmentNotificationList.firstOrNull { it.uri == downloadFileUri }?.let { elementToRemove ->
notificationManager?.cancel(elementToRemove.notificationId)
attachmentNotificationList.remove(elementToRemove)
}
}
if (downloadFileUris.isEmpty()) {
if (attachmentNotificationList.isEmpty()) {
stopSelf()
}
}
@@ -138,25 +124,35 @@ class AttachmentFileNotificationService: LockNotificationService() {
return START_REDELIVER_INTENT
}
@Synchronized
fun checkCurrentAttachmentProgress() {
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentProgress(entry.key, entry.value.entryAttachment)
}
attachmentNotificationList.forEach { attachmentNotification ->
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentAction(
attachmentNotification.uri,
attachmentNotification.entryAttachmentState
)
}
})
}
}
private fun newNotification(downloadFileUri: Uri,
entryAttachment: EntryAttachment,
notificationIdAttachment: Int) {
@Synchronized
fun removeAttachmentAction(entryAttachment: EntryAttachmentState) {
attachmentNotificationList.firstOrNull {
it.entryAttachmentState == entryAttachment
}?.let {
attachmentNotificationList.remove(it)
}
}
private fun newNotification(attachmentNotification: AttachmentNotification) {
val pendingContentIntent = PendingIntent.getActivity(this,
0,
Intent().apply {
action = Intent.ACTION_VIEW
setDataAndType(downloadFileUri, contentResolver.getType(downloadFileUri))
setDataAndType(attachmentNotification.uri,
contentResolver.getType(attachmentNotification.uri))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}, PendingIntent.FLAG_CANCEL_CURRENT)
@@ -164,54 +160,84 @@ class AttachmentFileNotificationService: LockNotificationService() {
0,
Intent(this, AttachmentFileNotificationService::class.java).apply {
// No action to delete the service
putExtra(DOWNLOAD_FILE_URI_KEY, downloadFileUri)
putExtra(FILE_URI_KEY, attachmentNotification.uri)
}, PendingIntent.FLAG_CANCEL_CURRENT)
val fileName = DocumentFile.fromSingleUri(this, downloadFileUri)?.name ?: ""
val fileName = DocumentFile.fromSingleUri(this, attachmentNotification.uri)?.name ?: ""
val builder = buildNewNotification().apply {
setSmallIcon(R.drawable.ic_file_download_white_24dp)
setContentTitle(getString(R.string.download_attachment, fileName))
when (attachmentNotification.entryAttachmentState.streamDirection) {
StreamDirection.UPLOAD -> {
setSmallIcon(R.drawable.ic_file_upload_white_24dp)
setContentTitle(getString(R.string.upload_attachment, fileName))
}
StreamDirection.DOWNLOAD -> {
setSmallIcon(R.drawable.ic_file_download_white_24dp)
setContentTitle(getString(R.string.download_attachment, fileName))
}
}
setAutoCancel(false)
when (entryAttachment.downloadState) {
when (attachmentNotification.entryAttachmentState.downloadState) {
AttachmentState.NULL, AttachmentState.START -> {
setContentText(getString(R.string.download_initialization))
setOngoing(true)
}
AttachmentState.IN_PROGRESS -> {
if (entryAttachment.downloadProgression > 100) {
if (attachmentNotification.entryAttachmentState.downloadProgression > 100) {
setContentText(getString(R.string.download_finalization))
} else {
setProgress(100, entryAttachment.downloadProgression, false)
setContentText(getString(R.string.download_progression, entryAttachment.downloadProgression))
setProgress(100,
attachmentNotification.entryAttachmentState.downloadProgression,
false)
setContentText(getString(R.string.download_progression,
attachmentNotification.entryAttachmentState.downloadProgression))
}
setOngoing(true)
}
AttachmentState.COMPLETE, AttachmentState.ERROR -> {
AttachmentState.COMPLETE -> {
setContentText(getString(R.string.download_complete))
setContentIntent(pendingContentIntent)
when (attachmentNotification.entryAttachmentState.streamDirection) {
StreamDirection.UPLOAD -> {
}
StreamDirection.DOWNLOAD -> {
setContentIntent(pendingContentIntent)
}
}
setDeleteIntent(pendingDeleteIntent)
setOngoing(false)
}
AttachmentState.ERROR -> {
setContentText(getString(R.string.error_file_not_create))
setOngoing(false)
}
}
}
when (attachmentNotification.entryAttachmentState.downloadState) {
AttachmentState.ERROR,
AttachmentState.COMPLETE -> {
stopForeground(false)
notificationManager?.notify(attachmentNotification.notificationId, builder.build())
} else -> {
startForeground(attachmentNotification.notificationId, builder.build())
}
}
notificationManager?.notify(notificationIdAttachment, builder.build())
}
override fun onDestroy() {
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
entry.value.attachmentTask?.onUpdate = null
notificationManager?.cancel(entry.value.notificationId)
}
})
attachmentNotificationList.forEach { attachmentNotification ->
attachmentNotification.attachmentFileAction?.listener = null
notificationManager?.cancel(attachmentNotification.notificationId)
}
attachmentNotificationList.clear()
super.onDestroy()
}
private data class AttachmentNotification(var notificationId: Int,
var entryAttachment: EntryAttachment,
var attachmentTask: AttachmentFileActionClass? = null) {
private data class AttachmentNotification(var uri: Uri,
var notificationId: Int,
var entryAttachmentState: EntryAttachmentState,
var attachmentFileAction: AttachmentFileAction? = null) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@@ -228,52 +254,90 @@ class AttachmentFileNotificationService: LockNotificationService() {
}
}
private class AttachmentFileActionClass(
private val fileUri: Uri,
private fun actionUploadOrDownload(downloadFileUri: Uri?,
intent: Intent,
streamDirection: StreamDirection) {
if (downloadFileUri != null
&& intent.hasExtra(ATTACHMENT_KEY)) {
try {
intent.getParcelableExtra<Attachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
val nextNotificationId = (attachmentNotificationList.maxByOrNull { it.notificationId }
?.notificationId ?: notificationId) + 1
val entryAttachmentState = EntryAttachmentState(entryAttachment, streamDirection)
val attachmentNotification = AttachmentNotification(downloadFileUri, nextNotificationId, entryAttachmentState)
// Add action to the list on start
attachmentNotificationList.add(attachmentNotification)
mainScope.launch {
AttachmentFileAction(attachmentNotification,
contentResolver).apply {
listener = attachmentFileActionListener
}.executeAction()
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to upload/download $downloadFileUri", e)
}
}
}
private class AttachmentFileAction(
private val attachmentNotification: AttachmentNotification,
private val contentResolver: ContentResolver) {
private val updateMinFrequency = 1000
private var previousSaveTime = System.currentTimeMillis()
var onUpdate: ((Uri, EntryAttachment, Int)->Unit)? = null
var listener: AttachmentFileActionListener? = null
interface AttachmentFileActionListener {
fun onUpdate(attachmentNotification: AttachmentNotification)
}
suspend fun executeAction() {
TimeoutHelper.temporarilyDisableTimeout()
// on pre execute
attachmentNotification.attachmentTask = this
attachmentNotification.entryAttachment.apply {
downloadState = AttachmentState.START
downloadProgression = 0
CoroutineScope(Dispatchers.Main).launch {
TimeoutHelper.temporarilyDisableTimeout()
attachmentNotification.attachmentFileAction = this@AttachmentFileAction
attachmentNotification.entryAttachmentState.apply {
downloadState = AttachmentState.START
downloadProgression = 0
}
listener?.onUpdate(attachmentNotification)
}
onUpdate?.invoke(fileUri,
attachmentNotification.entryAttachment,
attachmentNotification.notificationId)
withContext(Dispatchers.IO) {
// on Progress with thread
val asyncResult: Deferred<Boolean> = async {
var progressResult = true
try {
attachmentNotification.entryAttachment.apply {
attachmentNotification.entryAttachmentState.apply {
downloadState = AttachmentState.IN_PROGRESS
binaryAttachment.download(fileUri, contentResolver, 1024) { percent ->
// Publish progress
val currentTime = System.currentTimeMillis()
if (previousSaveTime + updateMinFrequency < currentTime) {
attachmentNotification.entryAttachment.apply {
downloadState = AttachmentState.IN_PROGRESS
downloadProgression = percent
when (streamDirection) {
StreamDirection.UPLOAD -> {
uploadToDatabase(
attachmentNotification.uri,
attachment.binaryAttachment,
contentResolver, 1024) { percent ->
publishProgress(percent)
}
}
StreamDirection.DOWNLOAD -> {
downloadFromDatabase(
attachmentNotification.uri,
attachment.binaryAttachment,
contentResolver, 1024) { percent ->
publishProgress(percent)
}
onUpdate?.invoke(fileUri,
attachmentNotification.entryAttachment,
attachmentNotification.notificationId)
Log.d(TAG, "Download file $fileUri : $percent%")
previousSaveTime = currentTime
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to upload or download file", e)
progressResult = false
}
progressResult
@@ -282,33 +346,96 @@ class AttachmentFileNotificationService: LockNotificationService() {
// on post execute
withContext(Dispatchers.Main) {
val result = asyncResult.await()
attachmentNotification.attachmentTask = null
attachmentNotification.entryAttachment.apply {
attachmentNotification.attachmentFileAction = null
attachmentNotification.entryAttachmentState.apply {
downloadState = if (result) AttachmentState.COMPLETE else AttachmentState.ERROR
downloadProgression = 100
}
onUpdate?.invoke(fileUri,
attachmentNotification.entryAttachment,
attachmentNotification.notificationId)
listener?.onUpdate(attachmentNotification)
TimeoutHelper.releaseTemporarilyDisableTimeout()
}
}
}
fun downloadFromDatabase(attachmentToUploadUri: Uri,
binaryAttachment: BinaryAttachment,
contentResolver: ContentResolver,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
update: ((percent: Int)->Unit)? = null) {
var dataDownloaded = 0L
val fileSize = binaryAttachment.length()
UriUtil.getUriOutputStream(contentResolver, attachmentToUploadUri)?.use { outputStream ->
binaryAttachment.getUnGzipInputDataStream().use { inputStream ->
inputStream.readBytes(bufferSize) { buffer ->
outputStream.write(buffer)
dataDownloaded += buffer.size
try {
val percentDownload = (100 * dataDownloaded / fileSize).toInt()
update?.invoke(percentDownload)
} catch (e: Exception) {
Log.e(TAG, "", e)
}
}
}
}
}
fun uploadToDatabase(attachmentFromDownloadUri: Uri,
binaryAttachment: BinaryAttachment,
contentResolver: ContentResolver,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
update: ((percent: Int)->Unit)? = null) {
var dataUploaded = 0L
val fileSize = contentResolver.openFileDescriptor(attachmentFromDownloadUri, "r")?.statSize ?: 0
UriUtil.getUriInputStream(contentResolver, attachmentFromDownloadUri)?.let { inputStream ->
binaryAttachment.getGzipOutputDataStream().use { outputStream ->
BufferedInputStream(inputStream).use { attachmentBufferedInputStream ->
attachmentBufferedInputStream.readBytes(bufferSize) { buffer ->
outputStream.write(buffer)
dataUploaded += buffer.size
try {
val percentDownload = (100 * dataUploaded / fileSize).toInt()
update?.invoke(percentDownload)
} catch (e: Exception) {
Log.e(TAG, "", e)
}
}
}
}
}
}
private fun publishProgress(percent: Int) {
// Publish progress
val currentTime = System.currentTimeMillis()
if (previousSaveTime + updateMinFrequency < currentTime) {
attachmentNotification.entryAttachmentState.apply {
downloadState = AttachmentState.IN_PROGRESS
downloadProgression = percent
}
CoroutineScope(Dispatchers.Main).launch {
listener?.onUpdate(attachmentNotification)
Log.d(TAG, "Download file ${attachmentNotification.uri} : $percent%")
}
previousSaveTime = currentTime
}
}
companion object {
private val TAG = AttachmentFileActionClass::class.java.name
private val TAG = AttachmentFileAction::class.java.name
}
}
companion object {
private val TAG = AttachmentFileNotificationService::javaClass.name
const val ACTION_ATTACHMENT_FILE_START_UPLOAD = "ACTION_ATTACHMENT_FILE_START_UPLOAD"
const val ACTION_ATTACHMENT_FILE_START_DOWNLOAD = "ACTION_ATTACHMENT_FILE_START_DOWNLOAD"
const val ACTION_ATTACHMENT_REMOVE = "ACTION_ATTACHMENT_REMOVE"
const val DOWNLOAD_FILE_URI_KEY = "DOWNLOAD_FILE_URI_KEY"
const val FILE_URI_KEY = "FILE_URI_KEY"
const val ATTACHMENT_KEY = "ATTACHMENT_KEY"
private val downloadFileUris = HashMap<Uri, AttachmentNotification>()
}
}

View File

@@ -141,6 +141,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
ACTION_DATABASE_RESTORE_ENTRY_HISTORY -> buildDatabaseRestoreEntryHistoryActionTask(intent)
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> buildDatabaseDeleteEntryHistoryActionTask(intent)
ACTION_DATABASE_UPDATE_COMPRESSION_TASK -> buildDatabaseUpdateCompressionActionTask(intent)
ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK -> buildDatabaseRemoveUnlinkedDataActionTask(intent)
ACTION_DATABASE_UPDATE_NAME_TASK,
ACTION_DATABASE_UPDATE_DESCRIPTION_TASK,
ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK,
@@ -711,6 +712,22 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
}
private fun buildDatabaseRemoveUnlinkedDataActionTask(intent: Intent): ActionRunnable? {
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
return RemoveUnlinkedDataDatabaseRunnable(this,
mDatabase,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
).apply {
mAfterSaveDatabase = { result ->
result.data = intent.extras
}
}
} else {
null
}
}
private fun buildDatabaseUpdateElementActionTask(intent: Intent): ActionRunnable? {
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
return SaveDatabaseRunnable(this,
@@ -760,6 +777,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK = "ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK"
const val ACTION_DATABASE_UPDATE_COLOR_TASK = "ACTION_DATABASE_UPDATE_COLOR_TASK"
const val ACTION_DATABASE_UPDATE_COMPRESSION_TASK = "ACTION_DATABASE_UPDATE_COMPRESSION_TASK"
const val ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK = "ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK"
const val ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK = "ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK"
const val ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK = "ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK"
const val ACTION_DATABASE_UPDATE_ENCRYPTION_TASK = "ACTION_DATABASE_UPDATE_ENCRYPTION_TASK"

View File

@@ -347,7 +347,7 @@ object OtpEntryFields {
* Build new generated fields in a new list from [fieldsToParse] in parameter,
* Remove parameters fields use to generate auto fields
*/
fun generateAutoFields(fieldsToParse: MutableList<Field>): MutableList<Field> {
fun generateAutoFields(fieldsToParse: List<Field>): MutableList<Field> {
val newCustomFields: MutableList<Field> = ArrayList(fieldsToParse)
// Remove parameter fields
val otpField = Field(OTP_FIELD)

View File

@@ -40,6 +40,11 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
}
}
override fun onDetach() {
mCallback = null
super.onDetach()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)

View File

@@ -135,7 +135,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
findPreference<Preference>(getString(R.string.database_version_key))
?.summary = mDatabase.version
val dbCompressionPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_compression_key))
val dbCompressionPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_data_key))
// Database compression
dbDataCompressionPref = findPreference(getString(R.string.database_data_compression_key))
@@ -150,7 +150,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
recycleBinGroupPref = findPreference(getString(R.string.recycle_bin_group_key))
// Recycle bin
if (mDatabase.allowRecycleBin) {
if (mDatabase.allowConfigurableRecycleBin) {
val recycleBinEnablePref: SwitchPreference? = findPreference(getString(R.string.recycle_bin_enable_key))
recycleBinEnablePref?.apply {
isChecked = mDatabase.isRecycleBinEnabled
@@ -482,6 +482,9 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
getString(R.string.database_data_compression_key) -> {
dialogFragment = DatabaseDataCompressionPreferenceDialogFragmentCompat.newInstance(preference.key)
}
getString(R.string.database_data_remove_unlinked_attachments_key) -> {
dialogFragment = DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat.newInstance(preference.key)
}
getString(R.string.max_history_items_key) -> {
dialogFragment = MaxHistoryItemsPreferenceDialogFragmentCompat.newInstance(preference.key)
}

View File

@@ -231,12 +231,6 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.biometric_auto_open_prompt_default))
}
fun isFullFilePathEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.full_file_path_enable_key),
context.resources.getBoolean(R.bool.full_file_path_enable_default))
}
fun getListSort(context: Context): SortNodeEnum {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.getString(context.getString(R.string.sort_node_key),

View File

@@ -42,7 +42,8 @@ import com.kunzisoft.keepass.view.showActionError
open class SettingsActivity
: LockingActivity(),
MainPreferenceFragment.Callback,
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
AssignMasterKeyDialogFragment.AssignPasswordDialogListener,
PasswordEncodingDialogFragment.Listener {
private var backupManager: BackupManager? = null
@@ -50,23 +51,6 @@ open class SettingsActivity
private var toolbar: Toolbar? = null
private var lockView: View? = null
companion object {
private const val SHOW_LOCK = "SHOW_LOCK"
private const val TAG_NESTED = "TAG_NESTED"
fun launch(activity: Activity, readOnly: Boolean, timeoutEnable: Boolean) {
val intent = Intent(activity, SettingsActivity::class.java)
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
intent.putExtra(TIMEOUT_ENABLE_KEY, timeoutEnable)
if (!timeoutEnable) {
activity.startActivity(intent)
} else if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
activity.startActivity(intent)
}
}
}
/**
* Retrieve the main fragment to show in first
* @return The main fragment
@@ -83,7 +67,11 @@ open class SettingsActivity
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
toolbar = findViewById(R.id.toolbar)
toolbar?.setTitle(R.string.settings)
if (savedInstanceState?.getString(TITLE_KEY).isNullOrEmpty())
toolbar?.setTitle(R.string.settings)
else
toolbar?.title = savedInstanceState?.getString(TITLE_KEY)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
@@ -128,6 +116,22 @@ open class SettingsActivity
super.onStop()
}
override fun onPasswordEncodingValidateListener(databaseUri: Uri?,
masterPasswordChecked: Boolean,
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?) {
databaseUri?.let {
mProgressDatabaseTaskProvider?.startDatabaseAssignPassword(
databaseUri,
masterPasswordChecked,
masterPassword,
keyFileChecked,
keyFile
)
}
}
override fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean,
masterPassword: String?,
keyFileChecked: Boolean,
@@ -144,18 +148,12 @@ open class SettingsActivity
keyFile
)
} else {
PasswordEncodingDialogFragment().apply {
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
mProgressDatabaseTaskProvider?.startDatabaseAssignPassword(
databaseUri,
masterPasswordChecked,
masterPassword,
keyFileChecked,
keyFile
)
}
show(supportFragmentManager, "passwordEncodingTag")
}
PasswordEncodingDialogFragment.getInstance(databaseUri,
masterPasswordChecked,
masterPassword,
keyFileChecked,
keyFile
).show(supportFragmentManager, "passwordEncodingTag")
}
}
}
@@ -164,8 +162,7 @@ open class SettingsActivity
override fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean,
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?) {
}
keyFile: Uri?) {}
private fun hideOrShowLockButton(key: NestedSettingsFragment.Screen) {
if (PreferencesUtil.showLockDatabaseButton(this)) {
@@ -220,5 +217,24 @@ open class SettingsActivity
super.onSaveInstanceState(outState)
outState.putBoolean(SHOW_LOCK, lockView?.visibility == View.VISIBLE)
outState.putString(TITLE_KEY, toolbar?.title?.toString())
}
companion object {
private const val SHOW_LOCK = "SHOW_LOCK"
private const val TITLE_KEY = "TITLE_KEY"
private const val TAG_NESTED = "TAG_NESTED"
fun launch(activity: Activity, readOnly: Boolean, timeoutEnable: Boolean) {
val intent = Intent(activity, SettingsActivity::class.java)
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
intent.putExtra(TIMEOUT_ENABLE_KEY, timeoutEnable)
if (!timeoutEnable) {
activity.startActivity(intent)
} else if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
activity.startActivity(intent)
}
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.settings.preference
import android.content.Context
import androidx.preference.DialogPreference
import android.util.AttributeSet
import com.kunzisoft.keepass.R
open class TextPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr)
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) {
override fun getDialogLayoutResource(): Int {
return R.layout.pref_dialog_text
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.view.View
import com.kunzisoft.keepass.R
class DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
explanationText = SpannableStringBuilder().apply {
append(getString(R.string.warning_remove_unlinked_attachment))
append("\n\n")
append(getString(R.string.warning_sure_remove_data))
}.toString()
}
override fun onDialogClosed(positiveResult: Boolean) {
database?.let { _ ->
if (positiveResult) {
mProgressDatabaseTaskProvider?.startDatabaseRemoveUnlinkedData(mDatabaseAutoSaveEnable)
}
}
}
companion object {
fun newInstance(key: String): DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat {
val fragment = DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat()
val bundle = Bundle(1)
bundle.putString(ARG_KEY, key)
fragment.arguments = bundle
return fragment
}
}
}

View File

@@ -48,6 +48,11 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat : InputPreferenceDialo
this.mDatabaseAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(context)
}
override fun onDetach() {
mProgressDatabaseTaskProvider = null
super.onDetach()
}
companion object {
private const val TAG = "DbSavePrefDialog"
}

View File

@@ -1,59 +0,0 @@
/*
* Copyright 2017 Brian Pellin, 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.stream
import java.io.IOException
import java.io.OutputStream
import java.io.RandomAccessFile
class RandomFileOutputStream internal constructor(private val mFile: RandomAccessFile) : OutputStream() {
@Throws(IOException::class)
override fun close() {
super.close()
mFile.close()
}
@Throws(IOException::class)
override fun write(buffer: ByteArray, offset: Int, count: Int) {
super.write(buffer, offset, count)
mFile.write(buffer, offset, count)
}
@Throws(IOException::class)
override fun write(buffer: ByteArray) {
super.write(buffer)
mFile.write(buffer)
}
@Throws(IOException::class)
override fun write(oneByte: Int) {
mFile.write(oneByte)
}
@Throws(IOException::class)
fun seek(pos: Long) {
mFile.seek(pos)
}
}

View File

@@ -28,9 +28,13 @@ import android.net.Uri
import android.os.Bundle
import android.os.IBinder
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_DOWNLOAD
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_UPLOAD
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_REMOVE
class AttachmentFileBinderManager(private val activity: FragmentActivity) {
@@ -43,8 +47,18 @@ class AttachmentFileBinderManager(private val activity: FragmentActivity) {
private var mServiceConnection: ServiceConnection? = null
private val mActionTaskListener = object: AttachmentFileNotificationService.ActionTaskListener {
override fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment) {
onActionTaskListener?.onAttachmentProgress(fileUri, attachment)
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
onActionTaskListener?.let {
it.onAttachmentAction(fileUri, entryAttachmentState)
when (entryAttachmentState.downloadState) {
AttachmentState.COMPLETE,
AttachmentState.ERROR -> {
// Finish the action when capture by activity
consumeAttachmentAction(entryAttachmentState)
}
else -> {}
}
}
}
}
@@ -85,22 +99,38 @@ class AttachmentFileBinderManager(private val activity: FragmentActivity) {
mServiceConnection = null
}
@Synchronized
fun consumeAttachmentAction(attachment: EntryAttachmentState) {
mBinder?.getService()?.removeAttachmentAction(attachment)
}
@Synchronized
private fun start(bundle: Bundle? = null, actionTask: String) {
activity.stopService(mIntentTask)
if (bundle != null)
mIntentTask.putExtras(bundle)
activity.runOnUiThread {
mIntentTask.action = actionTask
activity.startService(mIntentTask)
}
mIntentTask.action = actionTask
activity.startService(mIntentTask)
}
fun startUploadAttachment(uploadFileUri: Uri,
attachment: Attachment) {
start(Bundle().apply {
putParcelable(AttachmentFileNotificationService.FILE_URI_KEY, uploadFileUri)
putParcelable(AttachmentFileNotificationService.ATTACHMENT_KEY, attachment)
}, ACTION_ATTACHMENT_FILE_START_UPLOAD)
}
fun startDownloadAttachment(downloadFileUri: Uri,
entryAttachment: EntryAttachment) {
attachment: Attachment) {
start(Bundle().apply {
putParcelable(AttachmentFileNotificationService.DOWNLOAD_FILE_URI_KEY, downloadFileUri)
putParcelable(AttachmentFileNotificationService.ATTACHMENT_KEY, entryAttachment)
putParcelable(AttachmentFileNotificationService.FILE_URI_KEY, downloadFileUri)
putParcelable(AttachmentFileNotificationService.ATTACHMENT_KEY, attachment)
}, ACTION_ATTACHMENT_FILE_START_DOWNLOAD)
}
fun removeBinaryAttachment(attachment: Attachment) {
start(Bundle().apply {
putParcelable(AttachmentFileNotificationService.ATTACHMENT_KEY, attachment)
}, ACTION_ATTACHMENT_REMOVE)
}
}

View File

@@ -60,6 +60,15 @@ object ParcelableUtil {
}
}
// For writing map with string key and Int value to a Parcel
fun writeStringIntMap(parcel: Parcel, map: LinkedHashMap<String, Int>) {
parcel.writeInt(map.size)
for ((key, value) in map) {
parcel.writeString(key)
parcel.writeInt(value)
}
}
// For reading map with string key from a Parcel
fun <V : Parcelable> readStringParcelableMap(
parcel: Parcel, vClass: Class<V>): LinkedHashMap<String, V> {
@@ -74,6 +83,19 @@ object ParcelableUtil {
return map
}
// For reading map with string key and Int value from a Parcel
fun readStringIntMap(parcel: Parcel): LinkedHashMap<String, Int> {
val size = parcel.readInt()
val map = LinkedHashMap<String, Int>(size)
for (i in 0 until size) {
val key: String? = parcel.readString()
val value: Int? = parcel.readInt()
if (key != null && value != null)
map[key] = value
}
return map
}
// For writing map with string key and string value to a Parcel
fun writeStringParcelableMap(dest: Parcel, map: LinkedHashMap<String, String>) {

View File

@@ -27,10 +27,7 @@ import android.os.Build
import android.widget.Toast
import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.R
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.InputStream
import java.io.*
import java.util.*
@@ -52,6 +49,17 @@ object UriUtil {
}
}
@Throws(FileNotFoundException::class)
fun getUriOutputStream(contentResolver: ContentResolver, fileUri: Uri?): OutputStream? {
if (fileUri == null)
return null
return when {
isFileScheme(fileUri) -> fileUri.path?.let { FileOutputStream(it) }
isContentScheme(fileUri) -> contentResolver.openOutputStream(fileUri, "rwt")
else -> null
}
}
@Throws(FileNotFoundException::class)
fun getUriInputStream(contentResolver: ContentResolver, fileUri: Uri?): InputStream? {
if (fileUri == null)

View File

@@ -1,42 +0,0 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.util.AttributeSet
import com.google.android.material.textfield.TextInputEditText
class EditTextSelectable @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: TextInputEditText(context, attrs) {
// TODO constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
// after material design upgrade
private val mOnSelectionChangedListeners: MutableList<OnSelectionChangedListener>?
init {
mOnSelectionChangedListeners = ArrayList()
}
fun addOnSelectionChangedListener(onSelectionChangedListener: OnSelectionChangedListener) {
mOnSelectionChangedListeners?.add(onSelectionChangedListener)
}
fun removeOnSelectionChangedListener(onSelectionChangedListener: OnSelectionChangedListener) {
mOnSelectionChangedListeners?.remove(onSelectionChangedListener)
}
fun removeAllOnSelectionChangedListeners() {
mOnSelectionChangedListeners?.clear()
}
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
mOnSelectionChangedListeners?.forEach {
it.onSelectionChanged(selStart, selEnd)
}
}
interface OnSelectionChangedListener {
fun onSelectionChanged(start: Int, end: Int)
}
}

View File

@@ -19,7 +19,6 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.text.util.Linkify
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
@@ -28,18 +27,20 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.search.UuidUtil
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpType
import java.util.*
@@ -52,27 +53,14 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private var fontInVisibility: Boolean = false
private val userNameContainerView: View
private val userNameView: TextView
private val userNameActionView: ImageView
private val passwordContainerView: View
private val passwordView: TextView
private val passwordActionView: ImageView
private val otpContainerView: View
private val otpLabelView: TextView
private val otpView: TextView
private val otpActionView: ImageView
private val userNameFieldView: EntryField
private val passwordFieldView: EntryField
private val otpFieldView: EntryField
private val urlFieldView: EntryField
private val notesFieldView: EntryField
private var otpRunnable: Runnable? = null
private val urlContainerView: View
private val urlView: TextView
private val notesContainerView: View
private val notesView: TextView
private val extraFieldsContainerView: View
private val extraFieldsListView: ViewGroup
@@ -84,7 +72,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val attachmentsContainerView: View
private val attachmentsListView: RecyclerView
private val attachmentsAdapter = EntryAttachmentsItemsAdapter(context, false)
private val attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
private val historyContainerView: View
private val historyListView: RecyclerView
@@ -93,34 +81,26 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val uuidView: TextView
private val uuidReferenceView: TextView
val isUserNamePresent: Boolean
get() = userNameContainerView.visibility == View.VISIBLE
val isPasswordPresent: Boolean
get() = passwordContainerView.visibility == View.VISIBLE
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_entry_contents, this)
userNameContainerView = findViewById(R.id.entry_user_name_container)
userNameView = findViewById(R.id.entry_user_name)
userNameActionView = findViewById(R.id.entry_user_name_action_image)
userNameFieldView = findViewById(R.id.entry_user_name_field)
userNameFieldView.setLabel(R.string.entry_user_name)
passwordContainerView = findViewById(R.id.entry_password_container)
passwordView = findViewById(R.id.entry_password)
passwordActionView = findViewById(R.id.entry_password_action_image)
passwordFieldView = findViewById(R.id.entry_password_field)
passwordFieldView.setLabel(R.string.entry_password)
otpContainerView = findViewById(R.id.entry_otp_container)
otpLabelView = findViewById(R.id.entry_otp_label)
otpView = findViewById(R.id.entry_otp)
otpActionView = findViewById(R.id.entry_otp_action_image)
otpFieldView = findViewById(R.id.entry_otp_field)
otpFieldView.setLabel(R.string.entry_otp)
urlContainerView = findViewById(R.id.entry_url_container)
urlView = findViewById(R.id.entry_url)
urlFieldView = findViewById(R.id.entry_url_field)
urlFieldView.setLabel(R.string.entry_url)
urlFieldView.setValueAutoLink(true)
notesContainerView = findViewById(R.id.entry_notes_container)
notesView = findViewById(R.id.entry_notes)
notesFieldView = findViewById(R.id.entry_notes_field)
notesFieldView.setLabel(R.string.entry_notes)
notesFieldView.setValueAutoLink(true)
extraFieldsContainerView = findViewById(R.id.extra_fields_container)
extraFieldsListView = findViewById(R.id.extra_fields_list)
@@ -128,7 +108,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
attachmentsContainerView = findViewById(R.id.entry_attachments_container)
attachmentsListView = findViewById(R.id.entry_attachments_list)
attachmentsListView?.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adapter = attachmentsAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
@@ -154,86 +134,63 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
this.fontInVisibility = fontInVisibility
}
fun assignUserName(userName: String?) {
if (userName != null && userName.isNotEmpty()) {
userNameContainerView.visibility = View.VISIBLE
userNameView.apply {
text = userName
if (fontInVisibility)
applyFontVisibility()
}
} else {
userNameContainerView.visibility = View.GONE
fun firstEntryFieldCopyView(): View? {
return when {
userNameFieldView.isVisible && userNameFieldView.copyButtonView.isVisible -> userNameFieldView.copyButtonView
passwordFieldView.isVisible && passwordFieldView.copyButtonView.isVisible -> passwordFieldView.copyButtonView
otpFieldView.isVisible && otpFieldView.copyButtonView.isVisible -> otpFieldView.copyButtonView
urlFieldView.isVisible && urlFieldView.copyButtonView.isVisible -> urlFieldView.copyButtonView
notesFieldView.isVisible && notesFieldView.copyButtonView.isVisible -> notesFieldView.copyButtonView
else -> null
}
}
fun assignUserNameCopyListener(onClickListener: OnClickListener) {
userNameActionView.setOnClickListener(onClickListener)
}
fun assignPassword(password: String?, allowCopyPassword: Boolean) {
if (password != null && password.isNotEmpty()) {
passwordContainerView.visibility = View.VISIBLE
passwordView.apply {
text = password
if (fontInVisibility)
applyFontVisibility()
}
passwordActionView.isActivated = !allowCopyPassword
} else {
passwordContainerView.visibility = View.GONE
}
}
fun assignPasswordCopyListener(onClickListener: OnClickListener?) {
passwordActionView.apply {
setOnClickListener(onClickListener)
if (onClickListener == null) {
fun assignUserName(userName: String?,
onClickListener: OnClickListener?) {
userNameFieldView.apply {
if (userName != null && userName.isNotEmpty()) {
visibility = View.VISIBLE
setValue(userName)
applyFontVisibility(fontInVisibility)
} else {
visibility = View.GONE
}
assignCopyButtonClickListener(onClickListener)
}
}
fun atLeastOneFieldProtectedPresent(): Boolean {
extraFieldsListView.let {
for (i in 0 until it.childCount) {
val childCustomView = it.getChildAt(i)
if (childCustomView is EntryExtraField)
if (childCustomView.isProtected)
return true
}
}
return false
}
fun setHiddenPasswordStyle(hiddenStyle: Boolean) {
passwordView.applyHiddenStyle(hiddenStyle)
// Hidden style for custom fields
extraFieldsListView.let {
for (i in 0 until it.childCount) {
val childCustomView = it.getChildAt(i)
if (childCustomView is EntryExtraField)
childCustomView.setHiddenPasswordStyle(hiddenStyle)
fun assignPassword(password: String?,
allowCopyPassword: Boolean,
onClickListener: OnClickListener?) {
passwordFieldView.apply {
if (password != null && password.isNotEmpty()) {
visibility = View.VISIBLE
setValue(password, true)
applyFontVisibility(fontInVisibility)
activateCopyButton(allowCopyPassword)
}else {
visibility = View.GONE
}
assignCopyButtonClickListener(onClickListener)
}
}
fun assignOtp(otpElement: OtpElement?,
otpProgressView: ProgressBar?,
onClickListener: OnClickListener) {
otpContainerView.removeCallbacks(otpRunnable)
otpFieldView.removeCallbacks(otpRunnable)
if (otpElement != null) {
otpContainerView.visibility = View.VISIBLE
otpFieldView.visibility = View.VISIBLE
if (otpElement.token.isEmpty()) {
otpView.text = context.getString(R.string.error_invalid_OTP)
otpActionView.setColorFilter(ContextCompat.getColor(context, R.color.grey_dark))
assignOtpCopyListener(null)
otpFieldView.setValue(R.string.error_invalid_OTP)
otpFieldView.activateCopyButton(false)
otpFieldView.assignCopyButtonClickListener(null)
} else {
assignOtpCopyListener(onClickListener)
otpView.text = otpElement.token
otpLabelView.text = otpElement.type.name
otpFieldView.setLabel(otpElement.type.name)
otpFieldView.setValue(otpElement.token)
otpFieldView.assignCopyButtonClickListener(onClickListener)
when (otpElement.type) {
// Only add token if HOTP
@@ -249,47 +206,41 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
}
otpRunnable = Runnable {
if (otpElement.shouldRefreshToken()) {
otpView.text = otpElement.token
otpFieldView.setValue(otpElement.token)
}
otpProgressView?.progress = otpElement.secondsRemaining
otpContainerView.postDelayed(otpRunnable, 1000)
otpFieldView.postDelayed(otpRunnable, 1000)
}
otpContainerView.post(otpRunnable)
otpFieldView.post(otpRunnable)
}
}
}
} else {
otpContainerView.visibility = View.GONE
otpFieldView.visibility = View.GONE
otpProgressView?.visibility = View.GONE
}
}
fun assignOtpCopyListener(onClickListener: OnClickListener?) {
otpActionView.setOnClickListener(onClickListener)
}
fun assignURL(url: String?) {
if (url != null && url.isNotEmpty()) {
urlContainerView.visibility = View.VISIBLE
urlView.text = url
} else {
urlContainerView.visibility = View.GONE
urlFieldView.apply {
if (url != null && url.isNotEmpty()) {
visibility = View.VISIBLE
setValue(url)
} else {
visibility = View.GONE
}
}
}
fun assignComment(comment: String?) {
if (comment != null && comment.isNotEmpty()) {
notesContainerView.visibility = View.VISIBLE
notesView.apply {
text = comment
if (fontInVisibility)
applyFontVisibility()
fun assignNotes(notes: String?) {
notesFieldView.apply {
if (notes != null && notes.isNotEmpty()) {
visibility = View.VISIBLE
setValue(notes)
applyFontVisibility(fontInVisibility)
} else {
visibility = View.GONE
}
try {
Linkify.addLinks(notesView, Linkify.ALL)
} catch (e: Exception) {}
} else {
notesContainerView.visibility = View.GONE
}
}
@@ -322,6 +273,19 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
uuidReferenceView.text = UuidUtil.toHexString(uuid)
}
fun setHiddenProtectedValue(hiddenProtectedValue: Boolean) {
passwordFieldView.hiddenProtectedValue = hiddenProtectedValue
// Hidden style for custom fields
extraFieldsListView.let {
for (i in 0 until it.childCount) {
val childCustomView = it.getChildAt(i)
if (childCustomView is EntryField)
childCustomView.hiddenProtectedValue = hiddenProtectedValue
}
}
}
/* -------------
* Extra Fields
* -------------
@@ -334,14 +298,15 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
fun addExtraField(title: String,
value: ProtectedString,
allowCopy: Boolean,
onActionClickListener: OnClickListener?) {
onCopyButtonClickListener: OnClickListener?) {
val entryCustomField: EntryExtraField? = EntryExtraField(context, attrs, defStyle)
val entryCustomField: EntryField? = EntryField(context, attrs, defStyle)
entryCustomField?.apply {
setLabel(title)
setValue(value.toString(), value.isProtected)
activateActionButton(allowCopy)
assignActionButtonClickListener(onActionClickListener)
setValueAutoLink(true)
activateCopyButton(allowCopy)
assignCopyButtonClickListener(onCopyButtonClickListener)
applyFontVisibility(fontInVisibility)
}
entryCustomField?.let {
@@ -364,17 +329,18 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
attachmentsContainerView.visibility = if (show) View.VISIBLE else View.GONE
}
fun assignAttachments(attachments: ArrayList<EntryAttachment>,
onAttachmentClicked: (attachment: EntryAttachment)->Unit) {
fun assignAttachments(attachments: Set<Attachment>,
streamDirection: StreamDirection,
onAttachmentClicked: (attachment: Attachment)->Unit) {
showAttachments(attachments.isNotEmpty())
attachmentsAdapter.assignItems(attachments)
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
attachmentsAdapter.onItemClickListener = { item ->
onAttachmentClicked.invoke(item)
onAttachmentClicked.invoke(item.attachment)
}
}
fun updateAttachmentDownloadProgress(attachmentToDownload: EntryAttachment) {
attachmentsAdapter.updateProgress(attachmentToDownload)
fun putAttachment(attachmentToDownload: EntryAttachmentState) {
attachmentsAdapter.putItem(attachmentToDownload)
}
/* -------------

View File

@@ -1,300 +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.view
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.adapters.EntryExtraFieldsItemsAdapter
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.icons.assignDefaultDatabaseIcon
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.model.FocusedEditField
import org.joda.time.Duration
import org.joda.time.Instant
class EntryEditContentsView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: LinearLayout(context, attrs, defStyle) {
private var fontInVisibility: Boolean = false
private val entryTitleLayoutView: TextInputLayout
private val entryTitleView: EditText
private val entryIconView: ImageView
private val entryUserNameView: EditText
private val entryUrlView: EditText
private val entryPasswordLayoutView: TextInputLayout
private val entryPasswordView: EditText
val entryPasswordGeneratorView: View
private val entryExpiresCheckBox: CompoundButton
private val entryExpiresTextView: TextView
private val entryNotesView: EditText
private val extraFieldsContainerView: ViewGroup
private val extraFieldsListView: RecyclerView
private val attachmentsContainerView: View
private val attachmentsListView: RecyclerView
private val extraFieldsAdapter = EntryExtraFieldsItemsAdapter(context)
private val attachmentsAdapter = EntryAttachmentsItemsAdapter(context, true)
private var iconColor: Int = 0
private var expiresInstant: DateInstant = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
var onDateClickListener: OnClickListener? = null
set(value) {
field = value
if (entryExpiresCheckBox.isChecked)
entryExpiresTextView.setOnClickListener(value)
else
entryExpiresTextView.setOnClickListener(null)
}
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_entry_edit_contents, this)
entryTitleLayoutView = findViewById(R.id.entry_edit_container_title)
entryTitleView = findViewById(R.id.entry_edit_title)
entryIconView = findViewById(R.id.entry_edit_icon_button)
entryUserNameView = findViewById(R.id.entry_edit_user_name)
entryUrlView = findViewById(R.id.entry_edit_url)
entryPasswordLayoutView = findViewById(R.id.entry_edit_container_password)
entryPasswordView = findViewById(R.id.entry_edit_password)
entryPasswordGeneratorView = findViewById(R.id.entry_edit_password_generator_button)
entryExpiresCheckBox = findViewById(R.id.entry_edit_expires_checkbox)
entryExpiresTextView = findViewById(R.id.entry_edit_expires_text)
entryNotesView = findViewById(R.id.entry_edit_notes)
extraFieldsContainerView = findViewById(R.id.extra_fields_container)
extraFieldsListView = findViewById(R.id.extra_fields_list)
// To hide or not the container
extraFieldsAdapter.onListSizeChangedListener = { previousSize, newSize ->
if (previousSize > 0 && newSize == 0) {
extraFieldsContainerView.collapse(true)
} else if (previousSize == 0 && newSize == 1) {
extraFieldsContainerView.expand(true)
}
}
extraFieldsListView?.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adapter = extraFieldsAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
attachmentsContainerView = findViewById(R.id.entry_attachments_container)
attachmentsListView = findViewById(R.id.entry_attachments_list)
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
if (previousSize > 0 && newSize == 0) {
attachmentsContainerView.collapse(true)
} else if (previousSize == 0 && newSize == 1) {
attachmentsContainerView.expand(true)
}
}
attachmentsListView?.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
adapter = attachmentsAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
assignExpiresDateText()
}
// Retrieve the textColor to tint the icon
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
iconColor = taIconColor.getColor(0, Color.WHITE)
taIconColor.recycle()
}
fun applyFontVisibilityToFields(fontInVisibility: Boolean) {
this.fontInVisibility = fontInVisibility
this.extraFieldsAdapter.applyFontVisibility = fontInVisibility
}
var title: String
get() {
return entryTitleView.text.toString()
}
set(value) {
entryTitleView.setText(value)
if (fontInVisibility)
entryTitleView.applyFontVisibility()
}
fun setDefaultIcon(iconFactory: IconDrawableFactory) {
entryIconView.assignDefaultDatabaseIcon(iconFactory, iconColor)
}
fun setIcon(iconFactory: IconDrawableFactory, icon: IconImage) {
entryIconView.assignDatabaseIcon(iconFactory, icon, iconColor)
}
fun setOnIconViewClickListener(clickListener: () -> Unit) {
entryIconView.setOnClickListener { clickListener.invoke() }
}
var username: String
get() {
return entryUserNameView.text.toString()
}
set(value) {
entryUserNameView.setText(value)
if (fontInVisibility)
entryUserNameView.applyFontVisibility()
}
var url: String
get() {
return entryUrlView.text.toString()
}
set(value) {
entryUrlView.setText(value)
if (fontInVisibility)
entryUrlView.applyFontVisibility()
}
var password: String
get() {
return entryPasswordView.text.toString()
}
set(value) {
entryPasswordView.setText(value)
if (fontInVisibility) {
entryPasswordView.applyFontVisibility()
}
}
private fun assignExpiresDateText() {
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
entryExpiresTextView.setOnClickListener(onDateClickListener)
expiresInstant.getDateTimeString(resources)
} else {
entryExpiresTextView.setOnClickListener(null)
resources.getString(R.string.never)
}
if (fontInVisibility)
entryExpiresTextView.applyFontVisibility()
}
var expires: Boolean
get() {
return entryExpiresCheckBox.isChecked
}
set(value) {
entryExpiresCheckBox.isChecked = value
assignExpiresDateText()
}
var expiresDate: DateInstant
get() {
return expiresInstant
}
set(value) {
expiresInstant = value
assignExpiresDateText()
}
var notes: String
get() {
return entryNotesView.text.toString()
}
set(value) {
entryNotesView.setText(value)
if (fontInVisibility)
entryNotesView.applyFontVisibility()
}
/* -------------
* Extra Fields
* -------------
*/
fun getExtraField(): MutableList<Field> {
return extraFieldsAdapter.itemsList
}
fun getExtraFieldFocused(): FocusedEditField {
// To keep focused after an orientation change
return extraFieldsAdapter.getFocusedField()
}
/**
* Remove all children and add new views for each field
*/
fun assignExtraFields(fields: List<Field>, focusedExtraField: FocusedEditField? = null) {
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
// Reinit focused field
extraFieldsAdapter.assignItems(fields, focusedExtraField)
}
/**
* Update an extra field or create a new one if doesn't exists
*/
fun putExtraField(extraField: Field) {
extraFieldsContainerView.visibility = View.VISIBLE
val oldField = extraFieldsAdapter.itemsList.firstOrNull { it.name == extraField.name }
oldField?.let {
if (extraField.protectedValue.stringValue.isEmpty())
extraField.protectedValue.stringValue = it.protectedValue.stringValue
}
extraFieldsAdapter.putItem(extraField)
}
/* -------------
* Attachments
* -------------
*/
fun assignAttachments(attachments: ArrayList<EntryAttachment>,
onDeleteItem: (attachment: EntryAttachment)->Unit) {
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
attachmentsAdapter.assignItems(attachments)
attachmentsAdapter.onDeleteButtonClickListener = { item ->
onDeleteItem.invoke(item)
}
}
/**
* Validate or not the entry form
*
* @return ErrorValidation An error with a message or a validation without message
*/
fun isValid(): Boolean {
// TODO
return true
}
}

View File

@@ -1,77 +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.view
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.kunzisoft.keepass.R
class EntryExtraField @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: LinearLayout(context, attrs, defStyle) {
private val labelView: TextView
private val valueView: TextView
private val actionImageView: ImageView
var isProtected = false
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.item_entry_extra_field, this)
labelView = findViewById(R.id.title)
valueView = findViewById(R.id.value)
actionImageView = findViewById(R.id.action_image)
}
fun applyFontVisibility(fontInVisibility: Boolean) {
if (fontInVisibility)
valueView.applyFontVisibility()
}
fun setLabel(label: String?) {
labelView.text = label ?: ""
}
fun setValue(value: String?, isProtected: Boolean = false) {
valueView.text = value ?: ""
this.isProtected = isProtected
}
fun setHiddenPasswordStyle(hiddenStyle: Boolean) {
valueView.applyHiddenStyle(isProtected && hiddenStyle)
}
fun activateActionButton(enable: Boolean) {
// Reverse because isActivated show custom color and allow click
actionImageView.isActivated = !enable
}
fun assignActionButtonClickListener(onClickActionListener: OnClickListener?) {
actionImageView.setOnClickListener(onClickActionListener)
actionImageView.visibility = if (onClickActionListener == null) GONE else VISIBLE
}
}

View File

@@ -0,0 +1,125 @@
/*
* 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.view
import android.content.Context
import android.text.util.Linkify
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.core.text.util.LinkifyCompat
import com.kunzisoft.keepass.R
class EntryField @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: LinearLayout(context, attrs, defStyle) {
private val labelView: TextView
private val valueView: TextView
private val showButtonView: ImageView
val copyButtonView: ImageView
private var isProtected = false
var hiddenProtectedValue: Boolean
get() {
return showButtonView.isSelected
}
set(value) {
showButtonView.isSelected = !value
changeProtectedValueParameters()
}
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.item_entry_field, this)
labelView = findViewById(R.id.entry_field_label)
valueView = findViewById(R.id.entry_field_value)
showButtonView = findViewById(R.id.entry_field_show)
copyButtonView = findViewById(R.id.entry_field_copy)
copyButtonView.visibility = View.GONE
}
fun applyFontVisibility(fontInVisibility: Boolean) {
if (fontInVisibility)
valueView.applyFontVisibility()
}
fun setLabel(label: String?) {
labelView.text = label ?: ""
}
fun setLabel(@StringRes labelId: Int) {
labelView.setText(labelId)
}
fun setValue(value: String?,
isProtected: Boolean = false) {
valueView.text = value ?: ""
this.isProtected = isProtected
showButtonView.visibility = if (isProtected) View.VISIBLE else View.GONE
showButtonView.setOnClickListener {
showButtonView.isSelected = !showButtonView.isSelected
changeProtectedValueParameters()
}
changeProtectedValueParameters()
}
fun setValue(@StringRes valueId: Int,
isProtected: Boolean = false) {
setValue(resources.getString(valueId), isProtected)
}
private fun changeProtectedValueParameters() {
valueView.apply {
if (isProtected) {
isFocusable = false
} else {
setTextIsSelectable(true)
}
applyHiddenStyle(isProtected && !showButtonView.isSelected)
if (valueView.autoLinkMask == Linkify.ALL) {
try {
LinkifyCompat.addLinks(this, Linkify.ALL)
} catch (e: Exception) {}
}
}
}
fun setValueAutoLink(autoLink: Boolean) {
valueView.autoLinkMask = if (autoLink && !isProtected) Linkify.ALL else 0
changeProtectedValueParameters()
}
fun activateCopyButton(enable: Boolean) {
// Reverse because isActivated show custom color and allow click
copyButtonView.isActivated = !enable
}
fun assignCopyButtonClickListener(onClickActionListener: OnClickListener?) {
copyButtonView.setOnClickListener(onClickActionListener)
copyButtonView.visibility = if (onClickActionListener == null) GONE else VISIBLE
}
}

View File

@@ -44,13 +44,15 @@ fun TextView.applyFontVisibility() {
typeface = typeFace
}
fun TextView.applyHiddenStyle(hide: Boolean) {
fun TextView.applyHiddenStyle(hide: Boolean, changeMaxLines: Boolean = true) {
if (hide) {
transformationMethod = PasswordTransformationMethod.getInstance()
maxLines = 1
if (changeMaxLines)
maxLines = 1
} else {
transformationMethod = null
maxLines = 800
if (changeMaxLines)
maxLines = 800
}
}

View File

@@ -60,8 +60,12 @@ class FileDatabaseInfo : Serializable {
fun getModificationString(): String? {
return documentFile?.lastModified()?.let {
DateFormat.getDateTimeInstance()
.format(Date(it))
if (it != 0L) {
DateFormat.getDateTimeInstance()
.format(Date(it))
} else {
null
}
}
}
@@ -74,7 +78,6 @@ class FileDatabaseInfo : Serializable {
fun retrieveDatabaseAlias(alias: String): String? {
return when {
alias.isNotEmpty() -> alias
PreferencesUtil.isFullFilePathEnable(context) -> fileUri?.path
else -> if (exists) documentFile?.name else fileUri?.path
}
}

View File

@@ -1,9 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6 1.41,-1.41z"/>
</vector>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_arrow_up_white_24dp"
android:fromDegrees="180"
android:toDegrees="180"
android:visible="true" />

View File

@@ -1,9 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M8.59,16.34l4.58,-4.59 -4.58,-4.59L10,5.75l6,6 -6,6z"/>
</vector>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_arrow_left_white_24dp"
android:fromDegrees="180"
android:toDegrees="180"
android:visible="true" />

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M 10.191406 1 C 7.8864287 1 6 2.8866579 6 5.1914062 L 6 13.976562 L 7.5722656 13.355469 L 7.5722656 5.1914062 C 7.5722656 3.7248197 8.7248288 2.5722656 10.191406 2.5722656 C 11.657948 2.5722656 12.808594 3.7248197 12.808594 5.1914062 L 12.808594 9.2851562 L 14.380859 8.6640625 L 14.380859 5.1914062 C 14.380859 2.8866579 12.496402 1 10.191406 1 z M 15.953125 5.7148438 L 15.953125 8.0410156 L 17.523438 7.421875 L 17.523438 5.7148438 L 15.953125 5.7148438 z M 9.1425781 7.2851562 L 9.1425781 12.734375 L 10.714844 12.113281 L 10.714844 7.2851562 L 9.1425781 7.2851562 z M 17.523438 11.722656 L 15.953125 12.34375 L 15.953125 17.238281 C 15.953125 19.543273 14.066714 21.427734 11.761719 21.427734 C 9.5944825 21.427734 7.8200908 19.755748 7.6132812 17.640625 L 6.0917969 18.242188 C 6.5643368 20.953226 8.9101628 23 11.761719 23 C 14.956723 23 17.523438 20.433343 17.523438 17.238281 L 17.523438 11.722656 z M 14.380859 12.964844 L 12.808594 13.587891 L 12.808594 17.238281 C 12.808594 17.814256 12.337701 18.285156 11.761719 18.285156 C 11.185737 18.285156 10.714844 17.814256 10.714844 17.238281 L 10.714844 16.414062 L 9.1425781 17.035156 L 9.1425781 17.238281 C 9.1426137 18.704818 10.295141 19.857422 11.761719 19.857422 C 13.228314 19.857422 14.380859 18.704818 14.380859 17.238281 L 14.380859 12.964844 z" />
</vector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_activated="true"
android:drawable="@drawable/ic_file_upload_white_24dp" />
<item android:state_activated="false"
android:drawable="@drawable/ic_file_download_white_24dp" />
</selector>

View File

@@ -0,0 +1,5 @@
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_file_download_white_24dp"
android:fromDegrees="180"
android:toDegrees="180"
android:visible="true" />

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/background_button_color_secondary"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M22,3L7,3c-0.69,0 -1.23,0.35 -1.59,0.88L0,12l5.41,8.11c0.36,0.53 0.97,0.89 1.66,0.89L22,21c1.1,0 2,-0.9 2,-2L24,5c0,-1.1 -0.9,-2 -2,-2zM9,13.5c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM14,13.5c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM19,13.5c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/>
</vector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true"
android:drawable="@drawable/ic_visibility_white_24dp" />
<item android:state_selected="false"
android:drawable="@drawable/ic_visibility_off_white_24dp" />
</selector>

View File

@@ -54,7 +54,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.kunzisoft.keepass.view.EntryEditContentsView
<androidx.fragment.app.FragmentContainerView
android:id="@+id/entry_edit_contents"
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -69,9 +69,6 @@
android:id="@+id/entry_edit_bottom_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="@style/Widget.MaterialComponents.BottomAppBar"
android:backgroundTint="?attr/colorAccent"
app:backgroundTint="?attr/colorAccent"
app:fabAlignmentMode="center"
app:hideOnScroll="false"
app:layout_scrollFlags="scroll|enterAlways"

View File

@@ -124,26 +124,13 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="?attr/toolbarAppearance"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/ic_info_white_24dp"
app:navigationContentDescription="@string/about"
android:elevation="4dp"
app:layout_collapseMode="pin"
android:theme="?attr/toolbarHomeAppearance"
android:popupTheme="?attr/toolbarPopupAppearance" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="?attr/actionBarSize"
android:elevation="4dp"
android:layout_gravity="top|start"
app:layout_collapseMode="pin">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/file_manager_explanation_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_info_white_24dp"
android:contentDescription="@string/about"
style="@style/KeepassDXStyle.ImageButton.Simple.Secondary"/>
</FrameLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>

View File

@@ -156,8 +156,7 @@
android:layout_height="?attr/actionBarSize"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:theme="?attr/actionToolbarAppearance"
android:background="?attr/colorAccent" />
style="?attr/actionToolbarAppearance" />
<include
layout="@layout/view_button_lock"

View File

@@ -190,7 +190,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:maxLines="20"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:inputType="textMultiLine"
@@ -210,13 +209,12 @@
android:layout_marginBottom="@dimen/card_view_margin_bottom"
app:layout_constraintTop_toBottomOf="@+id/entry_edit_container"
app:layout_constraintBottom_toTopOf="@+id/entry_attachments_container">
<androidx.recyclerview.widget.RecyclerView
<LinearLayout
android:id="@+id/extra_fields_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/card_view_padding"
android:orientation="vertical">
</androidx.recyclerview.widget.RecyclerView>
android:orientation="vertical" />
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
@@ -247,7 +245,8 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/entry_attachments_list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:descendantFocusability="afterDescendants" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -34,9 +34,11 @@
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/entry_custom_field_label_container"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_custom_field_delete">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/entry_custom_field_label"
@@ -49,6 +51,16 @@
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/entry_custom_field_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:src="@drawable/ic_content_delete_white_24dp"
android:contentDescription="@string/menu_delete"
style="@style/KeepassDXStyle.ImageButton.Simple" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/entry_custom_field_protection"
style="@style/KeepassDXStyle.TextAppearance.Small"

View File

@@ -47,5 +47,7 @@
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/windowBackground" />
android:background="?android:attr/windowBackground"
android:paddingBottom="?attr/actionBarSize"
android:clipToPadding="false" />
</FrameLayout>

View File

@@ -28,6 +28,29 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:targetApi="o">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/credentials_information"
android:text="@string/assign_master_key"
style="@style/KeepassDXStyle.TextAppearance.Large"/>
<ImageView
android:id="@+id/credentials_information"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:src="@drawable/ic_info_white_24dp"
style="@style/KeepassDXStyle.ImageButton.Simple"
android:contentDescription="@string/content_description_credentials_information"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.cardview.widget.CardView
android:id="@+id/card_view_master_password"
android:layout_margin="4dp"

View File

@@ -23,15 +23,26 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:targetApi="p"
android:id="@+id/item_attachment_container"
android:focusable="false"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/item_attachment_broken"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:src="@drawable/ic_attach_file_broken_white_24dp"
android:contentDescription="@string/entry_attachments" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/item_attachment_title"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/item_attachment_size_container"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/item_attachment_broken"
app:layout_constraintEnd_toStartOf="@+id/item_attachment_size_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem"
@@ -70,40 +81,45 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="48dp"
android:layout_height="48dp">
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/item_attachment_delete_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_gravity="center"
android:contentDescription="@string/content_description_remove_field"
android:focusable="true"
android:src="@drawable/ic_content_delete_white_24dp"
style="@style/KeepassDXStyle.ImageButton.Simple" />
<FrameLayout
android:id="@+id/item_attachment_progress_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
android:layout_gravity="center">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/item_attachment_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="6dp"
android:src="@drawable/ic_file_download_white_24dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_file_stream_white_24dp"
android:contentDescription="@string/download"
android:tint="?attr/colorAccent" />
style="@style/KeepassDXStyle.ImageButton.Simple" />
<ProgressBar
android:id="@+id/item_attachment_progress"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_gravity="center"
style="@style/KeepassDXStyle.ProgressBar.Circle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="36dp"
android:layout_height="36dp"
android:max="100"
android:progress="60" />
</FrameLayout>

View File

@@ -32,9 +32,9 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_extra_field_delete">
app:layout_constraintEnd_toStartOf="@+id/entry_extra_field_edit">
<com.kunzisoft.keepass.view.EditTextSelectable
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/entry_extra_field_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -46,14 +46,15 @@
</com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/entry_extra_field_delete"
android:id="@+id/entry_extra_field_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:src="@drawable/ic_content_delete_white_24dp"
android:contentDescription="@string/menu_delete"
android:src="@drawable/ic_more_white_24dp"
android:contentDescription="@string/menu_edit"
style="@style/KeepassDXStyle.ImageButton.Simple"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -24,26 +24,34 @@
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/title"
android:id="@+id/entry_field_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/action_image"
app:layout_constraintEnd_toStartOf="@+id/entry_field_show"
tools:text="title"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<TextView
android:id="@+id/value"
android:id="@+id/entry_field_value"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/entry_field_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/action_image"
android:textIsSelectable="true"
app:layout_constraintEnd_toStartOf="@+id/entry_field_show"
tools:text="value"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/action_image"
android:id="@+id/entry_field_show"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_field_copy"
android:src="@drawable/ic_visibility_state"
android:contentDescription="@string/menu_showpass"
style="@style/KeepassDXStyle.ImageButton.Simple"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/entry_field_copy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"

View File

@@ -176,38 +176,49 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/file_precise_info_container"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/file_delete_button">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/file_modification"
<LinearLayout
android:id="@+id/file_modification_container"
android:layout_width="0dp"
android:layout_height="match_parent"
tools:text="3:40:14 PM"
android:textColor="?android:attr/textColorHintInverse"
android:layout_height="wrap_content"
android:paddingStart="@dimen/default_margin"
android:paddingLeft="@dimen/default_margin"
android:paddingEnd="@dimen/default_margin"
android:paddingRight="@dimen/default_margin"
android:paddingBottom="12dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/file_size"
android:gravity="start|bottom"/>
android:orientation="vertical"
android:gravity="start|center_vertical" >
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/file_modification_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/entry_modified"
android:textColor="?android:attr/textColorHintInverse"
android:textSize="12sp" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/file_modification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Aug 21, 2020 3:40:14 PM"
android:textColor="?android:attr/textColorHintInverse"/>
</LinearLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/file_size"
android:layout_width="0dp"
android:layout_height="match_parent"
android:gravity="end|bottom"
android:layout_height="wrap_content"
android:gravity="end|center_vertical"
android:textColor="?android:attr/textColorHintInverse"
android:paddingBottom="12dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/file_modification"
app:layout_constraintStart_toEndOf="@+id/file_modification_container"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="@dimen/default_margin"
android:layout_marginEnd="@dimen/default_margin"

View File

@@ -25,7 +25,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.Selectable.Item">
<RelativeLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
@@ -46,20 +46,26 @@
android:layout_marginEnd="@dimen/image_list_margin"
android:scaleType="fitXY"
android:src="@drawable/ic_blank_32dp"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="2dp"
android:paddingBottom="4dp"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_toRightOf="@+id/node_icon"
android:layout_toEndOf="@+id/node_icon">
android:layout_marginLeft="@dimen/image_list_margin"
android:layout_marginStart="@dimen/image_list_margin"
android:layout_marginRight="@dimen/image_list_margin"
android:layout_marginEnd="@dimen/image_list_margin"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/node_icon"
app:layout_constraintLeft_toRightOf="@+id/node_icon"
app:layout_constraintEnd_toStartOf="@+id/node_attachment_icon"
app:layout_constraintRight_toLeftOf="@+id/node_attachment_icon">
<TextView
android:id="@+id/node_text"
android:layout_height="wrap_content"
@@ -78,5 +84,19 @@
android:singleLine="true"
style="@style/KeepassDXStyle.TextAppearance.Entry.SubTitle" />
</LinearLayout>
</RelativeLayout>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/node_attachment_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/image_list_margin"
android:layout_marginStart="@dimen/image_list_margin"
android:layout_marginRight="@dimen/image_list_margin"
android:layout_marginEnd="@dimen/image_list_margin"
android:src="@drawable/ic_attach_file_white_24dp"
style="@style/KeepassDXStyle.TextAppearance.Entry.Icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/edit"
android:padding="20dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAutofill="noExcludeDescendants"
tools:targetApi="o">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/explanation_text"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
style="@style/KeepassDXStyle.TextAppearance.SmallTitle"/>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_element"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/enable"
app:layout_constraintTop_toBottomOf="@+id/explanation_text"
app:layout_constraintStart_toStartOf="parent"
android:minHeight="48dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -40,153 +40,41 @@
android:orientation="vertical">
<!-- Username -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/entry_user_name_container"
<com.kunzisoft.keepass.view.EntryField
android:id="@+id/entry_user_name_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_user_name_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_user_name_action_image"
android:text="@string/entry_user_name"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_user_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/entry_user_name_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_user_name_action_image"
android:textIsSelectable="true"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/entry_user_name_action_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:src="@drawable/ic_content_copy_white_24dp"
android:contentDescription="@string/menu_copy"
style="@style/KeepassDXStyle.ImageButton.Simple" />
</androidx.constraintlayout.widget.ConstraintLayout>
android:visibility="gone" />
<!-- Password -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/entry_password_container"
<com.kunzisoft.keepass.view.EntryField
android:id="@+id/entry_password_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_password_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_password_action_image"
android:text="@string/entry_password"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_password"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/entry_password_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_password_action_image"
android:focusable="false"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/entry_password_action_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:src="@drawable/ic_content_copy_white_24dp"
android:contentDescription="@string/menu_copy"
style="@style/KeepassDXStyle.ImageButton.Simple" />
</androidx.constraintlayout.widget.ConstraintLayout>
android:visibility="gone" />
<!-- OTP -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/entry_otp_container"
<com.kunzisoft.keepass.view.EntryField
android:id="@+id/entry_otp_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_otp_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_otp_action_image"
android:text="@string/entry_otp"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_otp"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/entry_otp_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_otp_action_image"
android:textIsSelectable="true"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/entry_otp_action_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:src="@drawable/ic_content_copy_white_24dp"
android:contentDescription="@string/menu_copy"
style="@style/KeepassDXStyle.ImageButton.Simple" />
</androidx.constraintlayout.widget.ConstraintLayout>
android:visibility="gone" />
<!-- URL -->
<LinearLayout
android:id="@+id/entry_url_container"
<com.kunzisoft.keepass.view.EntryField
android:id="@+id/entry_url_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_url_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/entry_url"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="all"
android:textIsSelectable="true"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
</LinearLayout>
android:visibility="gone" />
<!-- Comment -->
<LinearLayout
android:id="@+id/entry_notes_container"
<!-- Notes -->
<com.kunzisoft.keepass.view.EntryField
android:id="@+id/entry_notes_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_notes_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/entry_notes"
android:autoLink="all"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_notes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textIsSelectable="true"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
</LinearLayout>
android:visibility="gone" />
</LinearLayout>
</androidx.cardview.widget.CardView>
@@ -205,8 +93,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/card_view_padding"
android:orientation="vertical">
</LinearLayout>
android:orientation="vertical" />
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView

View File

@@ -19,11 +19,6 @@
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/menu_toggle_pass"
android:icon="@drawable/ic_visibility_white_24dp"
android:title="@string/menu_showpass"
android:orderInCategory="21"
app:showAsAction="always" />
<item android:id="@+id/menu_edit"
android:icon="@drawable/ic_mode_edit_white_24dp"
android:title="@string/menu_edit"

View File

@@ -26,13 +26,11 @@
android:title="@string/entry_add_field"
android:orderInCategory="92"
app:showAsAction="always" />
<!--
<item android:id="@+id/menu_add_attachment"
android:icon="@drawable/ic_attach_file_white_24dp"
android:title="@string/entry_add_attachment"
android:orderInCategory="93"
app:showAsAction="always" />
-->
<item android:id="@+id/menu_add_otp"
android:icon="@drawable/ic_otp_white_24dp"
android:title="@string/entry_setup_otp"

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