Compare commits

..

381 Commits
2.8.1 ... 2.8.5

Author SHA1 Message Date
J-Jamet
bcc8226ccc Merge branch 'release/2.8.5' 2020-09-18 16:13:25 +02:00
J-Jamet
ddec91a0c5 Upgrade to 2.8.5 2020-09-18 16:13:01 +02:00
J-Jamet
2c262bb29d Fix Base64 #708 2020-09-18 16:09:43 +02:00
J-Jamet
ccb32b045a Merge tag '2.8.4' into develop
2.8.4
2020-09-18 14:05:31 +02:00
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
32cd998c2a Merge branch 'release/2.8.2' 2020-08-21 16:04:02 +02:00
J-Jamet
691fc6335e Fix warnings 2020-08-21 15:48:58 +02:00
J-Jamet
b0025d1416 Refactor assign history 2020-08-21 13:49:04 +02:00
J-Jamet
2467d8b0e7 Try to fix new extra field 2020-08-21 13:06:13 +02:00
J-Jamet
28993c53e7 Fix keyboard shown 2020-08-21 12:25:32 +02:00
J-Jamet
efdea870f0 Upgrade kotlin to 1.4.0 2020-08-20 17:45:28 +02:00
J-Jamet
b2995ec862 Replace <strong> tags 2020-08-20 17:08:54 +02:00
J-Jamet
2bcc84dbb2 Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2020-08-20 16:49:47 +02:00
J-Jamet
70cc98ce33 Change strings according to #550 2020-08-20 16:48:56 +02:00
J-Jamet
6e055f398d Change focus timestamp variable 2020-08-20 14:20:51 +02:00
J-Jamet
9f6234f032 Fix show button in long tap mode 2020-08-20 14:16:20 +02:00
J-Jamet
6135544b72 Fix extra field cursor after orientation change 2020-08-20 14:04:48 +02:00
HARADA Hiroyuki
5f32ec218b 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-19 04:10:17 +02:00
J-Jamet
3b9a884db2 Remove TODO 2020-08-18 18:08:26 +02:00
J-Jamet
ada6b85868 Fix entry extra field replacement 2020-08-18 18:06:16 +02:00
HARADA Hiroyuki
2f8a4f447c 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-18 18:04:30 +02:00
J-Jamet
9bd6499271 Fix small issue 2020-08-18 17:09:51 +02:00
J-Jamet
76fcc919ef Encapsulate putItems 2020-08-18 16:46:09 +02:00
J-Jamet
a382297edf Encapsulate adapters in AnimatedItemsAdapter 2020-08-18 16:33:48 +02:00
HARADA Hiroyuki
4987dfe4f6 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-18 16:22:12 +02:00
J-Jamet
d1a2e50b8d Edit extra fields as recyclerview 2020-08-18 16:12:18 +02:00
HARADA Hiroyuki
b9e44b6166 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-18 14:12:31 +02:00
HARADA Hiroyuki
af601edc94 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-16 23:32:56 +02:00
Alexander Ritter
6640dcf9cd Translated using Weblate (German)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-08-16 23:32:56 +02:00
J-Jamet
9b2734ed38 Fix populate extra field 2020-08-16 10:40:17 +02:00
J-Jamet
afcfce056f Fix collapse animation 2020-08-16 09:53:47 +02:00
J-Jamet
8f3af2f27b Fix small issues in kitkat 2020-08-15 21:52:39 +02:00
J-Jamet
706aae47b3 Fix small cardView issue 2020-08-15 21:27:35 +02:00
J-Jamet
d494295b21 Harmonize image reverse buttons 2020-08-15 21:06:15 +02:00
J-Jamet
c1600a253b Harmonize image buttons 2020-08-15 20:41:57 +02:00
J-Jamet
a3bc29ad8f Fix expand uri info view 2020-08-15 15:54:08 +02:00
J-Jamet
83225ed157 Fix real attachment deletion 2020-08-15 15:38:46 +02:00
J-Jamet
f13f6dc01f Fix attachments deletion and change entry edit layout 2020-08-15 15:28:25 +02:00
J-Jamet
2d2489443a New icon to create extra field and fix focus 2020-08-15 12:41:16 +02:00
J-Jamet
0e5b7fbfa2 Possibility to remove attachment 2020-08-15 12:01:08 +02:00
HARADA Hiroyuki
d16a8068f7 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-14 16:35:05 +02:00
J-Jamet
c5ef11febc Tint password generator color 2020-08-14 15:50:54 +02:00
J-Jamet
027f31447a Fix delegation 2020-08-14 15:48:18 +02:00
HARADA Hiroyuki
41d1a4e5fb 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-14 09:49:39 +02:00
J-Jamet
924e3191cb Fix entry extra field view 2020-08-14 00:11:08 +02:00
J-Jamet
ebdc6b8fd9 Fix entry extra field view 2020-08-13 19:37:55 +02:00
HARADA Hiroyuki
5c05128cd7 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-13 18:41:25 +02:00
J-Jamet
5bf5685a12 Refactor extra field 2020-08-13 17:43:40 +02:00
J-Jamet
f7391cb4c4 Refactor extra field 2020-08-13 17:33:03 +02:00
HARADA Hiroyuki
bad7bd8884 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-13 17:20:25 +02:00
HARADA Hiroyuki
e0524a1656 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-13 17:05:45 +02:00
HARADA Hiroyuki
f59af9baa3 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-13 15:47:06 +02:00
HARADA Hiroyuki
b36890ca82 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-13 15:26:39 +02:00
HARADA Hiroyuki
22703af08b 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-13 15:12:46 +02:00
HARADA Hiroyuki
c4108269b3 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-13 14:55:52 +02:00
HARADA Hiroyuki
94c9d090cf 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-13 09:35:13 +02:00
HARADA Hiroyuki
28b9235a43 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-13 08:16:50 +02:00
librada
3aebae5e15 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-12 20:02:06 +02:00
librada
2631cb75d6 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-12 17:46:07 +02:00
librada
07b8b4156f 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-12 15:49:43 +02:00
librada
7978967c1a 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-12 14:26:02 +02:00
librada
3f24ff4de3 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-12 14:13:53 +02:00
librada
7253dd82a6 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-12 13:41:00 +02:00
librada
2b8eb3ae7e 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-12 13:36:08 +02:00
librada
429eae71cd 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-12 13:00:40 +02:00
librada
e5c552defb 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-12 12:42:27 +02:00
librada
5c950a2e2c 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-12 12:31:54 +02:00
librada
577ff78189 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-12 12:11:12 +02:00
librada
3f3cde05f7 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-12 11:30:28 +02:00
sivemortenfan
b48f2c3276 Translated using Weblate (Malayalam)
Currently translated at 65.4% (293 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2020-08-12 10:59:27 +02:00
ssantos
8e818846f0 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-12 10:59:26 +02:00
librada
1554e37f8c 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-12 10:59:24 +02:00
librada
affaabd011 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-12 10:47:35 +02:00
librada
f34a8f991c 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-12 08:30:37 +02:00
J-Jamet
c2b14d610b Revert "Small duration change in animation"
This reverts commit 23155279ab.
2020-08-11 17:52:32 +02:00
J-Jamet
02693d0cbb Fix database opening after creation 2020-08-11 16:57:16 +02:00
J-Jamet
23155279ab Small duration change in animation 2020-08-11 16:16:53 +02:00
J-Jamet
2d23f7403d Fix expand icon rotation 2020-08-11 15:59:35 +02:00
J-Jamet
ae411c6fd5 default database button at right 2020-08-10 20:26:23 +02:00
J-Jamet
ab8d6075a9 Allow dark style in free version 2020-08-10 18:10:19 +02:00
J-Jamet
bc5ae29a67 Default database in database selection 2020-08-10 17:36:06 +02:00
librada
1a8aabc30c 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-10 17:20:04 +02:00
J-Jamet
8c51d7f713 Add animation when delete custom field 2020-08-10 12:59:09 +02:00
J-Jamet
8a1485e7ce Change themes 2020-08-10 11:16:04 +02:00
J-Jamet
614145431a Replace info icon 2020-08-10 10:13:00 +02:00
J-Jamet
db25f1999f Remove flickering on the first database list load 2020-08-10 10:02:53 +02:00
J-Jamet
4ed231b9bb Replace and animate database info expand icon 2020-08-10 00:03:23 +02:00
J-Jamet
25a5342c11 Fix database opening after creation 2020-08-09 23:27:49 +02:00
J-Jamet
c7202e3ca9 Fix database list animation 2020-08-09 23:19:01 +02:00
C. Rüdinger
89c2e94cea Translated using Weblate (German)
Currently translated at 99.5% (446 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-08-09 22:32:47 +02:00
Oliver
3dc46771b5 Translated using Weblate (German)
Currently translated at 99.5% (446 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-08-09 22:32:47 +02:00
J-Jamet
0eac4d4d7f Fix database list change 2020-08-09 20:59:34 +02:00
J-Jamet
a0ceb788db Upgrade dependencies 2020-08-09 18:39:00 +02:00
J-Jamet
98fb36d03a Try to fix notification lock 2020-08-09 15:46:39 +02:00
J-Jamet
a670006517 Bottom app bar with center FAB 2020-08-09 14:25:22 +02:00
J-Jamet
9cdbe67cd4 Rollback stop task service before opening, ViewModel implementation seems fix the previous crash 2020-08-08 15:58:25 +02:00
Vachan
bbe8af452c Translated using Weblate (Malayalam)
Currently translated at 65.1% (292 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2020-08-08 15:32:47 +02:00
librada
f5edf28ce1 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-08 15:32:46 +02:00
J-Jamet
8fc30f590b Update CHANGELOG 2020-08-08 13:36:39 +02:00
J-Jamet
e578f23ebe Fix field order by using LinkedHashMap #611 2020-08-08 13:34:28 +02:00
J-Jamet
99c488fc9e Upgrade CHANGELOG 2020-08-08 10:41:07 +02:00
J-Jamet
f6a710660d Merge branch 'feature/ViewModel' into develop 2020-08-08 10:36:39 +02:00
J-Jamet
a61744bb65 Fix icon color 2020-08-07 21:14:27 +02:00
J-Jamet
17c3078c24 Better thread callback encapsulation 2020-08-07 21:00:20 +02:00
J-Jamet
5fa7731b56 Fix ViewModel methods call 2020-08-07 20:33:06 +02:00
J-Jamet
c8e2be4d8c Fix minor variable name 2020-08-07 16:51:55 +02:00
J-Jamet
e3db613a07 Rename progress database task provider 2020-08-07 16:45:52 +02:00
J-Jamet
0f7839027f Start using ViewModel for internal database action 2020-08-07 16:41:28 +02:00
J-Jamet
31b322a108 Replace AsyncTask by Coroutines 2020-08-07 11:29:16 +02:00
J-Jamet
b3c0494618 Better file attachment download implementation 2020-08-06 16:59:15 +02:00
J-Jamet
78ddb0533d Attachment download as coroutine 2020-08-06 16:35:15 +02:00
Vachan
da2158e7f2 Translated using Weblate (Malayalam)
Currently translated at 62.9% (282 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2020-08-06 10:32:49 +02:00
Stephan Paternotte
d2a1efb6e7 Translated using Weblate (Dutch)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2020-08-06 10:32:46 +02:00
librada
98a880db2d 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-06 10:32:46 +02:00
J-Jamet
cb82ef8703 Fix bottom app bar color in Kitkat 2020-08-05 17:00:01 +02:00
J-Jamet
d56246767b Better focus implementation 2020-08-05 16:37:30 +02:00
J-Jamet
bb477984aa Fix icons in arabic language 2020-08-05 15:51:06 +02:00
J-Jamet
5a3e650e02 Fix RTL views 2020-08-05 15:23:49 +02:00
J-Jamet
3c96dd2fac Upgrade CHANGELOG 2020-08-05 14:47:21 +02:00
J-Jamet
da051e3ff3 Fix biometric view visibility 2020-08-05 14:44:47 +02:00
J-Jamet
d15b6323c2 Upgrade CHANGELOG 2020-08-05 14:23:29 +02:00
J-Jamet
ec5f8fe4a4 Merge branch 'feature/CustomFieldLayout' into develop 2020-08-05 14:19:59 +02:00
J-Jamet
71d84d76f8 Fix last entry in Magikeyboard memory #653 2020-08-05 14:19:37 +02:00
J-Jamet
d33ed52ec2 Adjust Pan for EntryEdit 2020-08-05 11:00:23 +02:00
J-Jamet
3a970544bb Max password field lines -> 10 2020-08-04 19:58:02 +02:00
J-Jamet
792ac3a2e8 Fix password field 2020-08-04 19:51:22 +02:00
J-Jamet
60bbc27401 Unique password field with password generator button 2020-08-04 19:40:14 +02:00
J-Jamet
cf7cbcb6e6 Focus OTP view after creation 2020-08-04 19:03:04 +02:00
J-Jamet
c126a8eba9 Fix custom field creation 2020-08-04 18:51:00 +02:00
J-Jamet
66a60d0357 Fix manifest 2020-08-04 18:06:51 +02:00
J-Jamet
1acdadd027 Fix styles 2020-08-04 16:28:36 +02:00
J-Jamet
200be9dadd Fix toolbar padding with lock button 2020-08-04 15:19:49 +02:00
J-Jamet
a73e2872a4 Lock button color 2020-08-04 12:14:59 +02:00
J-Jamet
ce6f7729c5 Lock button with toolbar 2020-08-04 11:38:54 +02:00
J-Jamet
c285411371 Rollback lock button 2020-08-04 10:38:49 +02:00
J-Jamet
46394c600e New entry edit layout and custom lock 2020-08-03 20:54:20 +02:00
J-Jamet
bea9cb3248 Fix custom field error 2020-08-03 19:38:08 +02:00
J-Jamet
1087dcd714 New UI for custom field 2020-08-03 18:45:50 +02:00
Vachan
0b63029b7e Translated using Weblate (Malayalam)
Currently translated at 27.0% (121 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2020-08-01 09:47:48 +02:00
J-Jamet
9679d24414 New UI for Biometric 2020-08-01 01:00:53 +02:00
J-Jamet
7947fd53e5 Change biometric button layout 2020-07-31 23:34:00 +02:00
J-Jamet
8dedf75565 Minor style fix 2020-07-31 22:54:44 +02:00
J-Jamet
b5b7c12b49 Fix bar color in splashscreen 2020-07-31 22:15:04 +02:00
J-Jamet
51f4e3cc3a Change CardView margin 2020-07-31 21:59:16 +02:00
J-Jamet
24b0315d2e Update CHANGELOG 2020-07-31 21:31:05 +02:00
J-Jamet
be446220eb Fix themes 2020-07-31 21:29:44 +02:00
Vachan
a3ef2d332e Added translation using Weblate (Malayalam) 2020-07-31 05:33:41 +02:00
Milo Ivir
ba6fe576e3 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-07-30 08:42:39 +02:00
abidin toumi
abcef38102 Translated using Weblate (Arabic)
Currently translated at 61.3% (275 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2020-07-30 08:42:39 +02:00
ihor_ck
d5780b2f30 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-07-30 08:42:38 +02:00
librada
f7e498a0a2 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-07-30 08:42:00 +02:00
Petri Salminen
51ac7ca2de Translated using Weblate (Finnish)
Currently translated at 67.4% (302 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fi/
2020-07-30 08:41:57 +02:00
Aman ALam
c94535f6b5 Translated using Weblate (Punjabi)
Currently translated at 57.8% (259 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pa/
2020-07-25 10:10:55 +02:00
Thomas
07457ae368 Translated using Weblate (Chinese (Traditional))
Currently translated at 51.7% (232 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2020-07-25 10:10:55 +02:00
WaldiS
4767fff08c 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-07-25 10:10:55 +02:00
librada
f0c3498ecc Translated using Weblate (Japanese)
Currently translated at 71.4% (320 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-07-25 10:10:43 +02:00
Rodrigo Saldaña
1eca52d0fe Translated using Weblate (Spanish)
Currently translated at 76.3% (342 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-07-25 10:10:40 +02:00
SeerLite
7fbac9ad2f Translated using Weblate (Spanish)
Currently translated at 76.3% (342 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-07-25 10:10:40 +02:00
J-Jamet
6fb80ed50b Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2020-07-24 09:50:36 +02:00
J-Jamet
47f63ac81b Fix themes for free version 2020-07-24 09:50:26 +02:00
J-Jamet
42cc0b28ba Merge branch 'feature/Merge_Action_tasks_#628' into develop 2020-07-23 13:49:43 +02:00
J-Jamet
993806f781 Refactor Database.getInstance() in Task service 2020-07-22 23:48:07 +02:00
J-Jamet
8a5af33aaa Fix crash 2020-07-22 23:20:16 +02:00
J-Jamet
a974e36e9e Merge Open and Task services 2020-07-22 22:43:21 +02:00
Allan Nordhøy
17bcb2b39e Translated using Weblate (Norwegian Bokmål)
Currently translated at 80.3% (360 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-07-21 19:41:54 +02:00
ssantos
cddf02d0c1 Translated using Weblate (Portuguese (Portugal))
Currently translated at 60.9% (273 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2020-07-21 19:41:54 +02:00
librada
75ff7ece37 Translated using Weblate (Japanese)
Currently translated at 54.6% (245 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-07-21 19:41:54 +02:00
jan madsen
ec2b407a20 Translated using Weblate (Danish)
Currently translated at 97.3% (436 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-07-21 19:41:53 +02:00
ssantos
1dc9d78e54 Translated using Weblate (Portuguese (Portugal))
Currently translated at 60.4% (271 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2020-07-19 03:37:27 +02:00
WaldiS
5742a75c9d Translated using Weblate (Polish)
Currently translated at 99.1% (444 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-07-19 03:37:27 +02:00
zeritti
b5e9ad6d7e 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-07-19 03:37:27 +02:00
Oğuz Ersen
6393025219 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-07-18 08:42:19 +02:00
Eric
9ab3e289bc 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-07-18 08:42:19 +02:00
ihor_ck
6454474886 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-07-18 08:42:19 +02:00
solokot
c5720a7a03 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-07-18 08:42:18 +02:00
C. Rüdinger
41b15adc6d Translated using Weblate (German)
Currently translated at 98.8% (443 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-07-18 08:42:18 +02:00
J-Jamet
05b962e718 First commit to merge action tasks 2020-07-17 11:02:58 +02:00
Hosted Weblate
1f01ca7b85 Merge branch 'origin/master' into Weblate. 2020-07-17 10:31:51 +02:00
J-Jamet
5d3b4fa5ec Update CHANGELOG and fix descriptions by version 2020-07-17 10:26:26 +02:00
J-Jamet
754d2b2dd3 Merge tag '2.8.1' into develop
2.8.1
2020-07-17 10:12:30 +02:00
Milo Ivir
0461206a61 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-07-16 23:41:52 +02:00
Serdar Sağlam
663f9e3962 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-07-16 23:41:52 +02:00
Eric
34ee948c8e 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-07-16 23:41:51 +02:00
ihor_ck
1bb9c2e4fe 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-07-16 23:41:51 +02:00
solokot
8ab18ce5cc 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-07-16 23:41:51 +02:00
WaldiS
71be16826e 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-07-16 23:41:51 +02:00
Oğuz Ersen
926c09d9df 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-07-15 04:50:15 +02:00
ihor_ck
66c065ae7f Translated using Weblate (Ukrainian)
Currently translated at 98.8% (443 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-07-15 04:50:15 +02:00
Hosted Weblate
083ed7775c Merge branch 'origin/master' into Weblate. 2020-07-14 18:02:48 +02:00
Thomas
5185452495 Translated using Weblate (Chinese (Traditional))
Currently translated at 50.4% (224 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2020-07-14 18:02:47 +02:00
236 changed files with 7952 additions and 3738 deletions

View File

@@ -1,3 +1,29 @@
KeePassDX(2.8.5)
* Fix Base 64 #708
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
* Fix entry in Magikeyboard memory
* Fix biometric view visibility
* Fix fields order
* Upgrade code with ViewModel and LiveData
KeePassDX(2.8.1)
* Capture exceptions in coroutines

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass"
minSdkVersion 14
targetSdkVersion 29
versionCode = 37
versionName = "2.8.1"
versionCode = 41
versionName = "2.8.5"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
@@ -50,7 +50,7 @@ android {
buildConfigField "String", "BUILD_VERSION", "\"libre\""
buildConfigField "boolean", "FULL_VERSION", "true"
buildConfigField "boolean", "CLOSED_STORE", "false"
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
}
pro {
@@ -69,7 +69,7 @@ android {
buildConfigField "String", "BUILD_VERSION", "\"free\""
buildConfigField "boolean", "FULL_VERSION", "false"
buildConfigField "boolean", "CLOSED_STORE", "true"
buildConfigField "String[]", "STYLES_DISABLED", "{}"
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
}
@@ -95,16 +95,15 @@ def room_version = "2.2.5"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.0.1'
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
// TODO #538 implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
// Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:1.3.1"
implementation 'androidx.fragment:fragment-ktx:1.2.5'
// To upgrade with style
implementation 'com.google.android.material:material:1.0.0'
// Database

View File

@@ -124,8 +124,7 @@
android:configChanges="keyboardHidden" />
<activity
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustResize" />
android:windowSoftInputMode="adjustPan|stateAlwaysHidden" />
<!-- About and Settings -->
<activity
android:name="com.kunzisoft.keepass.activities.AboutActivity"
@@ -161,10 +160,6 @@
</intent-filter>
</activity>
<service
android:name="com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
android:enabled="true"

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
@@ -140,7 +141,7 @@ class EntryActivity : LockingActivity() {
// Init attachment service binder manager
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
mProgressDialogThread?.onActionFinish = { actionTask, result ->
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
@@ -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,24 +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 (element in entry.customFields.entries) {
val label = element.key
val value = element.value
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) {
@@ -332,28 +332,16 @@ class EntryActivity : LockingActivity() {
}
}
}
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
// Manage attachments
val attachments = entry.getAttachments()
val showAttachmentsView = attachments.isNotEmpty()
entryContentsView?.showAttachments(showAttachmentsView)
if (showAttachmentsView) {
entryContentsView?.assignAttachments(attachments)
entryContentsView?.onAttachmentClick { 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
}
}
}
entryContentsView?.refreshAttachments()
// Assign dates
entryContentsView?.assignCreationDate(entry.creationTime)
@@ -373,16 +361,9 @@ class EntryActivity : LockingActivity() {
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
taColorAccent.recycle()
}
val entryHistory = entry.getHistory()
val showHistoryView = entryHistory.isNotEmpty()
entryContentsView?.showHistory(showHistoryView)
if (showHistoryView) {
entryContentsView?.assignHistory(entryHistory)
entryContentsView?.onHistoryClick { historyItem, position ->
launch(this, historyItem, mReadOnly, position)
}
entryContentsView?.assignHistory(entry.getHistory()) { historyItem, position ->
launch(this, historyItem, mReadOnly, position)
}
entryContentsView?.refreshHistory()
// Assign special data
entryContentsView?.assignUUID(entry.nodeId.id)
@@ -411,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)
@@ -436,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
@@ -467,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)
}
)
}
}
@@ -498,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)
@@ -523,7 +482,7 @@ class EntryActivity : LockingActivity() {
}
R.id.menu_restore_entry_history -> {
mEntryLastVersion?.let { mainEntry ->
mProgressDialogThread?.startDatabaseRestoreEntryHistory(
mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory(
mainEntry,
mEntryHistoryPosition,
!mReadOnly && mAutoSaveEnable)
@@ -531,14 +490,14 @@ class EntryActivity : LockingActivity() {
}
R.id.menu_delete_entry_history -> {
mEntryLastVersion?.let { mainEntry ->
mProgressDialogThread?.startDatabaseDeleteEntryHistory(
mProgressDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(
mainEntry,
mEntryHistoryPosition,
!mReadOnly && mAutoSaveEnable)
}
}
R.id.menu_save_database -> {
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
}
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
}

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
@@ -31,59 +33,74 @@ import android.view.View
import android.widget.DatePicker
import android.widget.TimePicker
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.ActionMenuView
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.education.EntryEditActivityEducation
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,
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
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 entryEditAddToolBar: ActionMenuView? = null
private var entryEditFragment: EntryEditFragment? = null
private var entryEditAddToolBar: Toolbar? = null
private var validateButton: View? = null
private var lockView: View? = 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
@@ -102,19 +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")
}
}
lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener {
lockAndExit()
@@ -129,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
@@ -144,89 +150,103 @@ class EntryEditActivity : LockingActivity(),
entry.parent = mParent
}
}
// Create the new entry from the current one
if (savedInstanceState == null
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
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 == null
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
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 != null
&& savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
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
}
// 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 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 ->
when (item.itemId) {
R.id.menu_generate_password -> {
openPasswordGenerator()
true
}
R.id.menu_add_field -> {
addNewCustomField()
true
}
R.id.menu_add_attachment -> {
addNewAttachment(item)
true
}
R.id.menu_add_otp -> {
setupOTP()
true
@@ -236,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() }
@@ -244,12 +268,26 @@ class EntryEditActivity : LockingActivity(),
entryEditActivityEducation = EntryEditActivityEducation(this)
// Create progress dialog
mProgressDialogThread?.onActionFinish = { actionTask, result ->
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
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)
@@ -264,67 +302,58 @@ class EntryEditActivity : LockingActivity(),
} else {
View.GONE
}
}
private fun populateViewsWithEntry(newEntry: Entry) {
// Don't start the field reference manager, we want to see the raw ref
mDatabase?.stopManageEntry(newEntry)
// Padding if lock button visible
entryEditAddToolBar?.updateLockPaddingLeft()
// 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
for (entry in newEntry.customFields.entries) {
post {
putCustomField(entry.key, entry.value)
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.customFields.forEach { customField ->
putExtraField(customField.name, customField.protectedValue)
}
}
}
mDatabase?.stopManageEntry(newEntry)
}
private fun temporarilySaveAndShowSelectedIcon(icon: IconImage) {
mNewEntry?.icon = icon
mDatabase?.drawFactory?.let { iconDrawFactory ->
entryEditContentsView?.setIcon(iconDrawFactory, icon)
}
super.onPause()
}
/**
@@ -335,16 +364,98 @@ class EntryEditActivity : LockingActivity(),
}
/**
* Add a new customized field view and scroll to bottom
* Add a new customized field
*/
private fun addNewCustomField() {
entryEditContentsView?.addEmptyCustomField()
EntryCustomFieldDialogFragment.getInstance().show(supportFragmentManager, "customFieldDialog")
}
private fun editCustomField(field: Field) {
EntryCustomFieldDialogFragment.getInstance(field).show(supportFragmentManager, "customFieldDialog")
}
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")
}
@@ -352,24 +463,35 @@ class EntryEditActivity : LockingActivity(),
* Saves the new entry or update an existing entry in the database
*/
private fun saveEntry() {
// Get the temp entry
entryEditFragment?.getEntryInfo()?.let { newEntryInfo ->
// Launch a validation and show the error if present
if (entryEditContentsView?.isValid() == true) {
// Clone the entry
mNewEntry?.let { newEntry ->
if (mIsNew) {
// Create new one
mDatabase?.createEntry()
} else {
// Create a clone
Entry(mEntry!!)
}?.let { newEntry ->
// WARNING Add the parent previously deleted
newEntry.parent = mEntry?.parent
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) {
mParent?.let { parent ->
mProgressDialogThread?.startDatabaseCreateEntry(
mProgressDatabaseTaskProvider?.startDatabaseCreateEntry(
newEntry,
parent,
!mReadOnly && mAutoSaveEnable
@@ -377,7 +499,7 @@ class EntryEditActivity : LockingActivity(),
}
} else {
mEntry?.let { oldEntry ->
mProgressDialogThread?.startDatabaseUpdateEntry(
mProgressDatabaseTaskProvider?.startDatabaseUpdateEntry(
oldEntry,
newEntry,
!mReadOnly && mAutoSaveEnable
@@ -397,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? = entryEditAddToolBar?.findViewById(R.id.menu_generate_password)
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,
{
@@ -431,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()
}
)
}
}
}
}
@@ -445,7 +576,7 @@ class EntryEditActivity : LockingActivity(),
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_save_database -> {
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
}
R.id.menu_contribute -> {
MenuUtil.onContributionItemSelected(this)
@@ -460,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?.putCustomField(otpField.name, otpField.protectedValue)
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
}
}
@@ -477,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)
@@ -496,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)
@@ -507,17 +650,15 @@ class EntryEditActivity : LockingActivity(),
}
override fun onSaveInstanceState(outState: Bundle) {
mNewEntry?.let {
populateEntryWithViews(it)
outState.putParcelable(KEY_NEW_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 {
@@ -541,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)
@@ -568,7 +709,7 @@ class EntryEditActivity : LockingActivity(),
const val KEY_PARENT = "parent"
// SaveInstanceState
const val KEY_NEW_ENTRY = "new_entry"
const val TEMP_ATTACHMENTS = "TEMP_ATTACHMENTS"
// Keys for callback
const val ADD_ENTRY_RESULT_CODE = 31
@@ -576,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

@@ -32,9 +32,11 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
@@ -43,19 +45,22 @@ 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
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
import kotlinx.android.synthetic.main.activity_file_selection.*
import java.io.FileNotFoundException
@@ -64,10 +69,11 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
// Views
private var coordinatorLayout: CoordinatorLayout? = null
private var fileManagerExplanationButton: View? = null
private var createDatabaseButtonView: View? = null
private var openDatabaseButtonView: View? = null
private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels()
// Adapter to manage database history list
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
@@ -75,9 +81,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
private var mDatabaseFileUri: Uri? = null
private var mOpenFileHelper: OpenFileHelper? = null
private var mSelectFileHelper: SelectFileHelper? = null
private var mProgressDialogThread: ProgressDialogThread? = null
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -91,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)
}
@@ -117,26 +118,25 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
(fileDatabaseHistoryRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
// Construct adapter with listeners
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this)
mAdapterDatabaseHistory?.setOnDefaultDatabaseListener { databaseFile ->
databaseFilesViewModel.setDefaultDatabase(databaseFile)
}
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
UriUtil.parse(fileDatabaseHistoryEntityToOpen.databaseUri)?.let { databaseFileUri ->
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
launchPasswordActivity(
databaseFileUri,
UriUtil.parse(fileDatabaseHistoryEntityToOpen.keyFileUri))
fileDatabaseHistoryEntityToOpen.keyFileUri
)
}
}
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
// Remove from app database
mFileDatabaseHistoryAction?.deleteFileDatabaseHistory(fileDatabaseHistoryToDelete) { fileHistoryDeleted ->
// Remove from adapter
fileHistoryDeleted?.let { databaseFileHistoryDeleted ->
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileHistoryDeleted)
mAdapterDatabaseHistory?.notifyDataSetChanged()
}
}
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
true
}
mAdapterDatabaseHistory?.setOnSaveAliasListener { fileDatabaseHistoryWithNewAlias ->
mFileDatabaseHistoryAction?.addOrUpdateFileDatabaseHistory(fileDatabaseHistoryWithNewAlias)
// Update in app database
databaseFilesViewModel.updateDatabaseFile(fileDatabaseHistoryWithNewAlias)
}
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
@@ -159,12 +159,47 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
}
// Observe list of databases
databaseFilesViewModel.databaseFilesLoaded.observe(this, Observer { databaseFiles ->
when (databaseFiles.databaseFileAction) {
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
}
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
}
GroupActivity.launch(this@FileDatabaseSelectActivity)
}
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
}
}
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
}
}
}
databaseFilesViewModel.consumeAction()
})
// Observe default database
databaseFilesViewModel.defaultDatabase.observe(this, Observer {
// Retrieve settings for default database
mAdapterDatabaseHistory?.setDefaultDatabase(it)
})
// Attach the dialog thread to this activity
mProgressDialogThread = ProgressDialogThread(this).apply {
onActionFinish = { actionTask, _ ->
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
onActionFinish = { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_CREATE_TASK -> {
GroupActivity.launch(this@FileDatabaseSelectActivity)
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
val keyFileUri = result.data?.getParcelable<Uri?>(KEY_FILE_URI_KEY)
databaseFilesViewModel.addDatabaseFile(databaseUri, keyFileUri)
}
}
}
}
@@ -286,34 +321,20 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
} else {
// Construct adapter with listeners
if (PreferencesUtil.showRecentFiles(this)) {
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
databaseFileHistoryList?.let { historyList ->
val hideBrokenLocations = PreferencesUtil.hideBrokenLocations(this@FileDatabaseSelectActivity)
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(
// Show only uri accessible
historyList.filter {
if (hideBrokenLocations) {
FileDatabaseInfo(this@FileDatabaseSelectActivity,
it.databaseUri).exists
} else
true
})
mAdapterDatabaseHistory?.notifyDataSetChanged()
}
}
databaseFilesViewModel.loadListOfDatabases()
} else {
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
mAdapterDatabaseHistory?.notifyDataSetChanged()
}
// Register progress task
mProgressDialogThread?.registerProgressTask()
mProgressDatabaseTaskProvider?.registerProgressTask()
}
}
override fun onPause() {
// Unregister progress task
mProgressDialogThread?.unregisterProgressTask()
mProgressDatabaseTaskProvider?.unregisterProgressTask()
super.onPause()
}
@@ -334,7 +355,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
mDatabaseFileUri?.let { databaseUri ->
// Create the new database
mProgressDialogThread?.startDatabaseCreate(
mProgressDatabaseTaskProvider?.startDatabaseCreate(
databaseUri,
masterPasswordChecked,
masterPassword,
@@ -362,8 +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)
}
@@ -419,7 +439,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
openDatabaseButtonView!!,
{tapTargetView ->
tapTargetView?.let {
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
}
},
{}
@@ -428,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

@@ -76,10 +76,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.view.AddNodeButtonView
import com.kunzisoft.keepass.view.ToolbarAction
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.showActionError
import com.kunzisoft.keepass.view.*
class GroupActivity : LockingActivity(),
GroupEditDialogFragment.EditGroupListener,
@@ -108,6 +105,8 @@ class GroupActivity : LockingActivity(),
private var mCurrentGroupIsASearch: Boolean = false
private var mRequestStartupSearch = true
private var actionNodeMode: ActionMode? = null
// To manage history in selection mode
private var mSelectionModeCountBackStack = 0
@@ -123,9 +122,6 @@ class GroupActivity : LockingActivity(),
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isFinishing) {
return
}
mDatabase = Database.getInstance()
// Construct main view
@@ -222,7 +218,7 @@ class GroupActivity : LockingActivity(),
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, database)
// Init dialog thread
mProgressDialogThread?.onActionFinish = { actionTask, result ->
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
var oldNodes: List<Node> = ArrayList()
result.data?.getBundle(OLD_NODES_KEY)?.let { oldNodesBundle ->
@@ -481,7 +477,8 @@ class GroupActivity : LockingActivity(),
enableAddGroup(addGroupEnabled)
enableAddEntry(addEntryEnabled)
showButton()
if (actionNodeMode == null)
showButton()
}
}
@@ -511,7 +508,8 @@ class GroupActivity : LockingActivity(),
}
override fun onScrolled(dy: Int) {
addNodeButtonView?.hideButtonOnScrollListener(dy)
if (actionNodeMode == null)
addNodeButtonView?.hideOrShowButtonOnScrollListener(dy)
}
override fun onNodeClick(node: Node) {
@@ -554,18 +552,28 @@ class GroupActivity : LockingActivity(),
}
}
private var actionNodeMode: ActionMode? = null
private fun finishNodeAction() {
actionNodeMode?.finish()
actionNodeMode = null
addNodeButtonView?.showButton()
}
override fun onNodeSelected(nodes: List<Node>): Boolean {
if (nodes.isNotEmpty()) {
if (actionNodeMode == null || toolbarAction?.getSupportActionModeCallback() == null) {
mListNodesFragment?.actionNodesCallback(nodes, this)?.let {
mListNodesFragment?.actionNodesCallback(nodes, this, object: ActionMode.Callback {
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return true
}
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return true
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
return false
}
override fun onDestroyActionMode(mode: ActionMode?) {
actionNodeMode = null
addNodeButtonView?.showButton()
}
})?.let {
actionNodeMode = toolbarAction?.startSupportActionMode(it)
}
} else {
@@ -622,7 +630,7 @@ class GroupActivity : LockingActivity(),
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
// Copy
mCurrentGroup?.let { newParent ->
mProgressDialogThread?.startDatabaseCopyNodes(
mProgressDatabaseTaskProvider?.startDatabaseCopyNodes(
nodes,
newParent,
!mReadOnly && mAutoSaveEnable
@@ -632,7 +640,7 @@ class GroupActivity : LockingActivity(),
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
// Move
mCurrentGroup?.let { newParent ->
mProgressDialogThread?.startDatabaseMoveNodes(
mProgressDatabaseTaskProvider?.startDatabaseMoveNodes(
nodes,
newParent,
!mReadOnly && mAutoSaveEnable
@@ -666,7 +674,7 @@ class GroupActivity : LockingActivity(),
&& database.isRecycleBinEnabled
&& database.recycleBin != mCurrentGroup) {
mProgressDialogThread?.startDatabaseDeleteNodes(
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
nodes,
!mReadOnly && mAutoSaveEnable
)
@@ -681,7 +689,7 @@ class GroupActivity : LockingActivity(),
}
override fun permanentlyDeleteNodes(nodes: List<Node>) {
mProgressDialogThread?.startDatabaseDeleteNodes(
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
nodes,
!mReadOnly && mAutoSaveEnable
)
@@ -700,6 +708,8 @@ class GroupActivity : LockingActivity(),
assignGroupViewElements()
// Refresh suggestions to change preferences
mSearchSuggestionAdapter?.reInit(this)
// Padding if lock button visible
toolbarAction?.updateLockPaddingLeft()
}
override fun onPause() {
@@ -840,7 +850,7 @@ class GroupActivity : LockingActivity(),
//onSearchRequested();
return true
R.id.menu_save_database -> {
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
return true
}
R.id.menu_empty_recycle_bin -> {
@@ -874,7 +884,7 @@ class GroupActivity : LockingActivity(),
// Not really needed here because added in runnable but safe
newGroup.parent = currentGroup
mProgressDialogThread?.startDatabaseCreateGroup(
mProgressDatabaseTaskProvider?.startDatabaseCreateGroup(
newGroup,
currentGroup,
!mReadOnly && mAutoSaveEnable
@@ -896,7 +906,7 @@ class GroupActivity : LockingActivity(),
}
}
// If group updated save it in the database
mProgressDialogThread?.startDatabaseUpdateGroup(
mProgressDatabaseTaskProvider?.startDatabaseUpdateGroup(
oldGroupToUpdate,
updateGroup,
!mReadOnly && mAutoSaveEnable

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)
@@ -266,14 +272,15 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}
fun actionNodesCallback(nodes: List<Node>,
menuListener: NodesActionMenuListener?) : ActionMode.Callback {
menuListener: NodesActionMenuListener?,
actionModeCallback: ActionMode.Callback) : ActionMode.Callback {
return object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
nodeActionSelectionMode = false
nodeActionPasteMode = PasteMode.UNDEFINED
return true
return actionModeCallback.onCreateActionMode(mode, menu)
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
@@ -318,7 +325,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
// Add the number of items selected in title
mode?.title = nodes.size.toString()
return true
return actionModeCallback.onPrepareActionMode(mode, menu)
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
@@ -348,7 +355,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
nodeActionSelectionMode = false
returnValue
}
else -> false
else -> actionModeCallback.onActionItemClicked(mode, item)
}
}
@@ -358,6 +365,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
mAdapter?.unselectActionNodes()
nodeActionPasteMode = PasteMode.UNDEFINED
nodeActionSelectionMode = false
actionModeCallback.onDestroyActionMode(mode)
}
}
}

View File

@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.activities
import android.app.Activity
import android.app.assist.AssistStructure
import android.app.backup.BackupManager
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
@@ -34,24 +33,25 @@ import android.util.Log
import android.view.*
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.widget.*
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar
import androidx.biometric.BiometricManager
import androidx.core.app.ActivityCompat
import androidx.lifecycle.Observer
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.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
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.search.SearchHelper
@@ -60,14 +60,17 @@ import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
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
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import kotlinx.android.synthetic.main.activity_password.*
import java.io.FileNotFoundException
@@ -81,16 +84,17 @@ open class PasswordActivity : SpecialModeActivity() {
private var confirmButtonView: Button? = null
private var checkboxPasswordView: CompoundButton? = null
private var checkboxKeyFileView: CompoundButton? = null
private var checkboxDefaultDatabaseView: CompoundButton? = null
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
private var infoContainerView: ViewGroup? = null
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
private var mDatabaseFileUri: Uri? = null
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
@@ -105,7 +109,7 @@ open class PasswordActivity : SpecialModeActivity() {
field = value
}
private var mProgressDialogThread: ProgressDialogThread? = null
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
private var mAllowAutoOpenBiometricPrompt: Boolean = true
@@ -127,16 +131,16 @@ open class PasswordActivity : SpecialModeActivity() {
keyFileSelectionView = findViewById(R.id.keyfile_selection)
checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
advancedUnlockInfoView = findViewById(R.id.biometric_info)
infoContainerView = findViewById(R.id.activity_password_info_container)
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
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)
}
@@ -163,12 +167,34 @@ open class PasswordActivity : SpecialModeActivity() {
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
}
if (savedInstanceState?.containsKey(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) == true) {
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
}
mProgressDialogThread = ProgressDialogThread(this).apply {
// Observe database file change
databaseFileViewModel.databaseFileLoaded.observe(this, Observer { databaseFile ->
// Force read only if the file does not exists
mForceReadOnly = databaseFile?.let {
!it.databaseFileExists
} ?: true
invalidateOptionsMenu()
// Post init uri with KeyFile only if needed
val keyFileUri =
if (mRememberKeyFile
&& (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
databaseFile?.keyFileUri
} else {
mDatabaseKeyFileUri
}
// Define title
filenameView?.text = databaseFile?.databaseAlias ?: ""
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
})
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
onActionFinish = { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> {
@@ -205,7 +231,7 @@ open class PasswordActivity : SpecialModeActivity() {
result.data?.let { resultData ->
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
keyFileUri = resultData.getParcelable(KEY_FILE_KEY)
keyFileUri = resultData.getParcelable(KEY_FILE_URI_KEY)
readOnly = resultData.getBoolean(READ_ONLY_KEY)
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
}
@@ -351,7 +377,7 @@ open class PasswordActivity : SpecialModeActivity() {
clearCredentialsViews()
}
mProgressDialogThread?.registerProgressTask()
mProgressDatabaseTaskProvider?.registerProgressTask()
// Back to previous keyboard is setting activated
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this)) {
@@ -364,72 +390,23 @@ open class PasswordActivity : SpecialModeActivity() {
else
mAllowAutoOpenBiometricPrompt
initUriFromIntent()
mDatabaseFileUri?.let { databaseFileUri ->
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
}
checkPermission()
}
}
private fun initUriFromIntent() {
/*
// "canXrite" doesn't work with Google Drive, don't really know why?
mForceReadOnly = mDatabaseFileUri?.let {
!FileDatabaseInfo(this, it).canWrite
} ?: false
*/
mForceReadOnly = mDatabaseFileUri?.let {
!FileDatabaseInfo(this, it).exists
} ?: true
// Post init uri with KeyFile if needed
if (mRememberKeyFile && (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
// Retrieve KeyFile in a thread
mDatabaseFileUri?.let { databaseUri ->
FileDatabaseHistoryAction.getInstance(applicationContext)
.getKeyFileUriByDatabaseUri(databaseUri) {
onPostInitUri(databaseUri, it)
}
}
} else {
onPostInitUri(mDatabaseFileUri, mDatabaseKeyFileUri)
}
}
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
// Define title
databaseFileUri?.let {
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
filenameView?.text = title
}
}
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
// Define Key File text
if (mRememberKeyFile) {
populateKeyFileTextView(keyFileUri)
}
// Define listeners for default database checkbox and validate button
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
var newDefaultFileUri: Uri? = null
if (isChecked) {
newDefaultFileUri = databaseFileUri ?: newDefaultFileUri
}
PreferencesUtil.saveDefaultDatabasePath(this, newDefaultFileUri)
val backupManager = BackupManager(this@PasswordActivity)
backupManager.dataChanged()
}
// Define listener for validate button
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
// Retrieve settings for default database
val defaultFilename = PreferencesUtil.getDefaultDatabasePath(this)
if (databaseFileUri != null
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
&& databaseFileUri == UriUtil.parse(defaultFilename)) {
checkboxDefaultDatabaseView?.isChecked = true
}
// If Activity is launch with a password and want to open directly
val intent = intent
val password = intent.getStringExtra(KEY_PASSWORD)
@@ -446,7 +423,6 @@ open class PasswordActivity : SpecialModeActivity() {
var biometricInitialize = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
if (advancedUnlockedManager == null && databaseFileUri != null) {
advancedUnlockedManager = AdvancedUnlockedManager(this,
databaseFileUri,
@@ -478,6 +454,7 @@ open class PasswordActivity : SpecialModeActivity() {
biometricInitialize = true
} else {
advancedUnlockedManager?.destroy()
advancedUnlockInfoView?.visibility = View.GONE
}
}
if (!biometricInitialize) {
@@ -533,7 +510,7 @@ open class PasswordActivity : SpecialModeActivity() {
}
override fun onPause() {
mProgressDialogThread?.unregisterProgressTask()
mProgressDatabaseTaskProvider?.unregisterProgressTask()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
advancedUnlockedManager?.destroy()
@@ -608,7 +585,7 @@ open class PasswordActivity : SpecialModeActivity() {
readOnly: Boolean,
cipherDatabaseEntity: CipherDatabaseEntity?,
fixDuplicateUUID: Boolean) {
mProgressDialogThread?.startDatabaseLoad(
mProgressDatabaseTaskProvider?.startDatabaseLoad(
databaseUri,
password,
keyFile,
@@ -772,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

@@ -0,0 +1,182 @@
/*
* 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.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) {
super.onAttach(context)
try {
entryCustomFieldListener = context as EntryCustomFieldListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context.toString()
+ " must implement " + EntryCustomFieldListener::class.java.name)
}
}
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) { _, _ -> }
val dialogCreated = builder.create()
customFieldLabel?.requestFocus()
customFieldLabel?.imeOptions = EditorInfo.IME_ACTION_DONE
customFieldLabel?.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
approveIfValid()
}
false
}
dialogCreated.window?.setSoftInputMode(SOFT_INPUT_STATE_VISIBLE)
return dialogCreated
}
return super.onCreateDialog(savedInstanceState)
}
override fun onResume() {
super.onResume()
// To prevent auto dismiss
val d = dialog as AlertDialog?
if (d != null) {
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
positiveButton.setOnClickListener {
approveIfValid()
}
}
}
private fun approveIfValid() {
if (isValid()) {
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()
}
}
private fun isValid(): Boolean {
return if (customFieldLabel?.text?.toString()?.isNotEmpty() != true) {
setError(R.string.error_string_key)
false
} else {
setError(null)
true
}
}
fun setError(@StringRes errorId: Int?) {
customFieldLabelContainer?.error = if (errorId == null) null else {
requireContext().getString(errorId)
}
}
interface EntryCustomFieldListener {
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

@@ -28,6 +28,7 @@ import android.text.TextWatcher
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
@@ -106,6 +107,11 @@ class SetOTPDialogFragment : DialogFragment() {
}
}
override fun onDetach() {
mCreateOTPElementListener = null
super.onDetach()
}
@SuppressLint("ClickableViewAccessibility")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@@ -152,6 +158,28 @@ class SetOTPDialogFragment : DialogFragment() {
otpCounterTextView?.setOnTouchListener(mOnTouchListener)
otpDigitsTextView?.setOnTouchListener(mOnTouchListener)
// To manage focus
otpPeriodTextView?.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_NEXT) {
otpDigitsTextView?.requestFocus()
true
} else
false
}
otpCounterTextView?.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_NEXT) {
otpDigitsTextView?.requestFocus()
true
} else
false
}
otpCounterTextView?.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_NEXT) {
root?.requestFocus(View.FOCUS_DOWN)
true
} else
false
}
// HOTP / TOTP Type selection
val otpTypeArray = OtpType.values()

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

@@ -27,7 +27,7 @@ import android.view.View
import android.view.ViewGroup
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
@@ -51,7 +51,7 @@ abstract class LockingActivity : SpecialModeActivity() {
private var mReadOnlyToSave: Boolean = false
protected var mAutoSaveEnable: Boolean = true
var mProgressDialogThread: ProgressDialogThread? = null
var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
private set
override fun onCreate(savedInstanceState: Bundle?) {
@@ -80,7 +80,7 @@ abstract class LockingActivity : SpecialModeActivity() {
mExitLock = false
mProgressDialogThread = ProgressDialogThread(this)
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -96,7 +96,7 @@ abstract class LockingActivity : SpecialModeActivity() {
override fun onResume() {
super.onResume()
mProgressDialogThread?.registerProgressTask()
mProgressDatabaseTaskProvider?.registerProgressTask()
// To refresh when back to normal workflow from selection workflow
mReadOnlyToSave = ReadOnlyHelper.retrieveReadOnlyFromIntent(intent)
@@ -131,7 +131,7 @@ abstract class LockingActivity : SpecialModeActivity() {
override fun onPause() {
LOCKING_ACTIVITY_UI_VISIBLE = false
mProgressDialogThread?.unregisterProgressTask()
mProgressDatabaseTaskProvider?.unregisterProgressTask()
super.onPause()

View File

@@ -46,12 +46,19 @@ abstract class StylishFragment : Fragment() {
// To fix status bar color
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val window = requireActivity().window
val attrColorPrimaryDark = intArrayOf(android.R.attr.colorPrimaryDark)
val taColorPrimaryDark = contextThemed?.theme?.obtainStyledAttributes(attrColorPrimaryDark)
val defaultColor = Color.BLACK
window.statusBarColor = taColorPrimaryDark?.getColor(0, defaultColor) ?: defaultColor
taColorPrimaryDark?.recycle()
try {
val taStatusBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor))
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
taStatusBarColor?.recycle()
} catch (e: Exception) {}
try {
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
taNavigationBarColor?.recycle()
} catch (e: Exception) {}
}
return super.onCreateView(inflater, container, savedInstanceState)

View File

@@ -0,0 +1,126 @@
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.view.collapse
abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val context: Context)
: RecyclerView.Adapter<T>() {
protected val inflater: LayoutInflater = LayoutInflater.from(context)
var itemsList: MutableList<Item> = ArrayList()
private set
var onDeleteButtonClickListener: ((item: Item)->Unit)? = null
private var mItemToRemove: Item? = null
var onListSizeChangedListener: ((previousSize: Int, newSize: Int)->Unit)? = null
override fun getItemCount(): Int {
return itemsList.size
}
open fun assignItems(items: List<Item>) {
val previousSize = itemsList.size
itemsList.apply {
clear()
addAll(items)
}
notifyDataSetChanged()
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)) {
val index = itemsList.indexOf(item)
itemsList.removeAt(index)
itemsList.add(index, item)
notifyItemChanged(index)
} else {
itemsList.add(item)
notifyItemInserted(itemsList.indexOf(item))
}
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
}
/**
* 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 (performDeletion(holder, item)) {
setOnClickListener(null)
} else {
setOnClickListener {
onDeleteButtonClickListener?.invoke(item)
mItemToRemove = item
notifyItemChanged(position)
}
}
}
}
private fun deleteItem(item: Item) {
val previousSize = itemsList.size
val position = itemsList.indexOf(item)
if (position >= 0) {
itemsList.removeAt(position)
notifyItemRemoved(position)
mItemToRemove = null
for (i in 0 until itemsList.size) {
notifyItemChanged(i)
}
}
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
}
fun clear() {
itemsList.clear()
notifyDataSetChanged()
}
}

View File

@@ -1,100 +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.text.format.Formatter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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
class EntryAttachmentsAdapter(val context: Context) : RecyclerView.Adapter<EntryAttachmentsAdapter.EntryBinariesViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
var entryAttachmentsList: MutableList<EntryAttachment> = ArrayList()
var onItemClickListener: ((item: EntryAttachment, position: Int)->Unit)? = null
private val mDatabase = Database.getInstance()
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 = entryAttachmentsList[position]
holder.binaryFileTitle.text = entryAttachment.name
holder.binaryFileSize.text = Formatter.formatFileSize(context,
entryAttachment.binaryAttachment.length())
holder.binaryFileCompression.apply {
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip
|| entryAttachment.binaryAttachment.isCompressed == true) {
text = CompressionAlgorithm.GZip.getName(context.resources)
visibility = View.VISIBLE
} else {
text = ""
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
}
progress = entryAttachment.downloadProgression
}
holder.itemView.setOnClickListener {
onItemClickListener?.invoke(entryAttachment, position)
}
}
override fun getItemCount(): Int {
return entryAttachmentsList.size
}
fun updateProgress(entryAttachment: EntryAttachment) {
val indexEntryAttachment = entryAttachmentsList.indexOfLast { current -> current.name == entryAttachment.name }
if (indexEntryAttachment != -1) {
entryAttachmentsList[indexEntryAttachment] = entryAttachment
notifyItemChanged(indexEntryAttachment)
}
}
fun clear() {
entryAttachmentsList.clear()
}
inner class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
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 binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
}
}

View File

@@ -0,0 +1,148 @@
/*
* 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.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.CompressionAlgorithm
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
class EntryAttachmentsItemsAdapter(context: Context)
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
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 entryAttachmentState = itemsList[position]
holder.itemView.visibility = View.VISIBLE
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,
entryAttachmentState.attachment.binaryAttachment.length())
holder.binaryFileCompression.apply {
if (entryAttachmentState.attachment.binaryAttachment.isCompressed) {
text = CompressionAlgorithm.GZip.getName(context.resources)
visibility = View.VISIBLE
} else {
text = ""
visibility = View.GONE
}
}
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)
}
}
}
holder.itemView.setOnClickListener(null)
}
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)
}
}
}
}
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

@@ -22,31 +22,33 @@ package com.kunzisoft.keepass.adapters
import android.content.Context
import android.graphics.Color
import android.graphics.PorterDuff
import android.net.Uri
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.annotation.ColorInt
import androidx.recyclerview.widget.RecyclerView
import android.util.TypedValue
import android.view.*
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import android.widget.ViewSwitcher
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryEntity
import com.kunzisoft.keepass.utils.FileDatabaseInfo
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.view.collapse
import com.kunzisoft.keepass.view.expand
class FileDatabaseHistoryAdapter(private val context: Context)
class FileDatabaseHistoryAdapter(context: Context)
: RecyclerView.Adapter<FileDatabaseHistoryAdapter.FileDatabaseHistoryViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var fileItemOpenListener: ((FileDatabaseHistoryEntity)->Unit)? = null
private var fileSelectClearListener: ((FileDatabaseHistoryEntity)->Boolean)? = null
private var saveAliasListener: ((FileDatabaseHistoryEntity)->Unit)? = null
private var defaultDatabaseListener: ((DatabaseFile?) -> Unit)? = null
private var fileItemOpenListener: ((DatabaseFile)->Unit)? = null
private var fileSelectClearListener: ((DatabaseFile)->Boolean)? = null
private var saveAliasListener: ((DatabaseFile)->Unit)? = null
private val listDatabaseFiles = ArrayList<FileDatabaseHistoryEntity>()
private val listDatabaseFiles = ArrayList<DatabaseFile>()
private var mExpandedPosition = -1
private var mPreviousExpandedPosition = -1
private var mDefaultDatabaseFile: DatabaseFile? = null
private var mExpandedDatabaseFile: DatabaseFile? = null
private var mPreviousExpandedDatabaseFile: DatabaseFile? = null
@ColorInt
private val defaultColor: Int
@@ -63,43 +65,49 @@ class FileDatabaseHistoryAdapter(private val context: Context)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileDatabaseHistoryViewHolder {
val view = inflater.inflate(R.layout.item_file_row, parent, false)
val view = inflater.inflate(R.layout.item_file_info, parent, false)
return FileDatabaseHistoryViewHolder(view)
}
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
// Get info from position
val fileHistoryEntity = listDatabaseFiles[position]
val fileDatabaseInfo = FileDatabaseInfo(context, fileHistoryEntity.databaseUri)
val databaseFile = listDatabaseFiles[position]
// Click item to open file
if (fileItemOpenListener != null)
holder.fileContainer.setOnClickListener {
fileItemOpenListener?.invoke(fileHistoryEntity)
holder.fileContainer.setOnClickListener {
fileItemOpenListener?.invoke(databaseFile)
}
// Default database
holder.defaultFileButton.apply {
this.isChecked = mDefaultDatabaseFile == databaseFile
setOnClickListener {
defaultDatabaseListener?.invoke(if (isChecked) databaseFile else null)
}
}
// File alias
holder.fileAlias.text = fileDatabaseInfo.retrieveDatabaseAlias(fileHistoryEntity.databaseAlias)
holder.fileAlias.text = databaseFile.databaseAlias
// File path
holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString())
holder.filePath.text = databaseFile.databaseDecodedPath
if (fileDatabaseInfo.exists) {
holder.fileInformation.clearColorFilter()
if (databaseFile.databaseFileExists) {
holder.fileInformationButton.clearColorFilter()
} else {
holder.fileInformation.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
holder.fileInformationButton.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
}
// Modification
fileDatabaseInfo.getModificationString()?.let {
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
fileDatabaseInfo.getSizeString()?.let {
databaseFile.databaseSize?.let {
holder.fileSize.text = it
holder.fileSize.visibility = View.VISIBLE
} ?: run {
@@ -107,15 +115,24 @@ class FileDatabaseHistoryAdapter(private val context: Context)
}
// Click on information
val isExpanded = position == mExpandedPosition
//This line hides or shows the layout in question
holder.fileExpandContainer.visibility = if (isExpanded) View.VISIBLE else View.GONE
val isExpanded = databaseFile == mExpandedDatabaseFile
// Hides or shows info
holder.fileExpandContainer.apply {
if (isExpanded) {
if (visibility != View.VISIBLE) {
visibility = View.VISIBLE
expand(true, resources.getDimensionPixelSize(R.dimen.item_file_info_height))
}
} else {
collapse(true)
}
}
// Save alias modification
holder.fileAliasCloseButton.setOnClickListener {
// Change the alias
fileHistoryEntity.databaseAlias = holder.fileAliasEdit.text.toString()
saveAliasListener?.invoke(fileHistoryEntity)
databaseFile.databaseAlias = holder.fileAliasEdit.text.toString()
saveAliasListener?.invoke(databaseFile)
// Finish save mode
holder.fileMainSwitcher.showPrevious()
@@ -130,20 +147,22 @@ class FileDatabaseHistoryAdapter(private val context: Context)
}
holder.fileDeleteButton.setOnClickListener {
fileSelectClearListener?.invoke(fileHistoryEntity)
fileSelectClearListener?.invoke(databaseFile)
}
if (isExpanded) {
mPreviousExpandedPosition = position
mPreviousExpandedDatabaseFile = databaseFile
}
holder.fileInformation.setOnClickListener {
mExpandedPosition = if (isExpanded) -1 else position
// Notify change
if (mPreviousExpandedPosition < itemCount)
notifyItemChanged(mPreviousExpandedPosition)
notifyItemChanged(position)
holder.fileInformationButton.apply {
animate().rotation(if (isExpanded) 180F else 0F).start()
setOnClickListener {
mExpandedDatabaseFile = if (isExpanded) null else databaseFile
// Notify change
val previousExpandedPosition = listDatabaseFiles.indexOf(mPreviousExpandedDatabaseFile)
notifyItemChanged(previousExpandedPosition)
val expandedPosition = listDatabaseFiles.indexOf(mExpandedDatabaseFile)
notifyItemChanged(expandedPosition)
}
}
// Refresh View / Close alias modification if not contains fileAlias
@@ -160,33 +179,68 @@ class FileDatabaseHistoryAdapter(private val context: Context)
listDatabaseFiles.clear()
}
fun addDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<FileDatabaseHistoryEntity>) {
listDatabaseFiles.clear()
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
fun addDatabaseFileHistory(fileDatabaseHistoryToAdd: DatabaseFile) {
listDatabaseFiles.add(0, fileDatabaseHistoryToAdd)
notifyItemInserted(0)
}
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: FileDatabaseHistoryEntity) {
listDatabaseFiles.remove(fileDatabaseHistoryToDelete)
fun updateDatabaseFileHistory(fileDatabaseHistoryToUpdate: DatabaseFile) {
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToUpdate)
if (listDatabaseFiles.remove(fileDatabaseHistoryToUpdate)) {
listDatabaseFiles.add(index, fileDatabaseHistoryToUpdate)
notifyItemChanged(index)
}
}
fun setOnFileDatabaseHistoryOpenListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: DatabaseFile) {
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToDelete)
if (listDatabaseFiles.remove(fileDatabaseHistoryToDelete)) {
notifyItemRemoved(index)
}
}
fun replaceAllDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<DatabaseFile>) {
if (listDatabaseFiles.isEmpty()) {
listFileDatabaseHistoryToAdd.forEach {
listDatabaseFiles.add(it)
notifyItemInserted(listDatabaseFiles.size)
}
} else {
listDatabaseFiles.clear()
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
notifyDataSetChanged()
}
}
fun setDefaultDatabase(databaseUri: Uri?) {
val defaultDatabaseFile = listDatabaseFiles.firstOrNull { it.databaseUri == databaseUri }
mDefaultDatabaseFile = defaultDatabaseFile
notifyDataSetChanged()
}
fun setOnDefaultDatabaseListener(listener: ((DatabaseFile?) -> Unit)?) {
this.defaultDatabaseListener = listener
}
fun setOnFileDatabaseHistoryOpenListener(listener : ((DatabaseFile)->Unit)?) {
this.fileItemOpenListener = listener
}
fun setOnFileDatabaseHistoryDeleteListener(listener : ((FileDatabaseHistoryEntity)->Boolean)?) {
fun setOnFileDatabaseHistoryDeleteListener(listener : ((DatabaseFile)->Boolean)?) {
this.fileSelectClearListener = listener
}
fun setOnSaveAliasListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
fun setOnSaveAliasListener(listener : ((DatabaseFile)->Unit)?) {
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)
var defaultFileButton: CompoundButton = itemView.findViewById(R.id.default_file_button)
var fileAlias: TextView = itemView.findViewById(R.id.file_alias)
var fileInformation: ImageView = itemView.findViewById(R.id.file_information)
var fileInformationButton: ImageView = itemView.findViewById(R.id.file_information_button)
var fileMainSwitcher: ViewSwitcher = itemView.findViewById(R.id.file_main_switcher)
var fileAliasEdit: EditText = itemView.findViewById(R.id.file_alias_edit)
@@ -196,6 +250,7 @@ class FileDatabaseHistoryAdapter(private val 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

@@ -32,7 +32,7 @@ class CipherDatabaseAction(applicationContext: Context) {
fun getCipherDatabase(databaseUri: Uri,
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
ActionDatabaseAsyncTask(
IOActionTask(
{
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
},
@@ -51,7 +51,7 @@ class CipherDatabaseAction(applicationContext: Context) {
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
cipherDatabaseResultListener: (() -> Unit)? = null) {
ActionDatabaseAsyncTask(
IOActionTask(
{
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
@@ -70,7 +70,7 @@ class CipherDatabaseAction(applicationContext: Context) {
fun deleteByDatabaseUri(databaseUri: Uri,
cipherDatabaseResultListener: (() -> Unit)? = null) {
ActionDatabaseAsyncTask(
IOActionTask(
{
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
},
@@ -81,7 +81,7 @@ class CipherDatabaseAction(applicationContext: Context) {
}
fun deleteAll() {
ActionDatabaseAsyncTask(
IOActionTask(
{
cipherDatabaseDao.deleteAll()
}

View File

@@ -21,31 +21,44 @@ package com.kunzisoft.keepass.app.database
import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.SingletonHolderParameter
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
class FileDatabaseHistoryAction(applicationContext: Context) {
class FileDatabaseHistoryAction(private val applicationContext: Context) {
private val databaseFileHistoryDao =
AppDatabase
.getDatabase(applicationContext)
.fileDatabaseHistoryDao()
fun getFileDatabaseHistory(databaseUri: Uri,
fileHistoryResultListener: (fileDatabaseHistoryResult: FileDatabaseHistoryEntity?) -> Unit) {
ActionDatabaseAsyncTask(
fun getDatabaseFile(databaseUri: Uri,
databaseFileResult: (DatabaseFile?) -> Unit) {
IOActionTask(
{
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
val fileDatabaseHistoryEntity = databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
val fileDatabaseInfo = FileDatabaseInfo(applicationContext, databaseUri)
DatabaseFile(
databaseUri,
UriUtil.parse(fileDatabaseHistoryEntity?.keyFileUri),
UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""),
fileDatabaseInfo.exists,
fileDatabaseInfo.getModificationString(),
fileDatabaseInfo.getSizeString()
)
},
{
fileHistoryResultListener.invoke(it)
databaseFileResult.invoke(it)
}
).execute()
}
fun getKeyFileUriByDatabaseUri(databaseUri: Uri,
keyFileUriResultListener: (Uri?) -> Unit) {
ActionDatabaseAsyncTask(
IOActionTask(
{
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
},
@@ -59,61 +72,120 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
).execute()
}
fun getAllFileDatabaseHistories(fileHistoryResultListener: (fileDatabaseHistoryResult: List<FileDatabaseHistoryEntity>?) -> Unit) {
ActionDatabaseAsyncTask(
fun getDatabaseFileList(databaseFileListResult: (List<DatabaseFile>) -> Unit) {
IOActionTask(
{
databaseFileHistoryDao.getAll()
val hideBrokenLocations = PreferencesUtil.hideBrokenLocations(applicationContext)
// Show only uri accessible
val databaseFileListLoaded = ArrayList<DatabaseFile>()
databaseFileHistoryDao.getAll().forEach { fileDatabaseHistoryEntity ->
val fileDatabaseInfo = FileDatabaseInfo(applicationContext, fileDatabaseHistoryEntity.databaseUri)
if (hideBrokenLocations && fileDatabaseInfo.exists
|| !hideBrokenLocations) {
databaseFileListLoaded.add(
DatabaseFile(
UriUtil.parse(fileDatabaseHistoryEntity.databaseUri),
UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri),
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
fileDatabaseInfo.exists,
fileDatabaseInfo.getModificationString(),
fileDatabaseInfo.getSizeString()
)
)
}
}
databaseFileListLoaded
},
{
fileHistoryResultListener.invoke(it)
}
).execute()
}
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null) {
addOrUpdateFileDatabaseHistory(FileDatabaseHistoryEntity(
databaseUri.toString(),
"",
keyFileUri?.toString(),
System.currentTimeMillis()
), true)
}
fun addOrUpdateFileDatabaseHistory(fileDatabaseHistory: FileDatabaseHistoryEntity, unmodifiedAlias: Boolean = false) {
ActionDatabaseAsyncTask(
{
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(fileDatabaseHistory.databaseUri)
if (unmodifiedAlias) {
fileDatabaseHistory.databaseAlias = fileDatabaseHistoryRetrieve?.databaseAlias ?: ""
}
// Update values if history element not yet in the database
if (fileDatabaseHistoryRetrieve == null) {
databaseFileHistoryDao.add(fileDatabaseHistory)
} else {
databaseFileHistoryDao.update(fileDatabaseHistory)
databaseFileList ->
databaseFileList?.let {
databaseFileListResult.invoke(it)
}
}
).execute()
}
fun deleteFileDatabaseHistory(fileDatabaseHistory: FileDatabaseHistoryEntity,
fileHistoryDeletedResult: (FileDatabaseHistoryEntity?) -> Unit) {
ActionDatabaseAsyncTask(
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null,
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
addOrUpdateDatabaseFile(DatabaseFile(
databaseUri,
keyFileUri
), databaseFileAddedOrUpdatedResult)
}
fun addOrUpdateDatabaseFile(databaseFileToAddOrUpdate: DatabaseFile,
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
IOActionTask(
{
databaseFileHistoryDao.delete(fileDatabaseHistory)
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
?: fileDatabaseHistoryRetrieve?.databaseAlias
?: "",
databaseFileToAddOrUpdate.keyFileUri?.toString(),
System.currentTimeMillis()
)
// Update values if history element not yet in the database
if (fileDatabaseHistoryRetrieve == null) {
databaseFileHistoryDao.add(fileDatabaseHistory)
} else {
databaseFileHistoryDao.update(fileDatabaseHistory)
}
val fileDatabaseInfo = FileDatabaseInfo(applicationContext,
fileDatabaseHistory.databaseUri)
DatabaseFile(
UriUtil.parse(fileDatabaseHistory.databaseUri),
UriUtil.parse(fileDatabaseHistory.keyFileUri),
UriUtil.decode(fileDatabaseHistory.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
fileDatabaseInfo.exists,
fileDatabaseInfo.getModificationString(),
fileDatabaseInfo.getSizeString()
)
}
},
{
if (it != null && it > 0)
fileHistoryDeletedResult.invoke(fileDatabaseHistory)
else
fileHistoryDeletedResult.invoke(null)
databaseFileAddedOrUpdatedResult?.invoke(it)
}
).execute()
}
fun deleteDatabaseFile(databaseFileToDelete: DatabaseFile,
databaseFileDeletedResult: (DatabaseFile?) -> Unit) {
IOActionTask(
{
databaseFileToDelete.databaseUri?.let { databaseUri ->
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())?.let { fileDatabaseHistory ->
val returnValue = databaseFileHistoryDao.delete(fileDatabaseHistory)
if (returnValue > 0) {
DatabaseFile(
UriUtil.parse(fileDatabaseHistory.databaseUri),
UriUtil.parse(fileDatabaseHistory.keyFileUri),
UriUtil.decode(fileDatabaseHistory.databaseUri),
databaseFileToDelete.databaseAlias
)
} else {
null
}
}
}
},
{
databaseFileDeletedResult.invoke(it)
}
).execute()
}
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
ActionDatabaseAsyncTask(
IOActionTask(
{
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
}
@@ -121,7 +193,7 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
}
fun deleteAllKeyFiles() {
ActionDatabaseAsyncTask(
IOActionTask(
{
databaseFileHistoryDao.deleteAllKeyFiles()
}
@@ -129,7 +201,7 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
}
fun deleteAll() {
ActionDatabaseAsyncTask(
IOActionTask(
{
databaseFileHistoryDao.deleteAll()
}

View File

@@ -19,21 +19,27 @@
*/
package com.kunzisoft.keepass.app.database
import android.os.AsyncTask
import kotlinx.coroutines.*
/**
* Private class to invoke each method in a separate thread
* Class to invoke action in a separate IO thread
*/
class ActionDatabaseAsyncTask<T>(
class IOActionTask<T>(
private val action: () -> T ,
private val afterActionDatabaseListener: ((T?) -> Unit)? = null
) : AsyncTask<Void, Void, T>() {
private val afterActionDatabaseListener: ((T?) -> Unit)? = null) {
override fun doInBackground(vararg args: Void?): T? {
return action.invoke()
}
private val mainScope = CoroutineScope(Dispatchers.Main)
override fun onPostExecute(result: T?) {
afterActionDatabaseListener?.invoke(result)
fun execute() {
mainScope.launch {
withContext(Dispatchers.IO) {
val asyncResult: Deferred<T?> = async {
action.invoke()
}
withContext(Dispatchers.Main) {
afterActionDatabaseListener?.invoke(asyncResult.await())
}
}
}
}
}

View File

@@ -26,6 +26,7 @@ import android.provider.Settings
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import android.widget.CompoundButton
import android.widget.TextView
import androidx.annotation.RequiresApi
@@ -334,7 +335,9 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
}
private fun showFingerPrintViews(show: Boolean) {
context.runOnUiThread { advancedUnlockInfoView?.hide = !show }
context.runOnUiThread {
advancedUnlockInfoView?.visibility = if (show) View.VISIBLE else View.GONE
}
}
private fun setAdvancedUnlockedTitleView(textId: Int) {

View File

@@ -37,7 +37,7 @@ open class AssignPasswordInDatabaseRunnable (
: SaveDatabaseRunnable(context, database, true) {
private var mMasterPassword: String? = null
protected var mKeyFile: Uri? = null
protected var mKeyFileUri: Uri? = null
private var mBackupKey: ByteArray? = null
@@ -45,7 +45,7 @@ open class AssignPasswordInDatabaseRunnable (
if (withMasterPassword)
this.mMasterPassword = masterPassword
if (withKeyFile)
this.mKeyFile = keyFile
this.mKeyFileUri = keyFile
}
override fun onStartRun() {
@@ -55,7 +55,7 @@ open class AssignPasswordInDatabaseRunnable (
mBackupKey = ByteArray(database.masterKey.size)
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFile)
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFileUri)
database.retrieveMasterKey(mMasterPassword, uriInputStream)
} catch (e: Exception) {
erase(mBackupKey)

View File

@@ -34,7 +34,8 @@ class CreateDatabaseRunnable(context: Context,
withMasterPassword: Boolean,
masterPassword: String?,
withKeyFile: Boolean,
keyFile: Uri?)
keyFile: Uri?,
private val createDatabaseResult: ((Result) -> Unit)?)
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile) {
override fun onStartRun() {
@@ -42,29 +43,36 @@ class CreateDatabaseRunnable(context: Context,
// Create new database record
mDatabase.apply {
createData(mDatabaseUri, databaseName, rootName)
// Set Database state
loaded = true
}
} catch (e: Exception) {
mDatabase.closeAndClear()
mDatabase.closeAndClear(context.applicationContext.filesDir)
setError(e)
}
super.onStartRun()
}
override fun onFinishRun() {
super.onFinishRun()
override fun onActionRun() {
super.onActionRun()
if (result.isSuccess) {
// Add database to recent files
if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context.applicationContext)
.addOrUpdateDatabaseUri(mDatabaseUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mKeyFile else null)
if (PreferencesUtil.rememberKeyFileLocations(context)) mKeyFileUri else null)
}
// Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context)
} else {
Log.e("CreateDatabaseRunnable", "Unable to create the database")
}
}
override fun onFinishRun() {
super.onFinishRun()
createDatabaseResult?.invoke(result)
}
}

View File

@@ -40,14 +40,12 @@ class LoadDatabaseRunnable(private val context: Context,
private val mCipherEntity: CipherDatabaseEntity?,
private val mFixDuplicateUUID: Boolean,
private val progressTaskUpdater: ProgressTaskUpdater?,
private val mDuplicateUuidAction: ((Result) -> Unit)?)
private val mLoadDatabaseResult: ((Result) -> Unit)?)
: ActionRunnable() {
private val cacheDirectory = context.applicationContext.filesDir
override fun onStartRun() {
// Clear before we load
mDatabase.closeAndClear(cacheDirectory)
mDatabase.closeAndClear(context.applicationContext.filesDir)
}
override fun onActionRun() {
@@ -55,20 +53,17 @@ class LoadDatabaseRunnable(private val context: Context,
mDatabase.loadData(mUri, mPass, mKey,
mReadonly,
context.contentResolver,
cacheDirectory,
context.applicationContext.filesDir,
mFixDuplicateUUID,
progressTaskUpdater)
}
catch (e: DuplicateUuidDatabaseException) {
mDuplicateUuidAction?.invoke(result)
setError(e)
}
catch (e: LoadDatabaseException) {
setError(e)
}
}
override fun onFinishRun() {
if (result.isSuccess) {
// Save keyFile in app database
if (PreferencesUtil.rememberDatabaseLocations(context)) {
@@ -86,7 +81,11 @@ class LoadDatabaseRunnable(private val context: Context,
// Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context)
} else {
mDatabase.closeAndClear(cacheDirectory)
mDatabase.closeAndClear(context.applicationContext.filesDir)
}
}
override fun onFinishRun() {
mLoadDatabaseResult?.invoke(result)
}
}

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
@@ -71,12 +71,12 @@ import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import java.util.*
import kotlin.collections.ArrayList
class ProgressDialogThread(private val activity: FragmentActivity) {
class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
var onActionFinish: ((actionTask: String,
result: ActionRunnable.Result) -> Unit)? = null
private var intentDatabaseTask = Intent(activity, DatabaseTaskNotificationService::class.java)
private var intentDatabaseTask = Intent(activity.applicationContext, DatabaseTaskNotificationService::class.java)
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
@@ -218,12 +218,8 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
activity.stopService(intentDatabaseTask)
if (bundle != null)
intentDatabaseTask.putExtras(bundle)
intentDatabaseTask.action = actionTask
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.startForegroundService(intentDatabaseTask)
} else {
activity.startService(intentDatabaseTask)
}
intentDatabaseTask.action = actionTask
activity.startService(intentDatabaseTask)
}
/*
@@ -242,7 +238,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
}
, ACTION_DATABASE_CREATE_TASK)
}
@@ -256,7 +252,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
@@ -275,7 +271,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
}
, ACTION_DATABASE_ASSIGN_PASSWORD_TASK)
}
@@ -467,6 +463,13 @@ class ProgressDialogThread(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 {
@@ -70,6 +69,13 @@ class Database {
val drawFactory = IconDrawableFactory()
var loaded = false
set(value) {
field = value
loadTimestamp = if (field) System.currentTimeMillis() else null
}
var loadTimestamp: Long? = null
private set
val iconFactory: IconImageFactory
get() {
@@ -150,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)
@@ -261,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
@@ -286,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()
}
@@ -308,6 +325,8 @@ class Database {
fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
setDatabaseKDBX(DatabaseKDBX(databaseName, rootName))
this.fileUri = databaseUri
// Set Database state
this.loaded = true
}
@Throws(LoadDatabaseException::class)
@@ -419,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 {
@@ -464,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) }
@@ -709,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)
@@ -720,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)
@@ -772,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
*/
@@ -791,7 +849,7 @@ class Database {
rootGroup?.doForEachChildAndForIt(
object : NodeHandler<Entry>() {
override fun operate(node: Entry): Boolean {
removeOldestEntryHistory(node)
removeOldestEntryHistory(node, binaryPool)
return true
}
},
@@ -799,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)
}
}
@@ -835,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
}
@@ -848,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,9 +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.BinaryAttachment
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
@@ -285,66 +283,90 @@ 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>()
val binaryDescriptionKDB = entryKDB?.binaryDescription ?: ""
val binaryKDB = entryKDB?.binaryData
if (binaryKDB != null) {
attachments.add(EntryAttachment(binaryDescriptionKDB, binaryKDB))
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> {
val attachments = ArrayList<Attachment>()
entryKDB?.getAttachment()?.let {
attachments.add(it)
}
val actionEach = object : (Map.Entry<String, BinaryAttachment>)->Unit {
override fun invoke(mapEntry: Map.Entry<String, BinaryAttachment>) {
attachments.add(EntryAttachment(mapEntry.key, mapEntry.value))
}
entryKDBX?.getAttachments(binaryPool, inHistory)?.let {
attachments.addAll(it)
}
entryKDBX?.binaries?.forEach(actionEach)
return attachments
}
fun containsAttachment(): Boolean {
return entryKDB?.containsAttachment() == true
|| entryKDBX?.containsAttachment() == true
}
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> {
val history = ArrayList<Entry>()
val entryKDBXHistory = entryKDBX?.history ?: ArrayList()
@@ -360,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 {
@@ -396,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,7 @@ 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.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
@@ -46,6 +47,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 +175,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 +556,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

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

@@ -25,14 +25,12 @@ import android.os.Parcelable
import com.kunzisoft.keepass.utils.ParcelableUtil
import com.kunzisoft.keepass.utils.UnsignedInt
import java.util.HashMap
class AutoType : Parcelable {
var enabled = true
var obfuscationOptions = OBF_OPT_NONE
var defaultSequence = ""
private var windowSeqPairs = HashMap<String, String>()
private var windowSeqPairs = LinkedHashMap<String, String>()
constructor()

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,12 @@ 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 {
@@ -58,9 +61,10 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
super.icon = value
}
var iconCustom = IconImageCustom.UNKNOWN_ICON
private var customData = HashMap<String, String>()
var fields = HashMap<String, ProtectedString>()
var binaries = HashMap<String, BinaryAttachment>()
private var customData = LinkedHashMap<String, String>()
// TODO Private
var fields = LinkedHashMap<String, ProtectedString>()
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
var foregroundColor = ""
var backgroundColor = ""
var overrideURL = ""
@@ -69,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()
@@ -109,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
@@ -127,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)
@@ -166,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
}
@@ -260,23 +260,17 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|| key == STR_NOTES)
}
var customFields = HashMap<String, ProtectedString>()
var customFields = LinkedHashMap<String, ProtectedString>()
get() {
field.clear()
for (entry in fields.entries) {
val key = entry.key
val value = entry.value
if (!isStandardField(entry.key)) {
for ((key, value) in fields) {
if (!isStandardField(key)) {
field[key] = ProtectedString(value.isProtected, decodeRefKey(mDecodeRef, key))
}
}
return field
}
fun allowCustomFields(): Boolean {
return true
}
fun removeAllFields() {
fields.clear()
}
@@ -285,12 +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 sizeOfHistory(): Int {
return history.size
fun containsAttachment(): Boolean {
return binaries.isNotEmpty()
}
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) {
@@ -305,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
@@ -326,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

@@ -26,7 +26,7 @@ class ProtectedString : Parcelable {
var isProtected: Boolean = false
private set
private var stringValue: String = ""
var stringValue: String = ""
constructor(toCopy: ProtectedString) {
this.isProtected = toCopy.isProtected

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

@@ -23,6 +23,7 @@ import android.content.res.Resources
import android.util.SparseIntArray
import com.kunzisoft.keepass.R
import java.text.DecimalFormat
import java.util.*
/**
* Class who construct dynamically database icons contains in a separate library
@@ -35,17 +36,13 @@ import java.text.DecimalFormat
*
* See *icon-pack-classic* module as sample
*
*
*/
class IconPack
/**
* Construct dynamically the icon pack provide by the string resource id
*
* @param packageName Context of the app to retrieve the resources
* @param packageName Context of the app to retrieve the resources
* @param resourceId String Id of the pack (ex : com.kunzisoft.keepass.icon.classic.R.string.resource_id)
*/
internal constructor(packageName: String, resources: Resources, resourceId: Int) {
class IconPack(packageName: String, resources: Resources, resourceId: Int) {
private val icons: SparseIntArray = SparseIntArray()
/**
@@ -84,7 +81,7 @@ internal constructor(packageName: String, resources: Resources, resourceId: Int)
while (num <= NB_ICONS) {
// To construct the id with name_ic_XX_32dp (ex : classic_ic_08_32dp )
val resId = resources.getIdentifier(
id + "_" + DecimalFormat("00").format(num.toLong()) + "_32dp",
id + "_" + String.format(Locale.ENGLISH, "%02d", num) + "_32dp",
"drawable",
packageName)
icons.put(num, resId)

View File

@@ -108,8 +108,17 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
val closeView = popupFieldsView.findViewById<View>(R.id.keyboard_popup_close)
closeView.setOnClickListener { popupCustomKeys?.dismiss() }
if (!Database.getInstance().loaded)
// Remove entry info if the database is not loaded
// or if entry info timestamp is before database loaded timestamp
val database = Database.getInstance()
val databaseTime = database.loadTimestamp
val entryTime = entryInfoTimestamp
if (!database.loaded
|| databaseTime == null
|| entryTime == null
|| entryTime < databaseTime) {
removeEntryInfo()
}
assignKeyboardView()
keyboardView?.setOnKeyboardActionListener(this)
keyboardView?.isPreviewEnabled = false
@@ -321,10 +330,13 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
private const val KEY_URL = 520
private const val KEY_FIELDS = 530
// TODO Retrieve entry info from id and service when database is open
private var entryInfoKey: EntryInfo? = null
private var entryInfoTimestamp: Long? = null
private fun removeEntryInfo() {
entryInfoKey = null
entryInfoTimestamp = null
}
fun removeEntry(context: Context) {
@@ -334,6 +346,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
fun addEntryAndLaunchNotificationIfAllowed(context: Context, entry: EntryInfo, toast: Boolean = false) {
// Add a new entry
entryInfoKey = entry
entryInfoTimestamp = System.currentTimeMillis()
// Launch notification if allowed
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
}

View File

@@ -0,0 +1,26 @@
package com.kunzisoft.keepass.model
import android.net.Uri
data class DatabaseFile(var databaseUri: Uri? = null,
var keyFileUri: Uri? = null,
var databaseDecodedPath: String? = null,
var databaseAlias: String? = null,
var databaseFileExists: Boolean = false,
var databaseLastModified: String? = null,
var databaseSize: String? = null) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is DatabaseFile) return false
if (databaseUri == null || other.databaseUri == null) return false
if (databaseUri != other.databaseUri) return false
return true
}
override fun hashCode(): Int {
return databaseUri?.hashCode() ?: 0
}
}

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)
}
@@ -47,12 +48,25 @@ data class EntryAttachment(var name: String,
return 0
}
companion object CREATOR : Parcelable.Creator<EntryAttachment> {
override fun createFromParcel(parcel: Parcel): EntryAttachment {
return EntryAttachment(parcel)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is EntryAttachmentState) return false
if (attachment != other.attachment) return false
return true
}
override fun hashCode(): Int {
return attachment.hashCode()
}
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

@@ -49,9 +49,7 @@ class Field : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Field
if (other !is Field) return false
if (name != other.name) return false

View File

@@ -0,0 +1,56 @@
package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.Parcelable
class FocusedEditField : Parcelable {
var field: Field? = null
var cursorSelectionStart: Int = -1
var cursorSelectionEnd: Int = -1
constructor()
constructor(parcel: Parcel) {
this.field = parcel.readParcelable(Field::class.java.classLoader)
this.cursorSelectionStart = parcel.readInt()
this.cursorSelectionEnd = parcel.readInt()
}
fun destroy() {
this.field = null
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(field, flags)
parcel.writeInt(cursorSelectionStart)
parcel.writeInt(cursorSelectionEnd)
}
override fun describeContents(): Int {
return 0
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is FocusedEditField) return false
if (field != other.field) return false
return true
}
override fun hashCode(): Int {
return field?.hashCode() ?: 0
}
companion object CREATOR : Parcelable.Creator<FocusedEditField> {
override fun createFromParcel(parcel: Parcel): FocusedEditField {
return FocusedEditField(parcel)
}
override fun newArray(size: Int): Array<FocusedEditField?> {
return arrayOfNulls(size)
}
}
}

View File

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

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.notifications
import android.app.PendingIntent
import android.content.ContentResolver
import android.content.Intent
import android.net.Uri
import android.os.Binder
@@ -27,53 +28,55 @@ 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.tasks.AttachmentFileAsyncTask
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>()
private val mainScope = CoroutineScope(Dispatchers.Main)
inner class ActionTaskBinder: Binder() {
fun getService(): AttachmentFileNotificationService = this@AttachmentFileNotificationService
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? {
@@ -83,46 +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.maxBy { it.notificationId }
?.notificationId ?: notificationId) + 1
try {
intent.getParcelableExtra<EntryAttachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
val attachmentNotification = AttachmentNotification(nextNotificationId, entryAttachment)
downloadFileUris[downloadFileUri] = attachmentNotification
AttachmentFileAsyncTask(downloadFileUri,
attachmentNotification,
contentResolver).apply {
onUpdate = { uri, attachment, notificationIdAttach ->
newNotification(uri, attachment, notificationIdAttach)
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentProgress(downloadFileUri, attachment)
}
}
}.execute()
}
} 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()
}
}
@@ -131,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)
@@ -157,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()
}
data class AttachmentNotification(var notificationId: Int,
var entryAttachment: EntryAttachment,
var attachmentTask: AttachmentFileAsyncTask? = 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
@@ -221,15 +254,188 @@ class AttachmentFileNotificationService: LockNotificationService() {
}
}
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 listener: AttachmentFileActionListener? = null
interface AttachmentFileActionListener {
fun onUpdate(attachmentNotification: AttachmentNotification)
}
suspend fun executeAction() {
// on pre execute
CoroutineScope(Dispatchers.Main).launch {
TimeoutHelper.temporarilyDisableTimeout()
attachmentNotification.attachmentFileAction = this@AttachmentFileAction
attachmentNotification.entryAttachmentState.apply {
downloadState = AttachmentState.START
downloadProgression = 0
}
listener?.onUpdate(attachmentNotification)
}
withContext(Dispatchers.IO) {
// on Progress with thread
val asyncResult: Deferred<Boolean> = async {
var progressResult = true
try {
attachmentNotification.entryAttachmentState.apply {
downloadState = AttachmentState.IN_PROGRESS
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)
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to upload or download file", e)
progressResult = false
}
progressResult
}
// on post execute
withContext(Dispatchers.Main) {
val result = asyncResult.await()
attachmentNotification.attachmentFileAction = null
attachmentNotification.entryAttachmentState.apply {
downloadState = if (result) AttachmentState.COMPLETE else AttachmentState.ERROR
downloadProgression = 100
}
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 = 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

@@ -1,122 +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.notifications
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.closeDatabase
class DatabaseOpenNotificationService: LockNotificationService() {
override val notificationId: Int = 340
private fun stopNotificationAndSendLock() {
// Send lock action
sendBroadcast(Intent(LOCK_ACTION))
}
override fun actionOnLock() {
closeDatabase()
// Remove the lock timer (no more needed if it exists)
TimeoutHelper.cancelLockTimer(this)
// Service is stopped after receive the broadcast
super.actionOnLock()
}
private fun checkIntent(intent: Intent?) {
val notificationBuilder = buildNewNotification().apply {
setSmallIcon(R.drawable.notification_ic_database_open)
setContentTitle(getString(R.string.database_opened))
setAutoCancel(false)
}
when(intent?.action) {
ACTION_CLOSE_DATABASE -> {
startForeground(notificationId, notificationBuilder.build())
stopNotificationAndSendLock()
}
else -> {
val databaseIntent = Intent(this, GroupActivity::class.java)
var pendingDatabaseFlag = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
pendingDatabaseFlag = PendingIntent.FLAG_IMMUTABLE
}
val pendingDatabaseIntent = PendingIntent.getActivity(this, 0, databaseIntent, pendingDatabaseFlag)
val deleteIntent = Intent(this, DatabaseOpenNotificationService::class.java).apply {
action = ACTION_CLOSE_DATABASE
}
val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val database = Database.getInstance()
if (database.loaded) {
startForeground(notificationId, notificationBuilder.apply {
setContentText(database.name + " (" + database.version + ")")
setContentIntent(pendingDatabaseIntent)
// Unfortunately swipe is disabled in lollipop+
setDeleteIntent(pendingDeleteIntent)
addAction(R.drawable.ic_lock_white_24dp, getString(R.string.lock),
pendingDeleteIntent)
}.build())
} else {
startForeground(notificationId, notificationBuilder.build())
stopSelf()
}
}
}
}
override fun onBind(intent: Intent): IBinder? {
checkIntent(intent)
return super.onBind(intent)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
checkIntent(intent)
return START_STICKY
}
companion object {
const val ACTION_CLOSE_DATABASE = "ACTION_CLOSE_DATABASE"
fun start(context: Context) {
// Start the opening notification, keep it active to receive lock
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(Intent(context, DatabaseOpenNotificationService::class.java))
} else {
context.startService(Intent(context, DatabaseOpenNotificationService::class.java))
}
}
fun stop(context: Context) {
// Stop the opening notification
context.stopService(Intent(context, DatabaseOpenNotificationService::class.java))
}
}
}

View File

@@ -19,12 +19,15 @@
*/
package com.kunzisoft.keepass.notifications
import android.app.PendingIntent
import android.content.Intent
import android.net.Uri
import android.os.Binder
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.database.action.*
import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseRunnable
@@ -42,22 +45,28 @@ import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.closeDatabase
import kotlinx.coroutines.*
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.collections.ArrayList
class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdater {
open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater {
override val notificationId: Int = 575
private lateinit var mDatabase: Database
private val mainScope = CoroutineScope(Dispatchers.Main)
private var mActionTaskBinder = ActionTaskBinder()
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
private var mAllowFinishAction = AtomicBoolean()
private var mActionRunning = false
private var mTitleId: Int? = null
private var mIconId: Int = R.drawable.notification_ic_database_load
private var mTitleId: Int = R.string.database_opened
private var mMessageId: Int? = null
private var mWarningId: Int? = null
@@ -66,8 +75,8 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
fun getService(): DatabaseTaskNotificationService = this@DatabaseTaskNotificationService
fun addActionTaskListener(actionTaskListener: ActionTaskListener) {
mActionTaskListeners.add(actionTaskListener)
mAllowFinishAction.set(true)
mActionTaskListeners.add(actionTaskListener)
}
fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
@@ -84,66 +93,41 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
fun onStopAction(actionTask: String, result: ActionRunnable.Result)
}
/**
* Force to call [ActionTaskListener.onStartAction] if the action is still running
*/
fun checkAction() {
mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onStartAction(mTitleId, mMessageId, mWarningId)
}
}
private fun buildNotification(intent: Intent?) {
var saveAction = true
if (intent != null && intent.hasExtra(SAVE_DATABASE_KEY)) {
saveAction = intent.getBooleanExtra(SAVE_DATABASE_KEY, saveAction)
}
val intentAction = intent?.action
val titleId: Int = when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
ACTION_DATABASE_LOAD_TASK -> R.string.loading_database
else -> {
if (saveAction)
R.string.saving_database
else
R.string.command_execution
if (mActionRunning) {
mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onStartAction(mTitleId, mMessageId, mWarningId)
}
}
val messageId: Int? = when (intentAction) {
ACTION_DATABASE_LOAD_TASK -> null
else -> null
}
val warningId: Int? =
if (!saveAction
|| intentAction == ACTION_DATABASE_LOAD_TASK)
null
else
R.string.do_not_kill_app
// Assign elements for updates
mTitleId = titleId
mMessageId = messageId
mWarningId = warningId
// Create the notification
startForeground(notificationId, buildNewNotification()
.setSmallIcon(R.drawable.notification_ic_database_load)
.setContentTitle(getString(intent?.getIntExtra(DATABASE_TASK_TITLE_KEY, titleId) ?: titleId))
.setAutoCancel(false)
.setContentIntent(null).build())
}
override fun onBind(intent: Intent): IBinder? {
buildNotification(intent)
super.onBind(intent)
return mActionTaskBinder
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
buildNotification(intent)
mDatabase = Database.getInstance()
if (intent == null) return START_REDELIVER_INTENT
// Create the notification
buildMessage(intent)
val intentAction = intent.action
val actionRunnable: ActionRunnable? = when (intentAction) {
val intentAction = intent?.action
if (intentAction == null && !mDatabase.loaded) {
stopSelf()
}
if (intentAction == ACTION_DATABASE_CLOSE) {
// Send lock action
sendBroadcast(Intent(LOCK_ACTION))
}
val actionRunnable: ActionRunnable? = when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent)
ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent)
ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent)
@@ -157,6 +141,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
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,
@@ -172,12 +157,13 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
else -> null
}
actionRunnable?.let { actionRunnableNotNull ->
// Build and launch the action
// Build and launch the action
if (actionRunnable != null) {
mainScope.launch {
executeAction(this@DatabaseTaskNotificationService,
{
mActionRunning = true
sendBroadcast(Intent(DATABASE_START_TASK_ACTION).apply {
putExtra(DATABASE_TASK_TITLE_KEY, mTitleId)
putExtra(DATABASE_TASK_MESSAGE_KEY, mMessageId)
@@ -190,22 +176,160 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
},
{
actionRunnableNotNull
actionRunnable
},
{ result ->
mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onStopAction(intentAction!!, result)
try {
mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onStopAction(intentAction!!, result)
}
} finally {
removeIntentData(intent)
TimeoutHelper.releaseTemporarilyDisableTimeout()
if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) {
if (!mDatabase.loaded) {
stopSelf()
} else {
// Restart the service to open lock notification
startService(Intent(applicationContext,
DatabaseTaskNotificationService::class.java))
}
}
}
sendBroadcast(Intent(DATABASE_STOP_TASK_ACTION))
stopSelf()
mActionRunning = false
}
)
}
}
return START_REDELIVER_INTENT
return when (intentAction) {
ACTION_DATABASE_LOAD_TASK, null -> {
START_STICKY
}
else -> {
// Relaunch action if failed
START_REDELIVER_INTENT
}
}
}
private fun buildMessage(intent: Intent?) {
// Assign elements for updates
val intentAction = intent?.action
var saveAction = false
if (intent != null && intent.hasExtra(SAVE_DATABASE_KEY)) {
saveAction = intent.getBooleanExtra(SAVE_DATABASE_KEY, saveAction)
}
mIconId = if (intentAction == null)
R.drawable.notification_ic_database_open
else
R.drawable.notification_ic_database_load
mTitleId = when {
saveAction -> {
R.string.saving_database
}
intentAction == null -> {
R.string.database_opened
}
else -> {
when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
ACTION_DATABASE_LOAD_TASK -> R.string.loading_database
ACTION_DATABASE_SAVE -> R.string.saving_database
else -> {
R.string.command_execution
}
}
}
}
mMessageId = when (intentAction) {
ACTION_DATABASE_LOAD_TASK -> null
else -> null
}
mWarningId =
if (!saveAction
|| intentAction == ACTION_DATABASE_LOAD_TASK)
null
else
R.string.do_not_kill_app
val notificationBuilder = buildNewNotification().apply {
setSmallIcon(mIconId)
intent?.let {
setContentTitle(getString(intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mTitleId)))
}
setAutoCancel(false)
setContentIntent(null)
}
if (intentAction == null) {
// Database is normally open
if (mDatabase.loaded) {
// Build Intents for notification action
var pendingDatabaseFlag = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
pendingDatabaseFlag = PendingIntent.FLAG_IMMUTABLE
}
val pendingDatabaseIntent = PendingIntent.getActivity(this,
0,
Intent(this, GroupActivity::class.java),
pendingDatabaseFlag)
val deleteIntent = Intent(this, DatabaseTaskNotificationService::class.java).apply {
action = ACTION_DATABASE_CLOSE
}
val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
// Add actions in notifications
notificationBuilder.apply {
setContentText(mDatabase.name + " (" + mDatabase.version + ")")
setContentIntent(pendingDatabaseIntent)
// Unfortunately swipe is disabled in lollipop+
setDeleteIntent(pendingDeleteIntent)
addAction(R.drawable.ic_lock_white_24dp, getString(R.string.lock),
pendingDeleteIntent)
}
}
}
// Create the notification
startForeground(notificationId, notificationBuilder.build())
}
private fun removeIntentData(intent: Intent?) {
intent?.action = null
intent?.removeExtra(DATABASE_TASK_TITLE_KEY)
intent?.removeExtra(DATABASE_TASK_MESSAGE_KEY)
intent?.removeExtra(DATABASE_TASK_WARNING_KEY)
intent?.removeExtra(DATABASE_URI_KEY)
intent?.removeExtra(MASTER_PASSWORD_CHECKED_KEY)
intent?.removeExtra(MASTER_PASSWORD_KEY)
intent?.removeExtra(KEY_FILE_CHECKED_KEY)
intent?.removeExtra(KEY_FILE_URI_KEY)
intent?.removeExtra(READ_ONLY_KEY)
intent?.removeExtra(CIPHER_ENTITY_KEY)
intent?.removeExtra(FIX_DUPLICATE_UUID_KEY)
intent?.removeExtra(GROUP_KEY)
intent?.removeExtra(ENTRY_KEY)
intent?.removeExtra(GROUP_ID_KEY)
intent?.removeExtra(ENTRY_ID_KEY)
intent?.removeExtra(GROUPS_ID_KEY)
intent?.removeExtra(ENTRIES_ID_KEY)
intent?.removeExtra(PARENT_ID_KEY)
intent?.removeExtra(ENTRY_HISTORY_POSITION_KEY)
intent?.removeExtra(SAVE_DATABASE_KEY)
intent?.removeExtra(OLD_NODES_KEY)
intent?.removeExtra(NEW_NODES_KEY)
intent?.removeExtra(OLD_ELEMENT_KEY)
intent?.removeExtra(NEW_ELEMENT_KEY)
}
/**
@@ -235,15 +359,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
result
}
withContext(Dispatchers.Main) {
try {
onPostExecute.invoke(asyncResult.await())
} finally {
TimeoutHelper.releaseTemporarilyDisableTimeout()
// Start the opening notification
if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) {
DatabaseOpenNotificationService.start(this@DatabaseTaskNotificationService)
}
}
onPostExecute.invoke(asyncResult.await())
}
}
}
@@ -256,22 +372,32 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
}
}
override fun actionOnLock() {
if (!TimeoutHelper.temporarilyDisableTimeout) {
closeDatabase()
// Remove the lock timer (no more needed if it exists)
TimeoutHelper.cancelLockTimer(this)
// Service is stopped after receive the broadcast
super.actionOnLock()
}
}
private fun buildDatabaseCreateActionTask(intent: Intent): ActionRunnable? {
if (intent.hasExtra(DATABASE_URI_KEY)
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
&& intent.hasExtra(MASTER_PASSWORD_KEY)
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
&& intent.hasExtra(KEY_FILE_KEY)
&& intent.hasExtra(KEY_FILE_URI_KEY)
) {
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY)
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_URI_KEY)
if (databaseUri == null)
return null
return CreateDatabaseRunnable(this,
Database.getInstance(),
mDatabase,
databaseUri,
getString(R.string.database_default_name),
getString(R.string.database),
@@ -279,7 +405,12 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
intent.getStringExtra(MASTER_PASSWORD_KEY),
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
keyFileUri
)
) { result ->
result.data = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri)
putParcelable(KEY_FILE_URI_KEY, keyFileUri)
}
}
} else {
return null
}
@@ -289,15 +420,14 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
if (intent.hasExtra(DATABASE_URI_KEY)
&& intent.hasExtra(MASTER_PASSWORD_KEY)
&& intent.hasExtra(KEY_FILE_KEY)
&& intent.hasExtra(KEY_FILE_URI_KEY)
&& intent.hasExtra(READ_ONLY_KEY)
&& intent.hasExtra(CIPHER_ENTITY_KEY)
&& intent.hasExtra(FIX_DUPLICATE_UUID_KEY)
) {
val database = Database.getInstance()
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
val masterPassword: String? = intent.getStringExtra(MASTER_PASSWORD_KEY)
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY)
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_URI_KEY)
val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true)
val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY)
@@ -306,7 +436,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
return LoadDatabaseRunnable(
this,
database,
mDatabase,
databaseUri,
masterPassword,
keyFileUri,
@@ -319,7 +449,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
result.data = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri)
putString(MASTER_PASSWORD_KEY, masterPassword)
putParcelable(KEY_FILE_KEY, keyFileUri)
putParcelable(KEY_FILE_URI_KEY, keyFileUri)
putBoolean(READ_ONLY_KEY, readOnly)
putParcelable(CIPHER_ENTITY_KEY, cipherEntity)
}
@@ -334,16 +464,16 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
&& intent.hasExtra(MASTER_PASSWORD_KEY)
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
&& intent.hasExtra(KEY_FILE_KEY)
&& intent.hasExtra(KEY_FILE_URI_KEY)
) {
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null
AssignPasswordInDatabaseRunnable(this,
Database.getInstance(),
mDatabase,
databaseUri,
intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false),
intent.getStringExtra(MASTER_PASSWORD_KEY),
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
intent.getParcelableExtra(KEY_FILE_KEY)
intent.getParcelableExtra(KEY_FILE_URI_KEY)
)
} else {
null
@@ -365,7 +495,6 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(PARENT_ID_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
@@ -373,9 +502,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|| newGroup == null)
return null
database.getGroupById(parentId)?.let { parent ->
mDatabase.getGroupById(parentId)?.let { parent ->
AddGroupRunnable(this,
database,
mDatabase,
newGroup,
parent,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
@@ -391,7 +520,6 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(GROUP_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
val groupId: NodeId<*>? = intent.getParcelableExtra(GROUP_ID_KEY)
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
@@ -399,9 +527,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|| newGroup == null)
return null
database.getGroupById(groupId)?.let { oldGroup ->
mDatabase.getGroupById(groupId)?.let { oldGroup ->
UpdateGroupRunnable(this,
database,
mDatabase,
oldGroup,
newGroup,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
@@ -417,7 +545,6 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(PARENT_ID_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
@@ -425,9 +552,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|| newEntry == null)
return null
database.getGroupById(parentId)?.let { parent ->
mDatabase.getGroupById(parentId)?.let { parent ->
AddEntryRunnable(this,
database,
mDatabase,
newEntry,
parent,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
@@ -443,7 +570,6 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(ENTRY_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
val entryId: NodeId<UUID>? = intent.getParcelableExtra(ENTRY_ID_KEY)
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
@@ -451,9 +577,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|| newEntry == null)
return null
database.getEntryById(entryId)?.let { oldEntry ->
mDatabase.getEntryById(entryId)?.let { oldEntry ->
UpdateEntryRunnable(this,
database,
mDatabase,
oldEntry,
newEntry,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
@@ -470,13 +596,12 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(PARENT_ID_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
database.getGroupById(parentId)?.let { newParent ->
mDatabase.getGroupById(parentId)?.let { newParent ->
CopyNodesRunnable(this,
database,
getListNodesFromBundle(database, intent.extras!!),
mDatabase,
getListNodesFromBundle(mDatabase, intent.extras!!),
newParent,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
@@ -492,13 +617,12 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(PARENT_ID_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
database.getGroupById(parentId)?.let { newParent ->
mDatabase.getGroupById(parentId)?.let { newParent ->
MoveNodesRunnable(this,
database,
getListNodesFromBundle(database, intent.extras!!),
mDatabase,
getListNodesFromBundle(mDatabase, intent.extras!!),
newParent,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
@@ -513,10 +637,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(ENTRIES_ID_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
DeleteNodesRunnable(this,
database,
getListNodesFromBundle(database, intent.extras!!),
mDatabase,
getListNodesFromBundle(mDatabase, intent.extras!!),
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
} else {
@@ -529,12 +652,11 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(ENTRY_HISTORY_POSITION_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
database.getEntryById(entryId)?.let { mainEntry ->
mDatabase.getEntryById(entryId)?.let { mainEntry ->
RestoreEntryHistoryDatabaseRunnable(this,
database,
mDatabase,
mainEntry,
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
@@ -549,12 +671,11 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(ENTRY_HISTORY_POSITION_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
database.getEntryById(entryId)?.let { mainEntry ->
mDatabase.getEntryById(entryId)?.let { mainEntry ->
DeleteEntryHistoryDatabaseRunnable(this,
database,
mDatabase,
mainEntry,
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
@@ -577,7 +698,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
return null
return UpdateCompressionBinariesDatabaseRunnable(this,
Database.getInstance(),
mDatabase,
oldElement,
newElement,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
@@ -591,10 +712,26 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
}
}
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,
Database.getInstance(),
mDatabase,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
).apply {
mAfterSaveDatabase = { result ->
@@ -612,7 +749,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
private fun buildDatabaseSave(intent: Intent): ActionRunnable? {
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
SaveDatabaseRunnable(this,
Database.getInstance(),
mDatabase,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
} else {
null
@@ -623,10 +760,6 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
private val TAG = DatabaseTaskNotificationService::class.java.name
const val DATABASE_TASK_TITLE_KEY = "DATABASE_TASK_TITLE_KEY"
const val DATABASE_TASK_MESSAGE_KEY = "DATABASE_TASK_MESSAGE_KEY"
const val DATABASE_TASK_WARNING_KEY = "DATABASE_TASK_WARNING_KEY"
const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK"
const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK"
const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_TASK"
@@ -644,6 +777,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
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"
@@ -652,12 +786,17 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
const val ACTION_DATABASE_UPDATE_PARALLELISM_TASK = "ACTION_DATABASE_UPDATE_PARALLELISM_TASK"
const val ACTION_DATABASE_UPDATE_ITERATIONS_TASK = "ACTION_DATABASE_UPDATE_ITERATIONS_TASK"
const val ACTION_DATABASE_SAVE = "ACTION_DATABASE_SAVE"
const val ACTION_DATABASE_CLOSE = "ACTION_DATABASE_CLOSE"
const val DATABASE_TASK_TITLE_KEY = "DATABASE_TASK_TITLE_KEY"
const val DATABASE_TASK_MESSAGE_KEY = "DATABASE_TASK_MESSAGE_KEY"
const val DATABASE_TASK_WARNING_KEY = "DATABASE_TASK_WARNING_KEY"
const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
const val KEY_FILE_KEY = "KEY_FILE_KEY"
const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY"
const val READ_ONLY_KEY = "READ_ONLY_KEY"
const val CIPHER_ENTITY_KEY = "CIPHER_ENTITY_KEY"
const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY"

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.notifications
import android.content.Intent
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.LockReceiver
import com.kunzisoft.keepass.utils.registerLockReceiver
import com.kunzisoft.keepass.utils.unregisterLockReceiver

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
@@ -166,7 +166,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
refreshRecycleBinGroup()
// Save the database if not in readonly mode
(context as SettingsActivity?)?.
mProgressDialogThread?.startDatabaseSave(mDatabaseAutoSaveEnabled)
mProgressDatabaseTaskProvider?.startDatabaseSave(mDatabaseAutoSaveEnabled)
true
}
true
@@ -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)
}
@@ -546,7 +549,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
return when (item.itemId) {
R.id.menu_save_database -> {
settingActivity?.mProgressDialogThread?.startDatabaseSave(!mDatabaseReadOnly)
settingActivity?.mProgressDatabaseTaskProvider?.startDatabaseSave(!mDatabaseReadOnly)
true
}

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)
@@ -105,7 +93,7 @@ open class SettingsActivity
backupManager = BackupManager(this)
mProgressDialogThread?.onActionFinish = { actionTask, result ->
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
// Call result in fragment
(supportFragmentManager
.findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?)
@@ -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,
@@ -136,7 +140,7 @@ open class SettingsActivity
database.fileUri?.let { databaseUri ->
// Show the progress dialog now or after dialog confirmation
if (database.validatePasswordEncoding(masterPassword, keyFileChecked)) {
mProgressDialogThread?.startDatabaseAssignPassword(
mProgressDatabaseTaskProvider?.startDatabaseAssignPassword(
databaseUri,
masterPasswordChecked,
masterPassword,
@@ -144,18 +148,12 @@ open class SettingsActivity
keyFile
)
} else {
PasswordEncodingDialogFragment().apply {
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
mProgressDialogThread?.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

@@ -86,7 +86,7 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
}
val oldColor = database.customColor
database.customColor = newColor
mProgressDialogThread?.startDatabaseSaveColor(oldColor, newColor, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveColor(oldColor, newColor, mDatabaseAutoSaveEnable)
}
onDialogClosed(true)

View File

@@ -64,7 +64,7 @@ class DatabaseDataCompressionPreferenceDialogFragmentCompat
database.compressionAlgorithm = newCompression
if (oldCompression != null && newCompression != null)
mProgressDialogThread?.startDatabaseSaveCompression(oldCompression, newCompression, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveCompression(oldCompression, newCompression, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -36,7 +36,7 @@ class DatabaseDefaultUsernamePreferenceDialogFragmentCompat : DatabaseSavePrefer
val newDefaultUsername = inputText
val oldDefaultUsername = database.defaultUsername
database.defaultUsername = newDefaultUsername
mProgressDialogThread?.startDatabaseSaveDefaultUsername(oldDefaultUsername, newDefaultUsername, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(oldDefaultUsername, newDefaultUsername, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -36,7 +36,7 @@ class DatabaseDescriptionPreferenceDialogFragmentCompat : DatabaseSavePreference
val newDescription = inputText
val oldDescription = database.description
database.description = newDescription
mProgressDialogThread?.startDatabaseSaveDescription(oldDescription, newDescription, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveDescription(oldDescription, newDescription, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -65,7 +65,7 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
database.encryptionAlgorithm = newAlgorithm
if (oldAlgorithm != null && newAlgorithm != null)
mProgressDialogThread?.startDatabaseSaveEncryption(oldAlgorithm, newAlgorithm, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveEncryption(oldAlgorithm, newAlgorithm, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -66,7 +66,7 @@ class DatabaseKeyDerivationPreferenceDialogFragmentCompat
val oldKdfEngine = database.kdfEngine
if (newKdfEngine != null && oldKdfEngine != null) {
database.kdfEngine = newKdfEngine
mProgressDialogThread?.startDatabaseSaveKeyDerivation(oldKdfEngine, newKdfEngine, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveKeyDerivation(oldKdfEngine, newKdfEngine, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -36,7 +36,7 @@ class DatabaseNamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogF
val newName = inputText
val oldName = database.name
database.name = newName
mProgressDialogThread?.startDatabaseSaveName(oldName, newName, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveName(oldName, newName, mDatabaseAutoSaveEnable)
}
}
}

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

@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.content.Context
import android.os.Bundle
import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.settings.SettingsActivity
@@ -30,7 +30,7 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat : InputPreferenceDialo
protected var database: Database? = null
protected var mDatabaseAutoSaveEnable = true
protected var mProgressDialogThread: ProgressDialogThread? = null
protected var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -42,12 +42,17 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat : InputPreferenceDialo
super.onAttach(context)
// Attach dialog thread to start action
if (context is SettingsActivity) {
mProgressDialogThread = context.mProgressDialogThread
mProgressDatabaseTaskProvider = context.mProgressDatabaseTaskProvider
}
this.mDatabaseAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(context)
}
override fun onDetach() {
mProgressDatabaseTaskProvider = null
super.onDetach()
}
companion object {
private const val TAG = "DbSavePrefDialog"
}

View File

@@ -60,7 +60,7 @@ class MaxHistoryItemsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDial
// Remove all history items
database.removeOldestHistoryForEachEntry()
mProgressDialogThread?.startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems, maxHistoryItems, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems, maxHistoryItems, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -56,7 +56,7 @@ class MaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialo
val oldMaxHistorySize = database.historyMaxSize
database.historyMaxSize = maxHistorySize
mProgressDialogThread?.startDatabaseSaveMaxHistorySize(oldMaxHistorySize, maxHistorySize, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(oldMaxHistorySize, maxHistorySize, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -48,7 +48,7 @@ class MemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFr
val oldMemoryUsage = database.memoryUsage
database.memoryUsage = memoryUsage
mProgressDialogThread?.startDatabaseSaveMemoryUsage(oldMemoryUsage, memoryUsage, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(oldMemoryUsage, memoryUsage, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -44,7 +44,7 @@ class ParallelismPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFr
val oldParallelism = database.parallelism
database.parallelism = parallelism
mProgressDialogThread?.startDatabaseSaveParallelism(
mProgressDatabaseTaskProvider?.startDatabaseSaveParallelism(
oldParallelism,
parallelism,
mDatabaseAutoSaveEnable)

View File

@@ -54,7 +54,7 @@ class RoundsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmen
database.numberKeyEncryptionRounds = Long.MAX_VALUE
}
mProgressDialogThread?.startDatabaseSaveIterations(oldRounds, rounds, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveIterations(oldRounds, rounds, mDatabaseAutoSaveEnable)
}
}
}

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

@@ -1,93 +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.tasks
import android.content.ContentResolver
import android.net.Uri
import android.os.AsyncTask
import android.util.Log
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
class AttachmentFileAsyncTask(
private val fileUri: Uri,
private val attachmentNotification: AttachmentFileNotificationService.AttachmentNotification,
private val contentResolver: ContentResolver)
: AsyncTask<Void, Int, Boolean>() {
private val updateMinFrequency = 1000
private var previousSaveTime = System.currentTimeMillis()
var onUpdate: ((Uri, EntryAttachment, Int)->Unit)? = null
override fun onPreExecute() {
super.onPreExecute()
attachmentNotification.attachmentTask = this
attachmentNotification.entryAttachment.apply {
downloadState = AttachmentState.START
downloadProgression = 0
}
onUpdate?.invoke(fileUri, attachmentNotification.entryAttachment, attachmentNotification.notificationId)
}
override fun doInBackground(vararg params: Void?): Boolean {
try {
attachmentNotification.entryAttachment.apply {
downloadState = AttachmentState.IN_PROGRESS
binaryAttachment.download(fileUri, contentResolver, 1024) { percent ->
publishProgress(percent)
}
}
} catch (e: Exception) {
return false
}
return true
}
override fun onProgressUpdate(vararg values: Int?) {
super.onProgressUpdate(*values)
val percent = values[0] ?: 0
val currentTime = System.currentTimeMillis()
if (previousSaveTime + updateMinFrequency < currentTime) {
attachmentNotification.entryAttachment.apply {
downloadState = AttachmentState.IN_PROGRESS
downloadProgression = percent
}
onUpdate?.invoke(fileUri, attachmentNotification.entryAttachment, attachmentNotification.notificationId)
Log.d(TAG, "Download file $fileUri : $percent%")
previousSaveTime = currentTime
}
}
override fun onPostExecute(result: Boolean) {
super.onPostExecute(result)
attachmentNotification.attachmentTask = null
attachmentNotification.entryAttachment.apply {
downloadState = if (result) AttachmentState.COMPLETE else AttachmentState.ERROR
downloadProgression = 100
}
onUpdate?.invoke(fileUri, attachmentNotification.entryAttachment, attachmentNotification.notificationId)
}
companion object {
private val TAG = AttachmentFileAsyncTask::class.java.name
}
}

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

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.utils
import android.os.Parcel
import android.os.Parcelable
import java.util.*
import kotlin.collections.LinkedHashMap
object ParcelableUtil {
@@ -51,7 +52,7 @@ object ParcelableUtil {
// For writing map with string key to a Parcel
fun <V : Parcelable> writeStringParcelableMap(
parcel: Parcel, flags: Int, map: Map<String, V>) {
parcel: Parcel, flags: Int, map: LinkedHashMap<String, V>) {
parcel.writeInt(map.size)
for ((key, value) in map) {
parcel.writeString(key)
@@ -59,11 +60,20 @@ 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>): HashMap<String, V> {
parcel: Parcel, vClass: Class<V>): LinkedHashMap<String, V> {
val size = parcel.readInt()
val map = HashMap<String, V>(size)
val map = LinkedHashMap<String, V>(size)
for (i in 0 until size) {
val key: String? = parcel.readString()
val value: V? = vClass.cast(parcel.readParcelable(vClass.classLoader))
@@ -73,9 +83,22 @@ 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: Map<String, String>) {
fun writeStringParcelableMap(dest: Parcel, map: LinkedHashMap<String, String>) {
dest.writeInt(map.size)
for ((key, value) in map) {
dest.writeString(key)
@@ -84,9 +107,9 @@ object ParcelableUtil {
}
// For reading map with string key and string value from a Parcel
fun readStringParcelableMap(parcel: Parcel): HashMap<String, String> {
fun readStringParcelableMap(parcel: Parcel): LinkedHashMap<String, String> {
val size = parcel.readInt()
val map = HashMap<String, String>(size)
val map = LinkedHashMap<String, String>(size)
for (i in 0 until size) {
val key: String? = parcel.readString()
val value: String? = parcel.readString()

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