Compare commits

...

421 Commits

Author SHA1 Message Date
J-Jamet
462c29b769 Merge branch 'release/2.5.0.0beta26' 2020-02-07 21:45:40 +01:00
J-Jamet
3939276d58 Fix translations 2020-02-07 19:44:21 +01:00
J-Jamet
ff85f18c4c Update ReadMe.md 2020-02-07 19:24:12 +01:00
J-Jamet
dd14fe4123 Fix exception after changing theme 2020-02-07 19:20:43 +01:00
J-Jamet
d804659ee2 Change blue color of classic dark style 2020-02-07 19:10:48 +01:00
J-Jamet
e3152cf901 Remove AMOLED string for Black theme 2020-02-07 18:57:07 +01:00
J-Jamet
de236f321f Upgrade CHANGELOG 2020-02-07 18:26:27 +01:00
J-Jamet
66f4611866 Restore and delete entry history #335 2020-02-07 18:22:37 +01:00
heta1121
6a6ef052af Translated using Weblate (Swedish)
Currently translated at 94.9% (388 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2020-02-06 19:07:41 +01:00
J-Jamet
4e4606dabd Fix strings #436 2020-01-31 20:14:53 +01:00
J-Jamet
0ce11103ab Update CHANGELOG 2020-01-31 19:33:24 +01:00
J-Jamet
899d0e0557 Fix expired entries views 2020-01-31 19:29:40 +01:00
J-Jamet
4aefeff41f Hide expired entries from quick search when setting is activated #453 2020-01-31 17:26:36 +01:00
J-Jamet
08d59e50e8 Add android:resizeableActivity="true" for DeX mode 2020-01-31 16:48:15 +01:00
J-Jamet
e6c06aba8c Merge branch 'qtwyeuritoiy-feature/amoled_black' into develop 2020-01-31 13:12:37 +01:00
J-Jamet
e5c2a04922 Merge branch 'feature/amoled_black' of git://github.com/qtwyeuritoiy/KeePassDX into qtwyeuritoiy-feature/amoled_black 2020-01-31 13:12:18 +01:00
J-Jamet
c134ccf8d9 Encapsulate filters 2020-01-29 20:20:39 +01:00
J-Jamet
4f959d1ff6 Merge branch 'feature/hide_expired_entries' of git://github.com/qtwyeuritoiy/KeePassDX into qtwyeuritoiy-feature/hide_expired_entries 2020-01-29 17:22:40 +01:00
J-Jamet
1c06d93951 Fix custom fields in Magikeyboard 2020-01-29 17:10:26 +01:00
J-Jamet
b2dff29baa Auto performed "Go" key in Magikeyboard 2020-01-29 14:13:13 +01:00
J-Jamet
72633e9472 Change KEYCODE_ENTER by IME_ACTION_GO #369 2020-01-29 13:34:32 +01:00
J-Jamet
898a88f7d8 Fix app description 2020-01-27 12:52:46 +01:00
J-Jamet
c2f09f6666 Change "KeePass DX" name to "KeePassDX" and fix small issues #411 2020-01-27 12:43:45 +01:00
J-Jamet
d45bcbc347 Fix empty space in entry content 2020-01-27 11:18:54 +01:00
Jihoon Kim
1f834567f8 [Feature/HideExpiredEntries] Move 'Hide Expired Entries' Switch to 'Other' Category
Signed-off-by: Jihoon Kim <imsesaok@tuta.io>
2020-01-27 17:39:26 +08:00
Jihoon Kim
0b62c04867 [Feature/AmoledBlack] Add AMOLED Black Theme
Signed-off-by: Jihoon Kim <imsesaok@tuta.io>
2020-01-27 17:00:24 +08:00
J-Jamet
c71bb2a570 Password display box multiline #225 2020-01-26 18:02:11 +01:00
J-Jamet
0a575b5bf8 Prevent capture for all screens 2020-01-26 15:47:50 +01:00
J-Jamet
6f2bf903c5 Fix storage permission 2020-01-26 13:26:09 +01:00
Rodrigo Saldaña
210ad4b8db Translated using Weblate (Spanish)
Currently translated at 88.5% (362 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-01-25 19:21:30 +01:00
Allan Nordhøy
b42cf0e204 Translated using Weblate (Norwegian Bokmål)
Currently translated at 87.5% (358 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-01-23 09:21:33 +01:00
solokot
2045d3aab8 Translated using Weblate (Russian)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-01-23 09:21:32 +01:00
Allan Nordhøy
e20cb9431d Translated using Weblate (Danish)
Currently translated at 95.6% (391 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-01-23 09:21:31 +01:00
J-Jamet
7161eaea8b Allow write permission for specific file managers 2020-01-22 16:37:38 +01:00
J-Jamet
b786da52f5 Add error snackbar for error in entry edit view #431 2020-01-22 13:01:15 +01:00
J-Jamet
ccbfec838d Fix disclaimer formal string 2020-01-22 12:18:19 +01:00
J-Jamet
f5c1872225 Fix show read only warning 2020-01-22 10:53:24 +01:00
heta1121
3001013bad Translated using Weblate (Swedish)
Currently translated at 85.6% (350 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2020-01-21 17:21:30 +01:00
Allan Nordhøy
1280803e5e Translated using Weblate (Norwegian Bokmål)
Currently translated at 83.9% (343 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-01-19 15:22:00 +01:00
heta1121
f84fc07fe0 Translated using Weblate (Swedish)
Currently translated at 85.1% (348 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2020-01-19 15:22:00 +01:00
J-Jamet
d83e53f589 Fix read only for recycle bin 2020-01-19 12:17:27 +01:00
J-Jamet
52ba2c53f1 Fix progress for Android < 5 2020-01-18 15:23:51 +01:00
J-Jamet
f266e1de4c Add string to open the attachment downloaded 2020-01-18 12:24:21 +01:00
J-Jamet
5a0aafd3ce Update CHANGELOG 2020-01-18 12:19:56 +01:00
J-Jamet
c7ce07a43c Change file size format 2020-01-18 12:15:14 +01:00
J-Jamet
a73644c285 Merge branch 'feature/Stream_Binaries' into develop 2020-01-18 12:03:55 +01:00
J-Jamet
e28f1ffc99 Fix attachment orientation change 2020-01-18 11:33:17 +01:00
Deleted User
afc691b2f4 Translated using Weblate (Norwegian Bokmål)
Currently translated at 83.9% (343 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-01-16 06:27:50 +01:00
J-Jamet
00f6eb83c3 Fix close notification 2020-01-15 05:52:32 +01:00
J-Jamet
bbf51ebbec Multiple attachments download and add file compression 2020-01-14 19:44:10 +01:00
Jihoon Kim
7816c8f16e [Feature/HideExpiredEntries] Apply 'Hide Expired Entries' Setting And Adjust Item Count Accordingly
Signed-off-by: Jihoon Kim <imsesaok@tuta.io>
2020-01-14 17:28:27 +08:00
Jihoon Kim
fdbcba2412 [Feature/HideExpiredEntries] Add Preference Entry for 'Hide Expired Entries'
Signed-off-by: Jihoon Kim <imsesaok@tuta.io>
2020-01-14 17:28:27 +08:00
J-Jamet
50dc6cb0aa Fix download attachment 2020-01-13 10:05:34 +01:00
J-Jamet
e413198d12 Download attachment dynamic string 2020-01-10 19:40:00 +01:00
solokot
72592772f9 Translated using Weblate (Russian)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-01-09 09:21:27 +01:00
solokot
f918206bcd Translated using Weblate (Russian)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-01-07 22:27:33 +01:00
J-Jamet
6b34d67da8 Remove WRITE_EXTERNAL_STORAGE for Android > 18
Download attachment as service
2020-01-07 19:43:41 +01:00
Retrial
04fb093d4d Translated using Weblate (Greek)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2020-01-04 18:21:28 +01:00
J-Jamet
7de0e0cc4a Add Attachment view 2019-12-31 14:34:51 +01:00
J-Jamet
5cf7688e38 Merge branch 'develop' into feature/Stream_Binaries 2019-12-31 12:05:48 +01:00
J-Jamet
97cd06b52b Merge branch 'feature/Duplicate_UUID_Database1' into develop 2019-12-31 11:23:44 +01:00
J-Jamet
6d522ead1d Fix illegal argument exception and refactor KDB parser code 2019-12-31 08:26:15 +01:00
J-Jamet
8e0fae62f3 Merge branch 'develop' into feature/Stream_Binaries 2019-12-30 14:09:05 +01:00
J-Jamet
af72098d60 Refactor bytes utility methods 2019-12-30 12:59:50 +01:00
Taeko Hasegawa
62da582f4e Translated using Weblate (Japanese)
Currently translated at 36.4% (149 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2019-12-23 14:21:24 +01:00
J-Jamet
f3efa6eddc Add parameter to fix duplicate UUID in DatabaseV1 2019-12-22 18:26:35 +01:00
J-Jamet
c85fb7bd0a Better element output KDB implementation 2019-12-22 18:13:26 +01:00
J-Jamet
fc7b183461 Update CHANGELOG 2019-12-22 17:51:48 +01:00
J-Jamet
a378810f88 Improvement code for ClipboardHelper 2019-12-21 21:36:33 +01:00
J-Jamet
2f5b322fad Fix crash during clean clipboard in Android 9 2019-12-21 21:31:12 +01:00
J-Jamet
efb9b50f85 Fix issue #127 KDBX binaries 2019-12-21 21:18:58 +01:00
J-Jamet
84fdef8eb6 Merge branch 'develop' into feature/Stream_Binaries 2019-12-21 19:46:05 +01:00
J-Jamet
76050a261b Update to version 2.5.0.0beta26 2019-12-21 19:16:30 +01:00
Éfrit
2667361450 Translated using Weblate (French)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-12-19 17:18:38 +01:00
Éfrit
a2b3313cc0 Translated using Weblate (French)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-12-18 22:50:52 +01:00
J-Jamet
d343446235 Check file size to not corrupt a database v1 if too high 2019-12-18 18:58:26 +01:00
J-Jamet
cbce70f8a4 Save KDB binaries as file 2019-12-18 18:46:50 +01:00
WaldiS
4573d6b6fb Translated using Weblate (Polish)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2019-12-18 14:21:21 +01:00
Chen Wang
973305d13c Translated using Weblate (Chinese (Simplified))
Currently translated at 99.3% (406 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2019-12-16 08:21:13 +01:00
solokot
4637016372 Translated using Weblate (Russian)
Currently translated at 99.8% (408 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2019-12-16 08:21:12 +01:00
Chen Wang
698ba4f7f1 Translated using Weblate (English)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2019-12-16 08:21:11 +01:00
Oğuz Ersen
502ebabf1f Translated using Weblate (Turkish)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2019-12-16 08:21:10 +01:00
WaldiS
13f88626ca Translated using Weblate (Polish)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2019-12-08 15:13:26 +01:00
zeritti
a39f58f7b5 Translated using Weblate (Czech)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2019-12-08 15:13:25 +01:00
nautilusx
aeb36468ce Translated using Weblate (German)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-12-08 15:13:23 +01:00
J-Jamet
fdd196526d Better InputStream / OutputStream close 2019-12-05 19:28:54 +01:00
Mattias Münster
ecca892b16 Translated using Weblate (Swedish)
Currently translated at 25.9% (106 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2019-12-04 02:05:37 +01:00
Rudah Ximenes Alvarenga
03343d0301 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2019-12-04 02:05:36 +01:00
Allan Nordhøy
a61c8b0cb6 Translated using Weblate (Norwegian Bokmål)
Currently translated at 83.6% (342 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2019-12-02 03:33:26 +01:00
Kunzisoft
a92129da00 Translated using Weblate (French)
Currently translated at 99.5% (407 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-12-02 03:33:24 +01:00
Allan Nordhøy
0b783d6390 Translated using Weblate (English)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2019-12-02 03:33:23 +01:00
jan madsen
9aa1a2e30e Translated using Weblate (Danish)
Currently translated at 95.8% (392 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2019-12-02 03:33:23 +01:00
C. Rüdinger
15b3c69514 Translated using Weblate (German)
Currently translated at 99.5% (407 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-12-02 03:33:21 +01:00
Oğuz Ersen
f2722e09ec Translated using Weblate (Turkish)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2019-12-02 03:33:21 +01:00
Deleted User
b442859be0 Translated using Weblate (German)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-12-01 17:45:31 +01:00
J-Jamet
1496a2efb1 Fix database decryption with attachment 2019-11-30 19:26:26 +01:00
J-Jamet
a0edb111f0 Merge branch 'develop' into feature/Stream_Binaries 2019-11-30 18:27:21 +01:00
J-Jamet
6bcf54fe29 Merge tag '2.5.0.0beta25' into develop
2.5.0.0beta25
2019-11-30 17:43:19 +01:00
J-Jamet
3d7e24816a Merge branch 'release/2.5.0.0beta25' 2019-11-30 17:43:05 +01:00
J-Jamet
f5e02ec63f Merge branch 'translations' into develop 2019-11-30 17:27:39 +01:00
J-Jamet
ed1f4ebace Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2019-11-30 17:27:20 +01:00
J-Jamet
eb0e496cfd Try to decrypt DatabaseV1 by InputStream 2019-11-30 17:02:09 +01:00
J-Jamet
8b9fc30a6d Upgrade description 2019-11-30 10:12:43 +01:00
J-Jamet
12c2a6e99c Refactor bytes utility methods 2019-11-29 14:19:13 +01:00
J-Jamet
714433b62d Refactor stream methods 2019-11-28 16:32:16 +01:00
J-Jamet
e42933d786 Move SortNodeEnum 2019-11-27 22:49:59 +01:00
J-Jamet
e9531e4edd Merge branch 'feature/Refactor_Class' into develop 2019-11-27 19:21:36 +01:00
J-Jamet
0cd9cd294d Move database elements in packages 2019-11-27 19:19:42 +01:00
J-Jamet
b03fb12fca Refactor Input and Output 2019-11-27 18:59:09 +01:00
J-Jamet
b7b2e8dc4e Change class by KDB and KDBX 2019-11-27 18:51:28 +01:00
J-Jamet
96568abc51 Update CHANGELOG 2019-11-27 17:36:40 +01:00
J-Jamet
a180688858 Merge branch 'feature/OOM' into develop 2019-11-27 17:34:44 +01:00
J-Jamet
2590067558 Fix exception message 2019-11-27 17:34:26 +01:00
J-Jamet
b5499238f7 Change binary attachment compression after change compression setting 2019-11-27 16:46:14 +01:00
J-Jamet
cc5b96f539 Fix OOM by stream implementation and add KDBX version for DatabaseV2 2019-11-26 17:21:33 +01:00
J-Jamet
32343dc937 Refactor binary class 2019-11-26 11:07:29 +01:00
J-Jamet
1e71dd3031 Better binary reader 2019-11-25 21:14:39 +01:00
J-Jamet
ebf6f6a52a Replace Base64 lib 2019-11-25 19:25:08 +01:00
J-Jamet
6bf6d661af New databaseV2 XML write methods 2019-11-25 18:20:02 +01:00
J-Jamet
fe16879f35 New method to clear the clipboard 2019-11-25 15:12:27 +01:00
J-Jamet
ead384d1bb Update invalid credentials error message 2019-11-25 14:20:28 +01:00
J-Jamet
1ebdc0bacd Fix copy and move entry crash in root for DatabaseV1 2019-11-25 14:17:07 +01:00
zeritti
8eca8cdd53 Translated using Weblate (Czech)
Currently translated at 100.0% (399 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2019-11-25 14:04:59 +01:00
J-Jamet
24c61b1b37 Partial fix of Database V1 node deletion #394 2019-11-25 13:59:32 +01:00
J-Jamet
ea18cc7166 Remove unused element 2019-11-21 18:09:54 +01:00
J-Jamet
387c499829 Fix expires for new entry in DatabaseV1 2019-11-21 17:01:57 +01:00
J-Jamet
339470dd6e Fix open button 2019-11-21 16:15:21 +01:00
J-Jamet
02d1089dbc Better biometric state management 2019-11-21 13:12:38 +01:00
J-Jamet
1bc0932cec Fix fingerprint animation 2019-11-21 12:13:08 +01:00
J-Jamet
76cc2739c8 Change biometric prompt messages 2019-11-21 11:58:59 +01:00
J-Jamet
b23908aec2 Change biometric error 2019-11-21 10:50:49 +01:00
J-Jamet
6b1b8c0f7b Add biometric error message 2019-11-21 10:44:22 +01:00
J-Jamet
06320a7eba Add sw600dp-port dimen 2019-11-21 10:13:04 +01:00
WaldiS
fc02145d0c Translated using Weblate (Polish)
Currently translated at 99.7% (398 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2019-11-21 10:04:56 +01:00
Gaurav Sehar
5360738775 Translated using Weblate (Hindi)
Currently translated at 23.3% (93 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hi/
2019-11-21 10:04:54 +01:00
J-Jamet
0957df752a Fix creating database 2019-11-21 10:04:34 +01:00
J-Jamet
fe56d06b5d Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2019-11-21 09:16:34 +01:00
J-Jamet
f4955b16cd Remove specific ndk version to avoid error on FDroid 2019-11-20 23:10:56 +01:00
J-Jamet
07692ba73d Implicit confirmation for biometric #390 2019-11-20 19:36:05 +01:00
J-Jamet
64ac3e8f32 Change reader header and data equals 2019-11-20 16:06:59 +01:00
J-Jamet
8013d3109a Small code change 2019-11-20 12:55:14 +01:00
J-Jamet
be6f01dc99 Fix PWDate and blocks 2019-11-20 12:40:56 +01:00
Mattias Münster
30f7779ec6 Translated using Weblate (Swedish)
Currently translated at 26.8% (107 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2019-11-20 00:04:47 +01:00
solokot
6b5823ca70 Translated using Weblate (Russian)
Currently translated at 100.0% (399 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2019-11-20 00:04:45 +01:00
J-Jamet
d4a09ed569 Merge branch 'feature/Manually_Save_Satabase' into develop 2019-11-19 19:18:00 +01:00
J-Jamet
84fe409c4b Nested fragment for general and database settings 2019-11-18 21:18:34 +01:00
J-Jamet
51d90891ad Fix auto save database 2019-11-18 19:59:50 +01:00
J-Jamet
b1fa06099c Refactor action database and add new execution command string 2019-11-18 19:55:17 +01:00
J-Jamet
fa9d8ad6ad Refactor progress dialog thread / save database menu 2019-11-18 17:54:13 +01:00
Azurak
6703d7b451 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (399 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2019-11-17 02:04:55 +01:00
Azurak
73afd44d9c Translated using Weblate (Chinese (Traditional))
Currently translated at 48.4% (193 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2019-11-17 02:04:53 +01:00
Stephan Paternotte
93cb93bb9b Translated using Weblate (Dutch)
Currently translated at 99.2% (396 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2019-11-17 02:04:49 +01:00
WaldiS
82902cff71 Translated using Weblate (Polish)
Currently translated at 95.7% (382 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2019-11-17 02:04:46 +01:00
Azurak
3657f7e54c Translated using Weblate (English)
Currently translated at 100.0% (399 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2019-11-17 02:04:44 +01:00
Kunzisoft
a57a2f738d Translated using Weblate (English)
Currently translated at 100.0% (399 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2019-11-17 02:04:44 +01:00
jan madsen
b93592d703 Translated using Weblate (Danish)
Currently translated at 96.0% (383 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2019-11-17 02:04:44 +01:00
C. Rüdinger
fd288e624b Translated using Weblate (German)
Currently translated at 99.7% (398 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-11-17 02:04:42 +01:00
Rudah Ximenes Alvarenga
095fa3f6ef Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (399 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2019-11-17 02:04:41 +01:00
Oğuz Ersen
b1f6c578ad Translated using Weblate (Turkish)
Currently translated at 100.0% (399 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2019-11-17 02:04:37 +01:00
Mesut Akcan
1bc991bfcb Translated using Weblate (Turkish)
Currently translated at 100.0% (399 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2019-11-17 02:04:32 +01:00
J-Jamet
02feb478e8 Add autosave database setting 2019-11-16 20:18:07 +01:00
J-Jamet
c2df79f0c9 Fix save database in read only mode 2019-11-16 19:36:29 +01:00
J-Jamet
ef13965747 Fix save database in read only mode 2019-11-16 18:34:39 +01:00
Kunzisoft
13421601de Translated using Weblate (French)
Currently translated at 99.7% (398 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-11-16 11:53:24 +01:00
Azurak
5a9b46c4b5 Translated using Weblate (Chinese (Traditional))
Currently translated at 33.3% (133 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2019-11-15 18:31:47 +01:00
Thomas
6268642097 Translated using Weblate (Chinese (Traditional))
Currently translated at 33.3% (133 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2019-11-15 18:31:45 +01:00
J-Jamet
12eadbc092 Update CHANGELOG 2019-11-14 16:35:23 +01:00
J-Jamet
c9475d1dc2 Fix concurrent exception when update group 2019-11-14 16:28:44 +01:00
J-Jamet
56805defb6 Merge branch 'feature/RecyclerBin' into develop 2019-11-14 15:57:09 +01:00
J-Jamet
477a784201 Empty recycle bin 2019-11-14 15:56:30 +01:00
J-Jamet
f54bac15c9 Add recycle bin setting and fix delete node 2019-11-14 13:14:02 +01:00
J-Jamet
ae28ebe701 Add dialog when permanently delete nodes 2019-11-14 09:59:55 +01:00
oskamuelller4fs
f16adf00da Translated using Weblate (German)
Currently translated at 100.0% (399 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-11-14 03:20:56 +01:00
C. Rüdinger
d17699f6f7 Translated using Weblate (German)
Currently translated at 96.2% (384 of 399 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-11-14 01:43:58 +01:00
J-Jamet
8afcf043ee Fix database name 2019-11-13 20:06:44 +01:00
J-Jamet
dda9d034aa Same feature as KeePass2 for entry history 2019-11-13 19:17:36 +01:00
J-Jamet
652bad51b4 Fix infinite dialog after entry is save #387 2019-11-13 18:54:04 +01:00
J-Jamet
4d2ccc0789 Remove unused onPreferenceClick 2019-11-13 17:42:50 +01:00
J-Jamet
0e1c21e0f4 Small code change 2019-11-13 17:31:38 +01:00
J-Jamet
8d3f58b2cc Merge branch 'feature/RefactorImporter' into develop 2019-11-13 17:29:30 +01:00
J-Jamet
be78905d85 Rename input output utility class for db 2019-11-13 17:04:41 +01:00
J-Jamet
b3f232c840 Upgrade Importer V3 2019-11-13 15:57:52 +01:00
J-Jamet
075a16c1c3 Remove OTP menu from DatabaseV1 2019-11-13 12:58:10 +01:00
J-Jamet
18a13e6266 Fix OTP base32 length and remove spaces 2019-11-13 12:46:52 +01:00
Jérémy JAMET
7149bdbc3a Update bug_report.md 2019-11-12 20:03:19 +01:00
J-Jamet
9a4c4aa9bf Fix string bugs 2019-11-12 17:38:08 +01:00
Hosted Weblate
2b32cab9d1 Merge branch 'origin/master' into Weblate. 2019-11-12 17:06:11 +01:00
J-Jamet
66611db261 Fix TOTP secret uppercase #381 2019-11-12 15:45:40 +01:00
J-Jamet
fdc2095b42 Remove access view 2019-11-12 15:38:12 +01:00
J-Jamet
2f9cab0da2 Upgrade to 2.5.0.0beta25 2019-11-12 13:21:21 +01:00
J-Jamet
bd0d474751 Upgrade Pro description 2019-11-12 13:08:47 +01:00
J-Jamet
cca0ab2669 Merge tag '2.5.0.0beta24' into develop
2.5.0.0beta24
2019-11-12 12:35:28 +01:00
J-Jamet
cb5ca575d5 Merge branch 'release/2.5.0.0beta24' 2019-11-12 12:35:16 +01:00
J-Jamet
f4caaad9ee Fix invalid_db_same_uuid error 2019-11-12 12:09:58 +01:00
J-Jamet
b9cfb32a20 Fix OTP dialog 2019-11-12 11:56:19 +01:00
J-Jamet
095e5e5dd6 Add FLAG_GRANT_PREFIX_URI_PERMISSION flag 2019-11-12 10:43:32 +01:00
J-Jamet
ffc58688d8 Upgrade Gradle 2019-11-12 09:38:36 +01:00
Allan Nordhøy
6a69a7f416 Translated using Weblate (Norwegian Bokmål)
Currently translated at 94.5% (360 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2019-11-11 23:04:19 +01:00
J-Jamet
b4188b4712 Fix small bugs 2019-11-11 16:50:05 +01:00
J-Jamet
4a22c28df4 Merge branch 'translations' into develop 2019-11-11 16:34:39 +01:00
J-Jamet
76e9a25b1a Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2019-11-11 16:33:54 +01:00
J-Jamet
1928d0823e Fix small french translations 2019-11-11 14:47:57 +01:00
J-Jamet
c183d22412 Upgrade Biometric library 2019-11-11 14:44:38 +01:00
J-Jamet
b684353721 Upgrade CHANGELOG and Readme.md 2019-11-11 14:19:29 +01:00
J-Jamet
72f0e871c7 Fix close clipboard notification 2019-11-11 14:09:41 +01:00
J-Jamet
9a63962903 Fix clipboard notification with auto generated field 2019-11-11 14:00:30 +01:00
J-Jamet
938de28b49 Minimized hidden text #373 2019-11-10 16:30:22 +01:00
J-Jamet
20fc094d71 Steam OTP on closed and full version 2019-11-10 15:26:52 +01:00
J-Jamet
40180d5883 Merge branch 'feature/TOTP' into develop 2019-11-10 14:33:35 +01:00
J-Jamet
59e5865318 Fix OTP errors 2019-11-10 14:31:21 +01:00
J-Jamet
f63d6bdc1d Fix OTP orientation change listener 2019-11-10 14:10:36 +01:00
J-Jamet
fe33c0ae7d Fix init OTP elements 2019-11-10 13:20:19 +01:00
J-Jamet
ca4ad1c1fd Validate OTP dialog only if no error 2019-11-10 12:19:06 +01:00
J-Jamet
adf5382804 Add min and max values for OTP 2019-11-10 11:25:34 +01:00
J-Jamet
7f5406ac98 OTP errors implementation 2019-11-09 15:19:14 +01:00
J-Jamet
23b21ea154 Fix OTP dialog 2019-11-08 19:36:59 +01:00
Antonio F
49d4d0421a Translated using Weblate (Spanish)
Currently translated at 100.0% (381 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2019-11-08 18:04:10 +01:00
J-Jamet
23859a61bb Build OTP url 2019-11-07 19:28:54 +01:00
J-Jamet
221f81f51e Change ObjectNameResource and add OTP Dialog 2019-11-07 15:18:57 +01:00
J-Jamet
6e7c0d5073 Upgrade NDK version 2019-11-07 12:31:39 +01:00
Rudah Ximenes Alvarenga
e8e3d53685 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (381 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2019-11-06 21:04:12 +01:00
J-Jamet
e6d9df2b98 Add OTP auto generate field for Magikeyboard 2019-11-06 13:39:19 +01:00
J-Jamet
477a8f2e38 Update OTP code with AndOTP and kotlinized OtpEntryFields 2019-11-05 19:57:20 +01:00
J-Jamet
5e66697b8b Fix TOTP retrievement and add HOTP fields 2019-11-05 17:30:27 +01:00
J-Jamet
16320abb7d Global OTP variables, add progress bar, remove seconds 2019-11-05 16:04:30 +01:00
J-Jamet
f122c2832c Add OTP and fix TOTP generation 2019-11-05 13:42:51 +01:00
J-Jamet
d84c561f44 Merge branch 'studio315b-totpReadMode' into feature/TOTP 2019-11-04 18:23:17 +01:00
J-Jamet
da49c9c045 Merge branch 'totpReadMode' of git://github.com/studio315b/KeePassDX into studio315b-totpReadMode 2019-11-04 17:52:14 +01:00
J-Jamet
553920e37c Add setting to enable persistent notification 2019-11-03 11:29:28 +01:00
J-Jamet
450d2d113b Fix stop notification when edit entry 2019-11-03 10:55:47 +01:00
WaldiS
744c80e34d Translated using Weblate (Polish)
Currently translated at 99.7% (380 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2019-11-01 21:03:53 +01:00
J-Jamet
c0f8cca7c6 Fix history copy 2019-11-01 15:43:08 +01:00
J-Jamet
b129f220f7 Move copy button to the top #374 2019-11-01 15:09:41 +01:00
J-Jamet
7a3df02e38 Add explanation for settings 2019-10-30 21:40:53 +01:00
J-Jamet
befd29c396 Remove Magikeyboard explanation 2019-10-30 21:07:07 +01:00
J-Jamet
b8245621ea Remove fingerprint references 2019-10-30 20:19:33 +01:00
J-Jamet
ecda25a743 Fix biometric during orientation change 2019-10-30 19:36:52 +01:00
J-Jamet
d97a85b997 Remove biometric link if change credential 2019-10-30 15:21:50 +01:00
J-Jamet
8c0d7ab9ed Better Action runnable implementation 2019-10-30 14:49:40 +01:00
Allan Nordhøy
f3fa73ea34 Translated using Weblate (Norwegian Bokmål)
Currently translated at 94.5% (360 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2019-10-30 11:03:44 +01:00
Philipp Fischbeck
788734ccad Translated using Weblate (German)
Currently translated at 98.4% (375 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-10-30 11:03:43 +01:00
J-Jamet
e088f4a4ad Fix opening database after register biometric 2019-10-30 10:16:27 +01:00
J-Jamet
86bd018e4e Validate preference dialog with virtual done keyboard button 2019-10-29 11:25:20 +01:00
J-Jamet
283145034d Fix preference number 2019-10-29 10:50:09 +01:00
J-Jamet
163162497e Update libraries 2019-10-29 10:34:02 +01:00
J-Jamet
56911fb58f Refresh number of children after an action 2019-10-29 10:29:06 +01:00
J-Jamet
dae6481aff Update CHANGELOG 2019-10-29 10:16:31 +01:00
J-Jamet
6b2eb5e4f6 Merge branch 'feature/Database_Notification' into develop 2019-10-29 10:13:23 +01:00
J-Jamet
c563787f73 Change notification icons 2019-10-29 10:09:46 +01:00
J-Jamet
2737755b85 Change notification icons 2019-10-29 09:38:01 +01:00
Trygve John
fd9486ca77 Translated using Weblate (Norwegian Bokmål)
Currently translated at 94.5% (360 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2019-10-29 09:03:19 +01:00
J-Jamet
14020ec0b5 Fix clipboard notification timeout 2019-10-28 12:20:22 +01:00
J-Jamet
5a6c466ebd Add notification for opened database and use compat notification manager 2019-10-28 11:50:06 +01:00
WaldiS
76fcd5fe19 Translated using Weblate (Polish)
Currently translated at 99.2% (378 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2019-10-26 19:53:28 +02:00
J-Jamet
3732ff1ebc Fix clean clipboard after killing app by swap 2019-10-26 08:58:36 +02:00
Aykut ÖZDEMİR
22dd09954b Translated using Weblate (Turkish)
Currently translated at 100.0% (381 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2019-10-25 13:53:16 +02:00
J-Jamet
ef7387f2f3 Remove entry notifications after killing app (by swap) 2019-10-25 10:08:39 +02:00
J-Jamet
f774298587 Update CHANGELOG 2019-10-24 15:23:52 +02:00
J-Jamet
f8134307f6 Fix long click in paste mode 2019-10-24 13:10:52 +02:00
J-Jamet
fe461f2e7c Fix action error 2019-10-24 12:36:22 +02:00
J-Jamet
023c841747 Better code 2019-10-24 11:49:23 +02:00
J-Jamet
af95c0903a Better node error implementation 2019-10-24 11:29:43 +02:00
J-Jamet
0d756db8aa Beter code 2019-10-24 11:07:58 +02:00
J-Jamet
2c5dcc9b11 Fix back for action selection mode 2019-10-24 10:25:21 +02:00
Éfrit
21c6ea73b2 Translated using Weblate (French)
Currently translated at 100.0% (381 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-10-22 23:53:07 +02:00
C. Rüdinger
51dc302bb0 Translated using Weblate (German)
Currently translated at 97.9% (373 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-10-22 23:53:06 +02:00
J-Jamet
87760ab4f6 Fix save database and finish 2019-10-22 15:02:51 +02:00
J-Jamet
88ebe58a88 Fix action node 2019-10-22 14:46:13 +02:00
villabunterkunt
fb023b81b5 Translated using Weblate (German)
Currently translated at 97.9% (373 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-10-21 21:02:16 +02:00
J-Jamet
2de6bbc6c0 Fix selected color 2019-10-21 17:12:40 +02:00
J-Jamet
4ef436629d Merge branch 'feature/Loading_Database' into develop 2019-10-21 11:09:37 +02:00
Allan Nordhøy
9fd342f1e7 Translated using Weblate (Norwegian Bokmål)
Currently translated at 89.5% (341 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2019-10-21 02:53:03 +02:00
abidin toumi
8988f17765 Translated using Weblate (Arabic)
Currently translated at 71.1% (271 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2019-10-19 10:52:58 +02:00
J-Jamet
9d160db281 Add setting updates for dialog thread 2019-10-18 13:44:28 +02:00
J-Jamet
2e58c2f1b3 Fix duplication during deletion 2019-10-17 20:02:49 +02:00
J-Jamet
d1d2b99e09 Fix autofill selection 2019-10-17 15:17:18 +02:00
J-Jamet
def9744f75 Fix launch group activity when database loaded 2019-10-17 14:54:34 +02:00
J-Jamet
214e2cf109 Better database loading code 2019-10-17 14:07:39 +02:00
J-Jamet
b25180c617 Show update message and fix dialog retrievment 2019-10-17 11:29:30 +02:00
J-Jamet
6a5263df77 Fix biometric recognition error with new loading process 2019-10-17 10:28:24 +02:00
J-Jamet
2982f67717 Fix biometric recognition with new loading process 2019-10-17 10:24:12 +02:00
J-Jamet
b559670dff Fix result security by binder 2019-10-17 09:53:00 +02:00
J-Jamet
891d3142d2 Better code for bind callback 2019-10-16 18:49:15 +02:00
J-Jamet
2637788429 Fix dialog update 2019-10-16 15:31:07 +02:00
J-Jamet
a21de3b892 Fix update group 2019-10-16 11:48:35 +02:00
J-Jamet
e087e19120 Fix update entry 2019-10-16 10:46:56 +02:00
J-Jamet
721d61dda7 Fix update nodes 2019-10-15 18:05:33 +02:00
J-Jamet
e0e7e431cf Best update and nodeId type implementation 2019-10-15 17:02:59 +02:00
J-Jamet
93948e7c61 First commit for async loading 2019-10-15 16:01:50 +02:00
J-Jamet
b150c718a0 Fix edit notes field limited to 4 lines 2019-10-14 10:17:48 +02:00
J-Jamet
a71e4c3902 Add language parameter for issue template 2019-10-14 09:57:06 +02:00
Adolfo Jayme Barrientos
b7dc13d863 Translated using Weblate (Spanish)
Currently translated at 89.8% (342 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2019-10-12 16:52:53 +02:00
J-Jamet
1e3c58e359 Fix clipboard LOCK_DATABASE signal #366 2019-10-11 12:52:43 +02:00
J-Jamet
5f75599e9f Replace notification icons by PNG #364 2019-10-11 11:50:16 +02:00
J-Jamet
b602f9b77d Add icon for paste action 2019-10-11 10:58:21 +02:00
J-Jamet
aa948c1ece Remove null for BiometricUnlockCallback 2019-10-11 10:55:05 +02:00
J-Jamet
e599a51152 Refactor prompt build code 2019-10-11 10:41:45 +02:00
J-Jamet
ee6052f4d1 Update CHANGELOGS 2019-10-11 10:21:04 +02:00
J-Jamet
eba527f477 Add unrecoverable key exception 2019-10-11 10:18:28 +02:00
J-Jamet
09e0d6d3cc Update biometric and room lib 2019-10-11 10:03:44 +02:00
J-Jamet
9aefc984be Merge branch 'feature/Multiple_Node_Action' into develop #275 2019-10-10 19:19:08 +02:00
J-Jamet
2ce3b21f1b Add number of selected action nodes 2019-10-10 19:17:38 +02:00
J-Jamet
4d2f3cb4b1 Remove unused key for orientation change 2019-10-10 19:04:24 +02:00
J-Jamet
e62b46c4c0 Fix open and edit action bar 2019-10-10 18:38:15 +02:00
J-Jamet
6472601170 Add Toolbar action animation 2019-10-10 18:34:05 +02:00
J-Jamet
89dd7bfefb Fix finish node action when exit activity 2019-10-10 17:59:23 +02:00
J-Jamet
fb2ea4c0ed Multiple action node for Move Copy and Delete 2019-10-10 17:53:35 +02:00
J-Jamet
8d84358d48 Refactor code 2019-10-10 12:49:16 +02:00
J-Jamet
1d8661c633 Better node action refresh 2019-10-09 21:19:10 +02:00
J-Jamet
48130eee45 Change node action menu order 2019-10-09 20:23:22 +02:00
J-Jamet
2cf83962fe Merge branch 'develop' into feature/Multiple_Node_Action 2019-10-09 20:20:25 +02:00
J-Jamet
aecf7c0c39 Fix remove node at 2019-10-09 20:20:10 +02:00
J-Jamet
39606e2676 Custom Toolbar action 2019-10-09 20:16:11 +02:00
Ema Panz
6e00fa2d01 Translated using Weblate (Italian)
Currently translated at 95.5% (364 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2019-10-09 15:58:47 +02:00
J-Jamet
f79aa339e9 Selection by Contextual Menu 2019-10-09 15:58:00 +02:00
J-Jamet
f412fce912 Add safeNextText method to XmlPullParser 2019-10-07 15:51:44 +02:00
J-Jamet
cc20b7503c Refactor exceptions 2019-10-07 15:19:13 +02:00
Jérémy JAMET
2573434763 Update Readme.md
update FAQ
2019-10-07 09:33:21 +02:00
Jérémy JAMET
f153c26fef Set theme jekyll-theme-cayman 2019-10-07 09:15:48 +02:00
Ldm Public
125f461cbe Translated using Weblate (French)
Currently translated at 100.0% (381 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-10-06 16:56:46 +02:00
WaldiS
b705b4b712 Translated using Weblate (Polish)
Currently translated at 99.2% (378 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2019-10-06 16:56:45 +02:00
Retrial
c67b0bb858 Translated using Weblate (Greek)
Currently translated at 100.0% (381 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2019-10-06 16:56:45 +02:00
J-Jamet
ab1fc8c5d5 Remove CONTRIBUTORS file no longer updated, see https://github.com/Kunzisoft/KeePassDX/graphs/contributors 2019-10-06 11:46:35 +02:00
J-Jamet
8477f4ba08 Fix education in PasswordActivity 2019-10-06 11:15:56 +02:00
J-Jamet
e6518ffdc8 Upgrade ISSUE_TEMPLATE 2019-10-06 10:39:30 +02:00
J-Jamet
99917c7f28 Try to prevent XXE #200 2019-10-06 10:25:13 +02:00
J-Jamet
fcc29f67a3 Upgrade CHANGELOGS 2019-10-05 15:46:18 +02:00
J-Jamet
7dd49f050c Highlight expires entries 2019-10-05 15:43:45 +02:00
J-Jamet
5f96de84b0 Manage expires 2019-10-05 15:32:04 +02:00
J-Jamet
54c2f5a61f Fix Minor Accessibility Issues #334 2019-10-05 12:56:09 +02:00
J-Jamet
921c6f88aa Merge branch 'feature/OPEN_DOCUMENT' into develop 2019-10-05 12:43:40 +02:00
J-Jamet
a0cb579df4 Merge branch 'develop' into feature/OPEN_DOCUMENT 2019-10-05 11:47:27 +02:00
J-Jamet
d6a7c34ff3 Update gradle 2019-10-05 11:47:12 +02:00
J-Jamet
bf2e61f149 Merge branch 'develop' into feature/OPEN_DOCUMENT 2019-10-05 11:45:54 +02:00
J-Jamet
eaf5dc5988 Show number of children in toolbar #282 2019-10-05 11:27:34 +02:00
J-Jamet
879ee013db Fix multiple biometric menu #332 2019-10-05 10:12:45 +02:00
J-Jamet
e13d53eae4 Unable default username and custom color to Database V2 2019-10-05 09:22:41 +02:00
J-Jamet
d72c8184c9 Fix databaseV1 settings 2019-10-04 19:00:18 +02:00
Kunzisoft
c4d3c8cbfb Translated using Weblate (French)
Currently translated at 100.0% (381 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-10-04 18:37:57 +02:00
J-Jamet
02a3d85f80 Try to fix "Unable to launch Activity from MagiKeyboard" #348 2019-10-04 16:53:20 +02:00
J-Jamet
19b0722f1f Add KeePass DX version in ISSUE_TEMPLATE 2019-10-04 16:36:20 +02:00
J-Jamet
f14222b192 Merge branch 'develop' into Bug_Feature_Template 2019-10-04 16:22:37 +02:00
J-Jamet
4f4f6d30d9 Change menu biometric order 2019-10-04 16:22:08 +02:00
J-Jamet
fdd329e982 Fix unchecked "Use as default database" doesn't work #354 2019-10-04 13:20:30 +02:00
J-Jamet
55a4d388b3 Fix V1 locked with keyFile and no password #353 2019-10-04 13:15:10 +02:00
J-Jamet
5c6be448ec Upgrade Changelogs 2019-10-04 12:00:22 +02:00
J-Jamet
3e79ddcc21 Merge branch 'feature/Color_Preference' into develop 2019-10-04 11:50:24 +02:00
J-Jamet
5362758424 Disable custom database color 2019-10-04 11:50:11 +02:00
J-Jamet
c10e3df2a7 Enable settings as switch 2019-10-04 11:47:33 +02:00
J-Jamet
166784021a Fix color setting orientation change 2019-10-04 11:44:16 +02:00
Jérémy JAMET
5615c31e08 Update issue templates 2019-10-03 12:04:38 +02:00
J-Jamet
fb60dd5921 Fix Disable color 2019-10-02 19:27:35 +02:00
J-Jamet
ff4c1b779b First code for color preference 2019-10-02 15:38:03 +02:00
J-Jamet
53a7b99567 Fix package for preference 2019-10-01 18:00:41 +02:00
J-Jamet
a57103bafb Add default username and better setter database data implementation 2019-10-01 17:05:46 +02:00
J-Jamet
2540f32dbf Add NDK version 2019-10-01 15:52:46 +02:00
J-Jamet
499ccd6b7c #297 Duplicate UUID 2019-10-01 15:00:35 +02:00
J-Jamet
a4359560b9 Change duplicate UUID dialog message 2019-10-01 14:42:32 +02:00
J-Jamet
149483cc2d Fix duplicate UUID dialog orientation change 2019-10-01 14:33:50 +02:00
J-Jamet
a1d2022492 Fix index for duplicate UUID 2019-10-01 14:15:11 +02:00
J-Jamet
891036c35c Fix database after dialog positive click 2019-10-01 12:35:03 +02:00
J Smith
4e16ba5f56 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (381 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2019-10-01 11:56:16 +02:00
abidin toumi
7137a2fadb Translated using Weblate (Arabic)
Currently translated at 71.1% (271 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2019-10-01 11:56:14 +02:00
ButterflyOfFire
9d90d0eaba Translated using Weblate (Arabic)
Currently translated at 71.1% (271 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2019-10-01 11:56:12 +02:00
J-Jamet
94a9942db5 Show duplicate UUID message and fix cancel string 2019-10-01 11:38:46 +02:00
J-Jamet
5f347fe106 Show same UUID entries 2019-09-29 22:26:47 +02:00
J-Jamet
a34a84ae16 Fix refactoring 2019-09-26 20:57:10 +02:00
J-Jamet
40b0982298 Refactor load database 2019-09-26 20:29:33 +02:00
J-Jamet
4100258476 Remove unused import 2019-09-26 16:26:41 +02:00
J-Jamet
5f3f6661b7 Remove URI verification 2019-09-26 16:24:41 +02:00
J-Jamet
75af97e0ae Better code to open document 2019-09-26 16:04:05 +02:00
J-Jamet
58f158c457 Fix java.lang.IllegalStateException for CoordinatorLayout 2019-09-26 14:02:59 +02:00
J-Jamet
ce27eae1f0 Upgrade biometric lib 2019-09-26 13:20:00 +02:00
WaldiS
0aa0b3e993 Translated using Weblate (Polish)
Currently translated at 96.1% (366 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2019-09-25 20:28:03 +02:00
J-Jamet
1cc5a08236 Better delete implementation 2019-09-25 15:45:12 +02:00
J-Jamet
4c587eeb03 Remove unused open link, and expand view lib. Redo toolbar paste animation 2019-09-25 15:25:58 +02:00
J-Jamet
ab70c2d014 Add settings 2019-09-24 15:42:57 +02:00
J-Jamet
8413160ac5 Rename compression algorithm 2019-09-24 13:10:49 +02:00
J-Jamet
5abc403171 Add compression setting 2019-09-24 12:52:07 +02:00
J-Jamet
9b891013b8 Add database settings 2019-09-24 11:52:01 +02:00
J-Jamet
9413987355 Update CHANGELOG 2019-09-23 18:01:40 +02:00
J-Jamet
f95b514b41 Fix orientation change during entry edit 2019-09-23 17:40:53 +02:00
WaldiS
f6985c8944 Translated using Weblate (Polish)
Currently translated at 96.1% (366 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2019-09-23 15:28:06 +02:00
zeritti
4388d56c52 Translated using Weblate (Czech)
Currently translated at 100.0% (381 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2019-09-23 15:28:05 +02:00
solokot
a70fe24c97 Translated using Weblate (Russian)
Currently translated at 100.0% (381 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2019-09-23 15:28:02 +02:00
Avgerinos Panagiotis
8e0392753c Translated using Weblate (Greek)
Currently translated at 19.7% (75 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2019-09-23 15:28:01 +02:00
J-Jamet
9c9980bba6 Change TimeOut default to 5 minutes 2019-09-23 11:11:53 +02:00
J-Jamet
2226c15d29 Merge branch 'feature/Entry_History' into develop 2019-09-23 10:30:20 +02:00
J-Jamet
82a859bd9c Better KDF implementation 2019-09-22 20:23:59 +02:00
J-Jamet
83873fab81 Fix custom fields orientation change #337 2019-09-22 18:27:32 +02:00
J-Jamet
31f2be7b91 Hide history preference with database V3 2019-09-22 14:34:52 +02:00
J-Jamet
16458e6646 Add description for max history preferences 2019-09-22 14:12:58 +02:00
J-Jamet
2b9678707d Separate recycle bin preference 2019-09-22 13:54:26 +02:00
J-Jamet
cdbb23d7f1 Separate recycle bin preference 2019-09-22 13:52:30 +02:00
J-Jamet
23fd1b83f4 Better preference implementation 2019-09-22 13:46:10 +02:00
J-Jamet
40b0ebe49b Fix preferences 2019-09-22 13:14:47 +02:00
J-Jamet
7cd8682544 Better Preference implementation 2019-09-20 16:30:05 +02:00
J-Jamet
d0dd478ac8 Add max history preferences 2019-09-19 17:53:32 +02:00
J-Jamet
ffb547c452 Change history string 2019-09-19 15:37:07 +02:00
J-Jamet
bd829f129f Better condition to show history 2019-09-19 14:29:24 +02:00
J-Jamet
5ad3f62de5 Add history after each entry update for DatabaseV4 2019-09-19 14:20:28 +02:00
solokot
b0ec4942bc Translated using Weblate (Russian)
Currently translated at 100.0% (381 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2019-09-19 04:51:14 +02:00
tinect
2cbc9675f6 Translated using Weblate (German)
Currently translated at 97.1% (370 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-09-19 04:51:11 +02:00
J-Jamet
116643a45a Merge branch 'develop' into feature/Entry_History 2019-09-18 22:33:06 +02:00
J-Jamet
2f0eb283ed Fix selection mode color 2019-09-18 22:30:29 +02:00
J-Jamet
6d46fccdcd Show history by click 2019-09-18 22:14:50 +02:00
J-Jamet
f5dc94bfec Add date string for PwDate and new list view entry history 2019-09-18 12:57:11 +02:00
solokot
94bdb0e3da Translated using Weblate (Russian)
Currently translated at 99.7% (380 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2019-09-17 15:41:33 +02:00
Ldm Public
65360c2a1e Translated using Weblate (French)
Currently translated at 97.6% (372 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-09-16 22:24:14 +02:00
jan madsen
70d30bdbe6 Translated using Weblate (Danish)
Currently translated at 96.6% (368 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2019-09-16 22:24:13 +02:00
Mesut Akcan
66f7e6d1b1 Translated using Weblate (Turkish)
Currently translated at 100.0% (381 of 381 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2019-09-16 22:24:11 +02:00
J-Jamet
a8ccb67a87 Entry history first code 2019-09-15 17:56:25 +02:00
J-Jamet
66051382f1 Upgrade version 2019-09-15 15:56:54 +02:00
J-Jamet
0fb3028c91 Fix group copy 2019-09-15 10:42:10 +02:00
Hosted Weblate
5e5baa4892 Merge branch 'origin/master' into Weblate. 2019-09-15 10:17:14 +02:00
WaldiS
9d1257ed9d Translated using Weblate (Polish)
Currently translated at 97.6% (368 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2019-09-15 10:17:08 +02:00
Mesut Akcan
9d7546053d Translated using Weblate (Turkish)
Currently translated at 100.0% (377 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2019-09-15 10:17:07 +02:00
J-Jamet
a1b692abe5 Update fastlane 2019-09-14 12:47:42 +02:00
J-Jamet
4e06842d0f Fix #331 Crash if "Save keyfile" setting is unchecked 2019-09-14 12:47:04 +02:00
J-Jamet
f04c2ee1da Merge tag '2.5.0.0beta23' into develop
2.5.0.0beta23
2019-09-14 11:31:37 +02:00
somkun
9558fcaf21 Add Read support for TOTP Tokens 2018-09-16 22:11:23 -07:00
532 changed files with 22763 additions and 14359 deletions

44
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,44 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
** Keepass Database **
- Created with: [e.g Windows KeePass 2.42]
- Version: [e.g. 2]
- Location: [e.g. Remote file retrieved with GDrive app]
- Size: [e.g. 150Mo]
- Contains attachment: [e.g. Yes]
**KeePassDX (please complete the following information):**
- Version: [e.g. 2.5.0.0beta23]
- Build: [e.g. Free]
- Language: [e.g. French]
**Android (please complete the following information):**
- Device: [e.g. GalaxyS8]
- Version: [e.g. 8.1]
**Additional context**
Add any other context about the problem here.
- Browser for Autofill: [e.g. Chrome version X]

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

7
.gitignore vendored
View File

@@ -38,6 +38,13 @@ proguard/
# Android Studio captures folder
captures/
# Eclipse/VS Code
.project
.settings/*
*/.project
*/.classpath
*/.settings/*
# Intellij
*.iml
.idea/workspace.xml

View File

@@ -1,3 +1,34 @@
KeepassDX (2.5.0.0beta26)
* Download attachments
* Change file size string format
* Prevent screenshot for all screen
* Auto performed "Go" key in Magikeyboard
* Restore and delete entry history
* Setting to hide expired entries
* New Black theme
* Fix crash when clearing clipboard
* Fix attachments compressions
* Fix dates
* Fix UUID message for Database v1
KeepassDX (2.5.0.0beta25)
* Setting for Recycle Bin
* Fix Recycle bin issues
* Fix TOTP
* Fix infinite save
* Fix update group
* Fix OOM
KeepassDX (2.5.0.0beta24)
* Add OTP (HOTP / TOTP)
* Add settings (Color, Security, Master Key)
* Show history of each entry
* Auto repair database for nodes with same UUID
* Management of expired nodes
* Multi-selection for actions (Cut - Copy - Delete)
* Open/Save database as service / Add persistent notification
* Fix settings / edit group / small bugs
KeepassDX (2.5.0.0beta23)
* New, more secure database creation workflow
* Recognize more database files

View File

@@ -1,45 +0,0 @@
Original author:
Brian Pellin
Achim Weimert
Johan Berts - search patches
Mike Mohr - Better native code for aes and sha
Tobias Selig - icon support
Tolga Onbay, Dirk Bergstrom - password generator
Space Cowboy - holo theme
josefwells
Nicholas FitzRoy-Dale - auto launch intents
yulin2 - responsiveness improvements
Tadashi Saito
vhschlenker
bumper314 - Samsung multiwindow support
Hans Cappelle - fingerprint sensor integration
Jeremy Jamet - Keepass DX Material Design - Patches
Translations:
Diego Pierotto - Italian
Laurent, Norman Obry, Nam, Bruno Parmentier, Credomo - French
Maciej Bieniek, cod3r - Polish
Максим Сёмочкин, i.nedoboy, filimonic, bboa - Russian
MaWi, rvs2008, meviox, MaDill, EdlerProgrammierer, Jan Thomas - German
yslandro - Norwegian Nynorsk
王科峰 - Chinese
Typhoon - Slovak
Masahiro Inamura - Japanese
Matsuu Takuto - Japanese
Carlos Schlyter - Portugese (Brazil)
YSmhXQDd6Z - Portugese (Portugal)
andriykopanytsia - Ukranian
intel, Zoltán Antal - Hungarian
H Vanek - Czech
jipanos - Spanish
Erik Fdevriendt, Erik Jan Meijer - Dutch
Frederik Svarre - Danish
Oriol Garrote - Catalan
Mika Takala - Finnish
Niclas Burgren - Swedish
Raimonds - Latvian
dgarciabad - Basque
Arthur Zamarin - Hebrew
RaptorTFX - Greek
zygimantus - Lithuanian

View File

@@ -10,7 +10,8 @@
* Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm
* **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePassXC...)
* Allows **fast copy** of fields and opening of URI / URL
* **Fingerprint** for fast unlocking
* **Biometric recognition** for fast unlocking *(Fingerprint / Face unlock / ...)*
* **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA)
* Material design with **themes**
* **AutoFill** and Integration
* Field filling **keyboard**
@@ -54,7 +55,7 @@ You can contribute in different ways to help us on our work.
## F.A.Q.
Other questions? You can read the [F.A.Q.](https://www.keepassdx.com/FAQ)
Other questions? You can read the [F.A.Q.](https://github.com/Kunzisoft/KeePassDX/wiki/F.A.Q.)
## Other devices
@@ -64,7 +65,7 @@ Other questions? You can read the [F.A.Q.](https://www.keepassdx.com/FAQ)
## License
Copyright (c) 2019 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
Copyright (c) 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
This file is part of KeePassDX.

1
_config.yml Normal file
View File

@@ -0,0 +1 @@
theme: jekyll-theme-cayman

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass"
minSdkVersion 14
targetSdkVersion 28
versionCode = 23
versionName = "2.5.0.0beta23"
versionCode = 26
versionName = "2.5.0.0beta26"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
@@ -27,7 +27,6 @@ android {
}
}
buildTypes {
release {
minifyEnabled = false
@@ -80,7 +79,7 @@ android {
}
def spongycastleVersion = "1.58.0.0"
def room_version = "2.1.0"
def room_version = "2.2.1"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@@ -89,7 +88,7 @@ dependencies {
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.biometric:biometric:1.0.0-beta01'
implementation 'androidx.biometric:biometric:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation "androidx.room:room-runtime:$room_version"
@@ -97,17 +96,17 @@ dependencies {
implementation "com.madgag.spongycastle:core:$spongycastleVersion"
implementation "com.madgag.spongycastle:prov:$spongycastleVersion"
// Expandable view
implementation 'net.cachapa.expandablelayout:expandablelayout:2.9.2'
// Time
implementation 'joda-time:joda-time:2.9.9'
// Color
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3'
// Education
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.12.0'
// Apache Commons Collections
implementation 'commons-collections:commons-collections:3.2.1'
implementation 'org.apache.commons:commons-io:1.3.2'
// Base64
implementation 'biz.source_code:base64coder:2010-12-19'
// Apache Commons Codec
implementation 'commons-codec:commons-codec:1.11'
// Icon pack
implementation project(path: ':icon-pack-classic')
implementation project(path: ':icon-pack-material')

View File

@@ -1,37 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.tests
import junit.framework.TestCase
import com.kunzisoft.keepass.database.element.PwDate
import org.junit.Assert
class PwDateTest : TestCase() {
fun testDate() {
val jDate = PwDate(System.currentTimeMillis())
val intermediate = PwDate(jDate)
val cDate = PwDate(intermediate.byteArrayDate!!, 0)
Assert.assertTrue("jDate and intermediate not equal", jDate == intermediate)
Assert.assertTrue("jDate and cDate not equal", cDate == jDate)
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
@@ -19,20 +19,15 @@
*/
package com.kunzisoft.keepass.tests
import org.junit.Assert.assertArrayEquals
import java.io.ByteArrayOutputStream
import java.util.Calendar
import java.util.Random
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.ULONG_MAX_VALUE
import com.kunzisoft.keepass.stream.*
import junit.framework.TestCase
import org.junit.Assert.assertArrayEquals
import java.io.ByteArrayOutputStream
import java.util.*
import com.kunzisoft.keepass.database.element.PwDate
import com.kunzisoft.keepass.stream.LEDataInputStream
import com.kunzisoft.keepass.stream.LEDataOutputStream
import com.kunzisoft.keepass.utils.Types
class TypesTest : TestCase() {
class StringDatabaseKDBUtilsTest : TestCase() {
fun testReadWriteLongZero() {
testReadWriteLong(0.toByte())
@@ -56,15 +51,9 @@ class TypesTest : TestCase() {
private fun testReadWriteLong(value: Byte) {
val orig = ByteArray(8)
val dest = ByteArray(8)
setArray(orig, value, 0, 8)
val one = LEDataInputStream.readLong(orig, 0)
LEDataOutputStream.writeLong(one, dest, 0)
assertArrayEquals(orig, dest)
setArray(orig, value, 8)
assertArrayEquals(orig, longTo8Bytes(bytes64ToLong(orig)))
}
fun testReadWriteIntZero() {
@@ -81,24 +70,22 @@ class TypesTest : TestCase() {
private fun testReadWriteInt(value: Byte) {
val orig = ByteArray(4)
val dest = ByteArray(4)
for (i in 0..3) {
orig[i] = 0
}
setArray(orig, value, 0, 4)
setArray(orig, value, 4)
val one = LEDataInputStream.readInt(orig, 0)
LEDataOutputStream.writeInt(one, dest, 0)
val one = bytes4ToInt(orig)
val dest = intTo4Bytes(one)
assertArrayEquals(orig, dest)
}
private fun setArray(buf: ByteArray, value: Byte, offset: Int, size: Int) {
for (i in offset until offset + size) {
private fun setArray(buf: ByteArray, value: Byte, size: Int) {
for (i in 0 until size) {
buf[i] = value
}
}
@@ -109,11 +96,10 @@ class TypesTest : TestCase() {
orig[0] = 0
orig[1] = 1
val one = LEDataInputStream.readUShort(orig, 0)
val dest = LEDataOutputStream.writeUShortBuf(one)
val one = bytes2ToUShort(orig)
val dest = uShortTo2Bytes(one)
assertArrayEquals(orig, dest)
}
fun testReadWriteShortMin() {
@@ -126,15 +112,12 @@ class TypesTest : TestCase() {
private fun testReadWriteShort(value: Byte) {
val orig = ByteArray(2)
val dest = ByteArray(2)
setArray(orig, value, 2)
setArray(orig, value, 0, 2)
val one = LEDataInputStream.readUShort(orig, 0)
LEDataOutputStream.writeUShort(one, dest, 0)
val one = bytes2ToUShort(orig)
val dest = uShortTo2Bytes(one)
assertArrayEquals(orig, dest)
}
fun testReadWriteByteZero() {
@@ -150,16 +133,8 @@ class TypesTest : TestCase() {
}
private fun testReadWriteByte(value: Byte) {
val orig = ByteArray(1)
val dest = ByteArray(1)
setArray(orig, value, 0, 1)
val one = Types.readUByte(orig, 0)
Types.writeUByte(one, dest, 0)
assertArrayEquals(orig, dest)
val dest: Byte = uIntToByte(byteToUInt(value))
assert(value == dest)
}
fun testDate() {
@@ -168,27 +143,37 @@ class TypesTest : TestCase() {
val expected = Calendar.getInstance()
expected.set(2008, 1, 2, 3, 4, 5)
val buf = PwDate.writeTime(expected.time, cal)
val actual = Calendar.getInstance()
actual.time = PwDate.readTime(buf, 0, cal)
dateTo5Bytes(expected.time, cal)?.let { buf ->
actual.time = bytes5ToDate(buf, cal).date
}
val jDate = DateInstant(System.currentTimeMillis())
val intermediate = DateInstant(jDate)
val cDate = bytes5ToDate(dateTo5Bytes(intermediate.date)!!)
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR))
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH))
assertEquals("Day mismatch: ", 1, actual.get(Calendar.DAY_OF_MONTH))
assertEquals("Day mismatch: ", 2, actual.get(Calendar.DAY_OF_MONTH))
assertEquals("Hour mismatch: ", 3, actual.get(Calendar.HOUR_OF_DAY))
assertEquals("Minute mismatch: ", 4, actual.get(Calendar.MINUTE))
assertEquals("Second mismatch: ", 5, actual.get(Calendar.SECOND))
assertTrue("jDate and intermediate not equal", jDate == intermediate)
assertTrue("jDate $jDate and cDate $cDate not equal", cDate == jDate)
}
fun testUUID() {
val rnd = Random()
val bUUID = ByteArray(16)
rnd.nextBytes(bUUID)
Random().nextBytes(bUUID)
val uuid = Types.bytestoUUID(bUUID)
val eUUID = Types.UUIDtoBytes(uuid)
val uuid = bytes16ToUuid(bUUID)
val eUUID = uuidTo16Bytes(uuid)
val lUUID = bytes16ToUuid(bUUID)
val leUUID = uuidTo16Bytes(lUUID)
assertArrayEquals("UUID match failed", bUUID, eUUID)
assertArrayEquals("UUID match failed", bUUID, leUUID)
}
@Throws(Exception::class)
@@ -199,8 +184,8 @@ class TypesTest : TestCase() {
}
val bos = ByteArrayOutputStream()
val leos = LEDataOutputStream(bos)
leos.writeLong(Types.ULONG_MAX_VALUE)
val leos = LittleEndianDataOutputStream(bos)
leos.writeLong(ULONG_MAX_VALUE)
leos.close()
val uLongMax = bos.toByteArray()

View File

@@ -39,9 +39,8 @@ import junit.framework.TestCase
import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.crypto.engine.AesEngine
import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.stream.BetterCipherInputStream
import com.kunzisoft.keepass.stream.LEDataInputStream
import com.kunzisoft.keepass.stream.LittleEndianDataInputStream
class CipherTest : TestCase() {
private val rand = Random()
@@ -93,7 +92,7 @@ class CipherTest : TestCase() {
val bis = ByteArrayInputStream(secrettext)
val cis = BetterCipherInputStream(bis, decrypt)
val lis = LEDataInputStream(cis)
val lis = LittleEndianDataInputStream(cis)
val decrypttext = lis.readBytes(MESSAGE_LENGTH)

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.kunzisoft.keepass"
android:installLocation="auto">
<supports-screens
@@ -9,8 +10,8 @@
android:anyDensity="true" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:label="@string/app_name"
@@ -20,7 +21,10 @@
android:allowBackup="true"
android:fullBackupContent="@xml/backup"
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
android:theme="@style/KeepassDXStyle.Night">
android:largeHeap="true"
android:resizeableActivity="true"
android:theme="@style/KeepassDXStyle.Night"
tools:targetApi="n">
<!-- TODO backup API Key -->
<meta-data
android:name="com.google.android.backup.api_key"
@@ -131,7 +135,8 @@
<activity android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
<activity android:name="com.kunzisoft.keepass.settings.SettingsAutofillActivity" />
<activity android:name="com.kunzisoft.keepass.magikeyboard.KeyboardLauncherActivity"
android:label="@string/keyboard_name">
android:label="@string/keyboard_name"
android:exported="true">
</activity>
<activity android:name="com.kunzisoft.keepass.settings.MagikIMESettings"
android:label="@string/keyboard_setting_label">
@@ -140,11 +145,18 @@
</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"
android:exported="false" />
<service
android:name=".notifications.AttachmentFileNotificationService"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService"
android:enabled="true"

View File

@@ -21,45 +21,70 @@ package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import com.google.android.material.appbar.CollapsingToolbarLayout
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
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.LockingHideActivity
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.EntryVersioned
import com.kunzisoft.keepass.database.element.PwNodeId
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.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.settings.SettingsAutofillActivity
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.createDocument
import com.kunzisoft.keepass.utils.onCreateDocumentResult
import com.kunzisoft.keepass.view.EntryContentsView
import java.util.*
import kotlin.collections.HashMap
class EntryActivity : LockingHideActivity() {
class EntryActivity : LockingActivity() {
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var titleIconView: ImageView? = null
private var historyView: View? = null
private var entryContentsView: EntryContentsView? = null
private var entryProgress: ProgressBar? = null
private var toolbar: Toolbar? = null
private var mEntry: EntryVersioned? = null
private var mDatabase: Database? = null
private var mEntry: Entry? = null
private var mIsHistory: Boolean = false
private var mEntryLastVersion: Entry? = null
private var mEntryHistoryPosition: Int = -1
private var mShowPassword: Boolean = false
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAttachmentsToDownload: HashMap<Int, EntryAttachment> = HashMap()
private var clipboardHelper: ClipboardHelper? = null
private var firstLaunchOfActivity: Boolean = false
@@ -75,28 +100,11 @@ class EntryActivity : LockingHideActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
val currentDatabase = Database.getInstance()
mReadOnly = currentDatabase.isReadOnly || mReadOnly
mDatabase = Database.getInstance()
mReadOnly = mDatabase!!.isReadOnly || mReadOnly
mShowPassword = !PreferencesUtil.isPasswordMask(this)
// Get Entry from UUID
try {
val keyEntry: PwNodeId<*> = intent.getParcelableExtra(KEY_ENTRY)
mEntry = currentDatabase.getEntryById(keyEntry)
} catch (e: ClassCastException) {
Log.e(TAG, "Unable to retrieve the entry key")
}
if (mEntry == null) {
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
finish()
return
}
// Update last access time.
mEntry?.touch(modified = false, touchParents = false)
// Retrieve the textColor to tint the icon
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
iconColor = taIconColor.getColor(0, Color.BLACK)
@@ -108,17 +116,59 @@ class EntryActivity : LockingHideActivity() {
// Get views
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
titleIconView = findViewById(R.id.entry_icon)
historyView = findViewById(R.id.history_container)
entryContentsView = findViewById(R.id.entry_contents)
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
entryProgress = findViewById(R.id.entry_progress)
// Init the clipboard helper
clipboardHelper = ClipboardHelper(this)
firstLaunchOfActivity = true
// Init attachment service binder manager
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
mProgressDialogThread?.onActionFinish = { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
// Close the current activity after an history action
if (result.isSuccess)
finish()
}
}
// TODO Visual error for entry history
}
}
override fun onResume() {
super.onResume()
// Get Entry from UUID
try {
val keyEntry: NodeId<UUID> = intent.getParcelableExtra(KEY_ENTRY)
mEntry = mDatabase?.getEntryById(keyEntry)
mEntryLastVersion = mEntry
} catch (e: ClassCastException) {
Log.e(TAG, "Unable to retrieve the entry key")
}
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, mEntryHistoryPosition)
mEntryHistoryPosition = historyPosition
if (historyPosition >= 0) {
mIsHistory = true
mEntry = mEntry?.getHistory()?.get(historyPosition)
}
if (mEntry == null) {
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
finish()
return
}
// Update last access time.
mEntry?.touch(modified = false, touchParents = false)
mEntry?.let { entry ->
// Fill data in resume to update from EntryEditActivity
fillEntryDataInContentsView(entry)
@@ -138,10 +188,25 @@ class EntryActivity : LockingHideActivity() {
}
}
mAttachmentFileBinderManager?.apply {
registerProgressTask()
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
override fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment) {
entryContentsView?.updateAttachmentDownloadProgress(attachment)
}
}
}
firstLaunchOfActivity = false
}
private fun fillEntryDataInContentsView(entry: EntryVersioned) {
override fun onPause() {
mAttachmentFileBinderManager?.unregisterProgressTask()
super.onPause()
}
private fun fillEntryDataInContentsView(entry: Entry) {
val database = Database.getInstance()
database.startManageEntry(entry)
@@ -206,6 +271,17 @@ class EntryActivity : LockingHideActivity() {
}
}
//Assign OTP field
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
View.OnClickListener {
entry.getOtpElement()?.let { otpElement ->
clipboardHelper?.timeoutCopyToClipboard(
otpElement.token,
getString(R.string.copy_field, getString(R.string.entry_otp))
)
}
})
entryContentsView?.assignURL(entry.url)
entryContentsView?.assignComment(entry.notes)
@@ -237,23 +313,57 @@ class EntryActivity : LockingHideActivity() {
}
entryContentsView?.setHiddenPasswordStyle(!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
}
}
}
}
entryContentsView?.refreshAttachments()
// Assign dates
entry.creationTime.date?.let {
entryContentsView?.assignCreationDate(it)
}
entry.lastModificationTime.date?.let {
entryContentsView?.assignModificationDate(it)
}
entry.lastAccessTime.date?.let {
entryContentsView?.assignLastAccessDate(it)
}
val expires = entry.expiryTime.date
if (entry.isExpires && expires != null) {
entryContentsView?.assignExpiresDate(expires)
entryContentsView?.assignCreationDate(entry.creationTime)
entryContentsView?.assignModificationDate(entry.lastModificationTime)
entryContentsView?.assignLastAccessDate(entry.lastAccessTime)
entryContentsView?.setExpires(entry.isCurrentlyExpires)
if (entry.expires) {
entryContentsView?.assignExpiresDate(entry.expiryTime)
} else {
entryContentsView?.assignExpiresDate(getString(R.string.never))
}
// Manage history
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
if (mIsHistory) {
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
taColorAccent.recycle()
}
val entryHistory = entry.getHistory()
// TODO isMainEntry = not an history
val showHistoryView = entryHistory.isNotEmpty()
entryContentsView?.showHistory(showHistoryView)
if (showHistoryView) {
entryContentsView?.assignHistory(entryHistory)
entryContentsView?.onHistoryClick { historyItem, position ->
launch(this, historyItem, mReadOnly, position)
}
}
entryContentsView?.refreshHistory()
// Assign special data
entryContentsView?.assignUUID(entry.nodeId.id)
@@ -262,6 +372,7 @@ class EntryActivity : LockingHideActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE ->
// Not directly get the entry from intent data but from database
@@ -269,6 +380,15 @@ class EntryActivity : LockingHideActivity() {
fillEntryDataInContentsView(it)
}
}
onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
if (createdFileUri != null) {
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
mAttachmentFileBinderManager
?.startDownloadAttachment(createdFileUri, attachmentToDownload)
}
}
}
}
private fun changeShowPasswordIcon(togglePassword: MenuItem?) {
@@ -287,9 +407,12 @@ class EntryActivity : LockingHideActivity() {
val inflater = menuInflater
MenuUtil.contributionMenuInflater(inflater, menu)
inflater.inflate(R.menu.entry, menu)
inflater.inflate(R.menu.database_lock, menu)
if (mReadOnly) {
inflater.inflate(R.menu.database, menu)
if (mIsHistory && !mReadOnly) {
inflater.inflate(R.menu.entry_history, menu)
}
if (mIsHistory || mReadOnly) {
menu.findItem(R.id.menu_save_database)?.isVisible = false
menu.findItem(R.id.menu_edit)?.isVisible = false
}
@@ -358,21 +481,18 @@ class EntryActivity : LockingHideActivity() {
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)
}
return true
}
R.id.menu_goto_url -> {
var url: String = mEntry?.url ?: ""
@@ -382,18 +502,33 @@ class EntryActivity : LockingHideActivity() {
}
UriUtil.gotoUrl(this, url)
return true
}
R.id.menu_restore_entry_history -> {
mEntryLastVersion?.let { mainEntry ->
mProgressDialogThread?.startDatabaseRestoreEntryHistory(
mainEntry,
mEntryHistoryPosition,
!mReadOnly && mAutoSaveEnable)
}
}
R.id.menu_delete_entry_history -> {
mEntryLastVersion?.let { mainEntry ->
mProgressDialogThread?.startDatabaseDeleteEntryHistory(
mainEntry,
mEntryHistoryPosition,
!mReadOnly && mAutoSaveEnable)
}
}
R.id.menu_lock -> {
lockAndExit()
return true
}
R.id.menu_save_database -> {
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
}
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
}
return super.onOptionsItemSelected(item)
}
@@ -404,7 +539,7 @@ class EntryActivity : LockingHideActivity() {
TODO Slowdown when add entry as result
Intent intent = new Intent();
intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry);
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
onFinish(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
*/
super.finish()
}
@@ -412,13 +547,16 @@ class EntryActivity : LockingHideActivity() {
companion object {
private val TAG = EntryActivity::class.java.name
const val KEY_ENTRY = "entry"
const val KEY_ENTRY = "KEY_ENTRY"
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
fun launch(activity: Activity, pw: EntryVersioned, readOnly: Boolean) {
fun launch(activity: Activity, entry: Entry, readOnly: Boolean, historyPosition: Int? = null) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, pw.nodeId)
intent.putExtra(KEY_ENTRY, entry.nodeId)
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
if (historyPosition != null)
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
}
}

View File

@@ -22,47 +22,54 @@ import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import androidx.appcompat.widget.Toolbar
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.ScrollView
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
import com.kunzisoft.keepass.database.action.node.ActionNodeValues
import com.kunzisoft.keepass.database.action.node.AddEntryRunnable
import com.kunzisoft.keepass.database.action.node.AfterActionNodeFinishRunnable
import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
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.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.view.EntryEditContentsView
import com.kunzisoft.keepass.view.asError
import java.util.*
class EntryEditActivity : LockingHideActivity(),
class EntryEditActivity : LockingActivity(),
IconPickerDialogFragment.IconPickerListener,
GeneratePasswordDialogFragment.GeneratePasswordListener {
GeneratePasswordDialogFragment.GeneratePasswordListener,
SetOTPDialogFragment.CreateOtpListener {
private var mDatabase: Database? = null
// Refs of an entry and group in database, are not modifiable
private var mEntry: EntryVersioned? = null
private var mParent: GroupVersioned? = null
private var mEntry: Entry? = null
private var mParent: Group? = null
// New or copy of mEntry in the database to be modifiable
private var mNewEntry: EntryVersioned? = null
private var mNewEntry: Entry? = null
private var mIsNew: Boolean = false
// Views
private var coordinatorLayout: CoordinatorLayout? = null
private var scrollView: ScrollView? = null
private var entryEditContentsView: EntryEditContentsView? = null
private var saveView: View? = null
// Education
@@ -78,6 +85,8 @@ class EntryEditActivity : LockingHideActivity(),
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout)
scrollView = findViewById(R.id.entry_edit_scroll)
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
@@ -86,11 +95,14 @@ class EntryEditActivity : LockingHideActivity(),
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(entryEditContentsView)
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
// Likely the app has been killed exit the activity
mDatabase = Database.getInstance()
// Entry is retrieve, it's an entry to update
intent.getParcelableExtra<PwNodeId<*>>(KEY_ENTRY)?.let {
intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let {
mIsNew = false
// Create an Entry copy to modify from the database entry
mEntry = mDatabase?.getEntryById(it)
@@ -105,25 +117,27 @@ class EntryEditActivity : LockingHideActivity(),
}
}
// Retrieve the icon after an orientation change
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY) as EntryVersioned
} else {
// 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 = EntryVersioned(entry).also { newEntry ->
mNewEntry = Entry(entry).also { newEntry ->
// WARNING Remove the parent to keep memory with parcelable
newEntry.parent = null
newEntry.removeParent()
}
}
}
}
// Parent is retrieve, it's a new entry to create
intent.getParcelableExtra<PwNodeId<*>>(KEY_PARENT)?.let {
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
mDatabase?.drawFactory?.let { iconFactory ->
@@ -131,6 +145,12 @@ class EntryEditActivity : LockingHideActivity(),
}
}
// Retrieve the new entry after an orientation change
if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY)
}
// Close the activity if entry or parent can't be retrieve
if (mNewEntry == null || mParent == null) {
finish()
@@ -152,13 +172,35 @@ class EntryEditActivity : LockingHideActivity(),
saveView = findViewById(R.id.entry_edit_save)
saveView?.setOnClickListener { saveEntry() }
entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) { addNewCustomField() }
entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) {
addNewCustomField()
}
// Verify the education views
entryEditActivityEducation = EntryEditActivityEducation(this)
// Create progress dialog
mProgressDialogThread?.onActionFinish = { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_CREATE_ENTRY_TASK,
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
if (result.isSuccess)
finish()
}
}
private fun populateViewsWithEntry(newEntry: EntryVersioned) {
// Show error
if (!result.isSuccess) {
result.message?.let { resultMessage ->
Snackbar.make(coordinatorLayout!!,
resultMessage,
Snackbar.LENGTH_LONG).asError().show()
}
}
}
}
private fun populateViewsWithEntry(newEntry: Entry) {
// Don't start the field reference manager, we want to see the raw ref
mDatabase?.stopManageEntry(newEntry)
@@ -168,30 +210,33 @@ class EntryEditActivity : LockingHideActivity(),
// Set info in view
entryEditContentsView?.apply {
title = newEntry.title
username = newEntry.username
username = if (newEntry.username.isEmpty()) mDatabase?.defaultUsername ?:"" else newEntry.username
url = newEntry.url
password = newEntry.password
notes = newEntry.notes
for (entry in newEntry.customFields.entries) {
addNewCustomField(entry.key, entry.value)
post {
putCustomField(entry.key, entry.value)
}
}
}
}
private fun populateEntryWithViews(newEntry: EntryVersioned) {
private fun populateEntryWithViews(newEntry: Entry) {
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
notes = entryView.notes
entryView.customFields.forEach { customField ->
addExtraField(customField.name, customField.protectedValue)
putExtraField(customField.name, customField.protectedValue)
}
}
}
@@ -199,7 +244,7 @@ class EntryEditActivity : LockingHideActivity(),
mDatabase?.stopManageEntry(newEntry)
}
private fun temporarilySaveAndShowSelectedIcon(icon: PwIcon) {
private fun temporarilySaveAndShowSelectedIcon(icon: IconImage) {
mNewEntry?.icon = icon
mDatabase?.drawFactory?.let { iconDrawFactory ->
entryEditContentsView?.setIcon(iconDrawFactory, icon)
@@ -217,9 +262,7 @@ class EntryEditActivity : LockingHideActivity(),
* Add a new customized field view and scroll to bottom
*/
private fun addNewCustomField() {
entryEditContentsView?.addNewCustomField()
// Scroll bottom
scrollView?.post { scrollView?.fullScroll(ScrollView.FOCUS_DOWN) }
entryEditContentsView?.addEmptyCustomField()
}
/**
@@ -230,47 +273,32 @@ class EntryEditActivity : LockingHideActivity(),
// Launch a validation and show the error if present
if (entryEditContentsView?.isValid() == true) {
// Clone the entry
mDatabase?.let { database ->
mNewEntry?.let { newEntry ->
// WARNING Add the parent previously deleted
newEntry.parent = mEntry?.parent
// Build info
newEntry.lastAccessTime = PwDate()
newEntry.lastModificationTime = PwDate()
newEntry.lastAccessTime = DateInstant()
newEntry.lastModificationTime = DateInstant()
populateEntryWithViews(newEntry)
// Open a progress dialog and save entry
var actionRunnable: ActionRunnable? = null
val afterActionNodeFinishRunnable = object : AfterActionNodeFinishRunnable() {
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
if (actionNodeValues.result.isSuccess)
finish()
}
}
if (mIsNew) {
mParent?.let { parent ->
actionRunnable = AddEntryRunnable(this@EntryEditActivity,
database,
mProgressDialogThread?.startDatabaseCreateEntry(
newEntry,
parent,
afterActionNodeFinishRunnable,
!mReadOnly)
!mReadOnly && mAutoSaveEnable
)
}
} else {
mEntry?.let { oldEntry ->
actionRunnable = UpdateEntryRunnable(this@EntryEditActivity,
database,
mProgressDialogThread?.startDatabaseUpdateEntry(
oldEntry,
newEntry,
afterActionNodeFinishRunnable,
!mReadOnly)
}
}
actionRunnable?.let { runnable ->
ProgressDialogSaveDatabaseThread(this@EntryEditActivity) { runnable }.start()
!mReadOnly && mAutoSaveEnable
)
}
}
}
@@ -281,8 +309,12 @@ class EntryEditActivity : LockingHideActivity(),
super.onCreateOptionsMenu(menu)
val inflater = menuInflater
inflater.inflate(R.menu.database_lock, menu)
inflater.inflate(R.menu.database, menu)
// Save database not needed here
menu.findItem(R.id.menu_save_database)?.isVisible = false
MenuUtil.contributionMenuInflater(inflater, menu)
if (mDatabase?.allowOTP == true)
inflater.inflate(R.menu.entry_otp, menu)
entryEditActivityEducation?.let {
Handler().post { performedNextEducation(it) }
@@ -293,7 +325,7 @@ class EntryEditActivity : LockingHideActivity(),
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
val passwordView = entryEditContentsView?.generatePasswordView
val addNewFieldView = entryEditContentsView?.addNewFieldView
val addNewFieldView = entryEditContentsView?.addNewFieldButton
val generatePasswordEducationPerformed = passwordView != null
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
@@ -323,18 +355,34 @@ class EntryEditActivity : LockingHideActivity(),
lockAndExit()
return true
}
R.id.menu_save_database -> {
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
}
R.id.menu_contribute -> {
MenuUtil.onContributionItemSelected(this)
return true
}
R.id.menu_add_otp -> {
// Retrieve the current otpElement if exists
// and open the dialog to set up the OTP
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
.show(supportFragmentManager, "addOTPDialog")
return true
}
android.R.id.home -> finish()
}
return super.onOptionsItemSelected(item)
}
override fun onOtpCreated(otpElement: OtpElement) {
// 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)
}
override fun iconPicked(bundle: Bundle) {
IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon ->
temporarilySaveAndShowSelectedIcon(icon)
@@ -342,7 +390,10 @@ class EntryEditActivity : LockingHideActivity(),
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putParcelable(KEY_NEW_ENTRY, mNewEntry)
mNewEntry?.let {
populateEntryWithViews(it)
outState.putParcelable(KEY_NEW_ENTRY, it)
}
super.onSaveInstanceState(outState)
}
@@ -405,7 +456,7 @@ class EntryEditActivity : LockingHideActivity(),
* @param activity from activity
* @param pwEntry Entry to update
*/
fun launch(activity: Activity, pwEntry: EntryVersioned) {
fun launch(activity: Activity, pwEntry: Entry) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, pwEntry.nodeId)
@@ -419,7 +470,7 @@ class EntryEditActivity : LockingHideActivity(),
* @param activity from activity
* @param pwGroup Group who will contains new entry
*/
fun launch(activity: Activity, pwGroup: GroupVersioned) {
fun launch(activity: Activity, pwGroup: Group) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, pwGroup.nodeId)

View File

@@ -29,38 +29,32 @@ import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.preference.PreferenceManager
import androidx.annotation.RequiresApi
import com.google.android.material.snackbar.Snackbar
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.appcompat.widget.Toolbar
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.EditText
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
import com.kunzisoft.keepass.activities.stylish.StylishActivity
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.CreateDatabaseRunnable
import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.asError
import kotlinx.android.synthetic.main.activity_file_selection.*
import net.cachapa.expandablelayout.ExpandableLayout
import java.io.FileNotFoundException
class FileDatabaseSelectActivity : StylishActivity(),
@@ -69,11 +63,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
// Views
private var fileListContainer: View? = null
private var createButtonView: View? = null
private var browseButtonView: View? = null
private var openButtonView: View? = null
private var fileSelectExpandableButtonView: View? = null
private var fileSelectExpandableLayout: ExpandableLayout? = null
private var openFileNameView: EditText? = null
private var openDatabaseButtonView: View? = null
// Adapter to manage database history list
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
@@ -84,7 +74,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
private var mOpenFileHelper: OpenFileHelper? = null
private var mDefaultPath: String? = null
private var mProgressDialogThread: ProgressDialogThread? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -98,63 +88,22 @@ class FileDatabaseSelectActivity : StylishActivity(),
toolbar.title = ""
setSupportActionBar(toolbar)
openFileNameView = findViewById(R.id.file_filename)
// Set the initial value of the filename
mDefaultPath = (Environment.getExternalStorageDirectory().absolutePath
+ getString(R.string.database_file_path_default)
+ getString(R.string.database_file_name_default)
+ getString(R.string.database_file_extension_default))
openFileNameView?.setHint(R.string.open_link_database)
// Button to expand file selection
fileSelectExpandableButtonView = findViewById(R.id.file_select_expandable_button)
fileSelectExpandableLayout = findViewById(R.id.file_select_expandable)
fileSelectExpandableButtonView?.setOnClickListener { _ ->
if (fileSelectExpandableLayout?.isExpanded == true)
fileSelectExpandableLayout?.collapse()
else
fileSelectExpandableLayout?.expand()
}
// Open button
openButtonView = findViewById(R.id.open_database)
openButtonView?.setOnClickListener { _ ->
var fileName = openFileNameView?.text?.toString() ?: ""
mDefaultPath?.let {
if (fileName.isEmpty())
fileName = it
}
UriUtil.parse(fileName)?.let { fileNameUri ->
launchPasswordActivityWithPath(fileNameUri)
} ?: run {
Log.e(TAG, "Unable to open the database link")
Snackbar.make(activity_file_selection_coordinator_layout, getString(R.string.error_can_not_handle_uri), Snackbar.LENGTH_LONG).asError().show()
null
}
}
// Create button
createButtonView = findViewById(R.id.create_database)
if (Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/x-keepass"
}.resolveActivity(packageManager) == null) {
// No Activity found that can handle this intent.
createButtonView?.visibility = View.GONE
}
else{
createButtonView = findViewById(R.id.create_database_button)
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
// There is an activity which can handle this intent.
createButtonView?.visibility = View.VISIBLE
}
else{
// No Activity found that can handle this intent.
createButtonView?.visibility = View.GONE
}
createButtonView?.setOnClickListener { createNewFile() }
mOpenFileHelper = OpenFileHelper(this)
browseButtonView = findViewById(R.id.browse_button)
browseButtonView?.setOnClickListener(mOpenFileHelper!!.getOpenFileOnClickViewListener {
UriUtil.parse(openFileNameView?.text?.toString())
})
openDatabaseButtonView = findViewById(R.id.open_database_button)
openDatabaseButtonView?.setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
// History list
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
@@ -207,6 +156,20 @@ class FileDatabaseSelectActivity : StylishActivity(),
&& savedInstanceState.containsKey(EXTRA_DATABASE_URI)) {
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
}
// Attach the dialog thread to this activity
mProgressDialogThread = ProgressDialogThread(this).apply {
onActionFinish = { actionTask, _ ->
when (actionTask) {
ACTION_DATABASE_CREATE_TASK -> {
// TODO Check
// mAdapterDatabaseHistory?.notifyDataSetChanged()
// updateFileListVisibility()
GroupActivity.launch(this@FileDatabaseSelectActivity)
}
}
}
}
}
/**
@@ -214,18 +177,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
*/
@SuppressLint("InlinedApi")
private fun createNewFile() {
try {
startActivityForResult(Intent(
Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/x-keepass"
putExtra(Intent.EXTRA_TITLE, getString(R.string.database_file_name_default) +
getString(R.string.database_file_extension_default))
},
CREATE_FILE_REQUEST_CODE)
} catch (e: Exception) {
BrowserDialogFragment().show(supportFragmentManager, "browserDialog")
}
createDocument(this, getString(R.string.database_file_name_default) +
getString(R.string.database_file_extension_default), "application/x-keepass")
}
private fun fileNoFoundAction(e: FileNotFoundException) {
@@ -267,6 +220,23 @@ class FileDatabaseSelectActivity : StylishActivity(),
})
}
private fun launchGroupActivity(readOnly: Boolean) {
EntrySelectionHelper.doEntrySelectionAction(intent,
{
GroupActivity.launch(this@FileDatabaseSelectActivity, readOnly)
},
{
GroupActivity.launchForKeyboardSelection(this@FileDatabaseSelectActivity, readOnly)
// Do not keep history
finish()
},
{ assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity, assistStructure, readOnly)
}
})
}
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
launchPasswordActivity(databaseUri, null)
// Delete flickering for kitkat <=
@@ -294,6 +264,11 @@ class FileDatabaseSelectActivity : StylishActivity(),
}
override fun onResume() {
val database = Database.getInstance()
if (database.loaded) {
launchGroupActivity(database.isReadOnly)
}
super.onResume()
updateExternalStorageWarning()
@@ -306,6 +281,16 @@ class FileDatabaseSelectActivity : StylishActivity(),
mAdapterDatabaseHistory?.notifyDataSetChanged()
}
}
// Register progress task
mProgressDialogThread?.registerProgressTask()
}
override fun onPause() {
// Unregister progress task
mProgressDialogThread?.unregisterProgressTask()
super.onPause()
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -331,21 +316,13 @@ class FileDatabaseSelectActivity : StylishActivity(),
mDatabaseFileUri?.let { databaseUri ->
// Create the new database
ProgressDialogThread(this@FileDatabaseSelectActivity,
{
CreateDatabaseRunnable(this@FileDatabaseSelectActivity,
mProgressDialogThread?.startDatabaseCreate(
databaseUri,
Database.getInstance(),
masterPasswordChecked,
masterPassword,
keyFileChecked,
keyFile,
true, // TODO get readonly
LaunchGroupActivityFinish(databaseUri, keyFile)
keyFile
)
},
R.string.progress_create)
.start()
}
} catch (e: Exception) {
val error = getString(R.string.error_create_database_file)
@@ -354,28 +331,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
}
}
private inner class LaunchGroupActivityFinish(private val databaseFileUri: Uri,
private val keyFileUri: Uri?) : ActionRunnable() {
override fun run() {
finishRun(true, null)
}
override fun onFinishRun(result: Result) {
runOnUiThread {
if (result.isSuccess) {
// Add database to recent files
mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(databaseFileUri, keyFileUri)
mAdapterDatabaseHistory?.notifyDataSetChanged()
updateFileListVisibility()
GroupActivity.launch(this@FileDatabaseSelectActivity)
} else {
Log.e(TAG, "Unable to open the database")
}
}
}
}
override fun onAssignKeyDialogNegativeClick(
masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?) {
@@ -392,20 +347,16 @@ class FileDatabaseSelectActivity : StylishActivity(),
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
if (uri != null) {
if (PreferencesUtil.autoOpenSelectedFile(this@FileDatabaseSelectActivity)) {
launchPasswordActivityWithPath(uri)
} else {
fileSelectExpandableLayout?.expand(false)
openFileNameView?.setText(uri.toString())
}
}
}
// Retrieve the created URI from the file manager
if (requestCode == CREATE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
mDatabaseFileUri = data?.data
onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
mDatabaseFileUri = databaseFileCreatedUri
if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment().show(supportFragmentManager, "passwordDialog")
AssignMasterKeyDialogFragment.getInstance(true)
.show(supportFragmentManager, "passwordDialog")
}
// else {
// TODO Show error
@@ -438,20 +389,15 @@ class FileDatabaseSelectActivity : StylishActivity(),
})
if (!createDatabaseEducationPerformed) {
// selectDatabaseEducationPerformed
browseButtonView != null
openDatabaseButtonView != null
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
browseButtonView!!,
openDatabaseButtonView!!,
{tapTargetView ->
tapTargetView?.let {
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
}
},
{
fileSelectExpandableButtonView?.let {
fileDatabaseSelectActivityEducation
.checkAndPerformedOpenLinkDatabaseEducation(it)
}
}
{}
)
}
}
@@ -466,8 +412,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
private const val EXTRA_STAY = "EXTRA_STAY"
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
private const val CREATE_FILE_REQUEST_CODE = 3853
/*
* -------------------------
* No Standard Launch, pass by PasswordActivity

View File

@@ -18,7 +18,6 @@
*/
package com.kunzisoft.keepass.activities
import android.annotation.SuppressLint
import android.app.Activity
import android.app.SearchManager
import android.app.assist.AssistStructure
@@ -29,55 +28,71 @@ import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Handler
import androidx.annotation.RequiresApi
import androidx.fragment.app.FragmentManager
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.FragmentManager
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
import com.kunzisoft.keepass.activities.dialogs.ReadOnlyDialog
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.adapters.NodeAdapter
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.SortNodeEnum
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
import com.kunzisoft.keepass.database.action.node.*
import com.kunzisoft.keepass.database.action.node.ActionNodeDatabaseRunnable.Companion.NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
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 net.cachapa.expandablelayout.ExpandableLayout
import com.kunzisoft.keepass.view.ToolbarAction
import com.kunzisoft.keepass.view.asError
class GroupActivity : LockingActivity(),
GroupEditDialogFragment.EditGroupListener,
IconPickerDialogFragment.IconPickerListener,
NodeAdapter.NodeMenuListener,
ListNodesFragment.NodeClickListener,
ListNodesFragment.NodesActionMenuListener,
DeleteNodesDialogFragment.DeleteNodeListener,
ListNodesFragment.OnScrollListener,
NodeAdapter.NodeClickCallback,
SortDialogFragment.SortSelectionListener {
// Views
private var coordinatorLayout: CoordinatorLayout? = null
private var toolbar: Toolbar? = null
private var searchTitleView: View? = null
private var toolbarPasteExpandableLayout: ExpandableLayout? = null
private var toolbarPaste: Toolbar? = null
private var toolbarAction: ToolbarAction? = null
private var iconView: ImageView? = null
private var numberChildrenView: TextView? = null
private var modeTitleView: TextView? = null
private var addNodeButtonView: AddNodeButtonView? = null
private var groupNameView: TextView? = null
@@ -88,11 +103,9 @@ class GroupActivity : LockingActivity(),
private var mCurrentGroupIsASearch: Boolean = false
// Nodes
private var mRootGroup: GroupVersioned? = null
private var mCurrentGroup: GroupVersioned? = null
private var mOldGroupToUpdate: GroupVersioned? = null
private var mNodeToCopy: NodeVersioned? = null
private var mNodeToMove: NodeVersioned? = null
private var mRootGroup: Group? = null
private var mCurrentGroup: Group? = null
private var mOldGroupToUpdate: Group? = null
private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null
@@ -110,15 +123,19 @@ class GroupActivity : LockingActivity(),
setContentView(layoutInflater.inflate(R.layout.activity_group, null))
// Initialize views
iconView = findViewById(R.id.icon)
coordinatorLayout = findViewById(R.id.group_coordinator)
iconView = findViewById(R.id.group_icon)
numberChildrenView = findViewById(R.id.group_numbers)
addNodeButtonView = findViewById(R.id.add_node_button)
toolbar = findViewById(R.id.toolbar)
searchTitleView = findViewById(R.id.search_title)
groupNameView = findViewById(R.id.group_name)
toolbarPasteExpandableLayout = findViewById(R.id.expandable_toolbar_paste_layout)
toolbarPaste = findViewById(R.id.toolbar_paste)
toolbarAction = findViewById(R.id.toolbar_action)
modeTitleView = findViewById(R.id.mode_title_view)
toolbar?.title = ""
setSupportActionBar(toolbar)
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(addNodeButtonView)
@@ -126,13 +143,6 @@ class GroupActivity : LockingActivity(),
if (savedInstanceState != null) {
if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY))
mOldGroupToUpdate = savedInstanceState.getParcelable(OLD_GROUP_TO_UPDATE_KEY)
if (savedInstanceState.containsKey(NODE_TO_COPY_KEY)) {
mNodeToCopy = savedInstanceState.getParcelable(NODE_TO_COPY_KEY)
toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener())
} else if (savedInstanceState.containsKey(NODE_TO_MOVE_KEY)) {
mNodeToMove = savedInstanceState.getParcelable(NODE_TO_MOVE_KEY)
toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener())
}
}
try {
@@ -153,17 +163,6 @@ class GroupActivity : LockingActivity(),
// Update last access time.
mCurrentGroup?.touch(modified = false, touchParents = false)
toolbar?.title = ""
setSupportActionBar(toolbar)
toolbarPaste?.inflateMenu(R.menu.node_paste_menu)
toolbarPaste?.setNavigationIcon(R.drawable.ic_arrow_left_white_24dp)
toolbarPaste?.setNavigationOnClickListener {
toolbarPasteExpandableLayout?.collapse()
mNodeToCopy = null
mNodeToMove = null
}
// Retrieve the textColor to tint the icon
val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
mIconColor = taTextColor.getColor(0, Color.WHITE)
@@ -197,9 +196,75 @@ class GroupActivity : LockingActivity(),
}
})
// Search suggestion
mDatabase?.let { database ->
// Search suggestion
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, database)
// Init dialog thread
mProgressDialogThread?.onActionFinish = { actionTask, result ->
var oldNodes: List<Node> = ArrayList()
result.data?.getBundle(OLD_NODES_KEY)?.let { oldNodesBundle ->
oldNodes = getListNodesFromBundle(database, oldNodesBundle)
}
var newNodes: List<Node> = ArrayList()
result.data?.getBundle(NEW_NODES_KEY)?.let { newNodesBundle ->
newNodes = getListNodesFromBundle(database, newNodesBundle)
}
when (actionTask) {
ACTION_DATABASE_UPDATE_GROUP_TASK -> {
if (result.isSuccess) {
mListNodesFragment?.updateNodes(oldNodes, newNodes)
}
}
ACTION_DATABASE_CREATE_GROUP_TASK,
ACTION_DATABASE_COPY_NODES_TASK,
ACTION_DATABASE_MOVE_NODES_TASK -> {
if (result.isSuccess) {
mListNodesFragment?.addNodes(newNodes)
}
}
ACTION_DATABASE_DELETE_NODES_TASK -> {
if (result.isSuccess) {
// Rebuild all the list to avoid bug when delete node from sort
mListNodesFragment?.rebuildList()
// Add trash in views list if it doesn't exists
if (database.isRecycleBinEnabled) {
val recycleBin = database.recycleBin
val currentGroup = mCurrentGroup
if (currentGroup != null && recycleBin != null
&& currentGroup != recycleBin) {
// Recycle bin already here, simply update it
if (mListNodesFragment?.contains(recycleBin) == true) {
mListNodesFragment?.updateNode(recycleBin)
}
// Recycle bin not here, verify if parents are similar to add it
else if (currentGroup == recycleBin.parent) {
mListNodesFragment?.addNode(recycleBin)
}
}
}
}
}
}
if (!result.isSuccess) {
coordinatorLayout?.let { coordinatorLayout ->
result.exception?.errorId?.let { errorId ->
Snackbar.make(coordinatorLayout, errorId, Snackbar.LENGTH_LONG).asError().show()
} ?: result.message?.let { message ->
Snackbar.make(coordinatorLayout, message, Snackbar.LENGTH_LONG).asError().show()
}
}
}
finishNodeAction()
refreshNumberOfChildren()
}
}
Log.i(TAG, "Finished creating tree")
@@ -221,7 +286,7 @@ class GroupActivity : LockingActivity(),
}
}
private fun openSearchGroup(group: GroupVersioned?) {
private fun openSearchGroup(group: Group?) {
// Delete the previous search fragment
val searchFragment = supportFragmentManager.findFragmentByTag(SEARCH_FRAGMENT_TAG)
if (searchFragment != null) {
@@ -233,11 +298,11 @@ class GroupActivity : LockingActivity(),
openGroup(group, true)
}
private fun openChildGroup(group: GroupVersioned) {
private fun openChildGroup(group: Group) {
openGroup(group, false)
}
private fun openGroup(group: GroupVersioned?, isASearch: Boolean) {
private fun openGroup(group: Group?, isASearch: Boolean) {
// Check TimeoutHelper
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
// Open a group in a new fragment
@@ -274,16 +339,10 @@ class GroupActivity : LockingActivity(),
mOldGroupToUpdate?.let {
outState.putParcelable(OLD_GROUP_TO_UPDATE_KEY, it)
}
mNodeToCopy?.let {
outState.putParcelable(NODE_TO_COPY_KEY, it)
}
mNodeToMove?.let {
outState.putParcelable(NODE_TO_MOVE_KEY, it)
}
super.onSaveInstanceState(outState)
}
private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): GroupVersioned? {
private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): Group? {
// Force read only if the database is like that
mReadOnly = mDatabase?.isReadOnly == true || mReadOnly
@@ -294,7 +353,7 @@ class GroupActivity : LockingActivity(),
}
// else a real group
else {
var pwGroupId: PwNodeId<*>? = null
var pwGroupId: NodeId<*>? = null
if (savedInstanceState != null && savedInstanceState.containsKey(GROUP_ID_KEY)) {
pwGroupId = savedInstanceState.getParcelable(GROUP_ID_KEY)
} else {
@@ -303,7 +362,7 @@ class GroupActivity : LockingActivity(),
}
Log.w(TAG, "Creating tree view")
val currentGroup: GroupVersioned?
val currentGroup: Group?
currentGroup = if (pwGroupId == null) {
mRootGroup
} else {
@@ -359,6 +418,9 @@ class GroupActivity : LockingActivity(),
}
}
// Assign number of children
refreshNumberOfChildren()
// Show selection mode message if needed
if (mSelectionMode) {
modeTitleView?.visibility = View.VISIBLE
@@ -373,12 +435,8 @@ class GroupActivity : LockingActivity(),
val addGroupEnabled = !mReadOnly && !mCurrentGroupIsASearch
var addEntryEnabled = !mReadOnly && !mCurrentGroupIsASearch
mCurrentGroup?.let {
val isRoot = it == mRootGroup
if (!it.allowAddEntryIfIsRoot())
addEntryEnabled = !isRoot && addEntryEnabled
if (isRoot) {
showWarnings()
}
addEntryEnabled = it != mRootGroup && addEntryEnabled
}
enableAddGroup(addGroupEnabled)
enableAddEntry(addEntryEnabled)
@@ -388,20 +446,31 @@ class GroupActivity : LockingActivity(),
}
}
private fun refreshNumberOfChildren() {
numberChildrenView?.apply {
if (PreferencesUtil.showNumberEntries(context)) {
text = mCurrentGroup?.getChildEntries(*Group.ChildFilter.getDefaults(context))?.size?.toString() ?: ""
visibility = View.VISIBLE
} else {
visibility = View.GONE
}
}
}
override fun onScrolled(dy: Int) {
addNodeButtonView?.hideButtonOnScrollListener(dy)
}
override fun onNodeClick(node: NodeVersioned) {
override fun onNodeClick(node: Node) {
when (node.type) {
Type.GROUP -> try {
openChildGroup(node as GroupVersioned)
openChildGroup(node as Group)
} catch (e: ClassCastException) {
Log.e(TAG, "Node can't be cast in Group")
}
Type.ENTRY -> try {
val entryVersioned = node as EntryVersioned
val entryVersioned = node as Entry
EntrySelectionHelper.doEntrySelectionAction(intent,
{
EntryActivity.launch(this@GroupActivity, entryVersioned, mReadOnly)
@@ -419,8 +488,10 @@ class GroupActivity : LockingActivity(),
{
// Build response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
mDatabase?.let { database ->
AutofillHelper.buildResponseWhenEntrySelected(this@GroupActivity,
entryVersioned.getEntryInfo(mDatabase!!))
entryVersioned.getEntryInfo(database))
}
}
finish()
})
@@ -430,150 +501,134 @@ class GroupActivity : LockingActivity(),
}
}
override fun onOpenMenuClick(node: NodeVersioned): Boolean {
private var actionNodeMode: ActionMode? = null
private fun finishNodeAction() {
actionNodeMode?.finish()
actionNodeMode = null
}
override fun onNodeSelected(nodes: List<Node>): Boolean {
if (nodes.isNotEmpty()) {
if (actionNodeMode == null || toolbarAction?.getSupportActionModeCallback() == null) {
mListNodesFragment?.actionNodesCallback(nodes, this)?.let {
actionNodeMode = toolbarAction?.startSupportActionMode(it)
}
} else {
actionNodeMode?.invalidate()
}
} else {
finishNodeAction()
}
return true
}
override fun onOpenMenuClick(node: Node): Boolean {
finishNodeAction()
onNodeClick(node)
return true
}
override fun onEditMenuClick(node: NodeVersioned): Boolean {
override fun onEditMenuClick(node: Node): Boolean {
finishNodeAction()
when (node.type) {
Type.GROUP -> {
mOldGroupToUpdate = node as GroupVersioned
mOldGroupToUpdate = node as Group
GroupEditDialogFragment.build(mOldGroupToUpdate!!)
.show(supportFragmentManager,
GroupEditDialogFragment.TAG_CREATE_GROUP)
}
Type.ENTRY -> EntryEditActivity.launch(this@GroupActivity, node as EntryVersioned)
Type.ENTRY -> EntryEditActivity.launch(this@GroupActivity, node as Entry)
}
return true
}
override fun onCopyMenuClick(node: NodeVersioned): Boolean {
toolbarPasteExpandableLayout?.expand()
mNodeToCopy = node
toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener())
return false
}
override fun onCopyMenuClick(nodes: List<Node>): Boolean {
actionNodeMode?.invalidate()
private inner class OnCopyMenuItemClickListener : Toolbar.OnMenuItemClickListener {
override fun onMenuItemClick(item: MenuItem): Boolean {
toolbarPasteExpandableLayout?.collapse()
when (item.itemId) {
R.id.menu_paste -> {
when (mNodeToCopy?.type) {
Type.GROUP -> Log.e(TAG, "Copy not allowed for group")
Type.ENTRY -> {
mCurrentGroup?.let { currentGroup ->
copyEntry(mNodeToCopy as EntryVersioned, currentGroup)
}
}
}
mNodeToCopy = null
// Nothing here fragment calls onPasteMenuClick internally
return true
}
}
override fun onMoveMenuClick(nodes: List<Node>): Boolean {
actionNodeMode?.invalidate()
// Nothing here fragment calls onPasteMenuClick internally
return true
}
}
private fun copyEntry(entryToCopy: EntryVersioned, newParent: GroupVersioned) {
ProgressDialogSaveDatabaseThread(this) {
CopyEntryRunnable(this,
Database.getInstance(),
entryToCopy,
override fun onPasteMenuClick(pasteMode: ListNodesFragment.PasteMode?,
nodes: List<Node>): Boolean {
// Move or copy only if allowed (in root if allowed)
if (mCurrentGroup != mDatabase?.rootGroup
|| mDatabase?.rootCanContainsEntry() == true) {
when (pasteMode) {
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
// Copy
mCurrentGroup?.let { newParent ->
mProgressDialogThread?.startDatabaseCopyNodes(
nodes,
newParent,
AfterAddNodeRunnable(),
!mReadOnly)
}.start()
}
override fun onMoveMenuClick(node: NodeVersioned): Boolean {
toolbarPasteExpandableLayout?.expand()
mNodeToMove = node
toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener())
return false
}
private inner class OnMoveMenuItemClickListener : Toolbar.OnMenuItemClickListener {
override fun onMenuItemClick(item: MenuItem): Boolean {
toolbarPasteExpandableLayout?.collapse()
when (item.itemId) {
R.id.menu_paste -> {
when (mNodeToMove?.type) {
Type.GROUP -> {
mCurrentGroup?.let { currentGroup ->
moveGroup(mNodeToMove as GroupVersioned, currentGroup)
!mReadOnly && mAutoSaveEnable
)
}
}
Type.ENTRY -> {
mCurrentGroup?.let { currentGroup ->
moveEntry(mNodeToMove as EntryVersioned, currentGroup)
}
}
}
mNodeToMove = null
return true
}
}
return true
}
}
private fun moveGroup(groupToMove: GroupVersioned, newParent: GroupVersioned) {
ProgressDialogSaveDatabaseThread(this) {
MoveGroupRunnable(
this,
Database.getInstance(),
groupToMove,
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
// Move
mCurrentGroup?.let { newParent ->
mProgressDialogThread?.startDatabaseMoveNodes(
nodes,
newParent,
AfterAddNodeRunnable(),
!mReadOnly)
}.start()
!mReadOnly && mAutoSaveEnable
)
}
private fun moveEntry(entryToMove: EntryVersioned, newParent: GroupVersioned) {
ProgressDialogSaveDatabaseThread(this) {
MoveEntryRunnable(
this,
Database.getInstance(),
entryToMove,
newParent,
AfterAddNodeRunnable(),
!mReadOnly)
}.start()
}
override fun onDeleteMenuClick(node: NodeVersioned): Boolean {
when (node.type) {
Type.GROUP -> deleteGroup(node as GroupVersioned)
Type.ENTRY -> deleteEntry(node as EntryVersioned)
else -> {
}
}
} else {
coordinatorLayout?.let { coordinatorLayout ->
Snackbar.make(coordinatorLayout,
R.string.error_copy_entry_here,
Snackbar.LENGTH_LONG).asError().show()
}
}
finishNodeAction()
return true
}
private fun deleteGroup(group: GroupVersioned) {
//TODO Verify trash recycle bin
ProgressDialogSaveDatabaseThread(this) {
DeleteGroupRunnable(
this,
Database.getInstance(),
group,
AfterDeleteNodeRunnable(),
!mReadOnly)
}.start()
override fun onDeleteMenuClick(nodes: List<Node>): Boolean {
val database = mDatabase
// If recycle bin enabled, ensure it exists
if (database != null && database.isRecycleBinEnabled) {
database.ensureRecycleBinExists(resources)
}
private fun deleteEntry(entry: EntryVersioned) {
ProgressDialogSaveDatabaseThread(this) {
DeleteEntryRunnable(
this,
Database.getInstance(),
entry,
AfterDeleteNodeRunnable(),
!mReadOnly)
}.start()
// If recycle bin enabled and not in recycle bin, move in recycle bin
if (database != null
&& database.isRecycleBinEnabled
&& database.recycleBin != mCurrentGroup) {
mProgressDialogThread?.startDatabaseDeleteNodes(
nodes,
!mReadOnly && mAutoSaveEnable
)
}
// else open the dialog to confirm deletion
else {
DeleteNodesDialogFragment.getInstance(nodes)
.show(supportFragmentManager, "deleteNodesDialogFragment")
}
finishNodeAction()
return true
}
override fun permanentlyDeleteNodes(nodes: List<Node>) {
mProgressDialogThread?.startDatabaseDeleteNodes(
nodes,
!mReadOnly && mAutoSaveEnable
)
}
override fun onResume() {
@@ -584,16 +639,32 @@ class GroupActivity : LockingActivity(),
mSearchSuggestionAdapter?.reInit(this)
}
override fun onPause() {
super.onPause()
finishNodeAction()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.search, menu)
inflater.inflate(R.menu.database_lock, menu)
inflater.inflate(R.menu.database, menu)
if (mReadOnly) {
menu.findItem(R.id.menu_save_database)?.isVisible = false
}
if (!mSelectionMode) {
inflater.inflate(R.menu.default_menu, menu)
MenuUtil.contributionMenuInflater(inflater, menu)
}
// Menu for recycle bin
if (!mReadOnly
&& mDatabase?.isRecycleBinEnabled == true
&& mDatabase?.recycleBin == mCurrentGroup) {
inflater.inflate(R.menu.recycle_bin, menu)
}
// Get the SearchView and set the searchable configuration
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
@@ -702,6 +773,17 @@ class GroupActivity : LockingActivity(),
lockAndExit()
return true
}
R.id.menu_save_database -> {
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
return true
}
R.id.menu_empty_recycle_bin -> {
mCurrentGroup?.getChildren()?.let { listChildren ->
// Automatically delete all elements
onDeleteMenuClick(listChildren)
}
return true
}
else -> {
// Check the time lock before launching settings
MenuUtil.onDefaultMenuOptionsItemSelected(this, item, mReadOnly, true)
@@ -712,8 +794,7 @@ class GroupActivity : LockingActivity(),
override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
name: String?,
icon: PwIcon?) {
val database = Database.getInstance()
icon: IconImage?) {
if (name != null && name.isNotEmpty() && icon != null) {
when (action) {
@@ -721,111 +802,49 @@ class GroupActivity : LockingActivity(),
// If group creation
mCurrentGroup?.let { currentGroup ->
// Build the group
database.createGroup()?.let { newGroup ->
mDatabase?.createGroup()?.let { newGroup ->
newGroup.title = name
newGroup.icon = icon
// Not really needed here because added in runnable but safe
newGroup.parent = currentGroup
// If group created save it in the database
ProgressDialogSaveDatabaseThread(this) {
AddGroupRunnable(this,
Database.getInstance(),
mProgressDialogThread?.startDatabaseCreateGroup(
newGroup,
currentGroup,
AfterAddNodeRunnable(),
!mReadOnly)
}.start()
!mReadOnly && mAutoSaveEnable
)
}
}
}
GroupEditDialogFragment.EditGroupDialogAction.UPDATE ->
GroupEditDialogFragment.EditGroupDialogAction.UPDATE -> {
// If update add new elements
mOldGroupToUpdate?.let { oldGroupToUpdate ->
GroupVersioned(oldGroupToUpdate).let { updateGroup ->
updateGroup.title = name
// TODO custom icon
updateGroup.icon = icon
mListNodesFragment?.removeNode(oldGroupToUpdate)
val updateGroup = Group(oldGroupToUpdate).let { updateGroup ->
updateGroup.apply {
// WARNING remove parent and children to keep memory
removeParent()
removeChildren()
title = name
this.icon = icon // TODO custom icon
}
}
// If group updated save it in the database
ProgressDialogSaveDatabaseThread(this) {
UpdateGroupRunnable(this,
Database.getInstance(),
mProgressDialogThread?.startDatabaseUpdateGroup(
oldGroupToUpdate,
updateGroup,
AfterUpdateNodeRunnable(),
!mReadOnly)
}.start()
}
}
else -> {
}
}
}
}
internal inner class AfterAddNodeRunnable : AfterActionNodeFinishRunnable() {
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
runOnUiThread {
if (actionNodeValues.result.isSuccess) {
if (actionNodeValues.newNode != null)
mListNodesFragment?.addNode(actionNodeValues.newNode)
}
}
}
}
internal inner class AfterUpdateNodeRunnable : AfterActionNodeFinishRunnable() {
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
runOnUiThread {
if (actionNodeValues.result.isSuccess) {
if (actionNodeValues.oldNode!= null && actionNodeValues.newNode != null)
mListNodesFragment?.updateNode(actionNodeValues.oldNode, actionNodeValues.newNode)
}
}
}
}
internal inner class AfterDeleteNodeRunnable : AfterActionNodeFinishRunnable() {
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
runOnUiThread {
if (actionNodeValues.result.isSuccess) {
// If the action register the position, use it to remove the entry view
val positionNode = actionNodeValues.result.data?.getInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY)
if (PreferencesUtil.getListSort(this@GroupActivity) == SortNodeEnum.DB
&& positionNode != null) {
mListNodesFragment?.removeNodeAt(positionNode)
} else {
// else use the old Node that was the entry unchanged with the old parent
actionNodeValues.oldNode?.let { oldNode ->
mListNodesFragment?.removeNode(oldNode)
}
}
// Add trash in views list if it doesn't exists
val database = Database.getInstance()
if (database.isRecycleBinEnabled) {
val recycleBin = database.recycleBin
if (mCurrentGroup != null && recycleBin != null
&& mCurrentGroup!!.parent == null
&& mCurrentGroup != recycleBin) {
if (mListNodesFragment?.contains(recycleBin) == true)
mListNodesFragment?.updateNode(recycleBin)
else
mListNodesFragment?.addNode(recycleBin)
}
!mReadOnly && mAutoSaveEnable
)
}
}
else -> {}
}
}
}
override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
name: String?,
icon: PwIcon?) {
icon: IconImage?) {
// Do nothing here
}
@@ -836,14 +855,6 @@ class GroupActivity : LockingActivity(),
.iconPicked(bundle)
}
private fun showWarnings() {
if (Database.getInstance().isReadOnly) {
if (PreferencesUtil.showReadOnlyWarning(this)) {
ReadOnlyDialog().show(supportFragmentManager, "readOnlyDialog")
}
}
}
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
mListNodesFragment?.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom)
}
@@ -902,6 +913,9 @@ class GroupActivity : LockingActivity(),
}
override fun onBackPressed() {
if (mListNodesFragment?.nodeActionSelectionMode == true) {
finishNodeAction()
} else {
// Normal way when we are not in root
if (mRootGroup != null && mRootGroup != mCurrentGroup)
super.onBackPressed()
@@ -922,6 +936,7 @@ class GroupActivity : LockingActivity(),
removeSearchInIntent(intent)
assignGroupViewElements()
}
}
companion object {
@@ -931,13 +946,15 @@ class GroupActivity : LockingActivity(),
private const val LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG"
private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"
private const val NODE_TO_COPY_KEY = "NODE_TO_COPY_KEY"
private const val NODE_TO_MOVE_KEY = "NODE_TO_MOVE_KEY"
private fun buildAndLaunchIntent(activity: Activity, group: GroupVersioned?, readOnly: Boolean,
private fun buildAndLaunchIntent(context: Context, group: Group?, readOnly: Boolean,
intentBuildLauncher: (Intent) -> Unit) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, GroupActivity::class.java)
val checkTime = if (context is Activity)
TimeoutHelper.checkTimeAndLockIfTimeout(context)
else
TimeoutHelper.checkTime(context)
if (checkTime) {
val intent = Intent(context, GroupActivity::class.java)
if (group != null) {
intent.putExtra(GROUP_ID_KEY, group.nodeId)
}
@@ -953,10 +970,10 @@ class GroupActivity : LockingActivity(),
*/
@JvmOverloads
fun launch(activity: Activity, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) {
TimeoutHelper.recordTime(activity)
buildAndLaunchIntent(activity, null, readOnly) { intent ->
activity.startActivity(intent)
fun launch(context: Context, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
TimeoutHelper.recordTime(context)
buildAndLaunchIntent(context, null, readOnly) { intent ->
context.startActivity(intent)
}
}
@@ -967,10 +984,10 @@ class GroupActivity : LockingActivity(),
*/
// TODO implement pre search to directly open the direct group
fun launchForKeyboarSelection(activity: Activity, readOnly: Boolean) {
TimeoutHelper.recordTime(activity)
buildAndLaunchIntent(activity, null, readOnly) { intent ->
EntrySelectionHelper.startActivityForEntrySelection(activity, intent)
fun launchForKeyboardSelection(context: Context, readOnly: Boolean) {
TimeoutHelper.recordTime(context)
buildAndLaunchIntent(context, null, readOnly) { intent ->
EntrySelectionHelper.startActivityForEntrySelection(context, intent)
}
}

View File

@@ -1,3 +1,22 @@
/*
* 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
@@ -14,30 +33,39 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ActionMode
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.NodeAdapter
import com.kunzisoft.keepass.database.SortNodeEnum
import com.kunzisoft.keepass.database.element.GroupVersioned
import com.kunzisoft.keepass.database.element.NodeVersioned
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.node.Type
import java.util.*
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
private var nodeClickCallback: NodeAdapter.NodeClickCallback? = null
private var nodeMenuListener: NodeAdapter.NodeMenuListener? = null
private var nodeClickListener: NodeClickListener? = null
private var onScrollListener: OnScrollListener? = null
private var listView: RecyclerView? = null
var mainGroup: GroupVersioned? = null
var mainGroup: Group? = null
private set
private var mAdapter: NodeAdapter? = null
var nodeActionSelectionMode = false
private set
var nodeActionPasteMode: PasteMode = PasteMode.UNDEFINED
private set
private val listActionNodes = LinkedList<Node>()
private val listPasteNodes = LinkedList<Node>()
private var notFoundView: View? = null
private var isASearchResult: Boolean = false
@@ -56,22 +84,13 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
override fun onAttach(context: Context) {
super.onAttach(context)
try {
nodeClickCallback = context as NodeAdapter.NodeClickCallback
nodeClickListener = context as NodeClickListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context.toString()
+ " must implement " + NodeAdapter.NodeClickCallback::class.java.name)
}
try {
nodeMenuListener = context as NodeAdapter.NodeMenuListener
} catch (e: ClassCastException) {
nodeMenuListener = null
// Context menu can be omit
Log.w(TAG, context.toString()
+ " must implement " + NodeAdapter.NodeMenuListener::class.java.name)
}
try {
onScrollListener = context as OnScrollListener
} catch (e: ClassCastException) {
@@ -85,7 +104,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.let { currentActivity ->
setHasOptionsMenu(true)
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments)
@@ -101,18 +119,44 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}
contextThemed?.let { context ->
mAdapter = NodeAdapter(context, currentActivity.menuInflater)
mAdapter = NodeAdapter(context)
mAdapter?.apply {
setReadOnly(readOnly)
setIsASearchResult(isASearchResult)
setOnNodeClickListener(nodeClickCallback)
setActivateContextMenu(true)
setNodeMenuListener(nodeMenuListener)
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
override fun onNodeClick(node: Node) {
if (nodeActionSelectionMode) {
if (listActionNodes.contains(node)) {
// Remove selected item if already selected
listActionNodes.remove(node)
} else {
// Add selected item if not already selected
listActionNodes.add(node)
}
nodeClickListener?.onNodeSelected(listActionNodes)
setActionNodes(listActionNodes)
notifyNodeChanged(node)
} else {
nodeClickListener?.onNodeClick(node)
}
}
override fun onNodeLongClick(node: Node): Boolean {
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
// Select the first item after a long click
if (!listActionNodes.contains(node))
listActionNodes.add(node)
nodeClickListener?.onNodeSelected(listActionNodes)
setActionNodes(listActionNodes)
notifyNodeChanged(node)
}
return true
}
})
}
}
prefs = PreferenceManager.getDefaultSharedPreferences(context)
}
}
override fun onSaveInstanceState(outState: Bundle) {
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
@@ -148,10 +192,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
activity?.intent?.let {
selectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(it)
}
// Force read only mode if selection mode
mAdapter?.apply {
setReadOnly(readOnly)
}
// Refresh data
mAdapter?.notifyDataSetChanged()
@@ -207,8 +247,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
R.id.menu_sort -> {
context?.let { context ->
val sortDialogFragment: SortDialogFragment =
if (Database.getInstance().isRecycleBinAvailable
&& Database.getInstance().isRecycleBinEnabled) {
if (Database.getInstance().isRecycleBinEnabled) {
SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
@@ -230,6 +269,103 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}
}
fun actionNodesCallback(nodes: List<Node>,
menuListener: NodesActionMenuListener?) : ActionMode.Callback {
return object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
nodeActionSelectionMode = false
nodeActionPasteMode = PasteMode.UNDEFINED
return true
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
menu?.clear()
if (nodeActionPasteMode != PasteMode.UNDEFINED) {
mode?.menuInflater?.inflate(R.menu.node_paste_menu, menu)
} else {
nodeActionSelectionMode = true
mode?.menuInflater?.inflate(R.menu.node_menu, menu)
val database = Database.getInstance()
// Open and Edit for a single item
if (nodes.size == 1) {
// Edition
if (readOnly
|| (database.isRecycleBinEnabled && nodes[0] == database.recycleBin)) {
menu?.removeItem(R.id.menu_edit)
}
} else {
menu?.removeItem(R.id.menu_open)
menu?.removeItem(R.id.menu_edit)
}
// Copy and Move (not for groups)
if (readOnly
|| isASearchResult
|| nodes.any { it.type == Type.GROUP }) {
// TODO COPY For Group
menu?.removeItem(R.id.menu_copy)
menu?.removeItem(R.id.menu_move)
}
// Deletion
if (readOnly
|| (database.isRecycleBinEnabled && nodes.any { it == database.recycleBin })) {
menu?.removeItem(R.id.menu_delete)
}
}
// Add the number of items selected in title
mode?.title = nodes.size.toString()
return true
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
if (menuListener == null)
return false
return when (item?.itemId) {
R.id.menu_open -> menuListener.onOpenMenuClick(nodes[0])
R.id.menu_edit -> menuListener.onEditMenuClick(nodes[0])
R.id.menu_copy -> {
nodeActionPasteMode = PasteMode.PASTE_FROM_COPY
mAdapter?.unselectActionNodes()
val returnValue = menuListener.onCopyMenuClick(nodes)
nodeActionSelectionMode = false
returnValue
}
R.id.menu_move -> {
nodeActionPasteMode = PasteMode.PASTE_FROM_MOVE
mAdapter?.unselectActionNodes()
val returnValue = menuListener.onMoveMenuClick(nodes)
nodeActionSelectionMode = false
returnValue
}
R.id.menu_delete -> menuListener.onDeleteMenuClick(nodes)
R.id.menu_paste -> {
val returnValue = menuListener.onPasteMenuClick(nodeActionPasteMode, nodes)
nodeActionPasteMode = PasteMode.UNDEFINED
nodeActionSelectionMode = false
returnValue
}
else -> false
}
}
override fun onDestroyActionMode(mode: ActionMode?) {
listActionNodes.clear()
listPasteNodes.clear()
mAdapter?.unselectActionNodes()
nodeActionPasteMode = PasteMode.UNDEFINED
nodeActionSelectionMode = false
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
@@ -237,7 +373,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
data?.getParcelableExtra<NodeVersioned>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { newNode ->
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { newNode ->
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
mAdapter?.addNode(newNode)
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
@@ -252,26 +388,66 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}
}
fun contains(node: NodeVersioned): Boolean {
fun contains(node: Node): Boolean {
return mAdapter?.contains(node) ?: false
}
fun addNode(newNode: NodeVersioned) {
fun addNode(newNode: Node) {
mAdapter?.addNode(newNode)
}
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned? = null) {
fun addNodes(newNodes: List<Node>) {
mAdapter?.addNodes(newNodes)
}
fun updateNode(oldNode: Node, newNode: Node? = null) {
mAdapter?.updateNode(oldNode, newNode ?: oldNode)
}
fun removeNode(pwNode: NodeVersioned) {
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
mAdapter?.updateNodes(oldNodes, newNodes)
}
fun removeNode(pwNode: Node) {
mAdapter?.removeNode(pwNode)
}
fun removeNodes(nodes: List<Node>) {
mAdapter?.removeNodes(nodes)
}
fun removeNodeAt(position: Int) {
mAdapter?.removeNodeAt(position)
}
fun removeNodesAt(positions: IntArray) {
mAdapter?.removeNodesAt(positions)
}
/**
* Callback listener to redefine to do an action when a node is click
*/
interface NodeClickListener {
fun onNodeClick(node: Node)
fun onNodeSelected(nodes: List<Node>): Boolean
}
/**
* Menu listener to redefine to do an action in menu
*/
interface NodesActionMenuListener {
fun onOpenMenuClick(node: Node): Boolean
fun onEditMenuClick(node: Node): Boolean
fun onCopyMenuClick(nodes: List<Node>): Boolean
fun onMoveMenuClick(nodes: List<Node>): Boolean
fun onDeleteMenuClick(nodes: List<Node>): Boolean
fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List<Node>): Boolean
}
enum class PasteMode {
UNDEFINED, PASTE_FROM_COPY, PASTE_FROM_MOVE
}
interface OnScrollListener {
/**
@@ -290,7 +466,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
private const val GROUP_KEY = "GROUP_KEY"
private const val IS_SEARCH = "IS_SEARCH"
fun newInstance(group: GroupVersioned?, readOnly: Boolean, isASearch: Boolean): ListNodesFragment {
fun newInstance(group: Group?, readOnly: Boolean, isASearch: Boolean): ListNodesFragment {
val bundle = Bundle()
if (group != null) {
bundle.putParcelable(GROUP_KEY, group)

View File

@@ -22,17 +22,14 @@ package com.kunzisoft.keepass.activities
import android.app.Activity
import android.app.assist.AssistStructure
import android.app.backup.BackupManager
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.preference.PreferenceManager
import androidx.annotation.RequiresApi
import com.google.android.material.snackbar.Snackbar
import androidx.appcompat.widget.Toolbar
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
@@ -42,40 +39,47 @@ import android.view.MenuItem
import android.view.View
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.widget.*
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar
import androidx.biometric.BiometricManager
import androidx.core.app.ActivityCompat
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.FingerPrintExplanationDialog
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.utils.FileDatabaseInfo
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
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.MASTER_PASSWORD_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.FileDatabaseInfo
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.view.asError
import kotlinx.android.synthetic.main.activity_password.*
import java.io.FileNotFoundException
import java.lang.ref.WeakReference
class PasswordActivity : StylishActivity() {
// Views
private var toolbar: Toolbar? = null
private var containerView: View? = null
private var filenameView: TextView? = null
private var passwordView: EditText? = null
private var keyFileView: EditText? = null
@@ -87,13 +91,18 @@ class PasswordActivity : StylishActivity() {
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
private var mDatabaseFileUri: Uri? = null
private var mDatabaseKeyFileUri: Uri? = null
private var prefs: SharedPreferences? = null
private var mRememberKeyFile: Boolean = false
private var mOpenFileHelper: OpenFileHelper? = null
private var mPermissionAsked = false
private var readOnly: Boolean = false
private var mProgressDialogThread: ProgressDialogThread? = null
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
override fun onCreate(savedInstanceState: Bundle?) {
@@ -101,8 +110,7 @@ class PasswordActivity : StylishActivity() {
prefs = PreferenceManager.getDefaultSharedPreferences(this)
mRememberKeyFile = prefs!!.getBoolean(getString(R.string.keyfile_key),
resources.getBoolean(R.bool.keyfile_default))
mRememberKeyFile = PreferencesUtil.rememberKeyFiles(this)
setContentView(R.layout.activity_password)
@@ -112,6 +120,7 @@ class PasswordActivity : StylishActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
containerView = findViewById(R.id.container)
confirmButtonView = findViewById(R.id.pass_ok)
filenameView = findViewById(R.id.filename)
passwordView = findViewById(R.id.password)
@@ -119,11 +128,12 @@ class PasswordActivity : StylishActivity() {
checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
advancedUnlockInfoView = findViewById(R.id.fingerprint_info)
advancedUnlockInfoView = findViewById(R.id.biometric_info)
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
val browseView = findViewById<View>(R.id.browse_button)
val browseView = findViewById<View>(R.id.open_database_button)
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
browseView.setOnClickListener(mOpenFileHelper!!.openFileOnClickViewListener)
@@ -153,6 +163,93 @@ class PasswordActivity : StylishActivity() {
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
enableOrNotTheConfirmationButton()
}
mProgressDialogThread = ProgressDialogThread(this).apply {
onActionFinish = { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> {
// Recheck biometric if error
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) {
// Stay with the same mode and init it
advancedUnlockedManager?.initBiometricMode()
}
}
// Remove the password in view in all cases
removePassword()
if (result.isSuccess) {
launchGroupActivity()
} else {
var resultError = ""
val resultException = result.exception
val resultMessage = result.message
if (resultException != null) {
resultError = resultException.getLocalizedMessage(resources)
// Relaunch loading if we need to fix UUID
if (resultException is DuplicateUuidDatabaseException) {
showLoadDatabaseDuplicateUuidMessage {
var databaseUri: Uri? = null
var masterPassword: String? = null
var keyFileUri: Uri? = null
var readOnly = true
var cipherEntity: CipherDatabaseEntity? = null
result.data?.let { resultData ->
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
keyFileUri = resultData.getParcelable(KEY_FILE_KEY)
readOnly = resultData.getBoolean(READ_ONLY_KEY)
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
}
databaseUri?.let { databaseFileUri ->
showProgressDialogAndLoadDatabase(
databaseFileUri,
masterPassword,
keyFileUri,
readOnly,
cipherEntity,
true)
}
}
}
}
// Show error message
if (resultMessage != null && resultMessage.isNotEmpty()) {
resultError = "$resultError $resultMessage"
}
Log.e(TAG, resultError, resultException)
Snackbar.make(activity_password_coordinator_layout,
resultError,
Snackbar.LENGTH_LONG).asError().show()
}
}
}
}
}
}
private fun launchGroupActivity() {
EntrySelectionHelper.doEntrySelectionAction(intent,
{
GroupActivity.launch(this@PasswordActivity, readOnly)
},
{
GroupActivity.launchForKeyboardSelection(this@PasswordActivity, readOnly)
// Do not keep history
finish()
},
{ assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
GroupActivity.launchForAutofillResult(this@PasswordActivity, assistStructure, readOnly)
}
})
}
private val onEditorActionListener = object : TextView.OnEditorActionListener {
@@ -166,6 +263,9 @@ class PasswordActivity : StylishActivity() {
}
override fun onResume() {
if (Database.getInstance().loaded)
launchGroupActivity()
// If the database isn't accessible make sure to clear the password field, if it
// was saved in the instance state
if (Database.getInstance().loaded) {
@@ -175,10 +275,13 @@ class PasswordActivity : StylishActivity() {
// For check shutdown
super.onResume()
mProgressDialogThread?.registerProgressTask()
initUriFromIntent()
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
super.onSaveInstanceState(outState)
}
@@ -190,17 +293,10 @@ class PasswordActivity : StylishActivity() {
// If is a view intent
val action = intent.action
if (action != null && action == VIEW_INTENT) {
val databaseUriRetrieve = intent.data
// Stop activity here if we can't verify database URI
if (!UriUtil.verifyFileUri(databaseUriRetrieve)) {
Log.e(TAG, "File URI not validate")
finish()
}
databaseUri = databaseUriRetrieve
if (action != null
&& action == VIEW_INTENT) {
databaseUri = intent.data
keyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
} else {
databaseUri = intent.getParcelableExtra(KEY_FILENAME)
keyFileUri = intent.getParcelableExtra(KEY_KEYFILE)
@@ -222,6 +318,7 @@ class PasswordActivity : StylishActivity() {
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
mDatabaseFileUri = databaseFileUri
mDatabaseKeyFileUri = keyFileUri
// Define title
databaseFileUri?.let {
@@ -243,11 +340,13 @@ class PasswordActivity : StylishActivity() {
newDefaultFileName = databaseFileUri ?: newDefaultFileName
}
newDefaultFileName?.let {
prefs?.edit()?.apply {
newDefaultFileName?.let {
putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString())
apply()
} ?: kotlin.run {
remove(KEY_DEFAULT_DATABASE_PATH)
}
apply()
}
val backupManager = BackupManager(this@PasswordActivity)
@@ -273,15 +372,11 @@ class PasswordActivity : StylishActivity() {
if (launchImmediately) {
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
} else {
// Init FingerPrint elements
var fingerPrintInit = false
// Init Biometric elements
var biometricInitialize = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
advancedUnlockInfoView?.setOnClickListener {
FingerPrintExplanationDialog().show(supportFragmentManager, "fingerPrintExplanationDialog")
}
if (advancedUnlockedManager == null && databaseFileUri != null) {
advancedUnlockedManager = AdvancedUnlockedManager(this,
databaseFileUri,
@@ -303,18 +398,18 @@ class PasswordActivity : StylishActivity() {
{ passwordDecrypted ->
// Load the database if password is retrieve from biometric
passwordDecrypted?.let {
// Retrieve from fingerprint
// Retrieve from biometric
verifyKeyFileCheckboxAndLoadDatabase(it)
}
})
}
advancedUnlockedManager?.initBiometric()
fingerPrintInit = true
advancedUnlockedManager?.checkBiometricAvailability()
biometricInitialize = true
} else {
advancedUnlockedManager?.destroy()
}
}
if (!fingerPrintInit) {
if (!biometricInitialize) {
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
}
checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
@@ -368,9 +463,8 @@ class PasswordActivity : StylishActivity() {
}
override fun onPause() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
advancedUnlockedManager?.pause()
}
mProgressDialogThread?.unregisterProgressTask()
super.onPause()
}
@@ -391,14 +485,18 @@ class PasswordActivity : StylishActivity() {
keyFile: Uri?,
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
loadDatabase(keyPassword, keyFileUri, cipherDatabaseEntity)
verifyKeyFileCheckbox(keyFile)
loadDatabase(mDatabaseFileUri, keyPassword, mDatabaseKeyFileUri, cipherDatabaseEntity)
}
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
loadDatabase(password, keyFileUri)
verifyKeyFileCheckbox(keyFile)
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
}
private fun verifyKeyFileCheckbox(keyFile: Uri?) {
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
}
private fun removePassword() {
@@ -406,103 +504,47 @@ class PasswordActivity : StylishActivity() {
checkboxPasswordView?.isChecked = false
}
private fun loadDatabase(password: String?, keyFile: Uri?, cipherDatabaseEntity: CipherDatabaseEntity? = null) {
private fun loadDatabase(databaseFileUri: Uri?,
password: String?,
keyFileUri: Uri?,
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
runOnUiThread {
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
removePassword()
}
databaseFileUri?.let { databaseUri ->
// Show the progress dialog and load the database
showProgressDialogAndLoadDatabase(
databaseUri,
password,
keyFileUri,
readOnly,
cipherDatabaseEntity,
false)
}
}
// Clear before we load
val database = Database.getInstance()
database.closeAndClear(applicationContext.filesDir)
mDatabaseFileUri?.let { databaseUri ->
// Show the progress dialog and load the database
ProgressDialogThread(this,
{ progressTaskUpdater ->
LoadDatabaseRunnable(
WeakReference(this@PasswordActivity),
database,
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
password: String?,
keyFile: Uri?,
readOnly: Boolean,
cipherDatabaseEntity: CipherDatabaseEntity?,
fixDuplicateUUID: Boolean) {
mProgressDialogThread?.startDatabaseLoad(
databaseUri,
password,
keyFile,
progressTaskUpdater,
AfterLoadingDatabase(database, password, cipherDatabaseEntity))
},
R.string.loading_database).start()
}
readOnly,
cipherDatabaseEntity,
fixDuplicateUUID
)
}
/**
* Called after verify and try to opening the database
*/
private inner class AfterLoadingDatabase(val database: Database, val password: String?,
val cipherDatabaseEntity: CipherDatabaseEntity? = null)
: ActionRunnable() {
override fun onFinishRun(result: Result) {
runOnUiThread {
// Recheck fingerprint if error
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) {
// Stay with the same mode and init it
advancedUnlockedManager?.initBiometricMode()
}
}
if (result.isSuccess) {
// Remove the password in view in all cases
removePassword()
// Register the biometric
if (cipherDatabaseEntity != null) {
CipherDatabaseAction.getInstance(this@PasswordActivity)
.addOrUpdateCipherDatabase(cipherDatabaseEntity) {
checkAndLaunchGroupActivity(database, password)
}
} else {
checkAndLaunchGroupActivity(database, password)
}
} else {
if (result.message != null && result.message!!.isNotEmpty()) {
Snackbar.make(activity_password_coordinator_layout, result.message!!, Snackbar.LENGTH_LONG).asError().show()
}
}
}
}
}
private fun checkAndLaunchGroupActivity(database: Database, password: String?) {
if (database.validatePasswordEncoding(password)) {
launchGroupActivity()
} else {
PasswordEncodingDialogFragment().apply {
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
launchGroupActivity()
}
show(supportFragmentManager, "passwordEncodingTag")
}
}
}
private fun launchGroupActivity() {
EntrySelectionHelper.doEntrySelectionAction(intent,
{
GroupActivity.launch(this@PasswordActivity, readOnly)
},
{
GroupActivity.launchForKeyboarSelection(this@PasswordActivity, readOnly)
// Do not keep history
finish()
},
{ assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
GroupActivity.launchForAutofillResult(this@PasswordActivity, assistStructure, readOnly)
}
})
private fun showLoadDatabaseDuplicateUuidMessage(loadDatabaseWithFix: (() -> Unit)? = null) {
DuplicateUuidDialog().apply {
positiveAction = loadDatabaseWithFix
}.show(supportFragmentManager, "duplicateUUIDDialog")
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -514,53 +556,102 @@ class PasswordActivity : StylishActivity() {
MenuUtil.defaultMenuInflater(inflater, menu)
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Fingerprint menu
// biometric menu
advancedUnlockedManager?.inflateOptionsMenu(inflater, menu)
}
super.onCreateOptionsMenu(menu)
// Show education views
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) }
launchEducation(menu) {
launchCheckPermission()
}
return true
}
// Check permission
private fun launchCheckPermission() {
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
val permissions = arrayOf(writePermission)
if (Build.VERSION.SDK_INT >= 23
&& !readOnly
&& !mPermissionAsked) {
mPermissionAsked = true
// Check self permission to show or not the dialog
if (toolbar != null
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
WRITE_EXTERNAL_STORAGE_REQUEST -> {
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
Toast.makeText(this, R.string.read_only_warning, Toast.LENGTH_LONG).show()
}
}
}
}
// To fix multiple view education
private var performedEductionInProgress = false
private fun launchEducation(menu: Menu, onEducationFinished: ()-> Unit) {
if (!performedEductionInProgress) {
performedEductionInProgress = true
// Show education views
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu, onEducationFinished) }
}
}
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
menu: Menu) {
val unlockEducationPerformed = toolbar != null
menu: Menu,
onEducationFinished: ()-> Unit) {
val educationToolbar = toolbar
val unlockEducationPerformed = educationToolbar != null
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
toolbar!!,
educationToolbar,
{
performedNextEducation(passwordActivityEducation, menu)
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
},
{
performedNextEducation(passwordActivityEducation, menu)
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
})
if (!unlockEducationPerformed) {
val readOnlyEducationPerformed = toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
val readOnlyEducationPerformed =
educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
toolbar!!.findViewById(R.id.menu_open_file_read_mode_key),
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
{
onOptionsItemSelected(menu.findItem(R.id.menu_open_file_read_mode_key))
performedNextEducation(passwordActivityEducation, menu)
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
},
{
performedNextEducation(passwordActivityEducation, menu)
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
})
if (!readOnlyEducationPerformed) {
val biometricCanAuthenticate = BiometricManager.from(this).canAuthenticate()
// fingerprintEducationPerformed
val biometricEducationPerformed =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& PreferencesUtil.isBiometricUnlockEnable(applicationContext)
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.unlockIconImageView != null
&& passwordActivityEducation.checkAndPerformedFingerprintEducation(advancedUnlockInfoView?.unlockIconImageView!!)
&& passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!,
{
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
},
{
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
})
if (!biometricEducationPerformed) {
onEducationFinished.invoke()
}
}
}
}
@@ -583,7 +674,7 @@ class PasswordActivity : StylishActivity() {
readOnly = !readOnly
changeOpenFileReadIcon(item)
}
R.id.menu_fingerprint_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
R.id.menu_biometric_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
advancedUnlockedManager?.deleteEntryKey()
}
else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
@@ -636,6 +727,10 @@ class PasswordActivity : StylishActivity() {
private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
intentBuildLauncher: (Intent) -> Unit) {
val intent = Intent(activity, PasswordActivity::class.java)

View File

@@ -45,6 +45,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
private var rootView: View? = null
private var passwordCheckBox: CompoundButton? = null
private var passwordTextInputLayout: TextInputLayout? = null
private var passwordView: TextView? = null
private var passwordRepeatTextInputLayout: TextInputLayout? = null
private var passwordRepeatView: TextView? = null
@@ -96,6 +98,13 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
var allowNoMasterKey = false
arguments?.apply {
if (containsKey(ALLOW_NO_MASTER_KEY_ARG))
allowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
}
val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater
@@ -104,9 +113,10 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
.setTitle(R.string.assign_master_key)
// Add action buttons
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(R.string.cancel) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
passwordView = rootView?.findViewById(R.id.pass_password)
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
@@ -116,7 +126,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
keyFileView = rootView?.findViewById(R.id.pass_keyfile)
mOpenFileHelper = OpenFileHelper(this)
rootView?.findViewById<View>(R.id.browse_button)?.setOnClickListener { view ->
rootView?.findViewById<View>(R.id.open_database_button)?.setOnClickListener { view ->
mOpenFileHelper?.openFileOnClickViewListener?.onClick(view) }
val dialog = builder.create()
@@ -132,7 +142,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
var error = verifyPassword() || verifyFile()
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
error = true
if (allowNoMasterKey)
showNoKeyConfirmationDialog()
else {
passwordTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
}
}
if (!error) {
mListener?.onAssignKeyDialogPositiveClick(
@@ -193,6 +207,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
showEmptyPasswordConfirmationDialog()
}
}
return error
}
@@ -223,7 +238,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
this@AssignMasterKeyDialogFragment.dismiss()
}
}
.setNegativeButton(R.string.cancel) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
builder.create().show()
}
}
@@ -238,7 +253,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
keyFileCheckBox!!.isChecked, mKeyFile)
this@AssignMasterKeyDialogFragment.dismiss()
}
.setNegativeButton(R.string.cancel) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
builder.create().show()
}
}
@@ -255,4 +270,17 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
}
}
}
companion object {
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
fun getInstance(allowNoMasterKey: Boolean): AssignMasterKeyDialogFragment {
val fragment = AssignMasterKeyDialogFragment()
val args = Bundle()
args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey)
fragment.arguments = args
return fragment
}
}
}

View File

@@ -36,7 +36,7 @@ class BrowserDialogFragment : DialogFragment() {
// Get the layout inflater
val root = activity.layoutInflater.inflate(R.layout.fragment_browser_install, null)
builder.setView(root)
.setNegativeButton(R.string.cancel) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
val textDescription = root.findViewById<TextView>(R.id.file_manager_install_description)
textDescription.text = getString(R.string.file_manager_install_description)

View File

@@ -0,0 +1,93 @@
/*
* 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 androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
class DeleteNodesDialogFragment : DialogFragment() {
private var mNodesToDelete: List<Node> = ArrayList()
private var mListener: DeleteNodeListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as DeleteNodeListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + DeleteNodeListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
arguments?.apply {
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), this)
}
} ?: savedInstanceState?.apply {
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), savedInstanceState)
}
}
activity?.let { activity ->
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity)
builder.setMessage(getString(R.string.warning_permanently_delete_nodes))
builder.setPositiveButton(android.R.string.yes) { _, _ ->
mListener?.permanentlyDeleteNodes(mNodesToDelete)
}
builder.setNegativeButton(android.R.string.no) { _, _ -> dismiss() }
// Create the AlertDialog object and return it
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putAll(getBundleFromListNodes(mNodesToDelete))
}
interface DeleteNodeListener {
fun permanentlyDeleteNodes(nodes: List<Node>)
}
companion object {
fun getInstance(nodesToDelete: List<Node>): DeleteNodesDialogFragment {
return DeleteNodesDialogFragment().apply {
arguments = getBundleFromListNodes(nodesToDelete)
}
}
}
}

View File

@@ -20,37 +20,35 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.os.Build
import android.os.Bundle
import android.preference.PreferenceManager
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
class ReadOnlyDialog : DialogFragment() {
class DuplicateUuidDialog : DialogFragment() {
var positiveAction: (() -> Unit)? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
// Use the Builder class for convenient dialog construction
val builder = androidx.appcompat.app.AlertDialog.Builder(activity)
var warning = getString(R.string.read_only_warning)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
warning = warning + "\n\n" + getString(R.string.read_only_kitkat_warning)
}
builder.setMessage(warning)
builder.setPositiveButton(getString(android.R.string.ok)) { _, _ -> dismiss() }
builder.setNegativeButton(getString(R.string.beta_dontask)) { _, _ ->
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val edit = prefs.edit()
edit.putBoolean(getString(R.string.show_read_only_warning), false)
edit.apply()
val builder = androidx.appcompat.app.AlertDialog.Builder(activity).apply {
val message = getString(R.string.contains_duplicate_uuid) +
"\n\n" + getString(R.string.contains_duplicate_uuid_procedure)
setMessage(message)
setPositiveButton(getString(android.R.string.ok)) { _, _ ->
positiveAction?.invoke()
dismiss()
}
setNegativeButton(getString(android.R.string.cancel)) { _, _ -> dismiss() }
}
// Create the AlertDialog object and return it
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
override fun onPause() {
super.onPause()
this.dismiss()
}
}

View File

@@ -1,73 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.annotation.RequiresApi
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.view.View
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.biometric.FingerPrintAnimatedVector
import com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity
@RequiresApi(api = Build.VERSION_CODES.M)
class FingerPrintExplanationDialog : DialogFragment() {
private var fingerPrintAnimatedVector: FingerPrintAnimatedVector? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater
val rootView = inflater.inflate(R.layout.fragment_fingerprint_explanation, null)
rootView.findViewById<View>(R.id.fingerprint_setting_link_text).setOnClickListener {
startActivity(Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS))
}
rootView.findViewById<View>(R.id.auto_open_biometric_prompt_button).setOnClickListener {
startActivity(Intent(activity, SettingsAdvancedUnlockActivity::class.java))
}
fingerPrintAnimatedVector = FingerPrintAnimatedVector(activity,
rootView.findViewById(R.id.biometric_image))
builder.setView(rootView)
.setPositiveButton(android.R.string.ok) { _, _ -> }
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
override fun onResume() {
super.onResume()
fingerPrintAnimatedVector?.startScan()
}
override fun onPause() {
super.onPause()
fingerPrintAnimatedVector?.stopScan()
}
}

View File

@@ -114,7 +114,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
dismiss()
}
.setNegativeButton(R.string.cancel) { _, _ ->
.setNegativeButton(android.R.string.cancel) { _, _ ->
val bundle = Bundle()
mListener?.cancelPassword(bundle)

View File

@@ -33,8 +33,8 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.GroupVersioned
import com.kunzisoft.keepass.database.element.PwIcon
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.icons.assignDatabaseIcon
class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener {
@@ -45,7 +45,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
private var editGroupDialogAction: EditGroupDialogAction? = null
private var nameGroup: String? = null
private var iconGroup: PwIcon? = null
private var iconGroup: IconImage? = null
private var nameTextLayoutView: TextInputLayout? = null
private var nameTextView: TextView? = null
@@ -122,7 +122,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
val builder = AlertDialog.Builder(activity)
builder.setView(root)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(R.string.cancel) { _, _ ->
.setNegativeButton(android.R.string.cancel) { _, _ ->
editGroupListener?.cancelEditGroup(
editGroupDialogAction,
nameTextView?.text?.toString(),
@@ -186,8 +186,8 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
}
interface EditGroupListener {
fun approveEditGroup(action: EditGroupDialogAction?, name: String?, icon: PwIcon?)
fun cancelEditGroup(action: EditGroupDialogAction?, name: String?, icon: PwIcon?)
fun approveEditGroup(action: EditGroupDialogAction?, name: String?, icon: IconImage?)
fun cancelEditGroup(action: EditGroupDialogAction?, name: String?, icon: IconImage?)
}
companion object {
@@ -206,7 +206,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
return fragment
}
fun build(group: GroupVersioned): GroupEditDialogFragment {
fun build(group: Group): GroupEditDialogFragment {
val bundle = Bundle()
bundle.putString(KEY_NAME, group.title)
bundle.putParcelable(KEY_ICON, group.icon)

View File

@@ -35,7 +35,7 @@ import android.widget.GridView
import android.widget.ImageView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.element.PwIconStandard
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.icons.IconPack
import com.kunzisoft.keepass.icons.IconPackChooser
@@ -72,12 +72,12 @@ class IconPickerDialogFragment : DialogFragment() {
currIconGridView.setOnItemClickListener { _, _, position, _ ->
val bundle = Bundle()
bundle.putParcelable(KEY_ICON_STANDARD, PwIconStandard(position))
bundle.putParcelable(KEY_ICON_STANDARD, IconImageStandard(position))
iconPickerListener?.iconPicked(bundle)
dismiss()
}
builder.setNegativeButton(R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() }
builder.setNegativeButton(android.R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() }
return builder.create()
}
@@ -128,7 +128,7 @@ class IconPickerDialogFragment : DialogFragment() {
private const val KEY_ICON_STANDARD = "KEY_ICON_STANDARD"
fun getIconStandardFromBundle(bundle: Bundle): PwIconStandard? {
fun getIconStandardFromBundle(bundle: Bundle): IconImageStandard? {
return bundle.getParcelable(KEY_ICON_STANDARD)
}

View File

@@ -1,65 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.os.Bundle
import android.provider.Settings
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.view.View
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.UriUtil
class KeyboardExplanationDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let {
val builder = AlertDialog.Builder(activity!!)
val inflater = activity!!.layoutInflater
val rootView = inflater.inflate(R.layout.fragment_keyboard_explanation, null)
rootView.findViewById<View>(R.id.keyboards_activate_device_setting_button)
.setOnClickListener { launchActivateKeyboardSetting() }
val containerKeyboardSwitcher = rootView.findViewById<View>(R.id.container_keyboard_switcher)
if (BuildConfig.CLOSED_STORE) {
containerKeyboardSwitcher.setOnClickListener { UriUtil.gotoUrl(context!!, R.string.keyboard_switcher_play_store) }
} else {
containerKeyboardSwitcher.setOnClickListener { UriUtil.gotoUrl(context!!, R.string.keyboard_switcher_f_droid) }
}
builder.setView(rootView)
.setPositiveButton(android.R.string.ok) { _, _ -> }
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun launchActivateKeyboardSetting() {
val intent = Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)
intent.addFlags(FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}
}

View File

@@ -35,7 +35,7 @@ class PasswordEncodingDialogFragment : DialogFragment() {
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.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.cancel() }
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
return builder.create()
}

View File

@@ -0,0 +1,401 @@
/*
* 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.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.Spinner
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.OtpModel
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_HOTP_COUNTER
import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_OTP_DIGITS
import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_TOTP_PERIOD
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_HOTP_COUNTER
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_DIGITS
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
import com.kunzisoft.keepass.otp.OtpTokenType
import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.otp.TokenCalculator
import java.util.*
class SetOTPDialogFragment : DialogFragment() {
private var mCreateOTPElementListener: CreateOtpListener? = null
private var mOtpElement: OtpElement = OtpElement()
private var otpTypeSpinner: Spinner? = null
private var otpTokenTypeSpinner: Spinner? = null
private var otpSecretContainer: TextInputLayout? = null
private var otpSecretTextView: EditText? = null
private var otpPeriodContainer: TextInputLayout? = null
private var otpPeriodTextView: EditText? = null
private var otpCounterContainer: TextInputLayout? = null
private var otpCounterTextView: EditText? = null
private var otpDigitsContainer: TextInputLayout? = null
private var otpDigitsTextView: EditText? = null
private var otpAlgorithmSpinner: Spinner? = null
private var otpTypeAdapter: ArrayAdapter<OtpType>? = null
private var otpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
private var totpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
private var hotpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
private var otpAlgorithmAdapter: ArrayAdapter<TokenCalculator.HashAlgorithm>? = null
private var mManualEvent = false
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
if (!isFocus)
mManualEvent = true
}
private var mOnTouchListener = View.OnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
mManualEvent = true
}
}
false
}
private var mSecretWellFormed = false
private var mCounterWellFormed = true
private var mPeriodWellFormed = true
private var mDigitsWellFormed = true
override fun onAttach(context: Context) {
super.onAttach(context)
// Verify that the host activity implements the callback interface
try {
// Instantiate the NoticeDialogListener so we can send events to the host
mCreateOTPElementListener = context as CreateOtpListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context.toString()
+ " must implement " + CreateOtpListener::class.java.name)
}
}
@SuppressLint("ClickableViewAccessibility")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// Retrieve OTP model from instance state
if (savedInstanceState != null) {
if (savedInstanceState.containsKey(KEY_OTP)) {
savedInstanceState.getParcelable<OtpModel>(KEY_OTP)?.let { otpModel ->
mOtpElement = OtpElement(otpModel)
}
}
} else {
arguments?.apply {
if (containsKey(KEY_OTP)) {
getParcelable<OtpModel?>(KEY_OTP)?.let { otpModel ->
mOtpElement = OtpElement(otpModel)
}
}
}
}
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup?
otpTypeSpinner = root?.findViewById(R.id.setup_otp_type)
otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type)
otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label)
otpSecretTextView = root?.findViewById(R.id.setup_otp_secret)
otpAlgorithmSpinner = root?.findViewById(R.id.setup_otp_algorithm)
otpPeriodContainer= root?.findViewById(R.id.setup_otp_period_label)
otpPeriodTextView = root?.findViewById(R.id.setup_otp_period)
otpCounterContainer= root?.findViewById(R.id.setup_otp_counter_label)
otpCounterTextView = root?.findViewById(R.id.setup_otp_counter)
otpDigitsContainer = root?.findViewById(R.id.setup_otp_digits_label)
otpDigitsTextView = root?.findViewById(R.id.setup_otp_digits)
// To fix init element
// With tab keyboard selection
otpSecretTextView?.onFocusChangeListener = mOnFocusChangeListener
// With finger selection
otpTypeSpinner?.setOnTouchListener(mOnTouchListener)
otpTokenTypeSpinner?.setOnTouchListener(mOnTouchListener)
otpSecretTextView?.setOnTouchListener(mOnTouchListener)
otpAlgorithmSpinner?.setOnTouchListener(mOnTouchListener)
otpPeriodTextView?.setOnTouchListener(mOnTouchListener)
otpCounterTextView?.setOnTouchListener(mOnTouchListener)
otpDigitsTextView?.setOnTouchListener(mOnTouchListener)
// HOTP / TOTP Type selection
val otpTypeArray = OtpType.values()
otpTypeAdapter = ArrayAdapter<OtpType>(activity,
android.R.layout.simple_spinner_item, otpTypeArray).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
otpTypeSpinner?.adapter = otpTypeAdapter
// Otp Token type selection
val hotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues()
hotpTokenTypeAdapter = ArrayAdapter(activity,
android.R.layout.simple_spinner_item, hotpTokenTypeArray).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
// Proprietary only on closed and full version
val totpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION)
totpTokenTypeAdapter = ArrayAdapter(activity,
android.R.layout.simple_spinner_item, totpTokenTypeArray).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
otpTokenTypeAdapter = hotpTokenTypeAdapter
otpTokenTypeSpinner?.adapter = otpTokenTypeAdapter
// OTP Algorithm
val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values()
otpAlgorithmAdapter = ArrayAdapter<TokenCalculator.HashAlgorithm>(activity,
android.R.layout.simple_spinner_item, otpAlgorithmArray).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
otpAlgorithmSpinner?.adapter = otpAlgorithmAdapter
// Set the default value of OTP element
upgradeType()
upgradeTokenType()
upgradeParameters()
attachListeners()
val builder = AlertDialog.Builder(activity)
builder.apply {
setTitle(R.string.entry_setup_otp)
setView(root)
.setPositiveButton(android.R.string.ok) {_, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ ->
}
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
override fun onResume() {
super.onResume()
(dialog as AlertDialog).getButton(Dialog.BUTTON_POSITIVE).setOnClickListener {
if (mSecretWellFormed
&& mCounterWellFormed
&& mPeriodWellFormed
&& mDigitsWellFormed) {
mCreateOTPElementListener?.onOtpCreated(mOtpElement)
dismiss()
}
}
}
private fun attachListeners() {
// Set Type listener
otpTypeSpinner?.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
if (mManualEvent) {
(parent?.selectedItem as OtpType?)?.let {
mOtpElement.type = it
upgradeTokenType()
}
}
}
}
// Set type token listener
otpTokenTypeSpinner?.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
if (mManualEvent) {
(parent?.selectedItem as OtpTokenType?)?.let {
mOtpElement.tokenType = it
upgradeParameters()
}
}
}
}
// Set algorithm spinner
otpAlgorithmSpinner?.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
if (mManualEvent) {
(parent?.selectedItem as TokenCalculator.HashAlgorithm?)?.let {
mOtpElement.algorithm = it
}
}
}
}
// Set secret in OtpElement
otpSecretTextView?.addTextChangedListener(object: TextWatcher {
override fun afterTextChanged(s: Editable?) {
s?.toString()?.let { userString ->
try {
mOtpElement.setBase32Secret(userString.toUpperCase(Locale.ENGLISH))
otpSecretContainer?.error = null
} catch (exception: Exception) {
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
}
mSecretWellFormed = otpSecretContainer?.error == null
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
// Set counter in OtpElement
otpCounterTextView?.addTextChangedListener(object: TextWatcher {
override fun afterTextChanged(s: Editable?) {
if (mManualEvent) {
s?.toString()?.toLongOrNull()?.let {
try {
mOtpElement.counter = it
otpCounterContainer?.error = null
} catch (exception: Exception) {
otpCounterContainer?.error = getString(R.string.error_otp_counter,
MIN_HOTP_COUNTER, MAX_HOTP_COUNTER)
}
mCounterWellFormed = otpCounterContainer?.error == null
}
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
// Set period in OtpElement
otpPeriodTextView?.addTextChangedListener(object: TextWatcher {
override fun afterTextChanged(s: Editable?) {
if (mManualEvent) {
s?.toString()?.toIntOrNull()?.let {
try {
mOtpElement.period = it
otpPeriodContainer?.error = null
} catch (exception: Exception) {
otpPeriodContainer?.error = getString(R.string.error_otp_period,
MIN_TOTP_PERIOD, MAX_TOTP_PERIOD)
}
mPeriodWellFormed = otpPeriodContainer?.error == null
}
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
// Set digits in OtpElement
otpDigitsTextView?.addTextChangedListener(object: TextWatcher {
override fun afterTextChanged(s: Editable?) {
if (mManualEvent) {
s?.toString()?.toIntOrNull()?.let {
try {
mOtpElement.digits = it
otpDigitsContainer?.error = null
} catch (exception: Exception) {
otpDigitsContainer?.error = getString(R.string.error_otp_digits,
MIN_OTP_DIGITS, MAX_OTP_DIGITS)
}
mDigitsWellFormed = otpDigitsContainer?.error == null
}
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
}
private fun upgradeType() {
otpTypeSpinner?.setSelection(OtpType.values().indexOf(mOtpElement.type))
}
private fun upgradeTokenType() {
when (mOtpElement.type) {
OtpType.HOTP -> {
otpPeriodContainer?.visibility = View.GONE
otpCounterContainer?.visibility = View.VISIBLE
otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter
otpTokenTypeSpinner?.setSelection(OtpTokenType
.getHotpTokenTypeValues().indexOf(mOtpElement.tokenType))
}
OtpType.TOTP -> {
otpPeriodContainer?.visibility = View.VISIBLE
otpCounterContainer?.visibility = View.GONE
otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter
otpTokenTypeSpinner?.setSelection(OtpTokenType
.getTotpTokenTypeValues().indexOf(mOtpElement.tokenType))
}
}
}
private fun upgradeParameters() {
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
.indexOf(mOtpElement.algorithm))
otpSecretTextView?.apply {
setText(mOtpElement.getBase32Secret())
// Cursor at end
setSelection(this.text.length)
}
otpCounterTextView?.setText(mOtpElement.counter.toString())
otpPeriodTextView?.setText(mOtpElement.period.toString())
otpDigitsTextView?.setText(mOtpElement.digits.toString())
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(KEY_OTP, mOtpElement.otpModel)
}
interface CreateOtpListener {
fun onOtpCreated(otpElement: OtpElement)
}
companion object {
private const val KEY_OTP = "KEY_OTP"
fun build(otpModel: OtpModel? = null): SetOTPDialogFragment {
return SetOTPDialogFragment().apply {
if (otpModel != null) {
arguments = Bundle().apply {
putParcelable(KEY_OTP, otpModel)
}
}
}
}
}
}

View File

@@ -29,7 +29,7 @@ import android.view.View
import android.widget.CompoundButton
import android.widget.RadioGroup
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.SortNodeEnum
import com.kunzisoft.keepass.database.element.SortNodeEnum
class SortDialogFragment : DialogFragment() {
@@ -83,7 +83,7 @@ class SortDialogFragment : DialogFragment() {
// Add action buttons
.setPositiveButton(android.R.string.ok
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) }
.setNegativeButton(R.string.cancel) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
// Check if is ascending or descending

View File

@@ -1,7 +1,26 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.helpers
import android.app.Activity
import android.app.assist.AssistStructure
import android.content.Context
import android.content.Intent
import android.os.Build
import com.kunzisoft.keepass.autofill.AutofillHelper
@@ -11,10 +30,10 @@ object EntrySelectionHelper {
private const val EXTRA_ENTRY_SELECTION_MODE = "com.kunzisoft.keepass.extra.ENTRY_SELECTION_MODE"
private const val DEFAULT_ENTRY_SELECTION_MODE = false
fun startActivityForEntrySelection(activity: Activity, intent: Intent) {
fun startActivityForEntrySelection(context: Context, intent: Intent) {
addEntrySelectionModeExtraInIntent(intent)
// only to avoid visible flickering when redirecting
activity.startActivity(intent)
context.startActivity(intent)
}
fun addEntrySelectionModeExtraInIntent(intent: Intent) {

View File

@@ -19,6 +19,7 @@
*/
package com.kunzisoft.keepass.activities.helpers
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Activity.RESULT_OK
import android.content.Context
@@ -26,10 +27,10 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import android.util.Log
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
import com.kunzisoft.keepass.utils.UriUtil
@@ -39,7 +40,7 @@ class OpenFileHelper {
private var fragment: Fragment? = null
val openFileOnClickViewListener: OpenFileOnClickViewListener
get() = OpenFileOnClickViewListener(null)
get() = OpenFileOnClickViewListener()
constructor(context: Activity) {
this.activity = context
@@ -51,7 +52,7 @@ class OpenFileHelper {
this.fragment = context
}
inner class OpenFileOnClickViewListener(private val dataUri: (() -> Uri?)?) : View.OnClickListener {
inner class OpenFileOnClickViewListener : View.OnClickListener {
override fun onClick(v: View) {
try {
@@ -62,58 +63,50 @@ class OpenFileHelper {
}
} catch (e: Exception) {
Log.e(TAG, "Enable to start the file picker activity", e)
// Open File picker if can't open activity
if (lookForOpenIntentsFilePicker(dataUri?.invoke()))
// Open browser dialog
if (lookForOpenIntentsFilePicker())
showBrowserDialog()
}
}
}
@SuppressLint("InlinedApi")
private fun openActivityWithActionOpenDocument() {
val i = Intent(ACTION_OPEN_DOCUMENT)
i.addCategory(Intent.CATEGORY_OPENABLE)
i.type = "*/*"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
i.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
} else {
i.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
val intentOpenDocument = Intent(APP_ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
}
if (fragment != null)
fragment?.startActivityForResult(i, OPEN_DOC)
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
else
activity?.startActivityForResult(i, OPEN_DOC)
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
}
@SuppressLint("InlinedApi")
private fun openActivityWithActionGetContent() {
val i = Intent(Intent.ACTION_GET_CONTENT)
i.addCategory(Intent.CATEGORY_OPENABLE)
i.type = "*/*"
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
}
if (fragment != null)
fragment?.startActivityForResult(i, GET_CONTENT)
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
else
activity?.startActivityForResult(i, GET_CONTENT)
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
}
fun getOpenFileOnClickViewListener(dataUri: () -> Uri?): OpenFileOnClickViewListener {
return OpenFileOnClickViewListener(dataUri)
}
private fun lookForOpenIntentsFilePicker(dataUri: Uri?): Boolean {
private fun lookForOpenIntentsFilePicker(): Boolean {
var showBrowser = false
try {
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
// Get file path parent if possible
if (dataUri != null
&& dataUri.toString().isNotEmpty()
&& dataUri.scheme == "file") {
intent.data = dataUri
} else {
Log.w(javaClass.name, "Unable to read the URI")
}
if (fragment != null)
fragment?.startActivityForResult(intent, FILE_BROWSE)
else
@@ -190,22 +183,19 @@ class OpenFileHelper {
GET_CONTENT, OPEN_DOC -> {
if (resultCode == RESULT_OK) {
if (data != null) {
var uri = data.data
val uri = data.data
if (uri != null) {
try {
// try to persist read and write permissions
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
activity?.contentResolver?.apply {
takePersistableUriPermission(uri!!, Intent.FLAG_GRANT_READ_URI_PERMISSION)
takePersistableUriPermission(uri!!, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
}
} catch (e: Exception) {
// nop
}
if (requestCode == GET_CONTENT) {
uri = UriUtil.translateUri(activity!!, uri)
}
keyFileCallback?.invoke(uri)
}
}
@@ -220,16 +210,11 @@ class OpenFileHelper {
private const val TAG = "OpenFileHelper"
private var ACTION_OPEN_DOCUMENT: String
init {
ACTION_OPEN_DOCUMENT = try {
val openDocument = Intent::class.java.getField("ACTION_OPEN_DOCUMENT")
openDocument.get(null) as String
private var APP_ACTION_OPEN_DOCUMENT: String = try {
Intent::class.java.getField("ACTION_OPEN_DOCUMENT").get(null) as String
} catch (e: Exception) {
"android.intent.action.OPEN_DOCUMENT"
}
}
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"

View File

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

View File

@@ -32,9 +32,11 @@ import android.view.ViewGroup
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.LOCK_ACTION
@@ -62,6 +64,10 @@ abstract class LockingActivity : StylishActivity() {
return field || mSelectionMode
}
protected var mSelectionMode: Boolean = false
protected var mAutoSaveEnable: Boolean = true
var mProgressDialogThread: ProgressDialogThread? = null
private set
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -85,6 +91,8 @@ abstract class LockingActivity : StylishActivity() {
mExitLock = false
mReadOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState, intent)
mProgressDialogThread = ProgressDialogThread(this)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -100,8 +108,13 @@ abstract class LockingActivity : StylishActivity() {
override fun onResume() {
super.onResume()
mProgressDialogThread?.registerProgressTask()
// To refresh when back to normal workflow from selection workflow
mSelectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
invalidateOptionsMenu()
if (mTimeoutEnable) {
// End activity if database not loaded
@@ -118,8 +131,6 @@ abstract class LockingActivity : StylishActivity() {
if (!mExitLock)
TimeoutHelper.recordTime(this)
}
invalidateOptionsMenu()
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -129,6 +140,8 @@ abstract class LockingActivity : StylishActivity() {
}
override fun onPause() {
mProgressDialogThread?.unregisterProgressTask()
super.onPause()
if (mTimeoutEnable) {
@@ -199,6 +212,9 @@ fun Activity.lock() {
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
MagikIME.removeEntry(this)
// Stop the notification service
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
Log.i(Activity::class.java.name, "Shutdown " + localClassName +
" after inactivity or manual lock")
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {

View File

@@ -1,55 +0,0 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.lock
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.view.WindowManager
/**
* Locking Hide Activity that sets FLAG_SECURE to prevent screenshots, and from
* appearing in the recent app preview
*/
abstract class LockingHideActivity : LockingActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Several gingerbread devices have problems with FLAG_SECURE
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
}
/* (non-Javadoc) Workaround for HTC Linkify issues
* @see android.app.Activity#startActivity(android.content.Intent)
*/
override fun startActivity(intent: Intent) {
try {
if (intent.component != null && intent.component!!.shortClassName == ".HtcLinkifyDispatcherActivity") {
intent.component = null
}
super.startActivity(intent)
} catch (e: ActivityNotFoundException) {
/* Catch the bad HTC implementation case */
super.startActivity(Intent.createChooser(intent, null))
}
}
}

View File

@@ -61,6 +61,7 @@ object Stylish {
return when (themeString) {
context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night
context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black
context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark
context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue
context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red

View File

@@ -19,20 +19,45 @@
*/
package com.kunzisoft.keepass.activities.stylish
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity
import android.util.Log
import android.view.WindowManager
/**
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
* appearing in the recent app preview
*/
abstract class StylishActivity : AppCompatActivity() {
@StyleRes
private var themeId: Int = 0
/* (non-Javadoc) Workaround for HTC Linkify issues
* @see android.app.Activity#startActivity(android.content.Intent)
*/
override fun startActivity(intent: Intent) {
try {
if (intent.component != null && intent.component!!.shortClassName == ".HtcLinkifyDispatcherActivity") {
intent.component = null
}
super.startActivity(intent)
} catch (e: ActivityNotFoundException) {
/* Catch the bad HTC implementation case */
super.startActivity(Intent.createChooser(intent, null))
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.themeId = Stylish.getThemeId(this)
setTheme(themeId)
// Several gingerbread devices have problems with FLAG_SECURE
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
}
override fun onResume() {

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.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.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
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 (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,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.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Entry
class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
var entryHistoryList: MutableList<Entry> = ArrayList()
var onItemClickListener: ((item: Entry, position: Int)->Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryHistoryViewHolder {
return EntryHistoryViewHolder(inflater.inflate(R.layout.item_list_entry_history, parent, false))
}
override fun onBindViewHolder(holder: EntryHistoryViewHolder, position: Int) {
val entryHistory = entryHistoryList[position]
holder.lastModifiedView.text = entryHistory.lastModificationTime.getDateTimeString(context.resources)
holder.titleView.text = entryHistory.title
holder.usernameView.text = entryHistory.username
holder.urlView.text = entryHistory.url
holder.itemView.setOnClickListener {
onItemClickListener?.invoke(entryHistory, position)
}
}
override fun getItemCount(): Int {
return entryHistoryList.size
}
fun clear() {
entryHistoryList.clear()
}
inner class EntryHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var lastModifiedView: TextView = itemView.findViewById(R.id.entry_history_last_modified)
var titleView: TextView = itemView.findViewById(R.id.entry_history_title)
var usernameView: TextView = itemView.findViewById(R.id.entry_history_username)
var urlView: TextView = itemView.findViewById(R.id.entry_history_url)
}
}

View File

@@ -1,3 +1,22 @@
/*
* 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
@@ -15,10 +34,9 @@ import java.util.ArrayList
class FieldsAdapter(context: Context) : RecyclerView.Adapter<FieldsAdapter.FieldViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
var fields: MutableList<Field> = ArrayList()
private var fields: MutableList<Field> = ArrayList()
var onItemClickListener: OnItemClickListener? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FieldViewHolder {
val view = inflater.inflate(R.layout.keyboard_popup_fields_item, parent, false)
return FieldViewHolder(view)
@@ -34,6 +52,11 @@ class FieldsAdapter(context: Context) : RecyclerView.Adapter<FieldsAdapter.Field
return fields.size
}
fun setFields(fieldsToAdd: List<Field>) {
fields.clear()
fields.addAll(fieldsToAdd)
}
fun clear() {
fields.clear()
}

View File

@@ -21,30 +21,39 @@ package com.kunzisoft.keepass.adapters
import android.content.Context
import android.graphics.Color
import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedListAdapterCallback
import android.graphics.Paint
import android.util.Log
import android.util.TypedValue
import android.view.*
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.SortNodeEnum
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.strikeOut
import java.util.*
class NodeAdapter
/**
* Create node list adapter with contextMenu or not
* @param context Context to use
*/
(private val context: Context, private val menuInflater: MenuInflater)
(private val context: Context)
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
private val nodeSortedList: SortedList<NodeVersioned>
private val nodeSortedList: SortedList<Node>
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var calculateViewTypeTextSize = Array(2) { true} // number of view type
@@ -60,12 +69,10 @@ class NodeAdapter
private var recycleBinBottomSort: Boolean = true
private var showUserNames: Boolean = true
private var showNumberEntries: Boolean = true
private var entryFilters = arrayOf<Group.ChildFilter>()
private var actionNodesList = LinkedList<Node>()
private var nodeClickCallback: NodeClickCallback? = null
private var nodeMenuListener: NodeMenuListener? = null
private var activateContextMenu: Boolean = false
private var readOnly: Boolean = false
private var isASearchResult: Boolean = false
private val mDatabase: Database
@@ -81,22 +88,19 @@ class NodeAdapter
init {
assignPreferences()
this.activateContextMenu = false
this.readOnly = false
this.isASearchResult = false
this.nodeSortedList = SortedList(NodeVersioned::class.java, object : SortedListAdapterCallback<NodeVersioned>(this) {
override fun compare(item1: NodeVersioned, item2: NodeVersioned): Int {
this.nodeSortedList = SortedList(Node::class.java, object : SortedListAdapterCallback<Node>(this) {
override fun compare(item1: Node, item2: Node): Int {
return listSort.getNodeComparator(ascendingSort, groupsBeforeSort, recycleBinBottomSort).compare(item1, item2)
}
override fun areContentsTheSame(oldItem: NodeVersioned, newItem: NodeVersioned): Boolean {
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
return oldItem.type == newItem.type
&& oldItem.title == newItem.title
&& oldItem.icon == newItem.icon
}
override fun areItemsTheSame(item1: NodeVersioned, item2: NodeVersioned): Boolean {
override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
return item1 == item2
}
})
@@ -114,18 +118,6 @@ class NodeAdapter
taTextColor.recycle()
}
fun setReadOnly(readOnly: Boolean) {
this.readOnly = readOnly
}
fun setIsASearchResult(isASearchResult: Boolean) {
this.isASearchResult = isASearchResult
}
fun setActivateContextMenu(activate: Boolean) {
this.activateContextMenu = activate
}
private fun assignPreferences() {
this.prefTextSize = PreferencesUtil.getListTextSize(context)
this.infoTextSize = context.resources.getDimension(R.dimen.list_medium_size_default) * prefTextSize
@@ -140,6 +132,8 @@ class NodeAdapter
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
this.showNumberEntries = PreferencesUtil.showNumberEntries(context)
this.entryFilters = Group.ChildFilter.getDefaults(context)
// Reinit textSize for all view type
calculateViewTypeTextSize.forEachIndexed { index, _ -> calculateViewTypeTextSize[index] = true }
}
@@ -147,18 +141,19 @@ class NodeAdapter
/**
* Rebuild the list by clear and build children from the group
*/
fun rebuildList(group: GroupVersioned) {
fun rebuildList(group: Group) {
this.nodeSortedList.clear()
assignPreferences()
try {
this.nodeSortedList.addAll(group.getChildren())
this.nodeSortedList.addAll(group.getChildren(*entryFilters))
} catch (e: Exception) {
Log.e(TAG, "Can't add node elements to the list", e)
Toast.makeText(context, "Can't add node elements to the list : " + e.message, Toast.LENGTH_LONG).show()
}
notifyDataSetChanged()
}
fun contains(node: NodeVersioned): Boolean {
fun contains(node: Node): Boolean {
return nodeSortedList.indexOf(node) != SortedList.INVALID_POSITION
}
@@ -166,23 +161,55 @@ class NodeAdapter
* Add a node to the list
* @param node Node to add
*/
fun addNode(node: NodeVersioned) {
fun addNode(node: Node) {
nodeSortedList.add(node)
}
/**
* Add nodes to the list
* @param nodes Nodes to add
*/
fun addNodes(nodes: List<Node>) {
nodeSortedList.addAll(nodes)
}
/**
* Remove a node in the list
* @param node Node to delete
*/
fun removeNode(node: NodeVersioned) {
fun removeNode(node: Node) {
nodeSortedList.remove(node)
}
/**
* Remove nodes in the list
* @param nodes Nodes to delete
*/
fun removeNodes(nodes: List<Node>) {
nodes.forEach { node ->
nodeSortedList.remove(node)
}
}
/**
* Remove a node at [position] in the list
*/
fun removeNodeAt(position: Int) {
nodeSortedList.removeItemAt(position)
// Refresh all the next items
notifyItemRangeChanged(position, nodeSortedList.size() - position)
}
/**
* Remove nodes in the list by [positions]
* Note : algorithm remove the higher position at each iteration
*/
fun removeNodesAt(positions: IntArray) {
val positionsSortDescending = positions.toMutableList()
positionsSortDescending.sortDescending()
positionsSortDescending.forEach {
removeNodeAt(it)
}
}
/**
@@ -190,13 +217,47 @@ class NodeAdapter
* @param oldNode Node before the update
* @param newNode Node after the update
*/
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned) {
fun updateNode(oldNode: Node, newNode: Node) {
nodeSortedList.beginBatchedUpdates()
nodeSortedList.remove(oldNode)
nodeSortedList.add(newNode)
nodeSortedList.endBatchedUpdates()
}
/**
* Update nodes in the list
* @param oldNodes Nodes before the update
* @param newNodes Node after the update
*/
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
nodeSortedList.beginBatchedUpdates()
oldNodes.forEach { oldNode ->
nodeSortedList.remove(oldNode)
}
nodeSortedList.addAll(newNodes)
nodeSortedList.endBatchedUpdates()
}
fun notifyNodeChanged(node: Node) {
notifyItemChanged(nodeSortedList.indexOf(node))
}
fun setActionNodes(actionNodes: List<Node>) {
this.actionNodesList.apply {
clear()
addAll(actionNodes)
}
}
fun unselectActionNodes() {
actionNodesList.forEach {
notifyItemChanged(nodeSortedList.indexOf(it))
}
this.actionNodesList.apply {
clear()
}
}
/**
* Notify a change sort of the list
*/
@@ -234,46 +295,45 @@ class NodeAdapter
width = iconSize.toInt()
}
}
// Assign text
holder.text.apply {
text = subNode.title
setTextSize(textSizeUnit, infoTextSize)
strikeOut(subNode.isCurrentlyExpires)
}
// Assign click
holder.container.setOnClickListener { nodeClickCallback?.onNodeClick(subNode) }
// Context menu
if (activateContextMenu) {
holder.container.setOnCreateContextMenuListener(
ContextMenuBuilder(menuInflater, subNode, readOnly, isASearchResult, nodeMenuListener))
}
// Add subText with username
holder.subText.apply {
text = ""
strikeOut(subNode.isCurrentlyExpires)
visibility = View.GONE
if (subNode.type == Type.ENTRY) {
val entry = subNode as EntryVersioned
}
// Specific elements for entry
if (subNode.type == Type.ENTRY) {
val entry = subNode as Entry
mDatabase.startManageEntry(entry)
holder.text.text = entry.getVisualTitle()
holder.subText.apply {
val username = entry.username
if (showUserNames && username.isNotEmpty()) {
visibility = View.VISIBLE
text = username
setTextSize(textSizeUnit, subtextSize)
}
}
mDatabase.stopManageEntry(entry)
}
}
// Add number of entries in groups
if (subNode.type == Type.GROUP) {
if (showNumberEntries) {
holder.numberChildren?.apply {
text = (subNode as GroupVersioned).getChildEntries(true).size.toString()
text = (subNode as Group)
.getChildEntries(*entryFilters)
.size.toString()
setTextSize(textSizeUnit, numberChildrenTextSize)
visibility = View.VISIBLE
}
@@ -281,7 +341,18 @@ class NodeAdapter
holder.numberChildren?.visibility = View.GONE
}
}
// Assign click
holder.container.setOnClickListener {
nodeClickCallback?.onNodeClick(subNode)
}
holder.container.setOnLongClickListener {
nodeClickCallback?.onNodeLongClick(subNode) ?: false
}
holder.container.isSelected = actionNodesList.contains(subNode)
}
override fun getItemCount(): Int {
return nodeSortedList.size()
@@ -294,103 +365,12 @@ class NodeAdapter
this.nodeClickCallback = nodeClickCallback
}
/**
* Assign a listener when an element of menu is clicked
*/
fun setNodeMenuListener(nodeMenuListener: NodeMenuListener?) {
this.nodeMenuListener = nodeMenuListener
}
/**
* Callback listener to redefine to do an action when a node is click
*/
interface NodeClickCallback {
fun onNodeClick(node: NodeVersioned)
}
/**
* Menu listener to redefine to do an action in menu
*/
interface NodeMenuListener {
fun onOpenMenuClick(node: NodeVersioned): Boolean
fun onEditMenuClick(node: NodeVersioned): Boolean
fun onCopyMenuClick(node: NodeVersioned): Boolean
fun onMoveMenuClick(node: NodeVersioned): Boolean
fun onDeleteMenuClick(node: NodeVersioned): Boolean
}
/**
* Utility class for menu listener
*/
private class ContextMenuBuilder(val menuInflater: MenuInflater,
val node: NodeVersioned,
val readOnly: Boolean,
val isASearchResult: Boolean,
val menuListener: NodeMenuListener?)
: View.OnCreateContextMenuListener {
private val mOnMyActionClickListener = MenuItem.OnMenuItemClickListener { item ->
if (menuListener == null)
return@OnMenuItemClickListener false
when (item.itemId) {
R.id.menu_open -> menuListener.onOpenMenuClick(node)
R.id.menu_edit -> menuListener.onEditMenuClick(node)
R.id.menu_copy -> menuListener.onCopyMenuClick(node)
R.id.menu_move -> menuListener.onMoveMenuClick(node)
R.id.menu_delete -> menuListener.onDeleteMenuClick(node)
else -> false
}
}
override fun onCreateContextMenu(contextMenu: ContextMenu?,
view: View?,
contextMenuInfo: ContextMenu.ContextMenuInfo?) {
menuInflater.inflate(R.menu.node_menu, contextMenu)
// Opening
var menuItem = contextMenu?.findItem(R.id.menu_open)
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
val database = Database.getInstance()
// Edition
if (readOnly || node == database.recycleBin) {
contextMenu?.removeItem(R.id.menu_edit)
} else {
menuItem = contextMenu?.findItem(R.id.menu_edit)
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
}
// Copy (not for group)
if (readOnly
|| isASearchResult
|| node == database.recycleBin
|| node.type == Type.GROUP) {
// TODO COPY For Group
contextMenu?.removeItem(R.id.menu_copy)
} else {
menuItem = contextMenu?.findItem(R.id.menu_copy)
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
}
// Move
if (readOnly
|| isASearchResult
|| node == database.recycleBin) {
contextMenu?.removeItem(R.id.menu_move)
} else {
menuItem = contextMenu?.findItem(R.id.menu_move)
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
}
// Deletion
if (readOnly || node == database.recycleBin) {
contextMenu?.removeItem(R.id.menu_delete)
} else {
menuItem = contextMenu?.findItem(R.id.menu_delete)
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
}
}
fun onNodeClick(node: Node)
fun onNodeLongClick(node: Node): Boolean
}
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

View File

@@ -22,22 +22,20 @@ package com.kunzisoft.keepass.adapters
import android.content.Context
import android.database.Cursor
import android.graphics.Color
import androidx.cursoradapter.widget.CursorAdapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.cursor.EntryCursor
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.EntryVersioned
import com.kunzisoft.keepass.database.element.PwIcon
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.*
import com.kunzisoft.keepass.view.strikeOut
class SearchEntryCursorAdapter(context: Context, private val database: Database)
class SearchEntryCursorAdapter(private val context: Context,
private val database: Database)
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
private val cursorInflater: LayoutInflater = context.getSystemService(
@@ -72,34 +70,31 @@ class SearchEntryCursorAdapter(context: Context, private val database: Database)
override fun bindView(view: View, context: Context, cursor: Cursor) {
// Retrieve elements from cursor
val uuid = UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)),
cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS)))
val iconFactory = database.iconFactory
var icon: PwIcon = iconFactory.getIcon(
UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)),
cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS))))
if (icon.isUnknown) {
icon = iconFactory.getIcon(cursor.getInt(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_STANDARD)))
if (icon.isUnknown)
icon = iconFactory.keyIcon
}
val title = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_TITLE))
val username = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_USERNAME))
val url = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_URL))
database.getEntryFrom(cursor)?.let { currentEntry ->
val viewHolder = view.tag as ViewHolder
// Assign image
viewHolder.imageViewIcon?.assignDatabaseIcon(database.drawFactory, icon, iconColor)
viewHolder.imageViewIcon?.assignDatabaseIcon(
database.drawFactory,
currentEntry.icon,
iconColor)
// Assign title
val showTitle = EntryVersioned.getVisualTitle(false, title, username, url, uuid.toString())
viewHolder.textViewTitle?.text = showTitle
if (displayUsername && username.isNotEmpty()) {
viewHolder.textViewSubTitle?.text = String.format("(%s)", username)
viewHolder.textViewTitle?.apply {
text = currentEntry.getVisualTitle()
strikeOut(currentEntry.isCurrentlyExpires)
}
// Assign subtitle
viewHolder.textViewSubTitle?.apply {
val entryUsername = currentEntry.username
text = if (displayUsername && entryUsername.isNotEmpty()) {
String.format("(%s)", entryUsername)
} else {
viewHolder.textViewSubTitle?.text = ""
""
}
strikeOut(currentEntry.isCurrentlyExpires)
}
}
}
@@ -110,11 +105,11 @@ class SearchEntryCursorAdapter(context: Context, private val database: Database)
}
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
return database.searchEntries(constraint.toString())
return database.searchEntries(context, constraint.toString())
}
fun getEntryFromPosition(position: Int): EntryVersioned? {
var pwEntry: EntryVersioned? = null
fun getEntryFromPosition(position: Int): Entry? {
var pwEntry: Entry? = null
val cursor = this.cursor
if (cursor.moveToFirst() && cursor.move(position)) {

View File

@@ -1,3 +1,22 @@
/*
* 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.app.database
import android.os.AsyncTask

View File

@@ -1,3 +1,22 @@
/*
* 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.app.database
import androidx.room.Database

View File

@@ -1,3 +1,22 @@
/*
* 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.app.database
import android.content.Context

View File

@@ -1,3 +1,22 @@
/*
* 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.app.database
import androidx.room.*

View File

@@ -1,5 +1,26 @@
/*
* 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.app.database
import android.os.Parcel
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@@ -15,7 +36,33 @@ data class CipherDatabaseEntity(
@ColumnInfo(name = "specs_parameters")
var specParameters: String
) {
): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString()!!,
parcel.readString()!!,
parcel.readString()!!)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(databaseUri)
parcel.writeString(encryptedValue)
parcel.writeString(specParameters)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<CipherDatabaseEntity> {
override fun createFromParcel(parcel: Parcel): CipherDatabaseEntity {
return CipherDatabaseEntity(parcel)
}
override fun newArray(size: Int): Array<CipherDatabaseEntity?> {
return arrayOfNulls(size)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

View File

@@ -1,3 +1,22 @@
/*
* 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.app.database
import androidx.room.*
@@ -19,7 +38,7 @@ interface FileDatabaseHistoryDao {
@Delete
fun delete(fileDatabaseHistory: FileDatabaseHistoryEntity): Int
@Query("REPLACE INTO file_database_history(keyfile_uri) VALUES(null)")
@Query("UPDATE file_database_history SET keyfile_uri=null")
fun deleteAllKeyFiles()
@Query("DELETE FROM file_database_history")

View File

@@ -1,3 +1,22 @@
/*
* 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.app.database
import androidx.room.ColumnInfo

View File

@@ -1,13 +1,35 @@
/*
* 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.biometric
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
import android.widget.CompoundButton
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricConstants
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity
@@ -19,12 +41,12 @@ import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
@RequiresApi(api = Build.VERSION_CODES.M)
class AdvancedUnlockedManager(var context: FragmentActivity,
var databaseFileUri: Uri,
var advancedUnlockInfoView: AdvancedUnlockInfoView?,
var checkboxPasswordView: CompoundButton?,
var onCheckedPasswordChangeListener: CompoundButton.OnCheckedChangeListener? = null,
private var advancedUnlockInfoView: AdvancedUnlockInfoView?,
private var checkboxPasswordView: CompoundButton?,
private var onCheckedPasswordChangeListener: CompoundButton.OnCheckedChangeListener? = null,
var passwordView: TextView?,
var loadDatabaseAfterRegisterCredentials: (encryptedPassword: String?, ivSpec: String?) -> Unit,
var loadDatabaseAfterRetrieveCredentials: (decryptedPassword: String?) -> Unit)
private var loadDatabaseAfterRegisterCredentials: (encryptedPassword: String?, ivSpec: String?) -> Unit,
private var loadDatabaseAfterRetrieveCredentials: (decryptedPassword: String?) -> Unit)
: BiometricUnlockDatabaseHelper.BiometricUnlockCallback {
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
@@ -34,61 +56,54 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
private var cipherDatabaseAction = CipherDatabaseAction.getInstance(context.applicationContext)
// fingerprint related code here
fun initBiometric() {
// Check if fingerprint well init (be called the first time the fingerprint is configured
// and the activity still active)
if (biometricUnlockDatabaseHelper == null || !biometricUnlockDatabaseHelper!!.isFingerprintInitialized) {
biometricUnlockDatabaseHelper = BiometricUnlockDatabaseHelper(context, this)
// callback for fingerprint findings
biometricUnlockDatabaseHelper?.setAuthenticationCallback(biometricCallback)
}
init {
// Add a check listener to change fingerprint mode
checkboxPasswordView?.setOnCheckedChangeListener { compoundButton, checked ->
checkBiometricAvailability()
// Add old listener to enable the button, only be call here because of onCheckedChange bug
onCheckedPasswordChangeListener?.onCheckedChanged(compoundButton, checked)
}
checkBiometricAvailability()
}
@Synchronized
/**
* Check biometric availability and change the current mode depending of device's state
*/
fun checkBiometricAvailability() {
// fingerprint not supported (by API level or hardware) so keep option hidden
// biometric not supported (by API level or hardware) so keep option hidden
// or manually disable
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate()
if (!PreferencesUtil.isBiometricUnlockEnable(context)
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
toggleMode(Mode.UNAVAILABLE)
} else {
// fingerprint is available but not configured, show icon but in disabled state with some information
// biometric is available but not configured, show icon but in disabled state with some information
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
toggleMode(Mode.NOT_CONFIGURED)
toggleMode(Mode.BIOMETRIC_NOT_CONFIGURED)
} else {
// Check if fingerprint well init (be called the first time the fingerprint is configured
// and the activity still active)
if (biometricUnlockDatabaseHelper?.isKeyManagerInitialized != true) {
biometricUnlockDatabaseHelper = BiometricUnlockDatabaseHelper(context)
// callback for fingerprint findings
biometricUnlockDatabaseHelper?.biometricUnlockCallback = this
biometricUnlockDatabaseHelper?.authenticationCallback = biometricAuthenticationCallback
}
// Recheck to change the mode
if (biometricUnlockDatabaseHelper?.isKeyManagerInitialized != true) {
toggleMode(Mode.KEY_MANAGER_UNAVAILABLE)
} else {
if (checkboxPasswordView?.isChecked == true) {
// listen for encryption
toggleMode(Mode.STORE)
toggleMode(Mode.STORE_CREDENTIAL)
} else {
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) {
// fingerprint available but no stored password found yet for this DB so show info don't listen
toggleMode( if (it) {
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
// biometric available but no stored password found yet for this DB so show info don't listen
toggleMode( if (containsCipher) {
// listen for decryption
Mode.OPEN
Mode.EXTRACT_CREDENTIAL
} else {
// wait for typing
Mode.WAIT_CREDENTIAL
@@ -98,6 +113,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
}
}
}
}
private fun toggleMode(newBiometricMode: Mode) {
if (newBiometricMode != biometricMode) {
@@ -106,31 +122,36 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
}
}
private val biometricCallback = object : BiometricPrompt.AuthenticationCallback () {
private val biometricAuthenticationCallback = object : BiometricPrompt.AuthenticationCallback () {
override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence) {
context.runOnUiThread {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
setAdvancedUnlockedMessageView(errString.toString())
}
}
override fun onAuthenticationFailed() {
context.runOnUiThread {
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
setAdvancedUnlockedMessageView(R.string.biometric_not_recognized)
}
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
context.runOnUiThread {
when (biometricMode) {
Mode.UNAVAILABLE -> {}
Mode.PAUSE -> {}
Mode.NOT_CONFIGURED -> {}
Mode.BIOMETRIC_NOT_CONFIGURED -> {}
Mode.KEY_MANAGER_UNAVAILABLE -> {}
Mode.WAIT_CREDENTIAL -> {}
Mode.STORE -> {
Mode.STORE_CREDENTIAL -> {
// newly store the entered password in encrypted way
biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString())
}
Mode.OPEN -> {
Mode.EXTRACT_CREDENTIAL -> {
// retrieve the encrypted value from preferences
cipherDatabaseAction.getCipherDatabase(databaseFileUri) {
it?.encryptedValue?.let { value ->
@@ -141,15 +162,12 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
}
}
}
}
private fun initNotAvailable() {
showFingerPrintViews(false)
advancedUnlockInfoView?.setIconViewClickListener(null)
}
private fun initPause() {
advancedUnlockInfoView?.setIconViewClickListener(null)
advancedUnlockInfoView?.setIconViewClickListener(false, null)
}
private fun initNotConfigured() {
@@ -157,7 +175,19 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
setAdvancedUnlockedTitleView(R.string.configure_biometric)
setAdvancedUnlockedMessageView("")
advancedUnlockInfoView?.setIconViewClickListener(null)
advancedUnlockInfoView?.setIconViewClickListener(false) {
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
}
}
private fun initKeyManagerNotAvailable() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
setAdvancedUnlockedMessageView("")
advancedUnlockInfoView?.setIconViewClickListener(false) {
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
}
}
private fun initWaitData() {
@@ -165,7 +195,19 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
setAdvancedUnlockedMessageView("")
advancedUnlockInfoView?.setIconViewClickListener(null)
advancedUnlockInfoView?.setIconViewClickListener(false) {
biometricAuthenticationCallback.onAuthenticationError(
BiometricConstants.ERROR_UNABLE_TO_PROCESS
, context.getString(R.string.credential_before_click_biometric_button))
}
}
private fun openBiometricPrompt(biometricPrompt: BiometricPrompt?,
cryptoObject: BiometricPrompt.CryptoObject,
promptInfo: BiometricPrompt.PromptInfo) {
context.runOnUiThread {
biometricPrompt?.authenticate(promptInfo, cryptoObject)
}
}
private fun initEncryptData() {
@@ -178,9 +220,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
cryptoObject?.let { crypto ->
// Set listener to open the biometric dialog and save credential
advancedUnlockInfoView?.setIconViewClickListener { _ ->
context.runOnUiThread {
biometricPrompt?.authenticate(promptInfo, crypto)
}
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
}
}
@@ -201,17 +241,13 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
cryptoObject?.let { crypto ->
// Set listener to open the biometric dialog and check credential
advancedUnlockInfoView?.setIconViewClickListener { _ ->
context.runOnUiThread {
biometricPrompt?.authenticate(promptInfo, crypto)
}
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
}
// Auto open the biometric prompt
if (isBiometricPromptAutoOpenEnable) {
isBiometricPromptAutoOpenEnable = false
context.runOnUiThread {
biometricPrompt?.authenticate(promptInfo, crypto)
}
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
}
}
@@ -225,42 +261,40 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
fun initBiometricMode() {
when (biometricMode) {
Mode.UNAVAILABLE -> initNotAvailable()
Mode.PAUSE -> initPause()
Mode.NOT_CONFIGURED -> initNotConfigured()
Mode.BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
Mode.WAIT_CREDENTIAL -> initWaitData()
Mode.STORE -> initEncryptData()
Mode.OPEN -> initDecryptData()
Mode.STORE_CREDENTIAL -> initEncryptData()
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
}
// Show fingerprint key deletion
context.invalidateOptionsMenu()
}
fun pause() {
biometricMode = Mode.PAUSE
initBiometricMode()
}
fun destroy() {
// Restore the checked listener
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
biometricMode = Mode.UNAVAILABLE
initBiometricMode()
biometricUnlockDatabaseHelper = null
}
// Only to fix multiple fingerprint menu #332
private var addBiometricMenuInProgress = false
fun inflateOptionsMenu(menuInflater: MenuInflater, menu: Menu) {
if (!addBiometricMenuInProgress) {
addBiometricMenuInProgress = true
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) {
if ((biometricMode != Mode.UNAVAILABLE
&& biometricMode != Mode.NOT_CONFIGURED) && it)
if ((biometricMode != Mode.UNAVAILABLE && biometricMode != Mode.BIOMETRIC_NOT_CONFIGURED)
&& it) {
menuInflater.inflate(R.menu.advanced_unlock, menu)
addBiometricMenuInProgress = false
}
}
}
}
fun deleteEntryKey() {
biometricUnlockDatabaseHelper?.deleteEntryKey()
cipherDatabaseAction.deleteByDatabaseUri(databaseFileUri)
biometricMode = Mode.NOT_CONFIGURED
biometricMode = Mode.BIOMETRIC_NOT_CONFIGURED
checkBiometricAvailability()
}
@@ -306,7 +340,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
}
enum class Mode {
UNAVAILABLE, PAUSE, NOT_CONFIGURED, WAIT_CREDENTIAL, STORE, OPEN
UNAVAILABLE, BIOMETRIC_NOT_CONFIGURED, KEY_MANAGER_UNAVAILABLE, WAIT_CREDENTIAL, STORE_CREDENTIAL, EXTRACT_CREDENTIAL
}
companion object {

View File

@@ -42,8 +42,7 @@ import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
@RequiresApi(api = Build.VERSION_CODES.M)
class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
private val biometricUnlockCallback: BiometricUnlockCallback?) {
class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
private var biometricPrompt: BiometricPrompt? = null
@@ -53,35 +52,48 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
private var keyguardManager: KeyguardManager? = null
private var cryptoObject: BiometricPrompt.CryptoObject? = null
private var isBiometricInit = false
private var authenticationCallback: BiometricPrompt.AuthenticationCallback? = null
private var isKeyManagerInit = false
var authenticationCallback: BiometricPrompt.AuthenticationCallback? = null
var biometricUnlockCallback: BiometricUnlockCallback? = null
private val promptInfoStoreCredential = BiometricPrompt.PromptInfo.Builder()
.setTitle(context.getString(R.string.biometric_prompt_store_credential_title))
.setDescription(context.getString(R.string.biometric_prompt_store_credential_message))
//.setDeviceCredentialAllowed(true) TODO device credential
.setNegativeButtonText(context.getString(android.R.string.cancel))
.build()
private val promptInfoStoreCredential = BiometricPrompt.PromptInfo.Builder().apply {
setTitle(context.getString(R.string.biometric_prompt_store_credential_title))
setDescription(context.getString(R.string.biometric_prompt_store_credential_message))
setConfirmationRequired(true)
// TODO device credential
/*
if (keyguardManager?.isDeviceSecure == true)
setDeviceCredentialAllowed(true)
else
*/
setNegativeButtonText(context.getString(android.R.string.cancel))
}.build()
private val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder()
.setTitle(context.getString(R.string.biometric_prompt_extract_credential_title))
.setDescription(context.getString(R.string.biometric_prompt_extract_credential_message))
//.setDeviceCredentialAllowed(true)
.setNegativeButtonText(context.getString(android.R.string.cancel))
.build()
private val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
setTitle(context.getString(R.string.biometric_prompt_extract_credential_title))
//setDescription(context.getString(R.string.biometric_prompt_extract_credential_message))
setConfirmationRequired(false)
// TODO device credential
/*
if (keyguardManager?.isDeviceSecure == true)
setDeviceCredentialAllowed(true)
else
*/
setNegativeButtonText(context.getString(android.R.string.cancel))
}.build()
val isFingerprintInitialized: Boolean
val isKeyManagerInitialized: Boolean
get() {
if (!isBiometricInit && biometricUnlockCallback != null) {
biometricUnlockCallback.onBiometricException(Exception("FingerPrint not initialized"))
if (!isKeyManagerInit) {
biometricUnlockCallback?.onBiometricException(Exception("Biometric not initialized"))
}
return isBiometricInit
return isKeyManagerInit
}
init {
if (BiometricManager.from(context).canAuthenticate() != BiometricManager.BIOMETRIC_SUCCESS) {
// really not much to do when no fingerprint support found
isBiometricInit = false
isKeyManagerInit = false
} else {
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
@@ -93,17 +105,19 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
+ BIOMETRIC_BLOCKS_MODES + "/"
+ BIOMETRIC_ENCRYPTION_PADDING)
this.cryptoObject = BiometricPrompt.CryptoObject(cipher!!)
isBiometricInit = true
isKeyManagerInit = (keyStore != null
&& keyGenerator != null
&& cipher != null)
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize the keystore", e)
isBiometricInit = false
isKeyManagerInit = false
biometricUnlockCallback?.onBiometricException(e)
}
}
}
private fun getSecretKey(): SecretKey? {
if (!isFingerprintInitialized) {
if (!isKeyManagerInitialized) {
return null
}
try {
@@ -145,7 +159,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
: (biometricPrompt: BiometricPrompt?,
cryptoObject: BiometricPrompt.CryptoObject?,
promptInfo: BiometricPrompt.PromptInfo)->Unit) {
if (!isFingerprintInitialized) {
if (!isKeyManagerInitialized) {
return
}
try {
@@ -158,7 +172,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
deleteEntryKey()
biometricUnlockCallback?.onInvalidKeyException(unrecoverableKeyException)
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
biometricUnlockCallback?.onInvalidKeyException(invalidKeyException)
@@ -166,11 +180,10 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
Log.e(TAG, "Unable to initialize encrypt data", e)
biometricUnlockCallback?.onBiometricException(e)
}
}
fun encryptData(value: String) {
if (!isFingerprintInitialized) {
if (!isKeyManagerInitialized) {
return
}
try {
@@ -187,14 +200,13 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
Log.e(TAG, "Unable to encrypt data", e)
biometricUnlockCallback?.onBiometricException(e)
}
}
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
: (biometricPrompt: BiometricPrompt?,
cryptoObject: BiometricPrompt.CryptoObject?,
promptInfo: BiometricPrompt.PromptInfo)->Unit) {
if (!isFingerprintInitialized) {
if (!isKeyManagerInitialized) {
return
}
try {
@@ -219,11 +231,10 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
Log.e(TAG, "Unable to initialize decrypt data", e)
biometricUnlockCallback?.onBiometricException(e)
}
}
fun decryptData(encryptedValue: String) {
if (!isFingerprintInitialized) {
if (!isKeyManagerInitialized) {
return
}
try {
@@ -239,7 +250,6 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
Log.e(TAG, "Unable to decrypt data", e)
biometricUnlockCallback?.onBiometricException(e)
}
}
fun deleteEntryKey() {
@@ -252,10 +262,6 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
}
}
fun setAuthenticationCallback(authenticationCallback: BiometricPrompt.AuthenticationCallback) {
this.authenticationCallback = authenticationCallback
}
@Synchronized
fun initBiometricPrompt() {
if (biometricPrompt == null) {
@@ -289,22 +295,24 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
* Remove entry key in keystore
*/
fun deleteEntryKeyInKeystoreForBiometric(context: FragmentActivity,
biometricUnlockCallback: BiometricUnlockErrorCallback) {
val fingerPrintHelper = BiometricUnlockDatabaseHelper(context, object : BiometricUnlockCallback {
biometricCallback: BiometricUnlockErrorCallback) {
BiometricUnlockDatabaseHelper(context).apply {
biometricUnlockCallback = object : BiometricUnlockCallback {
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {}
override fun handleDecryptedResult(decryptedValue: String) {}
override fun onInvalidKeyException(e: Exception) {
biometricUnlockCallback.onInvalidKeyException(e)
biometricCallback.onInvalidKeyException(e)
}
override fun onBiometricException(e: Exception) {
biometricUnlockCallback.onBiometricException(e)
biometricCallback.onBiometricException(e)
}
}
deleteEntryKey()
}
})
fingerPrintHelper.deleteEntryKey()
}
}

View File

@@ -20,43 +20,44 @@
package com.kunzisoft.keepass.biometric
import android.content.Context
import android.graphics.drawable.Animatable2
import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import androidx.annotation.RequiresApi
import android.widget.ImageView
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.kunzisoft.keepass.R
@RequiresApi(api = Build.VERSION_CODES.M)
class FingerPrintAnimatedVector(context: Context, imageView: ImageView) {
private val scanFingerprint: AnimatedVectorDrawable =
context.getDrawable(R.drawable.scan_fingerprint) as AnimatedVectorDrawable
private val scanFingerprint: AnimatedVectorDrawableCompat? =
AnimatedVectorDrawableCompat.create(context, R.drawable.scan_fingerprint)
init {
imageView.setImageDrawable(scanFingerprint)
}
private var animationCallback = object : Animatable2.AnimationCallback() {
private var animationCallback = object : Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable) {
if (!scanFingerprint.isRunning)
scanFingerprint.start()
imageView.post {
scanFingerprint?.start()
}
}
}
fun startScan() {
scanFingerprint.registerAnimationCallback(animationCallback)
scanFingerprint?.registerAnimationCallback(animationCallback)
if (!scanFingerprint.isRunning)
scanFingerprint.start()
if (scanFingerprint?.isRunning != true)
scanFingerprint?.start()
}
fun stopScan() {
scanFingerprint.unregisterAnimationCallback(animationCallback)
scanFingerprint?.unregisterAnimationCallback(animationCallback)
if (scanFingerprint.isRunning)
if (scanFingerprint?.isRunning == true)
scanFingerprint.stop()
}
}

View File

@@ -19,15 +19,13 @@
*/
package com.kunzisoft.keepass.crypto
import com.kunzisoft.keepass.stream.LEDataOutputStream
import com.kunzisoft.keepass.stream.NullOutputStream
import com.kunzisoft.keepass.stream.longTo8Bytes
import java.io.IOException
import java.security.DigestOutputStream
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.Arrays
import java.util.*
import javax.crypto.Mac
import kotlin.math.min
@@ -60,7 +58,7 @@ object CryptoUtil {
throw RuntimeException(e)
}
val pbR = LEDataOutputStream.writeLongBuf(r)
val pbR = longTo8Bytes(r)
val part = hmac.doFinal(pbR)
val copy = min(cbOut - pos, part.size)

View File

@@ -21,8 +21,8 @@ package com.kunzisoft.keepass.crypto.engine
import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.database.element.PwEncryptionAlgorithm
import com.kunzisoft.keepass.utils.Types
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.stream.bytes16ToUuid
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
@@ -41,13 +41,28 @@ class AesEngine : CipherEngine() {
return cipher
}
override fun getPwEncryptionAlgorithm(): PwEncryptionAlgorithm {
return PwEncryptionAlgorithm.AESRijndael
override fun getPwEncryptionAlgorithm(): EncryptionAlgorithm {
return EncryptionAlgorithm.AESRijndael
}
companion object {
val CIPHER_UUID: UUID = Types.bytestoUUID(
byteArrayOf(0x31.toByte(), 0xC1.toByte(), 0xF2.toByte(), 0xE6.toByte(), 0xBF.toByte(), 0x71.toByte(), 0x43.toByte(), 0x50.toByte(), 0xBE.toByte(), 0x58.toByte(), 0x05.toByte(), 0x21.toByte(), 0x6A.toByte(), 0xFC.toByte(), 0x5A.toByte(), 0xFF.toByte()))
val CIPHER_UUID: UUID = bytes16ToUuid(
byteArrayOf(0x31.toByte(),
0xC1.toByte(),
0xF2.toByte(),
0xE6.toByte(),
0xBF.toByte(),
0x71.toByte(),
0x43.toByte(),
0x50.toByte(),
0xBE.toByte(),
0x58.toByte(),
0x05.toByte(),
0x21.toByte(),
0x6A.toByte(),
0xFC.toByte(),
0x5A.toByte(),
0xFF.toByte()))
}
}

View File

@@ -19,8 +19,8 @@
*/
package com.kunzisoft.keepass.crypto.engine
import com.kunzisoft.keepass.database.element.PwEncryptionAlgorithm
import com.kunzisoft.keepass.utils.Types
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.stream.bytes16ToUuid
import org.spongycastle.jce.provider.BouncyCastleProvider
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
@@ -44,13 +44,28 @@ class ChaCha20Engine : CipherEngine() {
return cipher
}
override fun getPwEncryptionAlgorithm(): PwEncryptionAlgorithm {
return PwEncryptionAlgorithm.ChaCha20
override fun getPwEncryptionAlgorithm(): EncryptionAlgorithm {
return EncryptionAlgorithm.ChaCha20
}
companion object {
val CIPHER_UUID: UUID = Types.bytestoUUID(
byteArrayOf(0xD6.toByte(), 0x03.toByte(), 0x8A.toByte(), 0x2B.toByte(), 0x8B.toByte(), 0x6F.toByte(), 0x4C.toByte(), 0xB5.toByte(), 0xA5.toByte(), 0x24.toByte(), 0x33.toByte(), 0x9A.toByte(), 0x31.toByte(), 0xDB.toByte(), 0xB5.toByte(), 0x9A.toByte()))
val CIPHER_UUID: UUID = bytes16ToUuid(
byteArrayOf(0xD6.toByte(),
0x03.toByte(),
0x8A.toByte(),
0x2B.toByte(),
0x8B.toByte(),
0x6F.toByte(),
0x4C.toByte(),
0xB5.toByte(),
0xA5.toByte(),
0x24.toByte(),
0x33.toByte(),
0x9A.toByte(),
0x31.toByte(),
0xDB.toByte(),
0xB5.toByte(),
0x9A.toByte()))
}
}

View File

@@ -19,7 +19,7 @@
*/
package com.kunzisoft.keepass.crypto.engine
import com.kunzisoft.keepass.database.element.PwEncryptionAlgorithm
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
@@ -46,6 +46,6 @@ abstract class CipherEngine {
return getCipher(opmode, key, IV, false)
}
abstract fun getPwEncryptionAlgorithm(): PwEncryptionAlgorithm
abstract fun getPwEncryptionAlgorithm(): EncryptionAlgorithm
}

View File

@@ -20,14 +20,12 @@
package com.kunzisoft.keepass.crypto.engine
import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.database.element.PwEncryptionAlgorithm
import com.kunzisoft.keepass.utils.Types
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.stream.bytes16ToUuid
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.util.UUID
import java.util.*
import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.IvParameterSpec
@@ -47,13 +45,28 @@ class TwofishEngine : CipherEngine() {
return cipher
}
override fun getPwEncryptionAlgorithm(): PwEncryptionAlgorithm {
return PwEncryptionAlgorithm.Twofish
override fun getPwEncryptionAlgorithm(): EncryptionAlgorithm {
return EncryptionAlgorithm.Twofish
}
companion object {
val CIPHER_UUID: UUID = Types.bytestoUUID(
byteArrayOf(0xAD.toByte(), 0x68.toByte(), 0xF2.toByte(), 0x9F.toByte(), 0x57.toByte(), 0x6F.toByte(), 0x4B.toByte(), 0xB9.toByte(), 0xA3.toByte(), 0x6A.toByte(), 0xD4.toByte(), 0x7A.toByte(), 0xF9.toByte(), 0x65.toByte(), 0x34.toByte(), 0x6C.toByte()))
val CIPHER_UUID: UUID = bytes16ToUuid(
byteArrayOf(0xAD.toByte(),
0x68.toByte(),
0xF2.toByte(),
0x9F.toByte(),
0x57.toByte(),
0x6F.toByte(),
0x4B.toByte(),
0xB9.toByte(),
0xA3.toByte(),
0x6A.toByte(),
0xD4.toByte(),
0x7A.toByte(),
0xF9.toByte(),
0x65.toByte(),
0x34.toByte(),
0x6C.toByte()))
}
}

View File

@@ -23,7 +23,7 @@ import android.content.res.Resources
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.CryptoUtil
import com.kunzisoft.keepass.crypto.finalkey.FinalKeyFactory
import com.kunzisoft.keepass.utils.Types
import com.kunzisoft.keepass.stream.bytes16ToUuid
import java.io.IOException
import java.security.SecureRandom
import java.util.*
@@ -32,12 +32,10 @@ class AesKdf internal constructor() : KdfEngine() {
override val defaultParameters: KdfParameters
get() {
val p = KdfParameters(uuid)
p.setParamUUID()
p.setUInt32(ParamRounds, DEFAULT_ROUNDS.toLong())
return p
return KdfParameters(uuid!!).apply {
setParamUUID()
setUInt32(PARAM_ROUNDS, DEFAULT_ROUNDS.toLong())
}
}
override val defaultKeyRounds: Long
@@ -54,8 +52,8 @@ class AesKdf internal constructor() : KdfEngine() {
@Throws(IOException::class)
override fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray {
var currentMasterKey = masterKey
val rounds = p.getUInt64(ParamRounds)
var seed = p.getByteArray(ParamSeed)
val rounds = p.getUInt64(PARAM_ROUNDS)
var seed = p.getByteArray(PARAM_SEED)
if (currentMasterKey.size != 32) {
currentMasterKey = CryptoUtil.hashSha256(currentMasterKey)
@@ -75,25 +73,40 @@ class AesKdf internal constructor() : KdfEngine() {
val seed = ByteArray(32)
random.nextBytes(seed)
p.setByteArray(ParamSeed, seed)
p.setByteArray(PARAM_SEED, seed)
}
override fun getKeyRounds(p: KdfParameters): Long {
return p.getUInt64(ParamRounds)
return p.getUInt64(PARAM_ROUNDS)
}
override fun setKeyRounds(p: KdfParameters, keyRounds: Long) {
p.setUInt64(ParamRounds, keyRounds)
p.setUInt64(PARAM_ROUNDS, keyRounds)
}
companion object {
private const val DEFAULT_ROUNDS = 6000
val CIPHER_UUID: UUID = Types.bytestoUUID(
byteArrayOf(0xC9.toByte(), 0xD9.toByte(), 0xF3.toByte(), 0x9A.toByte(), 0x62.toByte(), 0x8A.toByte(), 0x44.toByte(), 0x60.toByte(), 0xBF.toByte(), 0x74.toByte(), 0x0D.toByte(), 0x08.toByte(), 0xC1.toByte(), 0x8A.toByte(), 0x4F.toByte(), 0xEA.toByte()))
val CIPHER_UUID: UUID = bytes16ToUuid(
byteArrayOf(0xC9.toByte(),
0xD9.toByte(),
0xF3.toByte(),
0x9A.toByte(),
0x62.toByte(),
0x8A.toByte(),
0x44.toByte(),
0x60.toByte(),
0xBF.toByte(),
0x74.toByte(),
0x0D.toByte(),
0x08.toByte(),
0xC1.toByte(),
0x8A.toByte(),
0x4F.toByte(),
0xEA.toByte()))
const val ParamRounds = "R"
const val ParamSeed = "S"
const val PARAM_ROUNDS = "R"
const val PARAM_SEED = "S"
}
}

View File

@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.crypto.keyDerivation
import android.content.res.Resources
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.Types
import com.kunzisoft.keepass.stream.bytes16ToUuid
import java.io.IOException
import java.security.SecureRandom
import java.util.*
@@ -30,19 +30,19 @@ class Argon2Kdf internal constructor() : KdfEngine() {
override val defaultParameters: KdfParameters
get() {
val p = KdfParameters(uuid)
val p = KdfParameters(uuid!!)
p.setParamUUID()
p.setUInt32(ParamParallelism, DefaultParallelism)
p.setUInt64(ParamMemory, DefaultMemory)
p.setUInt64(ParamIterations, DefaultIterations)
p.setUInt32(ParamVersion, MaxVersion)
p.setUInt32(PARAM_PARALLELISM, DEFAULT_PARALLELISM)
p.setUInt64(PARAM_MEMORY, DEFAULT_MEMORY)
p.setUInt64(PARAM_ITERATIONS, DEFAULT_ITERATIONS)
p.setUInt32(PARAM_VERSION, MAX_VERSION)
return p
}
override val defaultKeyRounds: Long
get() = DefaultIterations
get() = DEFAULT_ITERATIONS
init {
uuid = CIPHER_UUID
@@ -55,13 +55,13 @@ class Argon2Kdf internal constructor() : KdfEngine() {
@Throws(IOException::class)
override fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray {
val salt = p.getByteArray(ParamSalt)
val parallelism = p.getUInt32(ParamParallelism).toInt()
val memory = p.getUInt64(ParamMemory)
val iterations = p.getUInt64(ParamIterations)
val version = p.getUInt32(ParamVersion)
val secretKey = p.getByteArray(ParamSecretKey)
val assocData = p.getByteArray(ParamAssocData)
val salt = p.getByteArray(PARAM_SALT)
val parallelism = p.getUInt32(PARAM_PARALLELISM).toInt()
val memory = p.getUInt64(PARAM_MEMORY)
val iterations = p.getUInt64(PARAM_ITERATIONS)
val version = p.getUInt32(PARAM_VERSION)
val secretKey = p.getByteArray(PARAM_SECRET_KEY)
val assocData = p.getByteArray(PARAM_ASSOC_DATA)
return Argon2Native.transformKey(masterKey, salt, parallelism, memory, iterations,
secretKey, assocData, version)
@@ -73,71 +73,102 @@ class Argon2Kdf internal constructor() : KdfEngine() {
val salt = ByteArray(32)
random.nextBytes(salt)
p.setByteArray(ParamSalt, salt)
p.setByteArray(PARAM_SALT, salt)
}
override fun getKeyRounds(p: KdfParameters): Long {
return p.getUInt64(ParamIterations)
return p.getUInt64(PARAM_ITERATIONS)
}
override fun setKeyRounds(p: KdfParameters, keyRounds: Long) {
p.setUInt64(ParamIterations, keyRounds)
p.setUInt64(PARAM_ITERATIONS, keyRounds)
}
override val minKeyRounds: Long
get() = MIN_ITERATIONS
override val maxKeyRounds: Long
get() = MAX_ITERATIONS
override fun getMemoryUsage(p: KdfParameters): Long {
return p.getUInt64(ParamMemory)
return p.getUInt64(PARAM_MEMORY)
}
override fun setMemoryUsage(p: KdfParameters, memory: Long) {
p.setUInt64(ParamMemory, memory)
p.setUInt64(PARAM_MEMORY, memory)
}
override fun getDefaultMemoryUsage(): Long {
return DefaultMemory
}
override val defaultMemoryUsage: Long
get() = DEFAULT_MEMORY
override val minMemoryUsage: Long
get() = MIN_MEMORY
override val maxMemoryUsage: Long
get() = MAX_MEMORY
override fun getParallelism(p: KdfParameters): Int {
return p.getUInt32(ParamParallelism).toInt() // TODO Verify
return p.getUInt32(PARAM_PARALLELISM).toInt() // TODO Verify
}
override fun setParallelism(p: KdfParameters, parallelism: Int) {
p.setUInt32(ParamParallelism, parallelism.toLong())
p.setUInt32(PARAM_PARALLELISM, parallelism.toLong())
}
override fun getDefaultParallelism(): Int {
return DefaultParallelism.toInt() // TODO Verify
}
override val defaultParallelism: Int
get() = DEFAULT_PARALLELISM.toInt()
override val minParallelism: Int
get() = MIN_PARALLELISM
override val maxParallelism: Int
get() = MAX_PARALLELISM
companion object {
val CIPHER_UUID: UUID = Types.bytestoUUID(
byteArrayOf(0xEF.toByte(), 0x63.toByte(), 0x6D.toByte(), 0xDF.toByte(), 0x8C.toByte(), 0x29.toByte(), 0x44.toByte(), 0x4B.toByte(), 0x91.toByte(), 0xF7.toByte(), 0xA9.toByte(), 0xA4.toByte(), 0x03.toByte(), 0xE3.toByte(), 0x0A.toByte(), 0x0C.toByte()))
val CIPHER_UUID: UUID = bytes16ToUuid(
byteArrayOf(0xEF.toByte(),
0x63.toByte(),
0x6D.toByte(),
0xDF.toByte(),
0x8C.toByte(),
0x29.toByte(),
0x44.toByte(),
0x4B.toByte(),
0x91.toByte(),
0xF7.toByte(),
0xA9.toByte(),
0xA4.toByte(),
0x03.toByte(),
0xE3.toByte(),
0x0A.toByte(),
0x0C.toByte()))
private const val ParamSalt = "S" // byte[]
private const val ParamParallelism = "P" // UInt32
private const val ParamMemory = "M" // UInt64
private const val ParamIterations = "I" // UInt64
private const val ParamVersion = "V" // UInt32
private const val ParamSecretKey = "K" // byte[]
private const val ParamAssocData = "A" // byte[]
private const val PARAM_SALT = "S" // byte[]
private const val PARAM_PARALLELISM = "P" // UInt32
private const val PARAM_MEMORY = "M" // UInt64
private const val PARAM_ITERATIONS = "I" // UInt64
private const val PARAM_VERSION = "V" // UInt32
private const val PARAM_SECRET_KEY = "K" // byte[]
private const val PARAM_ASSOC_DATA = "A" // byte[]
private const val MinVersion: Long = 0x10
private const val MaxVersion: Long = 0x13
private const val MIN_VERSION: Long = 0x10
private const val MAX_VERSION: Long = 0x13
private const val MinSalt = 8
private const val MaxSalt = Integer.MAX_VALUE
private const val MIN_SALT = 8
private const val MAX_SALT = Integer.MAX_VALUE
private const val MinIterations: Long = 1
private const val MaxIterations = 4294967295L
private const val MIN_ITERATIONS: Long = 1
private const val MAX_ITERATIONS = 4294967295L
private const val MinMemory = (1024 * 8).toLong()
private const val MaxMemory = Integer.MAX_VALUE.toLong()
private const val MIN_MEMORY = (1024 * 8).toLong()
private const val MAX_MEMORY = Integer.MAX_VALUE.toLong()
private const val MinParallelism = 1
private const val MaxParallelism = (1 shl 24) - 1
private const val MIN_PARALLELISM = 1
private const val MAX_PARALLELISM = (1 shl 24) - 1
private const val DefaultIterations: Long = 2
private const val DefaultMemory = (1024 * 1024).toLong()
private const val DefaultParallelism: Long = 2
private const val DEFAULT_ITERATIONS: Long = 2
private const val DEFAULT_MEMORY = (1024 * 1024).toLong()
private const val DEFAULT_PARALLELISM: Long = 2
}
}

View File

@@ -19,28 +19,44 @@
*/
package com.kunzisoft.keepass.crypto.keyDerivation
import com.kunzisoft.keepass.database.ObjectNameResource
import com.kunzisoft.keepass.utils.ObjectNameResource
import java.io.IOException
import java.io.Serializable
import java.util.UUID
abstract class KdfEngine : ObjectNameResource {
// TODO Parcelable
abstract class KdfEngine : ObjectNameResource, Serializable {
var uuid: UUID? = null
abstract val defaultParameters: KdfParameters
abstract val defaultKeyRounds: Long
@Throws(IOException::class)
abstract fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray
abstract fun randomize(p: KdfParameters)
/*
* ITERATIONS
*/
abstract fun getKeyRounds(p: KdfParameters): Long
abstract fun setKeyRounds(p: KdfParameters, keyRounds: Long)
abstract val defaultKeyRounds: Long
open val minKeyRounds: Long
get() = 1
open val maxKeyRounds: Long
get() = Int.MAX_VALUE.toLong()
/*
* MEMORY
*/
open fun getMemoryUsage(p: KdfParameters): Long {
return UNKNOWN_VALUE.toLong()
}
@@ -49,9 +65,18 @@ abstract class KdfEngine : ObjectNameResource {
// Do nothing by default
}
open fun getDefaultMemoryUsage(): Long {
return UNKNOWN_VALUE.toLong()
}
open val defaultMemoryUsage: Long
get() = UNKNOWN_VALUE.toLong()
open val minMemoryUsage: Long
get() = 1
open val maxMemoryUsage: Long
get() = Int.MAX_VALUE.toLong()
/*
* PARALLELISM
*/
open fun getParallelism(p: KdfParameters): Int {
return UNKNOWN_VALUE
@@ -61,13 +86,16 @@ abstract class KdfEngine : ObjectNameResource {
// Do nothing by default
}
open fun getDefaultParallelism(): Int {
return UNKNOWN_VALUE
}
open val defaultParallelism: Int
get() = UNKNOWN_VALUE
open val minParallelism: Int
get() = 1
open val maxParallelism: Int
get() = Int.MAX_VALUE
companion object {
const val UNKNOWN_VALUE = -1
const val UNKNOWN_VALUE_STRING = (-1).toString()
}
}

View File

@@ -19,37 +19,7 @@
*/
package com.kunzisoft.keepass.crypto.keyDerivation
import com.kunzisoft.keepass.database.exception.UnknownKDF
import java.util.ArrayList
object KdfFactory {
var aesKdf = AesKdf()
var argon2Kdf = Argon2Kdf()
var kdfListV3: MutableList<KdfEngine> = ArrayList()
var kdfListV4: MutableList<KdfEngine> = ArrayList()
init {
kdfListV3.add(aesKdf)
kdfListV4.add(aesKdf)
kdfListV4.add(argon2Kdf)
}
@Throws(UnknownKDF::class)
fun getEngineV4(kdfParameters: KdfParameters?): KdfEngine {
val unknownKDFException = UnknownKDF()
if (kdfParameters == null) {
throw unknownKDFException
}
for (engine in kdfListV4) {
if (engine.uuid == kdfParameters.uuid) {
return engine
}
}
throw unknownKDFException
}
}

View File

@@ -1,75 +0,0 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.crypto.keyDerivation;
import com.kunzisoft.keepass.utils.VariantDictionary;
import com.kunzisoft.keepass.stream.LEDataInputStream;
import com.kunzisoft.keepass.stream.LEDataOutputStream;
import com.kunzisoft.keepass.utils.Types;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.UUID;
public class KdfParameters extends VariantDictionary {
private UUID kdfUUID;
private static final String ParamUUID = "$UUID";
KdfParameters(UUID uuid) {
kdfUUID = uuid;
}
public UUID getUUID() {
return kdfUUID;
}
protected void setParamUUID() {
setByteArray(ParamUUID, Types.UUIDtoBytes(kdfUUID));
}
public static KdfParameters deserialize(byte[] data) throws IOException {
ByteArrayInputStream bis = new ByteArrayInputStream(data);
LEDataInputStream lis = new LEDataInputStream(bis);
VariantDictionary d = VariantDictionary.deserialize(lis);
if (d == null) {
return null;
}
UUID uuid = Types.bytestoUUID(d.getByteArray(ParamUUID));
KdfParameters kdfP = new KdfParameters(uuid);
kdfP.copyTo(d);
return kdfP;
}
public static byte[] serialize(KdfParameters kdf) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
LEDataOutputStream los = new LEDataOutputStream(bos);
KdfParameters.serialize(kdf, los);
return bos.toByteArray();
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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.crypto.keyDerivation
import com.kunzisoft.keepass.stream.LittleEndianDataInputStream
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
import com.kunzisoft.keepass.stream.bytes16ToUuid
import com.kunzisoft.keepass.stream.uuidTo16Bytes
import com.kunzisoft.keepass.utils.VariantDictionary
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.*
class KdfParameters internal constructor(val uuid: UUID) : VariantDictionary() {
fun setParamUUID() {
setByteArray(PARAM_UUID, uuidTo16Bytes(uuid))
}
companion object {
private const val PARAM_UUID = "\$UUID"
@Throws(IOException::class)
fun deserialize(data: ByteArray): KdfParameters? {
val bis = ByteArrayInputStream(data)
val lis = LittleEndianDataInputStream(bis)
val d = deserialize(lis) ?: return null
val uuid = bytes16ToUuid(d.getByteArray(PARAM_UUID))
val kdfP = KdfParameters(uuid)
kdfP.copyTo(d)
return kdfP
}
@Throws(IOException::class)
fun serialize(kdf: KdfParameters): ByteArray {
val bos = ByteArrayOutputStream()
val los = LittleEndianDataOutputStream(bos)
serialize(kdf, los)
return bos.toByteArray()
}
}
}

View File

@@ -21,25 +21,22 @@ package com.kunzisoft.keepass.database.action
import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.UriUtil
import java.io.IOException
open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
open class AssignPasswordInDatabaseRunnable (
context: Context,
database: Database,
protected val mDatabaseUri: Uri,
withMasterPassword: Boolean,
masterPassword: String?,
withKeyFile: Boolean,
keyFile: Uri?,
save: Boolean,
actionRunnable: ActionRunnable? = null)
: SaveDatabaseRunnable(context, database, save, actionRunnable) {
keyFile: Uri?)
: SaveDatabaseRunnable(context, database, true) {
private var mMasterPassword: String? = null
private var mKeyFile: Uri? = null
protected var mKeyFile: Uri? = null
private var mBackupKey: ByteArray? = null
@@ -50,7 +47,7 @@ open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
this.mKeyFile = keyFile
}
override fun run() {
override fun onStartRun() {
// Set key
try {
// TODO move master key methods
@@ -59,20 +56,21 @@ open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFile)
database.retrieveMasterKey(mMasterPassword, uriInputStream)
// To save the database
super.run()
finishRun(true)
} catch (e: InvalidKeyFileException) {
} catch (e: Exception) {
erase(mBackupKey)
finishRun(false, e.message)
} catch (e: IOException) {
erase(mBackupKey)
finishRun(false, e.message)
}
setError(e)
}
override fun onFinishRun(result: Result) {
super.onStartRun()
}
override fun onFinishRun() {
super.onFinishRun()
// Erase the biometric
CipherDatabaseAction.getInstance(context)
.deleteByDatabaseUri(mDatabaseUri)
if (!result.isSuccess) {
// Erase the current master key
erase(database.masterKey)
@@ -80,8 +78,6 @@ open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
database.masterKey = it
}
}
super.onFinishRun(result)
}
/**

View File

@@ -21,38 +21,46 @@ package com.kunzisoft.keepass.database.action
import android.content.Context
import android.net.Uri
import android.util.Log
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.tasks.ActionRunnable
class CreateDatabaseRunnable(context: Context,
private val mDatabaseUri: Uri,
private val mDatabase: Database,
databaseUri: Uri,
private val databaseName: String,
private val rootName: String,
withMasterPassword: Boolean,
masterPassword: String?,
withKeyFile: Boolean,
keyFile: Uri?,
save: Boolean,
actionRunnable: ActionRunnable? = null)
: AssignPasswordInDatabaseRunnable(context, mDatabase, withMasterPassword, masterPassword, withKeyFile, keyFile, save, actionRunnable) {
keyFile: Uri?)
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile) {
override fun run() {
override fun onStartRun() {
try {
// Create new database record
mDatabase.apply {
createData(mDatabaseUri)
createData(mDatabaseUri, databaseName, rootName)
// Set Database state
loaded = true
// Commit changes
super.run()
}
finishRun(true)
} catch (e: Exception) {
mDatabase.closeAndClear()
finishRun(false, e.message)
}
setError(e)
}
override fun onFinishRun(result: Result) {}
super.onStartRun()
}
override fun onFinishRun() {
super.onFinishRun()
if (result.isSuccess) {
// Add database to recent files
FileDatabaseHistoryAction.getInstance(context.applicationContext)
.addOrUpdateDatabaseUri(mDatabaseUri, mKeyFile)
} else {
Log.e("CreateDatabaseRunnable", "Unable to create the database")
}
}
}

View File

@@ -21,116 +21,79 @@ package com.kunzisoft.keepass.database.action
import android.content.Context
import android.net.Uri
import android.preference.PreferenceManager
import androidx.annotation.StringRes
import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import java.io.FileNotFoundException
import java.io.IOException
import java.lang.ref.WeakReference
class LoadDatabaseRunnable(private val mWeakContext: WeakReference<Context>,
class LoadDatabaseRunnable(private val context: Context,
private val mDatabase: Database,
private val mUri: Uri,
private val mPass: String?,
private val mKey: Uri?,
private val mReadonly: Boolean,
private val mCipherEntity: CipherDatabaseEntity?,
private val mOmitBackup: Boolean,
private val mFixDuplicateUUID: Boolean,
private val progressTaskUpdater: ProgressTaskUpdater?,
nestedAction: ActionRunnable)
: ActionRunnable(nestedAction, executeNestedActionIfResultFalse = true) {
private val mDuplicateUuidAction: ((Result) -> Unit)?)
: ActionRunnable() {
private val mRememberKeyFile: Boolean
get() {
return mWeakContext.get()?.let {
PreferenceManager.getDefaultSharedPreferences(it)
.getBoolean(it.getString(R.string.keyfile_key),
it.resources.getBoolean(R.bool.keyfile_default))
} ?: true
private val cacheDirectory = context.applicationContext.filesDir
override fun onStartRun() {
// Clear before we load
mDatabase.closeAndClear(cacheDirectory)
}
override fun run() {
override fun onActionRun() {
try {
mWeakContext.get()?.let {
mDatabase.loadData(it, mUri, mPass, mKey, progressTaskUpdater)
saveFileData(mUri, mKey)
finishRun(true)
} ?: finishRun(false, "Context null")
} catch (e: ArcFourException) {
catchError(e, R.string.error_arc4)
return
} catch (e: InvalidPasswordException) {
catchError(e, R.string.invalid_password)
return
} catch (e: ContentFileNotFoundException) {
catchError(e, R.string.file_not_found_content)
return
} catch (e: FileNotFoundException) {
catchError(e, R.string.file_not_found)
return
} catch (e: IOException) {
var messageId = R.string.error_load_database
e.message?.let {
if (it.contains("Hash failed with code"))
messageId = R.string.error_load_database_KDF_memory
mDatabase.loadData(mUri, mPass, mKey,
mReadonly,
context.contentResolver,
cacheDirectory,
mOmitBackup,
mFixDuplicateUUID,
progressTaskUpdater)
}
catchError(e, messageId, true)
return
} catch (e: KeyFileEmptyException) {
catchError(e, R.string.keyfile_is_empty)
return
} catch (e: InvalidAlgorithmException) {
catchError(e, R.string.invalid_algorithm)
return
} catch (e: InvalidKeyFileException) {
catchError(e, R.string.keyfile_does_not_exist)
return
} catch (e: InvalidDBSignatureException) {
catchError(e, R.string.invalid_db_sig)
return
} catch (e: InvalidDBVersionException) {
catchError(e, R.string.unsupported_db_version)
return
} catch (e: InvalidDBException) {
catchError(e, R.string.error_invalid_db)
return
} catch (e: OutOfMemoryError) {
catchError(e, R.string.error_out_of_memory)
return
} catch (e: Exception) {
catchError(e, R.string.error_load_database, true)
return
catch (e: DuplicateUuidDatabaseException) {
mDuplicateUuidAction?.invoke(result)
setError(e)
}
catch (e: LoadDatabaseException) {
setError(e)
}
}
private fun catchError(e: Throwable, @StringRes messageId: Int, addThrowableMessage: Boolean = false) {
var errorMessage = mWeakContext.get()?.getString(messageId)
Log.e(TAG, errorMessage, e)
if (addThrowableMessage)
errorMessage = errorMessage + " " + e.localizedMessage
finishRun(false, errorMessage)
override fun onFinishRun() {
if (result.isSuccess) {
// Save keyFile in app database
val rememberKeyFile = PreferencesUtil.rememberKeyFiles(context)
if (rememberKeyFile) {
var keyUri = mKey
if (!rememberKeyFile) {
keyUri = null
}
FileDatabaseHistoryAction.getInstance(context)
.addOrUpdateDatabaseUri(mUri, keyUri)
}
private fun saveFileData(uri: Uri, key: Uri?) {
var keyFileUri = key
if (!mRememberKeyFile) {
keyFileUri = null
}
mWeakContext.get()?.let {
FileDatabaseHistoryAction.getInstance(it).addOrUpdateDatabaseUri(uri, keyFileUri)
}
// Register the biometric
mCipherEntity?.let { cipherDatabaseEntity ->
CipherDatabaseAction.getInstance(context)
.addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called
}
override fun onFinishRun(result: Result) {
if (!result.isSuccess) {
mDatabase.closeAndClear(mWeakContext.get()?.filesDir)
// Start the opening notification
DatabaseOpenNotificationService.startIfAllowed(context)
} else {
mDatabase.closeAndClear(cacheDirectory)
}
}
companion object {
private val TAG = LoadDatabaseRunnable::class.java.name
}
}

View File

@@ -1,14 +0,0 @@
package com.kunzisoft.keepass.database.action
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
class ProgressDialogSaveDatabaseThread(activity: FragmentActivity,
actionRunnable: (ProgressTaskUpdater?)-> ActionRunnable)
: ProgressDialogThread(activity,
actionRunnable,
R.string.saving_database,
null,
R.string.do_not_kill_app)

View File

@@ -1,86 +1,557 @@
/*
* 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.action
import android.content.Intent
import android.os.AsyncTask
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 androidx.annotation.StringRes
import android.os.Bundle
import android.os.IBinder
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_TASK_TITLE_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
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_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
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COMPRESSION_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DESCRIPTION_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENCRYPTION_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ITERATIONS_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.retrieveProgressDialog
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 java.util.*
import kotlin.collections.ArrayList
open class ProgressDialogThread(private val activity: FragmentActivity,
private val actionRunnable: (ProgressTaskUpdater?)-> ActionRunnable,
@StringRes private val titleId: Int,
@StringRes private val messageId: Int? = null,
@StringRes private val warningId: Int? = null) {
class ProgressDialogThread(private val activity: FragmentActivity) {
private val progressTaskDialogFragment = ProgressTaskDialogFragment.build(
titleId,
messageId,
warningId)
private var actionRunnableAsyncTask: ActionRunnableAsyncTask? = null
var actionFinishInUIThread: ActionRunnable? = null
var onActionFinish: ((actionTask: String,
result: ActionRunnable.Result) -> Unit)? = null
private var intentDatabaseTask:Intent = Intent(activity, DatabaseTaskNotificationService::class.java)
private var intentDatabaseTask = Intent(activity, DatabaseTaskNotificationService::class.java)
init {
actionRunnableAsyncTask = ActionRunnableAsyncTask(progressTaskDialogFragment,
{
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
private var serviceConnection: ServiceConnection? = null
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
TimeoutHelper.temporarilyDisableTimeout(activity)
startOrUpdateDialog(titleId, messageId, warningId)
}
override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) {
TimeoutHelper.temporarilyDisableTimeout(activity)
startOrUpdateDialog(titleId, messageId, warningId)
}
override fun onStopAction(actionTask: String, result: ActionRunnable.Result) {
onActionFinish?.invoke(actionTask, result)
// Remove the progress task
ProgressTaskDialogFragment.stop(activity)
TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(activity)
}
}
private fun startOrUpdateDialog(titleId: Int?, messageId: Int?, warningId: Int?) {
var progressTaskDialogFragment = retrieveProgressDialog(activity)
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = ProgressTaskDialogFragment.build()
ProgressTaskDialogFragment.start(activity, progressTaskDialogFragment)
}
progressTaskDialogFragment.apply {
titleId?.let {
updateTitle(it)
}
messageId?.let {
updateMessage(it)
}
warningId?.let {
updateWarning(it)
}
}
}
@Synchronized
private fun initServiceConnection() {
if (serviceConnection == null) {
serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder).apply {
addActionTaskListener(actionTaskListener)
getService().checkAction()
}
}
override fun onServiceDisconnected(name: ComponentName?) {
mBinder?.removeActionTaskListener(actionTaskListener)
mBinder = null
}
}
}
}
@Synchronized
private fun bindService() {
initServiceConnection()
serviceConnection?.let {
activity.bindService(intentDatabaseTask, it, BIND_NOT_FOREGROUND or BIND_ABOVE_CLIENT)
}
}
/**
* Unbind the service and assign null to the service connection to check if already unbind or not
*/
@Synchronized
private fun unBindService() {
serviceConnection?.let {
activity.unbindService(it)
}
serviceConnection = null
}
@Synchronized
fun registerProgressTask() {
ProgressTaskDialogFragment.stop(activity)
// Register a database task receiver to stop loading dialog when service finish the task
databaseTaskBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
activity.runOnUiThread {
intentDatabaseTask.putExtra(DATABASE_TASK_TITLE_KEY, titleId)
when (intent?.action) {
DATABASE_START_TASK_ACTION -> {
// Bind to the service when is starting
bindService()
}
DATABASE_STOP_TASK_ACTION -> {
unBindService()
}
}
}
}
}
activity.registerReceiver(databaseTaskBroadcastReceiver,
IntentFilter().apply {
addAction(DATABASE_START_TASK_ACTION)
addAction(DATABASE_STOP_TASK_ACTION)
}
)
// Check if a service is currently running else do nothing
bindService()
}
@Synchronized
fun unregisterProgressTask() {
ProgressTaskDialogFragment.stop(activity)
mBinder?.removeActionTaskListener(actionTaskListener)
mBinder = null
unBindService()
try {
activity.unregisterReceiver(databaseTaskBroadcastReceiver)
} catch (e: IllegalArgumentException) {
// If receiver not register, do nothing
}
}
@Synchronized
private fun start(bundle: Bundle? = null, actionTask: String) {
activity.stopService(intentDatabaseTask)
if (bundle != null)
intentDatabaseTask.putExtras(bundle)
activity.runOnUiThread {
intentDatabaseTask.action = actionTask
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.startForegroundService(intentDatabaseTask)
} else {
activity.startService(intentDatabaseTask)
}
TimeoutHelper.temporarilyDisableTimeout()
// Show the dialog
ProgressTaskDialogFragment.start(activity, progressTaskDialogFragment)
}
}, { result ->
activity.runOnUiThread {
actionFinishInUIThread?.onFinishRun(result)
// Remove the progress task
ProgressTaskDialogFragment.stop(activity)
TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(activity)
activity.stopService(intentDatabaseTask)
}
})
}
fun start() {
actionRunnableAsyncTask?.execute(actionRunnable)
/*
----
Main methods
----
*/
fun startDatabaseCreate(databaseUri: Uri,
masterPasswordChecked: Boolean,
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
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)
}
, ACTION_DATABASE_CREATE_TASK)
}
private class ActionRunnableAsyncTask(private val progressTaskUpdater: ProgressTaskUpdater,
private val onPreExecute: () -> Unit,
private val onPostExecute: (result: ActionRunnable.Result) -> Unit)
: AsyncTask<((ProgressTaskUpdater?)-> ActionRunnable), Void, ActionRunnable.Result>() {
override fun onPreExecute() {
super.onPreExecute()
onPreExecute.invoke()
fun startDatabaseLoad(databaseUri: Uri,
masterPassword: String?,
keyFile: Uri?,
readOnly: Boolean,
cipherEntity: CipherDatabaseEntity?,
fixDuplicateUuid: Boolean) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile)
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
}
, ACTION_DATABASE_LOAD_TASK)
}
override fun doInBackground(vararg actionRunnables: ((ProgressTaskUpdater?)-> ActionRunnable)?): ActionRunnable.Result {
var resultTask = ActionRunnable.Result(false)
actionRunnables.forEach {
it?.invoke(progressTaskUpdater)?.apply {
run()
resultTask = result
fun startDatabaseAssignPassword(databaseUri: Uri,
masterPasswordChecked: Boolean,
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
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)
}
}
return resultTask
, ACTION_DATABASE_ASSIGN_PASSWORD_TASK)
}
override fun onPostExecute(result: ActionRunnable.Result) {
super.onPostExecute(result)
onPostExecute.invoke(result)
/*
----
Nodes Actions
----
*/
fun startDatabaseCreateGroup(newGroup: Group,
parent: Group,
save: Boolean) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, newGroup)
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_CREATE_GROUP_TASK)
}
fun startDatabaseUpdateGroup(oldGroup: Group,
groupToUpdate: Group,
save: Boolean) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.GROUP_ID_KEY, oldGroup.nodeId)
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_GROUP_TASK)
}
fun startDatabaseCreateEntry(newEntry: Entry,
parent: Group,
save: Boolean) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, newEntry)
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_CREATE_ENTRY_TASK)
}
fun startDatabaseUpdateEntry(oldEntry: Entry,
entryToUpdate: Entry,
save: Boolean) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, oldEntry.nodeId)
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_ENTRY_TASK)
}
private fun startDatabaseActionListNodes(actionTask: String,
nodesPaste: List<Node>,
newParent: Group?,
save: Boolean) {
val groupsIdToCopy = ArrayList<NodeId<*>>()
val entriesIdToCopy = ArrayList<NodeId<UUID>>()
nodesPaste.forEach { nodeVersioned ->
when (nodeVersioned.type) {
Type.GROUP -> {
(nodeVersioned as Group).nodeId?.let { groupId ->
groupsIdToCopy.add(groupId)
}
}
Type.ENTRY -> {
entriesIdToCopy.add((nodeVersioned as Entry).nodeId)
}
}
}
val newParentId = newParent?.nodeId
start(Bundle().apply {
putAll(getBundleFromListNodes(nodesPaste))
putParcelableArrayList(DatabaseTaskNotificationService.GROUPS_ID_KEY, groupsIdToCopy)
putParcelableArrayList(DatabaseTaskNotificationService.ENTRIES_ID_KEY, entriesIdToCopy)
if (newParentId != null)
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, actionTask)
}
fun startDatabaseCopyNodes(nodesToCopy: List<Node>,
newParent: Group,
save: Boolean) {
startDatabaseActionListNodes(ACTION_DATABASE_COPY_NODES_TASK, nodesToCopy, newParent, save)
}
fun startDatabaseMoveNodes(nodesToMove: List<Node>,
newParent: Group,
save: Boolean) {
startDatabaseActionListNodes(ACTION_DATABASE_MOVE_NODES_TASK, nodesToMove, newParent, save)
}
fun startDatabaseDeleteNodes(nodesToDelete: List<Node>,
save: Boolean) {
startDatabaseActionListNodes(ACTION_DATABASE_DELETE_NODES_TASK, nodesToDelete, null, save)
}
/*
-----------------
Entry History Settings
-----------------
*/
fun startDatabaseRestoreEntryHistory(mainEntry: Entry,
entryHistoryPosition: Int,
save: Boolean) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntry.nodeId)
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
}
fun startDatabaseDeleteEntryHistory(mainEntry: Entry,
entryHistoryPosition: Int,
save: Boolean) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntry.nodeId)
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
}
/*
-----------------
Main Settings
-----------------
*/
fun startDatabaseSaveName(oldName: String,
newName: String,
save: Boolean) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldName)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_NAME_TASK)
}
fun startDatabaseSaveDescription(oldDescription: String,
newDescription: String,
save: Boolean) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDescription)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
}
fun startDatabaseSaveDefaultUsername(oldDefaultUsername: String,
newDefaultUsername: String,
save: Boolean) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDefaultUsername)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
}
fun startDatabaseSaveColor(oldColor: String,
newColor: String,
save: Boolean) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldColor)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_COLOR_TASK)
}
fun startDatabaseSaveCompression(oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm,
save: Boolean) {
start(Bundle().apply {
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldCompression)
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
}
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
newMaxHistoryItems: Int,
save: Boolean) {
start(Bundle().apply {
putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistoryItems)
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
}
fun startDatabaseSaveMaxHistorySize(oldMaxHistorySize: Long,
newMaxHistorySize: Long,
save: Boolean) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistorySize)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
}
/*
-------------------
Security Settings
-------------------
*/
fun startDatabaseSaveEncryption(oldEncryption: EncryptionAlgorithm,
newEncryption: EncryptionAlgorithm,
save: Boolean) {
start(Bundle().apply {
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldEncryption)
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
}
fun startDatabaseSaveKeyDerivation(oldKeyDerivation: KdfEngine,
newKeyDerivation: KdfEngine,
save: Boolean) {
start(Bundle().apply {
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldKeyDerivation)
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
}
fun startDatabaseSaveIterations(oldIterations: Long,
newIterations: Long,
save: Boolean) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldIterations)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
}
fun startDatabaseSaveMemoryUsage(oldMemoryUsage: Long,
newMemoryUsage: Long,
save: Boolean) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMemoryUsage)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
}
fun startDatabaseSaveParallelism(oldParallelism: Int,
newParallelism: Int,
save: Boolean) {
start(Bundle().apply {
putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism)
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
}
/**
* Save Database without parameter
*/
fun startDatabaseSave(save: Boolean) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_SAVE)
}
}

View File

@@ -21,43 +21,30 @@ package com.kunzisoft.keepass.database.action
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.PwDbOutputException
import com.kunzisoft.keepass.database.exception.DatabaseException
import com.kunzisoft.keepass.tasks.ActionRunnable
import java.io.IOException
abstract class SaveDatabaseRunnable(protected var context: Context,
open class SaveDatabaseRunnable(protected var context: Context,
protected var database: Database,
private val save: Boolean,
nestedAction: ActionRunnable? = null) : ActionRunnable(nestedAction) {
private var saveDatabase: Boolean)
: ActionRunnable() {
// TODO Service to prevent background thread kill
override fun run() {
if (save) {
var mAfterSaveDatabase: ((Result) -> Unit)? = null
override fun onStartRun() {}
override fun onActionRun() {
if (saveDatabase && result.isSuccess) {
try {
database.saveData(context.contentResolver)
} catch (e: IOException) {
finishRun(false, e.message)
} catch (e: PwDbOutputException) {
finishRun(false, e.message)
} catch (e: DatabaseException) {
setError(e)
}
}
}
// Need to call super.run() in child class
}
override fun onFinishRun(result: Result) {
// Need to call super.onFinishRun(result) in child class
}
}
class SaveDatabaseActionRunnable(context: Context,
database: Database,
save: Boolean,
nestedAction: ActionRunnable? = null)
: SaveDatabaseRunnable(context, database, save, nestedAction) {
override fun run() {
super.run()
finishRun(true)
override fun onFinishRun() {
// Need to call super.onFinishRun() in child class
mAfterSaveDatabase?.invoke(result)
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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.action
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
class UpdateCompressionBinariesDatabaseRunnable (
context: Context,
database: Database,
private val oldCompressionAlgorithm: CompressionAlgorithm,
private val newCompressionAlgorithm: CompressionAlgorithm,
saveDatabase: Boolean)
: SaveDatabaseRunnable(context, database, saveDatabase) {
override fun onStartRun() {
// Set new compression
if (database.allowDataCompression) {
try {
database.apply {
updateDataBinaryCompression(oldCompressionAlgorithm, newCompressionAlgorithm)
compressionAlgorithm = newCompressionAlgorithm
}
} catch (e: Exception) {
setError(e)
}
}
super.onStartRun()
}
override fun onFinishRun() {
super.onFinishRun()
if (database.allowDataCompression) {
if (!result.isSuccess) {
try {
database.apply {
compressionAlgorithm = oldCompressionAlgorithm
updateDataBinaryCompression(newCompressionAlgorithm, oldCompressionAlgorithm)
}
} catch (e: Exception) {
setError(e)
}
}
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.history
import android.content.Context
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
class DeleteEntryHistoryDatabaseRunnable (
context: Context,
database: Database,
private val mainEntry: Entry,
private val entryHistoryPosition: Int,
saveDatabase: Boolean)
: SaveDatabaseRunnable(context, database, saveDatabase) {
override fun onStartRun() {
try {
mainEntry.removeEntryFromHistory(entryHistoryPosition)
} catch (e: Exception) {
setError(e)
}
super.onStartRun()
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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.history
import android.content.Context
import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.tasks.ActionRunnable
class RestoreEntryHistoryDatabaseRunnable (
private val context: Context,
private val database: Database,
private val mainEntry: Entry,
private val entryHistoryPosition: Int,
private val saveDatabase: Boolean)
: ActionRunnable() {
private var updateEntryRunnable: UpdateEntryRunnable? = null
override fun onStartRun() {
try {
val historyToRestore = Entry(mainEntry.getHistory()[entryHistoryPosition])
// Copy history of main entry in the restore entry
mainEntry.getHistory().forEach {
historyToRestore.addEntryToHistory(it)
}
// Update the entry with the fresh formatted entry to restore
updateEntryRunnable = UpdateEntryRunnable(context,
database,
mainEntry,
historyToRestore,
saveDatabase,
null)
updateEntryRunnable?.onStartRun()
} catch (e: Exception) {
setError(e)
}
}
override fun onActionRun() {
updateEntryRunnable?.onActionRun()
}
override fun onFinishRun() {
updateEntryRunnable?.onFinishRun()
}
}

View File

@@ -1,52 +1,54 @@
/*
* 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.action.node
import androidx.fragment.app.FragmentActivity
import android.util.Log
import android.content.Context
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
import com.kunzisoft.keepass.database.element.Database
abstract class ActionNodeDatabaseRunnable(
context: FragmentActivity,
context: Context,
database: Database,
private val callbackRunnable: AfterActionNodeFinishRunnable?,
private val afterActionNodesFinish: AfterActionNodesFinish?,
save: Boolean)
: SaveDatabaseRunnable(context, database, save) {
/**
* Function do to a node action, don't implements run() if used this
* Function do to a node action
*/
abstract fun nodeAction()
override fun run() {
try {
override fun onStartRun() {
nodeAction()
// To save the database
super.run()
finishRun(true)
} catch (e: Exception) {
Log.e("ActionNodeDBRunnable", e.message)
finishRun(false, e.message)
}
super.onStartRun()
}
/**
* Function do get the finish node action, don't implements onFinishRun() if used this
* Function do get the finish node action
*/
abstract fun nodeFinish(result: Result): ActionNodeValues
abstract fun nodeFinish(): ActionNodesValues
override fun onFinishRun(result: Result) {
callbackRunnable?.apply {
onActionNodeFinish(nodeFinish(result))
}
if (!result.isSuccess) {
displayMessage(context)
}
super.onFinishRun(result)
}
companion object {
const val NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY = "NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY"
override fun onFinishRun() {
super.onFinishRun()
afterActionNodesFinish?.apply {
onActionNodesFinish(result, nodeFinish())
}
}
}

View File

@@ -19,19 +19,20 @@
*/
package com.kunzisoft.keepass.database.action.node
import androidx.fragment.app.FragmentActivity
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.EntryVersioned
import com.kunzisoft.keepass.database.element.GroupVersioned
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
class AddEntryRunnable constructor(
context: FragmentActivity,
context: Context,
database: Database,
private val mNewEntry: EntryVersioned,
private val mParent: GroupVersioned,
finishRunnable: AfterActionNodeFinishRunnable?,
save: Boolean)
: ActionNodeDatabaseRunnable(context, database, finishRunnable, save) {
private val mNewEntry: Entry,
private val mParent: Group,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
override fun nodeAction() {
mNewEntry.touch(modified = true, touchParents = true)
@@ -39,12 +40,16 @@ class AddEntryRunnable constructor(
database.addEntryTo(mNewEntry, mParent)
}
override fun nodeFinish(result: Result): ActionNodeValues {
override fun nodeFinish(): ActionNodesValues {
if (!result.isSuccess) {
mNewEntry.parent?.let {
database.removeEntryFrom(mNewEntry, it)
}
}
return ActionNodeValues(result, null, mNewEntry)
val oldNodesReturn = ArrayList<Node>()
val newNodesReturn = ArrayList<Node>()
newNodesReturn.add(mNewEntry)
return ActionNodesValues(oldNodesReturn, newNodesReturn)
}
}

View File

@@ -19,18 +19,19 @@
*/
package com.kunzisoft.keepass.database.action.node
import androidx.fragment.app.FragmentActivity
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.GroupVersioned
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
class AddGroupRunnable constructor(
context: FragmentActivity,
context: Context,
database: Database,
private val mNewGroup: GroupVersioned,
private val mParent: GroupVersioned,
afterAddNodeRunnable: AfterActionNodeFinishRunnable?,
save: Boolean)
: ActionNodeDatabaseRunnable(context, database, afterAddNodeRunnable, save) {
private val mNewGroup: Group,
private val mParent: Group,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
override fun nodeAction() {
mNewGroup.touch(modified = true, touchParents = true)
@@ -38,10 +39,14 @@ class AddGroupRunnable constructor(
database.addGroupTo(mNewGroup, mParent)
}
override fun nodeFinish(result: Result): ActionNodeValues {
override fun nodeFinish(): ActionNodesValues {
if (!result.isSuccess) {
database.removeGroupFrom(mNewGroup, mParent)
}
return ActionNodeValues(result, null, mNewGroup)
val oldNodesReturn = ArrayList<Node>()
val newNodesReturn = ArrayList<Node>()
newNodesReturn.add(mNewGroup)
return ActionNodesValues(oldNodesReturn, newNodesReturn)
}
}

View File

@@ -1,37 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.action.node
import com.kunzisoft.keepass.database.element.NodeVersioned
import com.kunzisoft.keepass.tasks.ActionRunnable
/**
* Callback method who return the node(s) modified after an action
* - Add : @param oldNode NULL, @param newNode CreatedNode
* - Copy : @param oldNode NodeToCopy, @param newNode NodeCopied
* - Delete : @param oldNode NodeToDelete, @param NULL
* - Move : @param oldNode NULL, @param NodeToMove
* - Update : @param oldNode NodeToUpdate, @param NodeUpdated
*/
data class ActionNodeValues(val result: ActionRunnable.Result, val oldNode: NodeVersioned?, val newNode: NodeVersioned?)
abstract class AfterActionNodeFinishRunnable {
abstract fun onActionNodeFinish(actionNodeValues: ActionNodeValues)
}

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.database.action.node
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.tasks.ActionRunnable
/**
* Callback method who return the node(s) modified after an action
* - Add : @param oldNodes empty, @param newNodes CreatedNodes
* - Copy : @param oldNodes NodesToCopy, @param newNodes NodesCopied
* - Delete : @param oldNodes NodesToDelete, @param newNodes empty
* - Move : @param oldNodes empty, @param newNodes NodesToMove
* - Update : @param oldNodes NodesToUpdate, @param newNodes NodesUpdated
*/
class ActionNodesValues(val oldNodes: List<Node>, val newNodes: List<Node>)
abstract class AfterActionNodesFinish {
abstract fun onActionNodesFinish(result: ActionRunnable.Result, actionNodesValues: ActionNodesValues)
}

View File

@@ -1,76 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.action.node
import androidx.fragment.app.FragmentActivity
import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.EntryVersioned
import com.kunzisoft.keepass.database.element.GroupVersioned
class CopyEntryRunnable constructor(
context: FragmentActivity,
database: Database,
private val mEntryToCopy: EntryVersioned,
private val mNewParent: GroupVersioned,
afterAddNodeRunnable: AfterActionNodeFinishRunnable?,
save: Boolean)
: ActionNodeDatabaseRunnable(context, database, afterAddNodeRunnable, save) {
private var mEntryCopied: EntryVersioned? = null
override fun nodeAction() {
// Condition
var conditionAccepted = true
if(mNewParent == database.rootGroup && !database.rootCanContainsEntry())
conditionAccepted = false
if (conditionAccepted) {
// Update entry with new values
mNewParent.touch(modified = false, touchParents = true)
mEntryCopied = database.copyEntryTo(mEntryToCopy, mNewParent)
} else {
// Only finish thread
throw Exception(context.getString(R.string.error_copy_entry_here))
}
mEntryCopied?.apply {
touch(modified = true, touchParents = true)
} ?: Log.e(TAG, "Unable to create a copy of the entry")
}
override fun nodeFinish(result: Result): ActionNodeValues {
if (!result.isSuccess) {
// If we fail to save, try to delete the copy
try {
mEntryCopied?.let {
database.deleteEntry(it)
}
} catch (e: Exception) {
Log.i(TAG, "Unable to delete the copied entry")
}
}
return ActionNodeValues(result, mEntryToCopy, mEntryCopied)
}
companion object {
private val TAG = CopyEntryRunnable::class.java.name
}
}

View File

@@ -0,0 +1,92 @@
/*
* 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.action.node
import android.content.Context
import android.util.Log
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.CopyEntryDatabaseException
import com.kunzisoft.keepass.database.exception.CopyGroupDatabaseException
class CopyNodesRunnable constructor(
context: Context,
database: Database,
private val mNodesToCopy: List<Node>,
private val mNewParent: Group,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
private var mEntriesCopied = ArrayList<Entry>()
override fun nodeAction() {
foreachNode@ for(currentNode in mNodesToCopy) {
when (currentNode.type) {
Type.GROUP -> {
Log.e(TAG, "Copy not allowed for group")// Only finish thread
setError(CopyGroupDatabaseException())
break@foreachNode
}
Type.ENTRY -> {
// Root can contains entry
if (mNewParent != database.rootGroup || database.rootCanContainsEntry()) {
// Update entry with new values
mNewParent.touch(modified = false, touchParents = true)
val entryCopied = database.copyEntryTo(currentNode as Entry, mNewParent)
if (entryCopied != null) {
entryCopied.touch(modified = true, touchParents = true)
mEntriesCopied.add(entryCopied)
} else {
Log.e(TAG, "Unable to create a copy of the entry")
setError(CopyEntryDatabaseException())
break@foreachNode
}
} else {
// Only finish thread
setError(CopyEntryDatabaseException())
break@foreachNode
}
}
}
}
}
override fun nodeFinish(): ActionNodesValues {
if (!result.isSuccess) {
// If we fail to save, try to delete the copy
mEntriesCopied.forEach {
try {
database.deleteEntry(it)
} catch (e: Exception) {
Log.i(TAG, "Unable to delete the copied entry")
}
}
}
return ActionNodesValues(mNodesToCopy, mEntriesCopied)
}
companion object {
private val TAG = CopyNodesRunnable::class.java.name
}
}

View File

@@ -1,83 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.action.node
import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.EntryVersioned
import com.kunzisoft.keepass.database.element.GroupVersioned
class DeleteEntryRunnable constructor(
context: FragmentActivity,
database: Database,
private val mEntryToDelete: EntryVersioned,
finishRunnable: AfterActionNodeFinishRunnable?,
save: Boolean)
: ActionNodeDatabaseRunnable(context, database, finishRunnable, save) {
private var mParent: GroupVersioned? = null
private var mCanRecycle: Boolean = false
private var mEntryToDeleteBackup: EntryVersioned? = null
private var mNodePosition: Int? = null
override fun nodeAction() {
mParent = mEntryToDelete.parent
mParent?.touch(modified = false, touchParents = true)
// Get the node position
mNodePosition = mEntryToDelete.nodePositionInParent
// Create a copy to keep the old ref and remove it visually
mEntryToDeleteBackup = EntryVersioned(mEntryToDelete)
// Remove Entry from parent
mCanRecycle = database.canRecycle(mEntryToDelete)
if (mCanRecycle) {
database.recycle(mEntryToDelete, context.resources)
} else {
database.deleteEntry(mEntryToDelete)
}
}
override fun nodeFinish(result: Result): ActionNodeValues {
if (!result.isSuccess) {
mParent?.let {
if (mCanRecycle) {
database.undoRecycle(mEntryToDelete, it)
} else {
database.undoDeleteEntry(mEntryToDelete, it)
}
}
}
// Add position in bundle to delete the node in view
mNodePosition?.let { position ->
result.data = Bundle().apply {
putInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY, position )
}
}
// Return a copy of unchanged entry as old param
// and entry deleted or moved in recycle bin as new param
return ActionNodeValues(result, mEntryToDeleteBackup, mEntryToDelete)
}
}

View File

@@ -1,82 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.action.node
import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.GroupVersioned
class DeleteGroupRunnable(context: FragmentActivity,
database: Database,
private val mGroupToDelete: GroupVersioned,
finish: AfterActionNodeFinishRunnable,
save: Boolean) : ActionNodeDatabaseRunnable(context, database, finish, save) {
private var mParent: GroupVersioned? = null
private var mRecycle: Boolean = false
private var mGroupToDeleteBackup: GroupVersioned? = null
private var mNodePosition: Int? = null
override fun nodeAction() {
mParent = mGroupToDelete.parent
mParent?.touch(modified = false, touchParents = true)
// Get the node position
mNodePosition = mGroupToDelete.nodePositionInParent
// Create a copy to keep the old ref and remove it visually
mGroupToDeleteBackup = GroupVersioned(mGroupToDelete)
// Remove Group from parent
mRecycle = database.canRecycle(mGroupToDelete)
if (mRecycle) {
database.recycle(mGroupToDelete, context.resources)
} else {
database.deleteGroup(mGroupToDelete)
}
}
override fun nodeFinish(result: Result): ActionNodeValues {
if (!result.isSuccess) {
if (mRecycle) {
mParent?.let {
database.undoRecycle(mGroupToDelete, it)
}
}
// else {
// Let's not bother recovering from a failure to save a deleted tree. It is too much work.
// TODO database.undoDeleteGroupFrom(mGroup, mParent);
// }
}
// Add position in bundle to delete the node in view
mNodePosition?.let { position ->
result.data = Bundle().apply {
putInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY, position )
}
}
// Return a copy of unchanged group as old param
// and group deleted or moved in recycle bin as new param
return ActionNodeValues(result, mGroupToDeleteBackup, mGroupToDelete)
}
}

View File

@@ -0,0 +1,98 @@
/*
* 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.action.node
import android.content.Context
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type
class DeleteNodesRunnable(context: Context,
database: Database,
private val mNodesToDelete: List<Node>,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
private var mParent: Group? = null
private var mCanRecycle: Boolean = false
private var mNodesToDeleteBackup = ArrayList<Node>()
override fun nodeAction() {
foreachNode@ for(currentNode in mNodesToDelete) {
mParent = currentNode.parent
mParent?.touch(modified = false, touchParents = true)
when (currentNode.type) {
Type.GROUP -> {
// Create a copy to keep the old ref and remove it visually
mNodesToDeleteBackup.add(Group(currentNode as Group))
// Remove Node from parent
mCanRecycle = database.canRecycle(currentNode)
if (mCanRecycle) {
database.recycle(currentNode, context.resources)
} else {
database.deleteGroup(currentNode)
}
}
Type.ENTRY -> {
// Create a copy to keep the old ref and remove it visually
mNodesToDeleteBackup.add(Entry(currentNode as Entry))
// Remove Node from parent
mCanRecycle = database.canRecycle(currentNode)
if (mCanRecycle) {
database.recycle(currentNode, context.resources)
} else {
database.deleteEntry(currentNode)
}
}
}
}
}
override fun nodeFinish(): ActionNodesValues {
if (!result.isSuccess) {
if (mCanRecycle) {
mParent?.let {
mNodesToDeleteBackup.forEach { backupNode ->
when (backupNode.type) {
Type.GROUP -> {
database.undoRecycle(backupNode as Group, it)
}
Type.ENTRY -> {
database.undoRecycle(backupNode as Entry, it)
}
}
}
}
}
// else {
// Let's not bother recovering from a failure to save a deleted tree. It is too much work.
// TODO database.undoDeleteGroupFrom(mGroup, mParent);
// }
}
// Return a copy of unchanged nodes as old param
// and nodes deleted or moved in recycle bin as new param
return ActionNodesValues(mNodesToDeleteBackup, mNodesToDelete)
}
}

View File

@@ -1,77 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.action.node
import androidx.fragment.app.FragmentActivity
import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.EntryVersioned
import com.kunzisoft.keepass.database.element.GroupVersioned
class MoveEntryRunnable constructor(
context: FragmentActivity,
database: Database,
private val mEntryToMove: EntryVersioned?,
private val mNewParent: GroupVersioned,
afterAddNodeRunnable: AfterActionNodeFinishRunnable?,
save: Boolean)
: ActionNodeDatabaseRunnable(context, database, afterAddNodeRunnable, save) {
private var mOldParent: GroupVersioned? = null
override fun nodeAction() {
// Move entry in new parent
mEntryToMove?.let {
mOldParent = it.parent
// Condition
var conditionAccepted = true
if(mNewParent == database.rootGroup && !database.rootCanContainsEntry())
conditionAccepted = false
// Move only if the parent change
if (mOldParent != mNewParent && conditionAccepted) {
database.moveEntryTo(it, mNewParent)
} else {
// Only finish thread
throw Exception(context.getString(R.string.error_move_entry_here))
}
it.touch(modified = true, touchParents = true)
} ?: Log.e(TAG, "Unable to create a copy of the entry")
}
override fun nodeFinish(result: Result): ActionNodeValues {
if (!result.isSuccess) {
// If we fail to save, try to remove in the first place
try {
if (mEntryToMove != null && mOldParent != null)
database.moveEntryTo(mEntryToMove, mOldParent!!)
} catch (e: Exception) {
Log.i(TAG, "Unable to replace the entry")
}
}
return ActionNodeValues(result, null, mEntryToMove)
}
companion object {
private val TAG = MoveEntryRunnable::class.java.name
}
}

View File

@@ -1,71 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.action.node
import androidx.fragment.app.FragmentActivity
import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.GroupVersioned
class MoveGroupRunnable constructor(
context: FragmentActivity,
database: Database,
private val mGroupToMove: GroupVersioned?,
private val mNewParent: GroupVersioned,
afterAddNodeRunnable: AfterActionNodeFinishRunnable?,
save: Boolean)
: ActionNodeDatabaseRunnable(context, database, afterAddNodeRunnable, save) {
private var mOldParent: GroupVersioned? = null
override fun nodeAction() {
mGroupToMove?.let {
mOldParent = it.parent
// Move group in new parent if not in the current group
if (mGroupToMove != mNewParent && !mNewParent.isContainedIn(mGroupToMove)) {
database.moveGroupTo(mGroupToMove, mNewParent)
mGroupToMove.touch(modified = true, touchParents = true)
finishRun(true)
} else {
// Only finish thread
throw Exception(context.getString(R.string.error_move_folder_in_itself))
}
} ?: Log.e(TAG, "Unable to create a copy of the group")
}
override fun nodeFinish(result: Result): ActionNodeValues {
if (!result.isSuccess) {
// If we fail to save, try to move in the first place
try {
if (mGroupToMove != null && mOldParent != null)
database.moveGroupTo(mGroupToMove, mOldParent!!)
} catch (e: Exception) {
Log.i(TAG, "Unable to replace the group")
}
}
return ActionNodeValues(result, null, mGroupToMove)
}
companion object {
private val TAG = MoveGroupRunnable::class.java.name
}
}

View File

@@ -0,0 +1,102 @@
/*
* 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.action.node
import android.content.Context
import android.util.Log
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.EntryDatabaseException
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
class MoveNodesRunnable constructor(
context: Context,
database: Database,
private val mNodesToMove: List<Node>,
private val mNewParent: Group,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
private var mOldParent: Group? = null
override fun nodeAction() {
foreachNode@ for(nodeToMove in mNodesToMove) {
// Move node in new parent
mOldParent = nodeToMove.parent
when (nodeToMove.type) {
Type.GROUP -> {
val groupToMove = nodeToMove as Group
// Move group in new parent if not in the current group
if (groupToMove != mNewParent
&& !mNewParent.isContainedIn(groupToMove)) {
nodeToMove.touch(modified = true, touchParents = true)
database.moveGroupTo(groupToMove, mNewParent)
} else {
// Only finish thread
setError(MoveGroupDatabaseException())
break@foreachNode
}
}
Type.ENTRY -> {
val entryToMove = nodeToMove as Entry
// Move only if the parent change
if (mOldParent != mNewParent
// and root can contains entry
&& (mNewParent != database.rootGroup || database.rootCanContainsEntry())) {
nodeToMove.touch(modified = true, touchParents = true)
database.moveEntryTo(entryToMove, mNewParent)
} else {
// Only finish thread
setError(EntryDatabaseException())
break@foreachNode
}
}
}
}
}
override fun nodeFinish(): ActionNodesValues {
if (!result.isSuccess) {
try {
mNodesToMove.forEach { nodeToMove ->
// If we fail to save, try to move in the first place
if (mOldParent != null &&
mOldParent != nodeToMove.parent) {
when (nodeToMove.type) {
Type.GROUP -> database.moveGroupTo(nodeToMove as Group, mOldParent!!)
Type.ENTRY -> database.moveEntryTo(nodeToMove as Entry, mOldParent!!)
}
}
}
} catch (e: Exception) {
Log.i(TAG, "Unable to replace the node")
}
}
return ActionNodesValues(ArrayList(), mNodesToMove)
}
companion object {
private val TAG = MoveNodesRunnable::class.java.name
}
}

View File

@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database
package com.kunzisoft.keepass.database.action.node
/** "Delegate" class for operating on each group when traversing all of

View File

@@ -19,36 +19,50 @@
*/
package com.kunzisoft.keepass.database.action.node
import androidx.fragment.app.FragmentActivity
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.EntryVersioned
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.Node
class UpdateEntryRunnable constructor(
context: FragmentActivity,
context: Context,
database: Database,
private val mOldEntry: EntryVersioned,
private val mNewEntry: EntryVersioned,
finishRunnable: AfterActionNodeFinishRunnable?,
save: Boolean)
: ActionNodeDatabaseRunnable(context, database, finishRunnable, save) {
private val mOldEntry: Entry,
private val mNewEntry: Entry,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
// Keep backup of original values in case save fails
private var mBackupEntry: EntryVersioned? = null
private var mBackupEntryHistory: Entry = Entry(mOldEntry)
override fun nodeAction() {
mBackupEntry = database.addHistoryBackupTo(mOldEntry)
mOldEntry.touch(modified = true, touchParents = true)
// WARNING : Re attribute parent removed in entry edit activity to save memory
mNewEntry.addParentFrom(mOldEntry)
// 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)
// Only change data in index
database.updateEntry(mOldEntry)
}
override fun nodeFinish(result: Result): ActionNodeValues {
override fun nodeFinish(): ActionNodesValues {
if (!result.isSuccess) {
mOldEntry.updateWith(mBackupEntryHistory)
// If we fail to save, back out changes to global structure
mBackupEntry?.let {
mOldEntry.updateWith(it)
}
}
return ActionNodeValues(result, mOldEntry, mNewEntry)
database.updateEntry(mOldEntry)
}
val oldNodesReturn = ArrayList<Node>()
oldNodesReturn.add(mBackupEntryHistory)
val newNodesReturn = ArrayList<Node>()
newNodesReturn.add(mOldEntry)
return ActionNodesValues(oldNodesReturn, newNodesReturn)
}
}

View File

@@ -19,33 +19,47 @@
*/
package com.kunzisoft.keepass.database.action.node
import androidx.fragment.app.FragmentActivity
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.GroupVersioned
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
class UpdateGroupRunnable constructor(
context: FragmentActivity,
context: Context,
database: Database,
private val mOldGroup: GroupVersioned,
private val mNewGroup: GroupVersioned,
finishRunnable: AfterActionNodeFinishRunnable?,
save: Boolean)
: ActionNodeDatabaseRunnable(context, database, finishRunnable, save) {
private val mOldGroup: Group,
private val mNewGroup: Group,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
// Keep backup of original values in case save fails
private val mBackupGroup: GroupVersioned = GroupVersioned(mOldGroup)
private val mBackupGroup: Group = Group(mOldGroup)
override fun nodeAction() {
// WARNING : Re attribute parent and children removed in group activity to save memory
mNewGroup.addParentFrom(mOldGroup)
mNewGroup.addChildrenFrom(mOldGroup)
// Update group with new values
mOldGroup.touch(modified = true, touchParents = true)
mOldGroup.updateWith(mNewGroup)
mOldGroup.touch(modified = true, touchParents = true)
// Only change data in index
database.updateGroup(mOldGroup)
}
override fun nodeFinish(result: Result): ActionNodeValues {
override fun nodeFinish(): ActionNodesValues {
if (!result.isSuccess) {
// If we fail to save, back out changes to global structure
mOldGroup.updateWith(mBackupGroup)
database.updateGroup(mOldGroup)
}
return ActionNodeValues(result, mOldGroup, mNewGroup)
val oldNodesReturn = ArrayList<Node>()
oldNodesReturn.add(mBackupGroup)
val newNodesReturn = ArrayList<Node>()
newNodesReturn.add(mOldGroup)
return ActionNodesValues(oldNodesReturn, newNodesReturn)
}
}

View File

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

View File

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

View File

@@ -1,15 +1,34 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.cursor
import com.kunzisoft.keepass.database.element.PwEntryV4
import com.kunzisoft.keepass.database.element.PwIconFactory
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
import java.util.UUID
class EntryCursorV4 : EntryCursor<PwEntryV4>() {
class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
private val extraFieldCursor: ExtraFieldCursor = ExtraFieldCursor()
override fun addEntry(entry: PwEntryV4) {
override fun addEntry(entry: EntryKDBX) {
addRow(arrayOf(
entryId,
entry.id.mostSignificantBits,
@@ -21,7 +40,9 @@ class EntryCursorV4 : EntryCursor<PwEntryV4>() {
entry.username,
entry.password,
entry.url,
entry.notes
entry.notes,
entry.expiryTime,
entry.expires
))
for (element in entry.customFields.entries) {
@@ -31,7 +52,7 @@ class EntryCursorV4 : EntryCursor<PwEntryV4>() {
entryId++
}
override fun populateEntry(pwEntry: PwEntryV4, iconFactory: PwIconFactory) {
override fun populateEntry(pwEntry: EntryKDBX, iconFactory: IconImageFactory) {
super.populateEntry(pwEntry, iconFactory)
// Retrieve custom icon

View File

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

View File

@@ -1,25 +0,0 @@
package com.kunzisoft.keepass.database.cursor
import com.kunzisoft.keepass.database.element.PwDatabase
import com.kunzisoft.keepass.database.element.PwEntryV3
class EntryCursorV3 : EntryCursor<PwEntryV3>() {
override fun addEntry(entry: PwEntryV3) {
addRow(arrayOf(
entryId,
entry.id.mostSignificantBits,
entry.id.leastSignificantBits,
entry.title,
entry.icon.iconId,
PwDatabase.UUID_ZERO.mostSignificantBits,
PwDatabase.UUID_ZERO.leastSignificantBits,
entry.username,
entry.password,
entry.url,
entry.notes
))
entryId++
}
}

View File

@@ -1,9 +1,28 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.cursor
import android.database.MatrixCursor
import android.provider.BaseColumns
import com.kunzisoft.keepass.database.element.PwEntryV4
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.security.ProtectedString
class ExtraFieldCursor : MatrixCursor(arrayOf(
@@ -22,8 +41,8 @@ class ExtraFieldCursor : MatrixCursor(arrayOf(
fieldId++
}
fun populateExtraFieldInEntry(pwEntry: PwEntryV4) {
pwEntry.addExtraField(getString(getColumnIndex(COLUMN_LABEL)),
fun populateExtraFieldInEntry(pwEntry: EntryKDBX) {
pwEntry.putExtraField(getString(getColumnIndex(COLUMN_LABEL)),
ProtectedString(getInt(getColumnIndex(COLUMN_PROTECTION)) > 0,
getString(getColumnIndex(COLUMN_VALUE))))
}

View File

@@ -0,0 +1,150 @@
/*
* 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.content.res.Resources
import android.os.Parcel
import android.os.Parcelable
import androidx.core.os.ConfigurationCompat
import java.text.SimpleDateFormat
import java.util.*
class DateInstant : Parcelable {
private var jDate: Date = Date()
val date: Date
get() = jDate
constructor(source: DateInstant) {
this.jDate = Date(source.jDate.time)
}
constructor(date: Date) {
jDate = Date(date.time)
}
constructor(millis: Long) {
jDate = Date(millis)
}
constructor(string: String) {
jDate = dateFormat.parse(string)
}
constructor() {
jDate = Date()
}
protected constructor(parcel: Parcel) {
jDate = parcel.readSerializable() as Date
}
override fun describeContents(): Int {
return 0
}
fun getDateTimeString(resources: Resources): String {
return Companion.getDateTimeString(resources, this.date)
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeSerializable(date)
}
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (other == null) {
return false
}
if (javaClass != other.javaClass) {
return false
}
val date = other as DateInstant
return isSameDate(jDate, date.jDate)
}
override fun hashCode(): Int {
return jDate.hashCode()
}
override fun toString(): String {
return dateFormat.format(jDate)
}
companion object {
val NEVER_EXPIRE = neverExpire
private val dateFormat = SimpleDateFormat.getDateTimeInstance()
private val neverExpire: DateInstant
get() {
val cal = Calendar.getInstance()
cal.set(Calendar.YEAR, 2999)
cal.set(Calendar.MONTH, 11)
cal.set(Calendar.DAY_OF_MONTH, 28)
cal.set(Calendar.HOUR, 23)
cal.set(Calendar.MINUTE, 59)
cal.set(Calendar.SECOND, 59)
return DateInstant(cal.time)
}
@JvmField
val CREATOR: Parcelable.Creator<DateInstant> = object : Parcelable.Creator<DateInstant> {
override fun createFromParcel(parcel: Parcel): DateInstant {
return DateInstant(parcel)
}
override fun newArray(size: Int): Array<DateInstant?> {
return arrayOfNulls(size)
}
}
private fun isSameDate(d1: Date?, d2: Date?): Boolean {
val cal1 = Calendar.getInstance()
cal1.time = d1
cal1.set(Calendar.MILLISECOND, 0)
val cal2 = Calendar.getInstance()
cal2.time = d2
cal2.set(Calendar.MILLISECOND, 0)
return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH) &&
cal1.get(Calendar.DAY_OF_MONTH) == cal2.get(Calendar.DAY_OF_MONTH) &&
cal1.get(Calendar.HOUR) == cal2.get(Calendar.HOUR) &&
cal1.get(Calendar.MINUTE) == cal2.get(Calendar.MINUTE) &&
cal1.get(Calendar.SECOND) == cal2.get(Calendar.SECOND)
}
fun getDateTimeString(resources: Resources, date: Date): String {
return java.text.DateFormat.getDateTimeInstance(
java.text.DateFormat.MEDIUM,
java.text.DateFormat.MEDIUM,
ConfigurationCompat.getLocales(resources.configuration)[0])
.format(date)
}
}
}

View File

@@ -19,12 +19,13 @@
*/
package com.kunzisoft.keepass.database.element
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import java.util.Date
import java.util.UUID
class PwDeletedObject {
class DeletedObject {
var uuid: UUID = PwDatabase.UUID_ZERO
var uuid: UUID = DatabaseVersioned.UUID_ZERO
var deletionTime: Date? = null
get() = if (field == null) {
Date(System.currentTimeMillis())
@@ -43,7 +44,7 @@ class PwDeletedObject {
return true
if (other == null)
return false
if (other !is PwDeletedObject)
if (other !is DeletedObject)
return false
return uuid == other.uuid
}

View File

@@ -0,0 +1,442 @@
/*
* 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.DatabaseKDBX
import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.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
import com.kunzisoft.keepass.otp.OtpEntryFields
import java.util.*
import kotlin.collections.ArrayList
class Entry : Node, EntryVersionedInterface<Group> {
var entryKDB: EntryKDB? = null
private set
var entryKDBX: EntryKDBX? = null
private set
fun updateWith(entry: Entry, copyHistory: Boolean = true) {
entry.entryKDB?.let {
this.entryKDB?.updateWith(it)
}
entry.entryKDBX?.let {
this.entryKDBX?.updateWith(it, copyHistory)
}
}
/**
* Use this constructor to copy an Entry with exact same values
*/
constructor(entry: Entry, copyHistory: Boolean = true) {
if (entry.entryKDB != null) {
this.entryKDB = EntryKDB()
}
if (entry.entryKDBX != null) {
this.entryKDBX = EntryKDBX()
}
updateWith(entry, copyHistory)
}
constructor(entry: EntryKDB) {
this.entryKDBX = null
this.entryKDB = entry
}
constructor(entry: EntryKDBX) {
this.entryKDB = null
this.entryKDBX = entry
}
constructor(parcel: Parcel) {
entryKDB = parcel.readParcelable(EntryKDB::class.java.classLoader)
entryKDBX = parcel.readParcelable(EntryKDBX::class.java.classLoader)
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeParcelable(entryKDB, flags)
dest.writeParcelable(entryKDBX, flags)
}
override var nodeId: NodeId<UUID>
get() = entryKDBX?.nodeId ?: entryKDB?.nodeId ?: NodeIdUUID()
set(value) {
entryKDB?.nodeId = value
entryKDBX?.nodeId = value
}
override var title: String
get() = entryKDB?.title ?: entryKDBX?.title ?: ""
set(value) {
entryKDB?.title = value
entryKDBX?.title = value
}
override var icon: IconImage
get() {
return entryKDB?.icon ?: entryKDBX?.icon ?: IconImageStandard()
}
set(value) {
entryKDB?.icon = value
entryKDBX?.icon = value
}
override val type: Type
get() = Type.ENTRY
override var parent: Group?
get() {
entryKDB?.parent?.let {
return Group(it)
}
entryKDBX?.parent?.let {
return Group(it)
}
return null
}
set(value) {
entryKDB?.parent = value?.groupKDB
entryKDBX?.parent = value?.groupKDBX
}
override fun containsParent(): Boolean {
return entryKDB?.containsParent() ?: entryKDBX?.containsParent() ?: false
}
override fun afterAssignNewParent() {
entryKDBX?.afterChangeParent()
}
override fun touch(modified: Boolean, touchParents: Boolean) {
entryKDB?.touch(modified, touchParents)
entryKDBX?.touch(modified, touchParents)
}
override fun isContainedIn(container: Group): Boolean {
var contained: Boolean? = false
container.groupKDB?.let {
contained = entryKDB?.isContainedIn(it)
}
container.groupKDBX?.let {
contained = entryKDBX?.isContainedIn(it)
}
return contained ?: false
}
override var creationTime: DateInstant
get() = entryKDB?.creationTime ?: entryKDBX?.creationTime ?: DateInstant()
set(value) {
entryKDB?.creationTime = value
entryKDBX?.creationTime = value
}
override var lastModificationTime: DateInstant
get() = entryKDB?.lastModificationTime ?: entryKDBX?.lastModificationTime ?: DateInstant()
set(value) {
entryKDB?.lastModificationTime = value
entryKDBX?.lastModificationTime = value
}
override var lastAccessTime: DateInstant
get() = entryKDB?.lastAccessTime ?: entryKDBX?.lastAccessTime ?: DateInstant()
set(value) {
entryKDB?.lastAccessTime = value
entryKDBX?.lastAccessTime = value
}
override var expiryTime: DateInstant
get() = entryKDB?.expiryTime ?: entryKDBX?.expiryTime ?: DateInstant()
set(value) {
entryKDB?.expiryTime = value
entryKDBX?.expiryTime = value
}
override var expires: Boolean
get() = entryKDB?.expires ?: entryKDBX?.expires ?: false
set(value) {
entryKDB?.expires = value
entryKDBX?.expires = value
}
override val isCurrentlyExpires: Boolean
get() = entryKDB?.isCurrentlyExpires ?: entryKDBX?.isCurrentlyExpires ?: false
override var username: String
get() = entryKDB?.username ?: entryKDBX?.username ?: ""
set(value) {
entryKDB?.username = value
entryKDBX?.username = value
}
override var password: String
get() = entryKDB?.password ?: entryKDBX?.password ?: ""
set(value) {
entryKDB?.password = value
entryKDBX?.password = value
}
override var url: String
get() = entryKDB?.url ?: entryKDBX?.url ?: ""
set(value) {
entryKDB?.url = value
entryKDBX?.url = value
}
override var notes: String
get() = entryKDB?.notes ?: entryKDBX?.notes ?: ""
set(value) {
entryKDB?.notes = value
entryKDBX?.notes = value
}
private fun isTan(): Boolean {
return title == PMS_TAN_ENTRY && username.isNotEmpty()
}
/**
* {@inheritDoc}
* Get the display title from an entry, <br></br>
* [.startManageEntry] and [.stopManageEntry] must be called
* before and after [.getVisualTitle]
*/
fun getVisualTitle(): String {
return if (isTan()) {
"$PMS_TAN_ENTRY $username"
} else {
if (title.isEmpty())
if (username.isEmpty())
if (url.isEmpty())
nodeId.toString()
else
url
else
username
else
title
}
}
/*
------------
KDB Methods
------------
*/
/**
* If it's a node with only meta information like Meta-info SYSTEM Database Color
* @return false by default, true if it's a meta stream
*/
val isMetaStream: Boolean
get() = entryKDB?.isMetaStream ?: false
/*
------------
KDBX Methods
------------
*/
var iconCustom: IconImageCustom
get() = entryKDBX?.iconCustom ?: IconImageCustom.UNKNOWN_ICON
set(value) {
entryKDBX?.iconCustom = value
}
/**
* Retrieve custom 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()
}
/**
* 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 getOtpElement(): OtpElement? {
return OtpEntryFields.parseFields { key ->
customFields[key]?.toString()
}
}
fun startToManageFieldReferences(db: DatabaseKDBX) {
entryKDBX?.startToManageFieldReferences(db)
}
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))
}
val actionEach = object : (Map.Entry<String, BinaryAttachment>)->Unit {
override fun invoke(mapEntry: Map.Entry<String, BinaryAttachment>) {
attachments.add(EntryAttachment(mapEntry.key, mapEntry.value))
}
}
entryKDBX?.binaries?.forEach(actionEach)
return attachments
}
fun getHistory(): ArrayList<Entry> {
val history = ArrayList<Entry>()
val entryKDBXHistory = entryKDBX?.history ?: ArrayList()
for (entryHistory in entryKDBXHistory) {
history.add(Entry(entryHistory))
}
return history
}
fun addEntryToHistory(entry: Entry) {
entry.entryKDBX?.let {
entryKDBX?.addEntryToHistory(it)
}
}
fun removeEntryFromHistory(position: Int) {
entryKDBX?.removeEntryFromHistory(position)
}
fun removeAllHistory() {
entryKDBX?.removeAllHistory()
}
fun removeOldestEntryFromHistory() {
entryKDBX?.removeOldestEntryFromHistory()
}
fun getSize(): Long {
return entryKDBX?.size ?: 0L
}
fun containsCustomData(): Boolean {
return entryKDBX?.containsCustomData() ?: false
}
/*
------------
Converter
------------
*/
/**
* Retrieve generated entry info,
* Remove parameter fields and add auto generated elements in auto custom fields
*/
fun getEntryInfo(database: Database?, raw: Boolean = false): EntryInfo {
val entryInfo = EntryInfo()
if (raw)
database?.stopManageEntry(this)
else
database?.startManageEntry(this)
entryInfo.id = nodeId.toString()
entryInfo.title = title
entryInfo.username = username
entryInfo.password = password
entryInfo.url = url
entryInfo.notes = notes
for (entry in customFields.entries) {
entryInfo.customFields.add(
Field(entry.key, entry.value))
}
// Add otpElement to generate token
entryInfo.otpModel = getOtpElement()?.otpModel
// Replace parameter fields by generated OTP fields
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
if (!raw)
database?.stopManageEntry(this)
return entryInfo
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Entry
if (entryKDB != other.entryKDB) return false
if (entryKDBX != other.entryKDBX) return false
return true
}
override fun hashCode(): Int {
var result = entryKDB?.hashCode() ?: 0
result = 31 * result + (entryKDBX?.hashCode() ?: 0)
return result
}
companion object CREATOR : Parcelable.Creator<Entry> {
override fun createFromParcel(parcel: Parcel): Entry {
return Entry(parcel)
}
override fun newArray(size: Int): Array<Entry?> {
return arrayOfNulls(size)
}
const val PMS_TAN_ENTRY = "<TAN>"
}
}

View File

@@ -1,361 +0,0 @@
package com.kunzisoft.keepass.database.element
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Field
import java.util.*
import kotlin.collections.ArrayList
class EntryVersioned : NodeVersioned, PwEntryInterface<GroupVersioned> {
var pwEntryV3: PwEntryV3? = null
private set
var pwEntryV4: PwEntryV4? = null
private set
fun updateWith(entry: EntryVersioned) {
entry.pwEntryV3?.let {
this.pwEntryV3?.updateWith(it)
}
entry.pwEntryV4?.let {
this.pwEntryV4?.updateWith(it)
}
}
/**
* Use this constructor to copy an Entry with exact same values
*/
constructor(entry: EntryVersioned) {
if (entry.pwEntryV3 != null) {
this.pwEntryV3 = PwEntryV3()
}
if (entry.pwEntryV4 != null) {
this.pwEntryV4 = PwEntryV4()
}
updateWith(entry)
}
constructor(entry: PwEntryV3) {
this.pwEntryV4 = null
this.pwEntryV3 = entry
}
constructor(entry: PwEntryV4) {
this.pwEntryV3 = null
this.pwEntryV4 = entry
}
constructor(parcel: Parcel) {
pwEntryV3 = parcel.readParcelable(PwEntryV3::class.java.classLoader)
pwEntryV4 = parcel.readParcelable(PwEntryV4::class.java.classLoader)
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeParcelable(pwEntryV3, flags)
dest.writeParcelable(pwEntryV4, flags)
}
var nodeId: PwNodeId<UUID>
get() = pwEntryV4?.nodeId ?: pwEntryV3?.nodeId ?: PwNodeIdUUID()
set(value) {
pwEntryV3?.nodeId = value
pwEntryV4?.nodeId = value
}
override var title: String
get() = pwEntryV3?.title ?: pwEntryV4?.title ?: ""
set(value) {
pwEntryV3?.title = value
pwEntryV4?.title = value
}
override var icon: PwIcon
get() {
return pwEntryV3?.icon ?: pwEntryV4?.icon ?: PwIconStandard()
}
set(value) {
pwEntryV3?.icon = value
pwEntryV4?.icon = value
}
override val type: Type
get() = Type.ENTRY
override var parent: GroupVersioned?
get() {
pwEntryV3?.parent?.let {
return GroupVersioned(it)
}
pwEntryV4?.parent?.let {
return GroupVersioned(it)
}
return null
}
set(value) {
pwEntryV3?.parent = value?.pwGroupV3
pwEntryV4?.parent = value?.pwGroupV4
}
override fun containsParent(): Boolean {
return pwEntryV3?.containsParent() ?: pwEntryV4?.containsParent() ?: false
}
override fun afterAssignNewParent() {
pwEntryV4?.afterChangeParent()
}
override fun touch(modified: Boolean, touchParents: Boolean) {
pwEntryV3?.touch(modified, touchParents)
pwEntryV4?.touch(modified, touchParents)
}
override fun isContainedIn(container: GroupVersioned): Boolean {
var contained: Boolean? = false
container.pwGroupV3?.let {
contained = pwEntryV3?.isContainedIn(it)
}
container.pwGroupV4?.let {
contained = pwEntryV4?.isContainedIn(it)
}
return contained ?: false
}
override var creationTime: PwDate
get() = pwEntryV3?.creationTime ?: pwEntryV4?.creationTime ?: PwDate()
set(value) {
pwEntryV3?.creationTime = value
pwEntryV4?.creationTime = value
}
override var lastModificationTime: PwDate
get() = pwEntryV3?.lastModificationTime ?: pwEntryV4?.lastModificationTime ?: PwDate()
set(value) {
pwEntryV3?.lastModificationTime = value
pwEntryV4?.lastModificationTime = value
}
override var lastAccessTime: PwDate
get() = pwEntryV3?.lastAccessTime ?: pwEntryV4?.lastAccessTime ?: PwDate()
set(value) {
pwEntryV3?.lastAccessTime = value
pwEntryV4?.lastAccessTime = value
}
override var expiryTime: PwDate
get() = pwEntryV3?.expiryTime ?: pwEntryV4?.expiryTime ?: PwDate()
set(value) {
pwEntryV3?.expiryTime = value
pwEntryV4?.expiryTime = value
}
override var isExpires: Boolean
get() =pwEntryV3?.isExpires ?: pwEntryV4?.isExpires ?: false
set(value) {
pwEntryV3?.isExpires = value
pwEntryV4?.isExpires = value
}
override var username: String
get() = pwEntryV3?.username ?: pwEntryV4?.username ?: ""
set(value) {
pwEntryV3?.username = value
pwEntryV4?.username = value
}
override var password: String
get() = pwEntryV3?.password ?: pwEntryV4?.password ?: ""
set(value) {
pwEntryV3?.password = value
pwEntryV4?.password = value
}
override var url: String
get() = pwEntryV3?.url ?: pwEntryV4?.url ?: ""
set(value) {
pwEntryV3?.url = value
pwEntryV4?.url = value
}
override var notes: String
get() = pwEntryV3?.notes ?: pwEntryV4?.notes ?: ""
set(value) {
pwEntryV3?.notes = value
pwEntryV4?.notes = value
}
private fun isTan(): Boolean {
return title == PMS_TAN_ENTRY && username.isNotEmpty()
}
fun getVisualTitle(): String {
return getVisualTitle(isTan(),
title,
username,
url,
nodeId.toString())
}
/*
------------
V3 Methods
------------
*/
/**
* If it's a node with only meta information like Meta-info SYSTEM Database Color
* @return false by default, true if it's a meta stream
*/
val isMetaStream: Boolean
get() = pwEntryV3?.isMetaStream ?: false
/*
------------
V4 Methods
------------
*/
var iconCustom: PwIconCustom
get() = pwEntryV4?.iconCustom ?: PwIconCustom.UNKNOWN_ICON
set(value) {
pwEntryV4?.iconCustom = value
}
/**
* Retrieve custom 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() = pwEntryV4?.customFields ?: HashMap()
/**
* To redefine if version of entry allow custom field,
* @return true if entry allows custom field
*/
fun allowCustomFields(): Boolean {
return pwEntryV4?.allowCustomFields() ?: false
}
/**
* Add an extra field to the list (standard or custom)
* @param label Label of field, must be unique
* @param value Value of field
*/
fun addExtraField(label: String, value: ProtectedString) {
pwEntryV4?.addExtraField(label, value)
}
fun startToManageFieldReferences(db: PwDatabaseV4) {
pwEntryV4?.startToManageFieldReferences(db)
}
fun stopToManageFieldReferences() {
pwEntryV4?.stopToManageFieldReferences()
}
fun addBackupToHistory() {
pwEntryV4?.let {
val entryHistory = PwEntryV4()
entryHistory.updateWith(it)
it.addEntryToHistory(entryHistory)
}
}
fun removeOldestEntryFromHistory() {
pwEntryV4?.removeOldestEntryFromHistory()
}
fun getHistory(): ArrayList<PwEntryV4> {
return pwEntryV4?.history ?: ArrayList()
}
fun containsCustomData(): Boolean {
return pwEntryV4?.containsCustomData() ?: false
}
/*
------------
Converter
------------
*/
fun getEntryInfo(database: Database?, raw: Boolean = false): EntryInfo {
val entryInfo = EntryInfo()
if (raw)
database?.stopManageEntry(this)
else
database?.startManageEntry(this)
entryInfo.id = nodeId.toString()
entryInfo.title = title
entryInfo.username = username
entryInfo.password = password
entryInfo.url = url
entryInfo.notes = notes
for (entry in customFields.entries) {
entryInfo.customFields.add(
Field(entry.key, entry.value))
}
if (!raw)
database?.stopManageEntry(this)
return entryInfo
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as EntryVersioned
if (pwEntryV3 != other.pwEntryV3) return false
if (pwEntryV4 != other.pwEntryV4) return false
return true
}
override fun hashCode(): Int {
var result = pwEntryV3?.hashCode() ?: 0
result = 31 * result + (pwEntryV4?.hashCode() ?: 0)
return result
}
companion object CREATOR : Parcelable.Creator<EntryVersioned> {
override fun createFromParcel(parcel: Parcel): EntryVersioned {
return EntryVersioned(parcel)
}
override fun newArray(size: Int): Array<EntryVersioned?> {
return arrayOfNulls(size)
}
const val PMS_TAN_ENTRY = "<TAN>"
/**
* {@inheritDoc}
* Get the display title from an entry, <br></br>
* [.startManageEntry] and [.stopManageEntry] must be called
* before and after [.getVisualTitle]
*/
fun getVisualTitle(isTan: Boolean, title: String, userName: String, url: String, id: String): String {
return if (isTan) {
"$PMS_TAN_ENTRY $userName"
} else {
if (title.isEmpty())
if (userName.isEmpty())
if (url.isEmpty())
id
else
url
else
userName
else
title
}
}
}
}

View File

@@ -0,0 +1,400 @@
/*
* 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.content.Context
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.*
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.*
import kotlin.collections.ArrayList
class Group : Node, GroupVersionedInterface<Group, Entry> {
var groupKDB: GroupKDB? = null
private set
var groupKDBX: GroupKDBX? = null
private set
fun updateWith(group: Group) {
group.groupKDB?.let {
this.groupKDB?.updateWith(it)
}
group.groupKDBX?.let {
this.groupKDBX?.updateWith(it)
}
}
/**
* Use this constructor to copy a Group
*/
constructor(group: Group) {
if (group.groupKDB != null) {
if (this.groupKDB == null)
this.groupKDB = GroupKDB()
}
if (group.groupKDBX != null) {
if (this.groupKDBX == null)
this.groupKDBX = GroupKDBX()
}
updateWith(group)
}
constructor(group: GroupKDB) {
this.groupKDBX = null
this.groupKDB = group
}
constructor(group: GroupKDBX) {
this.groupKDB = null
this.groupKDBX = group
}
constructor(parcel: Parcel) {
groupKDB = parcel.readParcelable(GroupKDB::class.java.classLoader)
groupKDBX = parcel.readParcelable(GroupKDBX::class.java.classLoader)
}
enum class ChildFilter {
META_STREAM, EXPIRED;
companion object {
fun getDefaults(context: Context): Array<ChildFilter> {
return if (PreferencesUtil.showExpiredEntries(context)) {
arrayOf(META_STREAM)
} else {
arrayOf(META_STREAM, EXPIRED)
}
}
}
}
companion object CREATOR : Parcelable.Creator<Group> {
override fun createFromParcel(parcel: Parcel): Group {
return Group(parcel)
}
override fun newArray(size: Int): Array<Group?> {
return arrayOfNulls(size)
}
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeParcelable(groupKDB, flags)
dest.writeParcelable(groupKDBX, flags)
}
override val nodeId: NodeId<*>?
get() = groupKDBX?.nodeId ?: groupKDB?.nodeId
override var title: String
get() = groupKDB?.title ?: groupKDBX?.title ?: ""
set(value) {
groupKDB?.title = value
groupKDBX?.title = value
}
override var icon: IconImage
get() = groupKDB?.icon ?: groupKDBX?.icon ?: IconImageStandard()
set(value) {
groupKDB?.icon = value
groupKDBX?.icon = value
}
override val type: Type
get() = Type.GROUP
override var parent: Group?
get() {
groupKDB?.parent?.let {
return Group(it)
}
groupKDBX?.parent?.let {
return Group(it)
}
return null
}
set(value) {
groupKDB?.parent = value?.groupKDB
groupKDBX?.parent = value?.groupKDBX
}
override fun containsParent(): Boolean {
return groupKDB?.containsParent() ?: groupKDBX?.containsParent() ?: false
}
override fun afterAssignNewParent() {
groupKDB?.afterAssignNewParent()
groupKDBX?.afterAssignNewParent()
}
fun addChildrenFrom(group: Group) {
group.groupKDB?.getChildEntries()?.forEach { entryToAdd ->
groupKDB?.addChildEntry(entryToAdd)
}
group.groupKDB?.getChildGroups()?.forEach { groupToAdd ->
groupKDB?.addChildGroup(groupToAdd)
}
group.groupKDBX?.getChildEntries()?.forEach { entryToAdd ->
groupKDBX?.addChildEntry(entryToAdd)
}
group.groupKDBX?.getChildGroups()?.forEach { groupToAdd ->
groupKDBX?.addChildGroup(groupToAdd)
}
}
override fun touch(modified: Boolean, touchParents: Boolean) {
groupKDB?.touch(modified, touchParents)
groupKDBX?.touch(modified, touchParents)
}
override fun isContainedIn(container: Group): Boolean {
var contained: Boolean? = null
container.groupKDB?.let {
contained = groupKDB?.isContainedIn(it)
}
container.groupKDBX?.let {
contained = groupKDBX?.isContainedIn(it)
}
return contained ?: false
}
override var creationTime: DateInstant
get() = groupKDB?.creationTime ?: groupKDBX?.creationTime ?: DateInstant()
set(value) {
groupKDB?.creationTime = value
groupKDBX?.creationTime = value
}
override var lastModificationTime: DateInstant
get() = groupKDB?.lastModificationTime ?: groupKDBX?.lastModificationTime ?: DateInstant()
set(value) {
groupKDB?.lastModificationTime = value
groupKDBX?.lastModificationTime = value
}
override var lastAccessTime: DateInstant
get() = groupKDB?.lastAccessTime ?: groupKDBX?.lastAccessTime ?: DateInstant()
set(value) {
groupKDB?.lastAccessTime = value
groupKDBX?.lastAccessTime = value
}
override var expiryTime: DateInstant
get() = groupKDB?.expiryTime ?: groupKDBX?.expiryTime ?: DateInstant()
set(value) {
groupKDB?.expiryTime = value
groupKDBX?.expiryTime = value
}
override var expires: Boolean
get() = groupKDB?.expires ?: groupKDBX?.expires ?: false
set(value) {
groupKDB?.expires = value
groupKDBX?.expires = value
}
override val isCurrentlyExpires: Boolean
get() = groupKDB?.isCurrentlyExpires ?: groupKDBX?.isCurrentlyExpires ?: false
override fun getChildGroups(): MutableList<Group> {
val children = ArrayList<Group>()
groupKDB?.getChildGroups()?.forEach {
children.add(Group(it))
}
groupKDBX?.getChildGroups()?.forEach {
children.add(Group(it))
}
return children
}
override fun getChildEntries(): MutableList<Entry> {
// To cal function with vararg
return getChildEntries(*emptyArray<ChildFilter>())
}
fun getChildEntries(vararg filter: ChildFilter): MutableList<Entry> {
val children = ArrayList<Entry>()
val withoutMetaStream = filter.contains(ChildFilter.META_STREAM)
val showExpiredEntries = !filter.contains(ChildFilter.EXPIRED)
groupKDB?.getChildEntries()?.forEach {
val entryToAddAsChild = Entry(it)
if ((!withoutMetaStream || (withoutMetaStream && !entryToAddAsChild.isMetaStream))
&& (!entryToAddAsChild.isCurrentlyExpires or showExpiredEntries))
children.add(entryToAddAsChild)
}
groupKDBX?.getChildEntries()?.forEach {
val entryToAddAsChild = Entry(it)
if (!entryToAddAsChild.isCurrentlyExpires or showExpiredEntries)
children.add(entryToAddAsChild)
}
return children
}
/**
* Filter entries and return children
* @return List of direct children (one level below) as NodeVersioned
*/
fun getChildren(vararg filter: ChildFilter): List<Node> {
val children = ArrayList<Node>()
children.addAll(getChildGroups())
groupKDB?.let {
children.addAll(getChildEntries(*filter))
}
groupKDBX?.let {
// No MetasStream in V4
children.addAll(getChildEntries(*filter))
}
return children
}
override fun addChildGroup(group: Group) {
group.groupKDB?.let {
groupKDB?.addChildGroup(it)
}
group.groupKDBX?.let {
groupKDBX?.addChildGroup(it)
}
}
override fun addChildEntry(entry: Entry) {
entry.entryKDB?.let {
groupKDB?.addChildEntry(it)
}
entry.entryKDBX?.let {
groupKDBX?.addChildEntry(it)
}
}
override fun removeChildGroup(group: Group) {
group.groupKDB?.let {
groupKDB?.removeChildGroup(it)
}
group.groupKDBX?.let {
groupKDBX?.removeChildGroup(it)
}
}
override fun removeChildEntry(entry: Entry) {
entry.entryKDB?.let {
groupKDB?.removeChildEntry(it)
}
entry.entryKDBX?.let {
groupKDBX?.removeChildEntry(it)
}
}
override fun removeChildren() {
groupKDB?.removeChildren()
groupKDBX?.removeChildren()
}
override fun allowAddEntryIfIsRoot(): Boolean {
return groupKDB?.allowAddEntryIfIsRoot() ?: groupKDBX?.allowAddEntryIfIsRoot() ?: false
}
/*
------------
KDB Methods
------------
*/
var nodeIdKDB: NodeId<Int>
get() = groupKDB?.nodeId ?: NodeIdInt()
set(value) { groupKDB?.nodeId = value }
fun setNodeId(id: NodeIdInt) {
groupKDB?.nodeId = id
}
fun getLevel(): Int {
return groupKDB?.level ?: -1
}
fun setLevel(level: Int) {
groupKDB?.level = level
}
/*
------------
KDBX Methods
------------
*/
var nodeIdKDBX: NodeId<UUID>
get() = groupKDBX?.nodeId ?: NodeIdUUID()
set(value) { groupKDBX?.nodeId = value }
fun setNodeId(id: NodeIdUUID) {
groupKDBX?.nodeId = id
}
fun setEnableAutoType(enableAutoType: Boolean?) {
groupKDBX?.enableAutoType = enableAutoType
}
fun setEnableSearching(enableSearching: Boolean?) {
groupKDBX?.enableSearching = enableSearching
}
fun setExpanded(expanded: Boolean) {
groupKDBX?.isExpanded = expanded
}
fun containsCustomData(): Boolean {
return groupKDBX?.containsCustomData() ?: false
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Group
if (groupKDB != other.groupKDB) return false
if (groupKDBX != other.groupKDBX) return false
return true
}
override fun hashCode(): Int {
var result = groupKDB?.hashCode() ?: 0
result = 31 * result + (groupKDBX?.hashCode() ?: 0)
return result
}
}

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