Compare commits

...

326 Commits

Author SHA1 Message Date
J-Jamet
df33c5feb5 First commit for cancelable loading 2019-10-24 14:53:55 +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
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
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
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
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
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-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
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-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
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
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
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
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
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
J-Jamet
9700dbcc3f Merge branch 'release/2.5.0.0beta23' 2019-09-14 11:31:30 +02:00
J-Jamet
4e344458b2 Fix warnings 2019-09-14 11:17:16 +02:00
J-Jamet
6f95cc7296 Merge branch 'translations' into develop 2019-09-14 10:39:55 +02:00
J-Jamet
cd66f8f57e Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2019-09-14 10:39:24 +02:00
J-Jamet
83f0eb9a33 Fix database preferences 2019-09-13 22:20:56 +02:00
J-Jamet
ca89bba768 Update screen image 2019-09-13 21:50:37 +02:00
J-Jamet
2d2bd5013e Upgrade Store Screens 2019-09-13 21:45:55 +02:00
J-Jamet
b93d7bbf41 Better message view for fingerprint problem #114 2019-09-13 19:49:53 +02:00
J-Jamet
000277705a Fix #316 Search stop selection mode 2019-09-13 17:04:33 +02:00
J-Jamet
088712e784 Small log change 2019-09-13 16:02:48 +02:00
J-Jamet
117592387e Change SDK version for icon packs 2019-09-13 15:30:34 +02:00
J-Jamet
76bb1a369c Fix switch to previous keyboard deprecation 2019-09-13 14:39:34 +02:00
J-Jamet
9331c281fe Fix small variables names 2019-09-13 13:44:12 +02:00
J-Jamet
f2666316e1 Better UriUtil methods 2019-09-12 17:07:42 +02:00
Kwangho Kim
7c2ff5067d Translated using Weblate (Korean)
Currently translated at 43.5% (164 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ko/
2019-09-12 16:24:01 +02:00
zeritti
5fed641c7c Translated using Weblate (Czech)
Currently translated at 99.7% (376 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2019-09-12 16:24:01 +02:00
Allan Nordhøy
18bd62ee5a Translated using Weblate (English)
Currently translated at 99.7% (376 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2019-09-12 16:23:56 +02:00
Allan Nordhøy
bc57e6e257 Translated using Weblate (German)
Currently translated at 98.7% (372 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-09-12 16:23:56 +02:00
C. Rüdinger
69b1aba218 Translated using Weblate (German)
Currently translated at 98.7% (372 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-09-12 16:23:56 +02:00
zeritti
df04d998c2 Translated using Weblate (German)
Currently translated at 98.7% (372 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-09-12 16:23:55 +02:00
Mesut Akcan
e986fe5f60 Translated using Weblate (Turkish)
Currently translated at 99.7% (376 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2019-09-12 16:23:54 +02:00
J-Jamet
e4ac0ee258 Fix seekbar value 2019-09-12 14:39:27 +02:00
J-Jamet
426aa0e7da Better StringUtil code 2019-09-12 14:19:14 +02:00
J-Jamet
6c0a48af48 Remove unused Tests 2019-09-12 14:15:34 +02:00
J-Jamet
d865da1613 Fix small issue 2019-09-12 13:49:14 +02:00
J-Jamet
6fa4c1e06e Fix warnings 2019-09-12 12:39:45 +02:00
J-Jamet
52a6b3e046 Upgrade Preference library 2019-09-12 12:39:27 +02:00
J-Jamet
fa26d2f938 Fix mini fab color 2019-09-12 11:52:43 +02:00
J-Jamet
4777cdc7ae Fix file_modification view 2019-09-12 11:28:12 +02:00
J-Jamet
cd8d3cbf6a Fix margins 2019-09-12 10:52:04 +02:00
J-Jamet
e6a6feb5c0 Fix margins 2019-09-12 10:46:35 +02:00
J-Jamet
98d6fb9214 Remove file_manager links 2019-09-12 10:41:31 +02:00
J-Jamet
94244cd15b Fix #242 Null AutoType 2019-09-11 18:03:03 +02:00
J-Jamet
c5e2ca9907 Fix #327 save to previous database version when conditions are required 2019-09-11 16:54:58 +02:00
Kwangho Kim
e4fef44caf Translated using Weblate (English)
Currently translated at 99.7% (376 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2019-09-11 15:59:08 +02:00
J-Jamet
5bd9da9bb1 Small change in ic_launcher 2019-09-10 16:31:37 +02:00
J-Jamet
5f0e899679 Add TODO for history 2019-09-10 16:26:17 +02:00
J-Jamet
4b1806900b Fix Broadcast Receiver don't working in Android 9 2019-09-10 16:26:02 +02:00
J-Jamet
30e2912885 Remove unused code 2019-09-10 16:19:52 +02:00
J-Jamet
fc3608ff69 Refactor startActivityForEntrySelection 2019-09-10 16:11:20 +02:00
J-Jamet
6de47ec9b2 Fix small bug 2019-09-10 16:06:43 +02:00
J-Jamet
f7253764a2 Multiline to password generator and up to 128 chars max 2019-09-10 12:53:03 +02:00
J-Jamet
c54b134c31 Masked password in generator #324 2019-09-10 12:43:29 +02:00
J-Jamet
a6bdca52be Fix Search shows Backup entries #320 2019-09-10 12:33:59 +02:00
J-Jamet
29eec05f8f Fix warning with parcelable 2019-09-09 13:35:29 +02:00
J-Jamet
250aef9738 ImageView Key Generator as Button 2019-09-09 12:30:58 +02:00
J-Jamet
50097914a2 Let Copy UUID 2019-09-09 11:59:26 +02:00
J-Jamet
ecff4fb2c5 Update CHANGELOG 2019-09-09 11:00:19 +02:00
J-Jamet
ab3d17f352 Fix OOM #256 2019-09-09 10:55:34 +02:00
J-Jamet
a75d237e53 Kotlinized MemUtil 2019-09-09 10:11:16 +02:00
Allan Nordhøy
a0c7786e1b Translated using Weblate (Norwegian Bokmål)
Currently translated at 93.6% (353 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2019-09-06 21:24:59 +02:00
J-Jamet
0d32c38c79 Fix custom fields hidden 2019-09-06 14:06:51 +02:00
J-Jamet
b846eda410 Update CHANGELOG 2019-09-06 12:44:06 +02:00
J-Jamet
7c33c9ec02 Better custom fields implementation, fix references 2019-09-06 12:39:27 +02:00
J-Jamet
74c08340a6 Fix field references #61 2019-09-06 11:10:04 +02:00
J-Jamet
82161536be Add DE translations 2019-09-05 15:42:18 +02:00
J-Jamet
752dbca356 Update CHANGELOG 2019-09-05 15:40:34 +02:00
J-Jamet
9ef56f6fd8 Register fingerprint only if database is open #322 2019-09-05 14:56:07 +02:00
J-Jamet
0239f115ae Fix show biometric view issue 2019-09-05 13:11:48 +02:00
J-Jamet
9775e09221 Fix small issue 2019-09-04 14:46:02 +02:00
J-Jamet
c975a1bfc0 Fix small issues 2019-09-04 14:33:31 +02:00
J-Jamet
c263536078 Change icon background for custom image 2019-09-04 14:02:55 +02:00
J-Jamet
b559eeaad0 Merge branch 'feature/Biometric' into develop 2019-09-04 13:17:59 +02:00
J-Jamet
63373083ab Store biometric cipher values in database 2019-09-04 13:17:33 +02:00
Swann Martinet
bf525807b0 Translated using Weblate (French)
Currently translated at 100.0% (377 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-09-04 07:25:10 +02:00
Swann Martinet
921021078b Translated using Weblate (Italian)
Currently translated at 99.2% (374 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2019-09-04 07:25:10 +02:00
J-Jamet
e5b60a8413 Small changes 2019-09-02 20:08:17 +02:00
J-Jamet
e01402f2fa Remove unused comments 2019-09-02 20:04:15 +02:00
J-Jamet
27f92e1bb5 Remove unused dependencies 2019-09-02 19:54:27 +02:00
J-Jamet
63db6de30b Links as buttons in description dialog 2019-09-02 19:42:28 +02:00
J-Jamet
7a038126cf Replace fingerprint remove icon and fix small issue 2019-09-02 19:11:51 +02:00
J-Jamet
edcfa8cf7b Better biometric key implementation 2019-09-02 18:46:55 +02:00
J-Jamet
c9594948a2 Fix small biometric issues 2019-09-02 18:30:21 +02:00
J-Jamet
66988ecb66 Better biometric view message implementation 2019-09-02 17:36:14 +02:00
J-Jamet
186ca30be8 Add preference to auto open biometric prompt 2019-09-02 14:47:03 +02:00
J-Jamet
027d581dcc Small enhancements 2019-09-02 14:04:05 +02:00
J-Jamet
adcc1c745a Fix small issues 2019-09-02 13:37:07 +02:00
J-Jamet
8682856c01 Add explanation dialog and fix store bug 2019-09-02 13:26:18 +02:00
J-Jamet
9ec976d246 Add preferences for advanced unlock 2019-09-02 12:31:56 +02:00
J Smith
9d17b49586 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (377 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2019-09-02 12:25:09 +02:00
Stephan Paternotte
698496d37c Translated using Weblate (Dutch)
Currently translated at 98.4% (371 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2019-09-02 12:25:01 +02:00
Allan Nordhøy
2af8c4f3c8 Translated using Weblate (English)
Currently translated at 99.7% (376 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2019-09-02 12:24:54 +02:00
kee
b164099b6d Translated using Weblate (German)
Currently translated at 95.0% (358 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-09-02 12:24:53 +02:00
Mesut Akcan
c19357605f 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-02 12:24:51 +02:00
J-Jamet
5a08fa0088 New state mode for biometric unlock 2019-09-02 12:01:36 +02:00
J-Jamet
bff87d16b1 Start biometric feature 2019-09-01 17:54:01 +02:00
J-Jamet
5b0afa447c Text size by percent 2019-09-01 17:45:39 +02:00
J-Jamet
5a882a954f Text size by percent 2019-09-01 17:44:36 +02:00
J-Jamet
47f340d576 Fix null inheritance 2019-09-01 15:49:49 +02:00
WaldiS
41df139c17 Translated using Weblate (Polish)
Currently translated at 96.6% (364 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2019-08-31 11:25:06 +02:00
underlineGalaxy
469c267161 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.4% (371 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2019-08-31 11:25:05 +02:00
Mesut Akcan
f336d4fe58 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-08-31 11:25:05 +02:00
J-Jamet
e1d997cc91 Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2019-08-30 13:18:36 +02:00
J-Jamet
53cf4bba1b Merge branch 'feature/AndroidX' into develop 2019-08-30 10:08:40 +02:00
J-Jamet
030417dbe1 Better education methods 2019-08-30 10:07:49 +02:00
J-Jamet
59abcb115c Add .cxx to gitignore 2019-08-29 17:36:39 +02:00
J-Jamet
598dbd3794 Migrate to AndroidX and remove unused libs 2019-08-29 17:13:14 +02:00
J-Jamet
2c4a7e5576 Migrate to API 28 2019-08-29 13:58:59 +02:00
J-Jamet
02429d5790 Change lock back default value to false 2019-08-29 13:36:55 +02:00
J-Jamet
d990cb24ea Fix #314 magikeyboard notification 2019-08-29 13:35:30 +02:00
J-Jamet
e549b16dce Update CHANGELOG 2019-08-29 13:29:24 +02:00
J-Jamet
e11864a64f Remove long press in history list 2019-08-29 13:23:55 +02:00
J-Jamet
a3e74f8ee5 Fix text size in list 2019-08-29 13:15:59 +02:00
J-Jamet
36cb683404 Better file history listener implementation 2019-08-29 11:13:29 +02:00
ssantos
f7f0028033 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (377 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2019-08-27 21:24:47 +02:00
WaldiS
f06088fa12 Translated using Weblate (Polish)
Currently translated at 96.0% (362 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2019-08-27 21:24:29 +02:00
J-Jamet
b62ef8a2ed Update database alias 2019-08-27 13:13:31 +02:00
J-Jamet
622ba65841 Prevent information to be closed 2019-08-26 15:07:22 +02:00
J-Jamet
3a48d20d12 Change list history views 2019-08-26 14:53:23 +02:00
J-Jamet
8d6db78f55 Add file database history info as sub part of recyclerview element 2019-08-26 13:32:33 +02:00
J-Jamet
f3ba6e800a Rename OpenFileHelper 2019-08-26 10:35:18 +02:00
J-Jamet
1c4aaf9807 Encapsulate URI methods 2019-08-26 09:46:17 +02:00
J-Jamet
c65ed41efd Move error in init uri method 2019-08-26 08:03:16 +02:00
J-Jamet
1965336077 Remove WeakReference to retrieve FileDatabaseHistory 2019-08-25 12:51:32 +02:00
J-Jamet
9b7095ad4c Change App database package and name 2019-08-25 12:28:13 +02:00
J-Jamet
33767c2bf9 Add room and fully manage database file history by SQL 2019-08-25 12:04:10 +02:00
Kunzisoft
82f7e861e7 Translated using Weblate (French)
Currently translated at 100.0% (377 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-08-23 23:24:20 +02:00
J-Jamet
846b5fa449 Add octet-stream as mimetype 2019-08-22 14:08:53 +02:00
J-Jamet
baf4e676eb Updates intent-filter to recognize more .kdbx files 2019-08-22 13:59:16 +02:00
J-Jamet
a9bf3e83c4 Fix activity_password layout 2019-08-22 13:14:55 +02:00
J-Jamet
710a1b0996 Better password generation errors 2019-08-22 12:11:15 +02:00
J-Jamet
c4671b84a0 Better error management for AssignMasterKey 2019-08-22 11:53:33 +02:00
J-Jamet
c0c98d0299 Remove unused Toast and strings 2019-08-21 18:18:56 +02:00
J-Jamet
ffdec77d11 Better error and url management 2019-08-21 17:56:49 +02:00
Ldm Public
868bbe2e70 Translated using Weblate (French)
Currently translated at 96.6% (364 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-08-21 16:24:18 +02:00
jan madsen
4aac655a5d Translated using Weblate (Danish)
Currently translated at 95.5% (360 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2019-08-21 16:24:17 +02:00
Wilker Santana da Silva
510244aa70 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.1% (370 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2019-08-21 16:24:16 +02:00
Mesut Akcan
50840b04b4 Translated using Weblate (Turkish)
Currently translated at 96.3% (363 of 377 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2019-08-21 16:24:14 +02:00
J-Jamet
4d5962f5ca Merge branch 'feature/Intent_Create_File' into develop 2019-08-21 14:04:57 +02:00
J-Jamet
c9456c771c Fix fist time clipboard warning and better views implementation 2019-08-21 14:04:34 +02:00
J-Jamet
41a7a583d4 Upgrade CHANGELOG 2019-08-21 12:33:01 +02:00
J-Jamet
2e5220fa8a Better file manager installation description 2019-08-21 12:23:44 +02:00
J-Jamet
b75475d785 Fix master key creation during orientation change 2019-08-21 11:50:06 +02:00
J-Jamet
362ef06bb5 Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2019-08-20 21:41:44 +02:00
J-Jamet
4a80c9f9f9 Remove unused permission dispatcher 2019-08-20 14:07:10 +02:00
J-Jamet
f1fdb9fc84 Remove SAF setting and fix education when create button is gone 2019-08-20 13:55:57 +02:00
J-Jamet
5bb9168c29 Simplify database file creation 2019-08-20 13:12:44 +02:00
J-Jamet
0245dcd8e8 Merge branch 'develop' into feature/Intent_Create_File 2019-08-20 10:33:47 +02:00
J-Jamet
b31bfa1d4f Upgrade version to 2.5.0.0beta23 2019-08-20 10:33:02 +02:00
J-Jamet
0778f22b68 Create database with additional button 2019-08-19 20:01:05 +02:00
J-Jamet
4808696398 Remove unused create file verification 2019-08-19 19:28:08 +02:00
J-Jamet
0ea7b5b25f Merge branch 'develop' into feature/Intent_Create_File 2019-08-19 19:10:13 +02:00
Hosted Weblate
995785de9f Merge branch 'origin/master' into Weblate. 2019-08-19 18:01:01 +02:00
Marco Scaglioni
5deef427c0 Translated using Weblate (Italian)
Currently translated at 100.0% (356 of 356 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2019-08-19 18:01:01 +02:00
J-Jamet
024c6631d8 Merge tag '2.5.0.0beta22' into develop
2.5.0.0beta22
2019-08-19 17:58:00 +02:00
J-Jamet
78354e4736 Merge branch 'release/2.5.0.0beta22' 2019-08-19 17:57:42 +02:00
J-Jamet
b9a792e6bd Upgrade default fastfile 2019-08-19 17:48:56 +02:00
J-Jamet
c533d21250 Fix small view warning 2019-08-19 17:30:56 +02:00
J-Jamet
74572c8102 Upgrade version and CHANGELOGS 2019-08-19 17:13:28 +02:00
J-Jamet
b8de64fab0 New icon background 2019-08-19 17:06:38 +02:00
J-Jamet
877b909205 Merge branch 'translations' into develop 2019-08-19 16:43:51 +02:00
J-Jamet
1b1dcc0f45 Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2019-08-19 16:43:23 +02:00
J-Jamet
94e5988794 Fix remove entry for databaseV1 2019-08-19 15:17:00 +02:00
J-Jamet
b22333fda5 Show empty title in entry content view if title is empty 2019-08-19 15:00:33 +02:00
J-Jamet
400c6bef78 Disable recycle bin at bottom option if natural order 2019-08-19 14:44:56 +02:00
J-Jamet
edf6c2ff07 Fix remove action for natural order 2019-08-19 14:16:29 +02:00
J-Jamet
5eec1a276c Fix recyclebin update 2019-08-19 01:42:23 +02:00
J-Jamet
f707fd7649 Fix move and copy entry in database V1 root 2019-08-19 01:03:09 +02:00
J-Jamet
75b028daf3 Fix actions duplicate entries 2019-08-19 00:07:14 +02:00
J-Jamet
c6f259d18f Fix move action 2019-08-18 22:57:44 +02:00
J-Jamet
954d522341 Add UUID in Entry View 2019-08-18 22:32:33 +02:00
J-Jamet
1f8d17d27e Merge tag '2.5.0.0beta21' into develop
2.5.0.0beta21
2019-08-18 19:18:36 +02:00
J-Jamet
c2781af38d Merge branch 'release/2.5.0.0beta21' 2019-08-18 19:18:27 +02:00
J-Jamet
a1cd2683d4 Fix small views issues 2019-08-18 18:31:14 +02:00
J-Jamet
6d671f53c2 Fix content_description and translation 2019-08-18 17:48:21 +02:00
J-Jamet
4efb81f597 Fix translations 2019-08-18 17:11:19 +02:00
J-Jamet
faddeb6e2d Fix for entry duplicate and import 2019-08-18 16:47:02 +02:00
J-Jamet
e36629968d Fix edit group #296 2019-08-18 15:07:32 +02:00
J-Jamet
0b756797f6 Fix entry number size 2019-08-18 14:55:22 +02:00
J-Jamet
110c7bbbc7 Upgrade CHANGELOG 2019-08-18 14:46:00 +02:00
J-Jamet
639c6dc4ac Add number of entries in node view 2019-08-18 14:43:05 +02:00
J-Jamet
f605d36adf Remove TODO sort 2019-08-18 14:17:04 +02:00
J-Jamet
f6d6c134f4 Fix colorTextInverse 2019-08-18 14:12:31 +02:00
J-Jamet
a0c07654df Update CHANGELOGS 2019-08-18 11:14:07 +02:00
J-Jamet
03ab688abe Fix bug open button disabled with only keyFile #295 2019-08-18 11:10:07 +02:00
J-Jamet
4a28802b02 Add RecycleBin at Bottom sort option 2019-08-17 17:11:06 +02:00
J-Jamet
18f8fe7cc3 Upgrade CHANGELOG 2019-08-17 16:36:45 +02:00
J-Jamet
459606f5d5 Fix sort for natural database 2019-08-17 16:34:56 +02:00
J-Jamet
3d53c31680 Better sort implementation 2019-08-17 15:15:21 +02:00
J-Jamet
173dd5b59b Add natural database sort 2019-08-17 14:29:18 +02:00
J-Jamet
8536de3555 Remove email (no more code) 2019-08-17 13:21:48 +02:00
J-Jamet
722ba5c34d Move construct tree methods in Importer 2019-08-17 13:20:53 +02:00
J-Jamet
3ccfd7226b Upgrade to version 2.5.0.0beta21 and fix nested groups in v1 #292 2019-08-17 12:58:11 +02:00
J-Jamet
578da7bae5 Fix copy_field error 2019-08-16 21:01:06 +02:00
J-Jamet
6916389657 Merge tag '2.5.0.0beta20' into develop
2.5.0.0beta20
2019-08-16 20:34:23 +02:00
J-Jamet
b737501d4d Start change database file creation 2019-08-16 11:24:00 +02:00
Allan Nordhøy
c303ffafb5 Translated using Weblate (Norwegian Bokmål)
Currently translated at 96.3% (343 of 356 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2019-08-16 01:23:47 +02:00
Kunzisoft
6f513b4920 Translated using Weblate (French)
Currently translated at 99.4% (354 of 356 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-08-16 01:23:46 +02:00
Wilker Santana da Silva
a83c60583f Translated using Weblate (English)
Currently translated at 99.7% (355 of 356 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2019-08-16 01:23:45 +02:00
Kunzisoft
cde8950257 Translated using Weblate (English)
Currently translated at 99.7% (355 of 356 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2019-08-16 01:23:45 +02:00
jan madsen
0b4dd1e909 Translated using Weblate (Danish)
Currently translated at 96.6% (344 of 356 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2019-08-16 01:23:45 +02:00
Wilker Santana da Silva
28e2600271 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.4% (354 of 356 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2019-08-16 01:23:44 +02:00
J-Jamet
53cc4f74c8 Add logo for Google Play Store guidelines 2019-08-15 13:28:02 +02:00
449 changed files with 13974 additions and 11963 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]
**KeePass DX (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]
- Browser: [e.g. Chrome]
**Additional context**
Add any other context about the problem here.

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.

View File

@@ -1,3 +1,37 @@
KeepassDX (2.5.0.0beta24)
* 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)
* Fix settings
* Fix edit group
* Fix small bugs
KeepassDX (2.5.0.0beta23)
* New, more secure database creation workflow
* Recognize more database files
* Add alias for history files (WARNING: history is erased)
* New Biometric unlock (Fingerprint with new API)
* Fix entry references
* Fix OOM with KeyFile
* Fix small issues
KeepassDX (2.5.0.0beta22)
* Rebuild code for actions
* Add UUID as entry view
* Fix bug with natural order
* Fix number of entries in databaseV1
* New entry views
KeepassDX (2.5.0.0beta21)
* Fix nested groups no longer visible in V1 databases
* Improved data import algorithm for V1 databases
* Add natural database sort
* Add username database sort
* Fix button disabled with only KeyFile
* Show the number of entries in a group
KeepassDX (2.5.0.0beta20)
* Fix a major bug that displays an entry history

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

1
app/.gitignore vendored
View File

@@ -1 +1,2 @@
.cxx
.externalNativeBuild

View File

@@ -4,15 +4,16 @@ apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 27
compileSdkVersion 28
buildToolsVersion '28.0.3'
ndkVersion "20.0.5594570"
defaultConfig {
applicationId "com.kunzisoft.keepass"
minSdkVersion 14
targetSdkVersion 27
versionCode = 20
versionName = "2.5.0.0beta20"
targetSdkVersion 28
versionCode = 24
versionName = "2.5.0.0beta24"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
@@ -79,42 +80,35 @@ android {
}
}
def supportVersion = "27.1.1"
def spongycastleVersion = "1.58.0.0"
def permissionDispatcherVersion = "3.3.1"
def room_version = "2.2.0"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.android.support:appcompat-v7:$supportVersion"
implementation "com.android.support:design:$supportVersion"
implementation "com.android.support:preference-v7:$supportVersion"
implementation "com.android.support:preference-v14:$supportVersion"
implementation "com.android.support:cardview-v7:$supportVersion"
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.preference:preference:1.1.0'
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-rc01'
implementation 'com.google.android.material:material:1.0.0'
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
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'
implementation 'org.sufficientlysecure:html-textview:3.5'
implementation 'com.nononsenseapps:filepicker:4.1.0'
// Color
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3'
// Education
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.12.0'
// Permissions
implementation("com.github.hotchemi:permissionsdispatcher:$permissionDispatcherVersion") {
// if you don't use android.app.Fragment you can exclude support for them
exclude module: "support-v13"
}
kapt "com.github.hotchemi:permissionsdispatcher-processor:$permissionDispatcherVersion"
// 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'
// IO-Extras
implementation 'com.github.davidmoten:io-extras:0.1'
implementation 'com.google.code.gson:gson:2.8.4'
implementation 'com.google.guava:guava:23.0-android'
// Icon pack
implementation project(path: ':icon-pack-classic')
implementation project(path: ':icon-pack-material')

View File

@@ -1,44 +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.tests;
import android.test.AndroidTestCase;
import com.kunzisoft.keepass.tests.database.TestData;
public class AccentTest extends AndroidTestCase {
private static final String KEYFILE = "";
private static final String PASSWORD = "é";
private static final String ASSET = "accent.kdb";
private static final String FILENAME = "/sdcard/accent.kdb";
public void testOpen() {
/*
try {
TestData.GetDb(getContext(), ASSET, PASSWORD, KEYFILE, FILENAME);
} catch (Exception e) {
assertTrue("Failed to open database", false);
}
*/
}
}

View File

@@ -1,34 +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.tests;
import junit.framework.Test;
import junit.framework.TestSuite;
import android.test.suitebuilder.TestSuiteBuilder;
public class AllTests extends TestSuite {
public static Test suite() {
return new TestSuiteBuilder(AllTests.class)
.includeAllPackagesUnderHere()
.build();
}
}

View File

@@ -1,35 +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.tests;
import junit.framework.Test;
import junit.framework.TestSuite;
import android.test.suitebuilder.TestSuiteBuilder;
public class OutputTests extends TestSuite {
public static Test suite() {
return new TestSuiteBuilder(AllTests.class)
.includePackages("com.kunzisoft.keepass.tests.output")
.build();
}
}

View File

@@ -1,63 +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.tests;
import static org.junit.Assert.assertArrayEquals;
import java.io.UnsupportedEncodingException;
import java.util.Calendar;
import android.test.AndroidTestCase;
import com.kunzisoft.keepass.database.element.PwEntryV3;
import com.kunzisoft.keepass.tests.database.TestData;
public class PwEntryTestV3 extends AndroidTestCase {
PwEntryV3 mPE;
@Override
protected void setUp() throws Exception {
super.setUp();
// mPE = (PwEntryV3) TestData.GetTest1(getContext()).getEntryAt(0);
}
public void testName() {
assertTrue("Name was " + mPE.getTitle(), mPE.getTitle().equals("Amazon"));
}
public void testPassword() throws UnsupportedEncodingException {
String sPass = "12345";
byte[] password = sPass.getBytes("UTF-8");
assertArrayEquals(password, mPE.getPasswordBytes());
}
public void testCreation() {
Calendar cal = Calendar.getInstance();
cal.setTime(mPE.getCreationTime().getDate());
assertEquals("Incorrect year.", cal.get(Calendar.YEAR), 2009);
assertEquals("Incorrect month.", cal.get(Calendar.MONTH), 3);
assertEquals("Incorrect day.", cal.get(Calendar.DAY_OF_MONTH), 23);
}
}

View File

@@ -1,59 +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.tests;
import junit.framework.TestCase;
public class PwEntryTestV4 extends TestCase {
public void testAssign() {
/*
TODO Test
PwEntryV4 entry = new PwEntryV4();
entry.setAdditional("test223");
entry.setAutoType(new AutoType());
entry.getAutoType().defaultSequence = "1324";
entry.getAutoType().enabled = true;
entry.getAutoType().obfuscationOptions = 123412432109L;
entry.getAutoType().put("key", "value");
entry.setBackgroundColor("blue");
entry.putProtectedBinary("key1", new ProtectedBinary(false, new byte[] {0,1}));
entry.setIconCustom(new PwIconCustom(UUID.randomUUID(), new byte[0]));
entry.setForegroundColor("red");
entry.addToHistory(new PwEntryV4());
entry.setIconStandard(new PwIconStandard(5));
entry.setOverrideURL("override");
entry.setParent(new PwGroupV4());
entry.addExtraField("key2", new ProtectedString(false, "value2"));
entry.setUrl("http://localhost");
entry.setNodeId(UUID.randomUUID());
PwEntryV4 target = new PwEntryV4();
target.updateWith(entry);
/* This test is not so useful now that I am not implementing value equality for Entries
assertTrue("Entries do not match.", entry.equals(target));
*/
}
}

View File

@@ -1,44 +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.tests;
import android.test.AndroidTestCase;
import com.kunzisoft.keepass.database.element.PwGroupV3;
import com.kunzisoft.keepass.tests.database.TestData;
public class PwGroupTest extends AndroidTestCase {
PwGroupV3 mPG;
@Override
protected void setUp() throws Exception {
super.setUp();
//mPG = (PwGroupV3) TestData.GetTest1(getContext()).getGroups().get(0);
}
public void testGroupName() {
//assertTrue("Name was " + mPG.getTitle(), mPG.getTitle().equals("Internet"));
}
}

View File

@@ -1,56 +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.tests;
import android.content.Context;
import android.content.res.AssetManager;
import android.os.Environment;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
public class TestUtil {
private static final File sdcard = Environment.getExternalStorageDirectory();
public static void extractKey(Context ctx, String asset, String target) throws Exception {
InputStream key = ctx.getAssets().open(asset, AssetManager.ACCESS_STREAMING);
FileOutputStream keyFile = new FileOutputStream(target);
while (true) {
byte[] buf = new byte[1024];
int read = key.read(buf);
if ( read == -1 ) {
break;
} else {
keyFile.write(buf, 0, read);
}
}
keyFile.close();
}
public static String getSdPath(String filename) {
File file = new File(sdcard, filename);
return file.getAbsolutePath();
}
}

View File

@@ -1,211 +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.tests;
import static org.junit.Assert.assertArrayEquals;
import java.io.ByteArrayOutputStream;
import java.util.Calendar;
import java.util.Random;
import java.util.UUID;
import junit.framework.TestCase;
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;
public class TypesTest extends TestCase {
public void testReadWriteLongZero() {
testReadWriteLong((byte) 0);
}
public void testReadWriteLongMax() {
testReadWriteLong(Byte.MAX_VALUE);
}
public void testReadWriteLongMin() {
testReadWriteLong(Byte.MIN_VALUE);
}
public void testReadWriteLongRnd() {
Random rnd = new Random();
byte[] buf = new byte[1];
rnd.nextBytes(buf);
testReadWriteLong(buf[0]);
}
private void testReadWriteLong(byte value) {
byte[] orig = new byte[8];
byte[] dest = new byte[8];
setArray(orig, value, 0, 8);
long one = LEDataInputStream.readLong(orig, 0);
LEDataOutputStream.writeLong(one, dest, 0);
assertArrayEquals(orig, dest);
}
public void testReadWriteIntZero() {
testReadWriteInt((byte) 0);
}
public void testReadWriteIntMin() {
testReadWriteInt(Byte.MIN_VALUE);
}
public void testReadWriteIntMax() {
testReadWriteInt(Byte.MAX_VALUE);
}
private void testReadWriteInt(byte value) {
byte[] orig = new byte[4];
byte[] dest = new byte[4];
for (int i = 0; i < 4; i++ ) {
orig[i] = 0;
}
setArray(orig, value, 0, 4);
int one = LEDataInputStream.readInt(orig, 0);
LEDataOutputStream.writeInt(one, dest, 0);
assertArrayEquals(orig, dest);
}
private void setArray(byte[] buf, byte value, int offset, int size) {
for (int i = offset; i < offset + size; i++) {
buf[i] = value;
}
}
public void testReadWriteShortOne() {
byte[] orig = new byte[2];
byte[] dest = new byte[2];
orig[0] = 0;
orig[1] = 1;
int one = LEDataInputStream.readUShort(orig, 0);
dest = LEDataOutputStream.writeUShortBuf(one);
assertArrayEquals(orig, dest);
}
public void testReadWriteShortMin() {
testReadWriteShort(Byte.MIN_VALUE);
}
public void testReadWriteShortMax() {
testReadWriteShort(Byte.MAX_VALUE);
}
private void testReadWriteShort(byte value) {
byte[] orig = new byte[2];
byte[] dest = new byte[2];
setArray(orig, value, 0, 2);
int one = LEDataInputStream.readUShort(orig, 0);
LEDataOutputStream.writeUShort(one, dest, 0);
assertArrayEquals(orig, dest);
}
public void testReadWriteByteZero() {
testReadWriteByte((byte) 0);
}
public void testReadWriteByteMin() {
testReadWriteByte(Byte.MIN_VALUE);
}
public void testReadWriteByteMax() {
testReadWriteShort(Byte.MAX_VALUE);
}
private void testReadWriteByte(byte value) {
byte[] orig = new byte[1];
byte[] dest = new byte[1];
setArray(orig, value, 0, 1);
int one = Types.readUByte(orig, 0);
Types.writeUByte(one, dest, 0);
assertArrayEquals(orig, dest);
}
public void testDate() {
Calendar cal = Calendar.getInstance();
Calendar expected = Calendar.getInstance();
expected.set(2008, 1, 2, 3, 4, 5);
byte[] buf = PwDate.Companion.writeTime(expected.getTime(), cal);
Calendar actual = Calendar.getInstance();
actual.setTime(PwDate.Companion.readTime(buf, 0, cal));
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("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));
}
public void testUUID() {
Random rnd = new Random();
byte[] bUUID = new byte[16];
rnd.nextBytes(bUUID);
UUID uuid = Types.bytestoUUID(bUUID);
byte[] eUUID = Types.UUIDtoBytes(uuid);
assertArrayEquals("UUID match failed", bUUID, eUUID);
}
public void testULongMax() throws Exception {
byte[] ulongBytes = new byte[8];
for (int i = 0; i < ulongBytes.length; i++) {
ulongBytes[i] = -1;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
LEDataOutputStream leos = new LEDataOutputStream(bos);
leos.writeLong(Types.ULONG_MAX_VALUE);
leos.close();
byte[] uLongMax = bos.toByteArray();
assertArrayEquals(ulongBytes, uLongMax);
}
}

View File

@@ -0,0 +1,210 @@
/*
* 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.tests
import org.junit.Assert.assertArrayEquals
import java.io.ByteArrayOutputStream
import java.util.Calendar
import java.util.Random
import junit.framework.TestCase
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() {
fun testReadWriteLongZero() {
testReadWriteLong(0.toByte())
}
fun testReadWriteLongMax() {
testReadWriteLong(java.lang.Byte.MAX_VALUE)
}
fun testReadWriteLongMin() {
testReadWriteLong(java.lang.Byte.MIN_VALUE)
}
fun testReadWriteLongRnd() {
val rnd = Random()
val buf = ByteArray(1)
rnd.nextBytes(buf)
testReadWriteLong(buf[0])
}
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)
}
fun testReadWriteIntZero() {
testReadWriteInt(0.toByte())
}
fun testReadWriteIntMin() {
testReadWriteInt(java.lang.Byte.MIN_VALUE)
}
fun testReadWriteIntMax() {
testReadWriteInt(java.lang.Byte.MAX_VALUE)
}
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)
val one = LEDataInputStream.readInt(orig, 0)
LEDataOutputStream.writeInt(one, dest, 0)
assertArrayEquals(orig, dest)
}
private fun setArray(buf: ByteArray, value: Byte, offset: Int, size: Int) {
for (i in offset until offset + size) {
buf[i] = value
}
}
fun testReadWriteShortOne() {
val orig = ByteArray(2)
orig[0] = 0
orig[1] = 1
val one = LEDataInputStream.readUShort(orig, 0)
val dest = LEDataOutputStream.writeUShortBuf(one)
assertArrayEquals(orig, dest)
}
fun testReadWriteShortMin() {
testReadWriteShort(java.lang.Byte.MIN_VALUE)
}
fun testReadWriteShortMax() {
testReadWriteShort(java.lang.Byte.MAX_VALUE)
}
private fun testReadWriteShort(value: Byte) {
val orig = ByteArray(2)
val dest = ByteArray(2)
setArray(orig, value, 0, 2)
val one = LEDataInputStream.readUShort(orig, 0)
LEDataOutputStream.writeUShort(one, dest, 0)
assertArrayEquals(orig, dest)
}
fun testReadWriteByteZero() {
testReadWriteByte(0.toByte())
}
fun testReadWriteByteMin() {
testReadWriteByte(java.lang.Byte.MIN_VALUE)
}
fun testReadWriteByteMax() {
testReadWriteShort(java.lang.Byte.MAX_VALUE)
}
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)
}
fun testDate() {
val cal = Calendar.getInstance()
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)
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("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))
}
fun testUUID() {
val rnd = Random()
val bUUID = ByteArray(16)
rnd.nextBytes(bUUID)
val uuid = Types.bytestoUUID(bUUID)
val eUUID = Types.UUIDtoBytes(uuid)
assertArrayEquals("UUID match failed", bUUID, eUUID)
}
@Throws(Exception::class)
fun testULongMax() {
val ulongBytes = ByteArray(8)
for (i in ulongBytes.indices) {
ulongBytes[i] = -1
}
val bos = ByteArrayOutputStream()
val leos = LEDataOutputStream(bos)
leos.writeLong(Types.ULONG_MAX_VALUE)
leos.close()
val uLongMax = bos.toByteArray()
assertArrayEquals(ulongBytes, uLongMax)
}
}

View File

@@ -1,83 +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.tests.crypto;
import com.kunzisoft.keepass.crypto.CipherFactory;
import junit.framework.TestCase;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import static org.junit.Assert.assertArrayEquals;
public class AESTest extends TestCase {
private Random mRand = new Random();
public void testEncrypt() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
// Test above below and at the blocksize
testFinal(15);
testFinal(16);
testFinal(17);
// Test random larger sizes
int size = mRand.nextInt(494) + 18;
testFinal(size);
}
private void testFinal(int dataSize) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
// Generate some input
byte[] input = new byte[dataSize];
mRand.nextBytes(input);
// Generate key
byte[] keyArray = new byte[32];
mRand.nextBytes(keyArray);
SecretKeySpec key = new SecretKeySpec(keyArray, "AES");
// Generate IV
byte[] ivArray = new byte[16];
mRand.nextBytes(ivArray);
IvParameterSpec iv = new IvParameterSpec(ivArray);
Cipher android = CipherFactory.INSTANCE.getInstance("AES/CBC/PKCS5Padding", true);
android.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] outAndroid = android.doFinal(input, 0, dataSize);
Cipher nat = CipherFactory.INSTANCE.getInstance("AES/CBC/PKCS5Padding");
nat.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] outNative = nat.doFinal(input, 0, dataSize);
assertArrayEquals("Arrays differ on size: " + dataSize, outAndroid, outNative);
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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.tests.crypto
import com.kunzisoft.keepass.crypto.CipherFactory
import junit.framework.TestCase
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.util.Random
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.IllegalBlockSizeException
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import org.junit.Assert.assertArrayEquals
class AESTest : TestCase() {
private val mRand = Random()
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidAlgorithmParameterException::class)
fun testEncrypt() {
// Test above below and at the blocksize
testFinal(15)
testFinal(16)
testFinal(17)
// Test random larger sizes
val size = mRand.nextInt(494) + 18
testFinal(size)
}
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
private fun testFinal(dataSize: Int) {
// Generate some input
val input = ByteArray(dataSize)
mRand.nextBytes(input)
// Generate key
val keyArray = ByteArray(32)
mRand.nextBytes(keyArray)
val key = SecretKeySpec(keyArray, "AES")
// Generate IV
val ivArray = ByteArray(16)
mRand.nextBytes(ivArray)
val iv = IvParameterSpec(ivArray)
val android = CipherFactory.getInstance("AES/CBC/PKCS5Padding", true)
android.init(Cipher.ENCRYPT_MODE, key, iv)
val outAndroid = android.doFinal(input, 0, dataSize)
val nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding")
nat.init(Cipher.ENCRYPT_MODE, key, iv)
val outNative = nat.doFinal(input, 0, dataSize)
assertArrayEquals("Arrays differ on size: $dataSize", outAndroid, outNative)
}
}

View File

@@ -1,100 +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.tests.crypto;
import static org.junit.Assert.assertArrayEquals;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
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;
public class CipherTest extends TestCase {
private Random rand = new Random();
public void testCipherFactory() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
byte[] key = new byte[32];
byte[] iv = new byte[16];
byte[] plaintext = new byte[1024];
rand.nextBytes(key);
rand.nextBytes(iv);
rand.nextBytes(plaintext);
CipherEngine aes = CipherFactory.INSTANCE.getInstance(AesEngine.CIPHER_UUID);
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);
byte[] secrettext = encrypt.doFinal(plaintext);
byte[] decrypttext = decrypt.doFinal(secrettext);
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext);
}
public void testCipherStreams() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException {
final int MESSAGE_LENGTH = 1024;
byte[] key = new byte[32];
byte[] iv = new byte[16];
byte[] plaintext = new byte[MESSAGE_LENGTH];
rand.nextBytes(key);
rand.nextBytes(iv);
rand.nextBytes(plaintext);
CipherEngine aes = CipherFactory.INSTANCE.getInstance(AesEngine.CIPHER_UUID);
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
CipherOutputStream cos = new CipherOutputStream(bos, encrypt);
cos.write(plaintext);
cos.close();
byte[] secrettext = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(secrettext);
BetterCipherInputStream cis = new BetterCipherInputStream(bis, decrypt);
LEDataInputStream lis = new LEDataInputStream(cis);
byte[] decrypttext = lis.readBytes(MESSAGE_LENGTH);
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext);
}
}

View File

@@ -0,0 +1,102 @@
/*
* 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.tests.crypto
import org.junit.Assert.assertArrayEquals
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.util.Random
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
import javax.crypto.IllegalBlockSizeException
import javax.crypto.NoSuchPaddingException
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
class CipherTest : TestCase() {
private val rand = Random()
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidAlgorithmParameterException::class, IllegalBlockSizeException::class, BadPaddingException::class)
fun testCipherFactory() {
val key = ByteArray(32)
val iv = ByteArray(16)
val plaintext = ByteArray(1024)
rand.nextBytes(key)
rand.nextBytes(iv)
rand.nextBytes(plaintext)
val aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID)
val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
val secrettext = encrypt.doFinal(plaintext)
val decrypttext = decrypt.doFinal(secrettext)
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
}
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidAlgorithmParameterException::class, IllegalBlockSizeException::class, BadPaddingException::class, IOException::class)
fun testCipherStreams() {
val MESSAGE_LENGTH = 1024
val key = ByteArray(32)
val iv = ByteArray(16)
val plaintext = ByteArray(MESSAGE_LENGTH)
rand.nextBytes(key)
rand.nextBytes(iv)
rand.nextBytes(plaintext)
val aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID)
val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
val bos = ByteArrayOutputStream()
val cos = CipherOutputStream(bos, encrypt)
cos.write(plaintext)
cos.close()
val secrettext = bos.toByteArray()
val bis = ByteArrayInputStream(secrettext)
val cis = BetterCipherInputStream(bis, decrypt)
val lis = LEDataInputStream(cis)
val decrypttext = lis.readBytes(MESSAGE_LENGTH)
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
}
}

View File

@@ -1,66 +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.tests.crypto;
import static org.junit.Assert.assertArrayEquals;
import java.io.IOException;
import java.util.Random;
import junit.framework.TestCase;
import com.kunzisoft.keepass.crypto.finalkey.AndroidFinalKey;
import com.kunzisoft.keepass.crypto.finalkey.NativeFinalKey;
public class FinalKeyTest extends TestCase {
private Random mRand;
@Override
protected void setUp() throws Exception {
super.setUp();
mRand = new Random();
}
public void testNativeAndroid() throws IOException {
// Test both an old and an even number to test my flip variable
testNativeFinalKey(5);
testNativeFinalKey(6);
}
private void testNativeFinalKey(int rounds) throws IOException {
byte[] seed = new byte[32];
byte[] key = new byte[32];
byte[] nativeKey;
byte[] androidKey;
mRand.nextBytes(seed);
mRand.nextBytes(key);
AndroidFinalKey aKey = new AndroidFinalKey();
androidKey = aKey.transformMasterKey(seed, key, rounds);
NativeFinalKey nKey = new NativeFinalKey();
nativeKey = nKey.transformMasterKey(seed, key, rounds);
assertArrayEquals("Does not match", androidKey, nativeKey);
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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.tests.crypto
import org.junit.Assert.assertArrayEquals
import java.io.IOException
import java.util.Random
import junit.framework.TestCase
import com.kunzisoft.keepass.crypto.finalkey.AndroidFinalKey
import com.kunzisoft.keepass.crypto.finalkey.NativeFinalKey
class FinalKeyTest : TestCase() {
private var mRand: Random? = null
@Throws(Exception::class)
override fun setUp() {
super.setUp()
mRand = Random()
}
@Throws(IOException::class)
fun testNativeAndroid() {
// Test both an old and an even number to test my flip variable
testNativeFinalKey(5)
testNativeFinalKey(6)
}
@Throws(IOException::class)
private fun testNativeFinalKey(rounds: Int) {
val seed = ByteArray(32)
val key = ByteArray(32)
val nativeKey: ByteArray
val androidKey: ByteArray
mRand!!.nextBytes(seed)
mRand!!.nextBytes(key)
val aKey = AndroidFinalKey()
androidKey = aKey.transformMasterKey(seed, key, rounds.toLong())
val nKey = NativeFinalKey()
nativeKey = nKey.transformMasterKey(seed, key, rounds.toLong())
assertArrayEquals("Does not match", androidKey, nativeKey)
}
}

View File

@@ -1,111 +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.tests.database;
import android.test.AndroidTestCase;
import com.kunzisoft.keepass.database.element.GroupVersioned;
import com.kunzisoft.keepass.database.element.PwDatabase;
import com.kunzisoft.keepass.database.element.PwDatabaseV3;
import com.kunzisoft.keepass.database.element.PwEntryV3;
public class DeleteEntry extends AndroidTestCase {
private static final String GROUP1_NAME = "Group1";
private static final String ENTRY1_NAME = "Test1";
private static final String ENTRY2_NAME = "Test2";
private static final String KEYFILE = "";
private static final String PASSWORD = "12345";
private static final String ASSET = "delete.kdb";
private static final String FILENAME = "/sdcard/delete.kdb";
public void testDelete() {
/*
Database db;
Context ctx = getContext();
try {
db = TestData.GetDb(ctx, ASSET, PASSWORD, KEYFILE, FILENAME);
} catch (Exception e) {
assertTrue("Failed to open database: " + e.getMessage(), false);
return;
}
PwDatabaseV3 pm = (PwDatabaseV3) db.getPwDatabase();
GroupVersioned group1 = getGroup(pm, GROUP1_NAME);
assertNotNull("Could not find group1", group1);
// Delete the group
DeleteGroupRunnable task = new DeleteGroupRunnable(null, db, group1, null, true);
task.run();
// Verify the entries were deleted
PwEntryInterface entry1 = getEntry(pm, ENTRY1_NAME);
assertNull("Entry 1 was not removed", entry1);
PwEntryInterface entry2 = getEntry(pm, ENTRY2_NAME);
assertNull("Entry 2 was not removed", entry2);
// Verify the entries were removed from the search index
SearchDbHelper dbHelp = new SearchDbHelper(ctx);
GroupVersioned results1 = dbHelp.search(db.getPwDatabase(), ENTRY1_NAME, 100);
GroupVersioned results2 = dbHelp.search(db.getPwDatabase(), ENTRY2_NAME, 100);
assertEquals("Entry1 was not removed from the search results", 0, results1.numbersOfChildEntries());
assertEquals("Entry2 was not removed from the search results", 0, results2.numbersOfChildEntries());
// Verify the group was deleted
group1 = getGroup(pm, GROUP1_NAME);
assertNull("Group 1 was not removed.", group1);
*/
}
private PwEntryV3 getEntry(PwDatabaseV3 pm, String name) {
/*
TODO test
List<PwEntryV3> entries = pm.getEntries();
for ( int i = 0; i < entries.size(); i++ ) {
PwEntryV3 entry = entries.get(i);
if ( entry.getTitle().equals(name) ) {
return entry;
}
}
*/
return null;
}
private GroupVersioned getGroup(PwDatabase pm, String name) {
/*
List<GroupVersioned> groups = pm.getGroups();
for ( int i = 0; i < groups.size(); i++ ) {
GroupVersioned group = groups.get(i);
if ( group.getTitle().equals(name) ) {
return group;
}
}
*/
return null;
}
}

View File

@@ -1,56 +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.tests.database;
import com.kunzisoft.keepass.database.element.PwDatabaseV4;
import com.kunzisoft.keepass.database.element.PwEntryV4;
import junit.framework.TestCase;
public class EntryV4 extends TestCase {
public void testBackup() {
/*
PwDatabaseV4 db = new PwDatabaseV4();
db.setHistoryMaxItems(2);
PwEntryV4 entry = new PwEntryV4();
entry.startToManageFieldReferences(db);
entry.setTitle("Title1");
entry.setUsername("User1");
entry.createBackup(db);
entry.setTitle("Title2");
entry.setUsername("User2");
entry.createBackup(db);
entry.setTitle("Title3");
entry.setUsername("User3");
entry.createBackup(db);
PwEntryV4 backup = entry.getHistory().get(0);
entry.stopToManageFieldReferences();
assertEquals("Title2", backup.getTitle());
assertEquals("User2", backup.getUsername());
*/
}
}

View File

@@ -1,53 +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.tests.database;
import android.test.AndroidTestCase;
public class Kdb3 extends AndroidTestCase {
private void testKeyfile(String dbAsset, String keyAsset, String password) throws Exception {
/*
Context ctx = getContext();
File sdcard = Environment.getExternalStorageDirectory();
String keyPath = sdcard.getAbsolutePath() + "/key";
TestUtil.extractKey(ctx, keyAsset, keyPath);
AssetManager am = ctx.getAssets();
InputStream is = am.open(dbAsset, AssetManager.ACCESS_STREAMING);
ImporterV3 importer = new ImporterV3();
importer.openDatabase(is, password, TestUtil.getKeyFileInputStream(ctx, keyPath));
is.close();
*/
}
public void testXMLKeyFile() throws Exception {
testKeyfile("kdb_with_xml_keyfile.kdb", "keyfile.key", "12345");
}
public void testBinary64KeyFile() throws Exception {
testKeyfile("binary-key.kdb", "binary.key", "12345");
}
}

View File

@@ -1,41 +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.tests.database;
import android.test.AndroidTestCase;
public class Kdb3Twofish extends AndroidTestCase {
public void testReadTwofish() throws Exception {
/*
Context ctx = getContext();
AssetManager am = ctx.getAssets();
InputStream is = am.open("twofish.kdb", AssetManager.ACCESS_STREAMING);
ImporterV3 importer = new ImporterV3();
PwDatabaseV3 db = importer.openDatabase(is, "12345", null);
assertTrue(db.getEncryptionAlgorithm() == PwEncryptionAlgorithm.Twofish);
is.close();
*/
}
}

View File

@@ -1,171 +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.tests.database;
import android.content.Context;
import android.content.res.AssetManager;
import android.test.AndroidTestCase;
import com.kunzisoft.keepass.database.exception.InvalidDBException;
import com.kunzisoft.keepass.database.exception.PwDbOutputException;
import com.kunzisoft.keepass.tests.TestUtil;
import java.io.IOException;
import java.io.InputStream;
public class Kdb4 extends AndroidTestCase {
public void testDetection() throws IOException, InvalidDBException {
Context ctx = getContext();
AssetManager am = ctx.getAssets();
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
/*
TODO Test
Importer importer = ImporterFactory.createImporter(is);
assertTrue(importer instanceof ImporterV4);
is.close();
*/
}
public void testParsing() throws IOException, InvalidDBException {
Context ctx = getContext();
AssetManager am = ctx.getAssets();
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
/*
TODO Test
ImporterV4 importer = new ImporterV4();
importer.openDatabase(is, "12345", null);
is.close();
*/
}
public void testSavingKDBXV3() throws IOException, InvalidDBException, PwDbOutputException {
testSaving("test.kdbx", "12345", "test-out.kdbx");
}
public void testSavingKDBXV4() throws IOException, InvalidDBException, PwDbOutputException {
testSaving("test-kdbxv4.kdbx", "1", "test-kdbxv4-out.kdbx");
}
private void testSaving(String inputFile, String password, String outputFile) throws IOException, InvalidDBException, PwDbOutputException {
Context ctx = getContext();
AssetManager am = ctx.getAssets();
InputStream is = am.open(inputFile, AssetManager.ACCESS_STREAMING);
/*
TODO Test
ImporterV4 importer = new ImporterV4();
PwDatabaseV4 db = importer.openDatabase(is, password, null);
is.close();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
PwDbV4Output output = (PwDbV4Output) PwDbOutput.getInstance(db, bos);
output.output();
byte[] data = bos.toByteArray();
FileOutputStream fos = new FileOutputStream(TestUtil.getSdPath(outputFile), false);
InputStream bis = new ByteArrayInputStream(data);
bis = new CopyInputStream(bis, fos);
importer = new ImporterV4();
db = importer.openDatabase(bis, password, null);
bis.close();
fos.close();
*/
}
@Override
protected void setUp() throws Exception {
super.setUp();
TestUtil.extractKey(getContext(), "keyfile.key", TestUtil.getSdPath("key"));
TestUtil.extractKey(getContext(), "binary.key", TestUtil.getSdPath("key-binary"));
}
public void testComposite() throws IOException, InvalidDBException {
Context ctx = getContext();
AssetManager am = ctx.getAssets();
InputStream is = am.open("keyfile.kdbx", AssetManager.ACCESS_STREAMING);
/*
TODO Test
ImporterV4 importer = new ImporterV4();
importer.openDatabase(is, "12345", TestUtil.getKeyFileInputStream(ctx, TestUtil.getSdPath("key")));
is.close();
*/
}
public void testCompositeBinary() throws IOException, InvalidDBException {
Context ctx = getContext();
AssetManager am = ctx.getAssets();
InputStream is = am.open("keyfile-binary.kdbx", AssetManager.ACCESS_STREAMING);
/*
TODO Test
ImporterV4 importer = new ImporterV4();
importer.openDatabase(is, "12345", TestUtil.getKeyFileInputStream(ctx,TestUtil.getSdPath("key-binary")));
is.close();
*/
}
public void testKeyfile() throws IOException, InvalidDBException {
Context ctx = getContext();
AssetManager am = ctx.getAssets();
InputStream is = am.open("key-only.kdbx", AssetManager.ACCESS_STREAMING);
/*
TODO Test
ImporterV4 importer = new ImporterV4();
importer.openDatabase(is, "", TestUtil.getKeyFileInputStream(ctx, TestUtil.getSdPath("key")));
is.close();
*/
}
public void testNoGzip() throws IOException, InvalidDBException {
Context ctx = getContext();
AssetManager am = ctx.getAssets();
InputStream is = am.open("no-encrypt.kdbx", AssetManager.ACCESS_STREAMING);
/*
TODO Test
ImporterV4 importer = new ImporterV4();
importer.openDatabase(is, "12345", null);
is.close();
*/
}
}

View File

@@ -1,49 +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.tests.database;
import android.content.Context;
import android.content.res.AssetManager;
import android.test.AndroidTestCase;
import java.io.InputStream;
public class Kdb4Header extends AndroidTestCase {
public void testReadHeader() throws Exception {
Context ctx = getContext();
AssetManager am = ctx.getAssets();
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
/*
TODO Test
ImporterV4 importer = new ImporterV4();
PwDatabaseV4 db = importer.openDatabase(is, "12345", null);
assertEquals(6000, db.getNumberKeyEncryptionRounds());
assertTrue(db.getDataCipher().equals(AesEngine.CIPHER_UUID));
is.close();
*/
}
}

View File

@@ -1,70 +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.tests.database;
import android.content.Context;
import android.content.res.AssetManager;
import android.test.AndroidTestCase;
import com.kunzisoft.keepass.database.element.PwDatabaseV4;
import com.kunzisoft.keepass.database.element.SprEngineV4;
import java.io.InputStream;
public class SprEngineTest extends AndroidTestCase {
private PwDatabaseV4 db;
private SprEngineV4 spr;
@Override
protected void setUp() throws Exception {
super.setUp();
Context ctx = getContext();
AssetManager am = ctx.getAssets();
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
/*
TODO Test
ImporterV4 importer = new ImporterV4();
db = importer.openDatabase(is, "12345", null);
is.close();
spr = new SprEngineV4();
*/
}
private final String REF = "{REF:P@I:2B1D56590D961F48A8CE8C392CE6CD35}";
private final String ENCODE_UUID = "IN7RkON49Ui1UZ2ddqmLcw==";
private final String RESULT = "Password";
public void testRefReplace() {
/*
TODO TEST
UUID entryUUID = decodeUUID(ENCODE_UUID);
PwEntryV4 entry = (PwEntryV4) db.getEntryById(entryUUID);
assertEquals(RESULT, spr.compile(REF, entry, db));
*/
}
}

View File

@@ -1,71 +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.tests.database;
import com.kunzisoft.keepass.database.element.Database;
public class TestData {
private static final String TEST1_KEYFILE = "";
private static final String TEST1_KDB = "test1.kdb";
private static final String TEST1_PASSWORD = "12345";
private static Database mDb1;
/*
public static Database GetDb1(Context ctx) throws Exception {
return GetDb1(ctx, false);
}
public static Database GetDb1(Context ctx, boolean forceReload) throws Exception {
if ( mDb1 == null || forceReload ) {
mDb1 = GetDb(ctx, TEST1_KDB, TEST1_PASSWORD, TEST1_KEYFILE, "/sdcard/test1.kdb");
}
return mDb1;
}
public static Database GetDb(Context ctx, String asset, String password, String keyfile, String filename) throws Exception {
AssetManager am = ctx.getAssets();
InputStream is = am.open(asset, AssetManager.ACCESS_STREAMING);
Database Db = new Database();
InputStream keyIs = TestUtil.getKeyFileInputStream(ctx, keyfile);
Db.loadData(ctx, is, password, keyIs, Importer.DEBUG);
Uri.Builder b = new Uri.Builder();
Db.setUri(b.scheme("file").path(filename).build());
return Db;
}
public static PwDatabaseV3Debug GetTest1(Context ctx) throws Exception {
if ( mDb1 == null ) {
GetDb1(ctx);
}
//return (PwDatabaseV3Debug) mDb1.getPwDatabase();
return null;
}
*/
}

View File

@@ -1,126 +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.tests.output;
import android.test.AndroidTestCase;
public class PwManagerOutputTest extends AndroidTestCase {
// PwDatabaseV3Debug mPM;
/*
@Override
protected void setUp() throws Exception {
super.setUp();
mPM = TestData.GetTest1(getContext());
}
public void testPlainContent() throws IOException, PwDbOutputException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
PwDbV3Output pos = new PwDbV3OutputDebug(mPM, bos, true);
pos.outputPlanGroupAndEntries(bos);
assertTrue("No output", bos.toByteArray().length > 0);
assertArrayEquals("Group and entry output doesn't match.", mPM.getPostHeader(), bos.toByteArray());
}
public void testChecksum() throws NoSuchAlgorithmException, IOException, PwDbOutputException {
//FileOutputStream fos = new FileOutputStream("/dev/null");
NullOutputStream nos = new NullOutputStream();
MessageDigest md = MessageDigest.getInstance("SHA-256");
DigestOutputStream dos = new DigestOutputStream(nos, md);
PwDbV3Output pos = new PwDbV3OutputDebug(mPM, dos, true);
pos.outputPlanGroupAndEntries(dos);
dos.close();
byte[] digest = md.digest();
assertTrue("No output", digest.length > 0);
assertArrayEquals("Hash of groups and entries failed.", mPM.getDbHeader().contentsHash, digest);
}
private void assertHeadersEquals(PwDbHeaderV3 expected, PwDbHeaderV3 actual) {
assertEquals("Flags unequal", expected.flags, actual.flags);
assertEquals("Entries unequal", expected.numEntries, actual.numEntries);
assertEquals("Groups unequal", expected.numGroups, actual.numGroups);
assertEquals("Key Rounds unequal", expected.numKeyEncRounds, actual.numKeyEncRounds);
assertEquals("Signature1 unequal", expected.signature1, actual.signature1);
assertEquals("Signature2 unequal", expected.signature2, actual.signature2);
assertTrue("Version incompatible", PwDbHeaderV3.compatibleHeaders(expected.version, actual.version));
assertArrayEquals("Hash unequal", expected.contentsHash, actual.contentsHash);
assertArrayEquals("IV unequal", expected.encryptionIV, actual.encryptionIV);
assertArrayEquals("Seed unequal", expected.masterSeed, actual.masterSeed);
assertArrayEquals("Seed2 unequal", expected.transformSeed, actual.transformSeed);
}
public void testHeader() throws PwDbOutputException, IOException {
ByteArrayOutputStream bActual = new ByteArrayOutputStream();
PwDbV3Output pActual = new PwDbV3OutputDebug(mPM, bActual, true);
PwDbHeaderV3 header = pActual.outputHeader(bActual);
ByteArrayOutputStream bExpected = new ByteArrayOutputStream();
PwDbHeaderOutputV3 outExpected = new PwDbHeaderOutputV3(mPM.getDbHeader(), bExpected);
outExpected.output();
assertHeadersEquals(mPM.getDbHeader(), header);
assertTrue("No output", bActual.toByteArray().length > 0);
assertArrayEquals("Header does not match.", bExpected.toByteArray(), bActual.toByteArray());
}
public void testFinalKey() throws PwDbOutputException {
ByteArrayOutputStream bActual = new ByteArrayOutputStream();
PwDbV3Output pActual = new PwDbV3OutputDebug(mPM, bActual, true);
PwDbHeader hActual = pActual.outputHeader(bActual);
byte[] finalKey = pActual.getFinalKey(hActual);
assertArrayEquals("Keys mismatched", mPM.getFinalKey(), finalKey);
}
public void testFullWrite() throws IOException, PwDbOutputException {
AssetManager am = getContext().getAssets();
InputStream is = am.open("test1.kdb");
// Pull file into byte array (for streaming fun)
ByteArrayOutputStream bExpected = new ByteArrayOutputStream();
while (true) {
int data = is.read();
if ( data == -1 ) {
break;
}
bExpected.write(data);
}
ByteArrayOutputStream bActual = new ByteArrayOutputStream();
PwDbV3Output pActual = new PwDbV3OutputDebug(mPM, bActual, true);
pActual.output();
//pActual.close();
FileOutputStream fos = new FileOutputStream(TestUtil.getSdPath("test1_out.kdb"));
fos.write(bActual.toByteArray());
fos.close();
assertArrayEquals("Databases do not match.", bExpected.toByteArray(), bActual.toByteArray());
}
*/
}

View File

@@ -1,70 +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.tests.search;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.test.AndroidTestCase;
import com.kunzisoft.keepass.database.element.Database;
import com.kunzisoft.keepass.database.element.GroupVersioned;
public class SearchTest extends AndroidTestCase {
private Database mDb;
@Override
protected void setUp() throws Exception {
super.setUp();
//mDb = TestData.GetDb1(getContext(), true);
}
public void testSearch() {
GroupVersioned results = mDb.search("Amazon");
//assertTrue("Search result not found.", results.numbersOfChildEntries() > 0);
}
public void testBackupIncluded() {
updateOmitSetting(false);
GroupVersioned results = mDb.search("BackupOnly");
//assertTrue("Search result not found.", results.numbersOfChildEntries() > 0);
}
public void testBackupExcluded() {
updateOmitSetting(true);
GroupVersioned results = mDb.search("BackupOnly");
//assertFalse("Search result found, but should not have been.", results.numbersOfChildEntries() > 0);
}
private void updateOmitSetting(boolean setting) {
Context ctx = getContext();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("settings_omitbackup_key", setting);
editor.commit();
}
}

View File

@@ -1,110 +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.tests.stream;
import static org.junit.Assert.assertArrayEquals;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Random;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import junit.framework.TestCase;
import com.kunzisoft.keepass.stream.HashedBlockInputStream;
import com.kunzisoft.keepass.stream.HashedBlockOutputStream;
public class HashedBlock extends TestCase {
private static Random rand = new Random();
public void testBlockAligned() throws IOException {
testSize(1024, 1024);
}
public void testOffset() throws IOException {
testSize(1500, 1024);
}
private void testSize(int blockSize, int bufferSize) throws IOException {
byte[] orig = new byte[blockSize];
rand.nextBytes(orig);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HashedBlockOutputStream output = new HashedBlockOutputStream(bos, bufferSize);
output.write(orig);
output.close();
byte[] encoded = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
HashedBlockInputStream input = new HashedBlockInputStream(bis);
ByteArrayOutputStream decoded = new ByteArrayOutputStream();
while ( true ) {
byte[] buf = new byte[1024];
int read = input.read(buf);
if ( read == -1 ) {
break;
}
decoded.write(buf, 0, read);
}
byte[] out = decoded.toByteArray();
assertArrayEquals(orig, out);
}
public void testGZIPStream() throws IOException {
final int testLength = 32000;
byte[] orig = new byte[testLength];
rand.nextBytes(orig);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HashedBlockOutputStream hos = new HashedBlockOutputStream(bos);
GZIPOutputStream zos = new GZIPOutputStream(hos);
zos.write(orig);
zos.close();
byte[] compressed = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(compressed);
HashedBlockInputStream his = new HashedBlockInputStream(bis);
GZIPInputStream zis = new GZIPInputStream(his);
byte[] uncompressed = new byte[testLength];
int read = 0;
while (read != -1 && testLength - read > 0) {
read += zis.read(uncompressed, read, testLength - read);
}
assertArrayEquals("Output not equal to input", orig, uncompressed);
}
}

View File

@@ -0,0 +1,117 @@
/*
* 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.tests.stream
import org.junit.Assert.assertArrayEquals
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.Random
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import junit.framework.TestCase
import com.kunzisoft.keepass.stream.HashedBlockInputStream
import com.kunzisoft.keepass.stream.HashedBlockOutputStream
class HashedBlock : TestCase() {
@Throws(IOException::class)
fun testBlockAligned() {
testSize(1024, 1024)
}
@Throws(IOException::class)
fun testOffset() {
testSize(1500, 1024)
}
@Throws(IOException::class)
private fun testSize(blockSize: Int, bufferSize: Int) {
val orig = ByteArray(blockSize)
rand.nextBytes(orig)
val bos = ByteArrayOutputStream()
val output = HashedBlockOutputStream(bos, bufferSize)
output.write(orig)
output.close()
val encoded = bos.toByteArray()
val bis = ByteArrayInputStream(encoded)
val input = HashedBlockInputStream(bis)
val decoded = ByteArrayOutputStream()
while (true) {
val buf = ByteArray(1024)
val read = input.read(buf)
if (read == -1) {
break
}
decoded.write(buf, 0, read)
}
val out = decoded.toByteArray()
assertArrayEquals(orig, out)
}
@Throws(IOException::class)
fun testGZIPStream() {
val testLength = 32000
val orig = ByteArray(testLength)
rand.nextBytes(orig)
val bos = ByteArrayOutputStream()
val hos = HashedBlockOutputStream(bos)
val zos = GZIPOutputStream(hos)
zos.write(orig)
zos.close()
val compressed = bos.toByteArray()
val bis = ByteArrayInputStream(compressed)
val his = HashedBlockInputStream(bis)
val zis = GZIPInputStream(his)
val uncompressed = ByteArray(testLength)
var read = 0
while (read != -1 && testLength - read > 0) {
read += zis.read(uncompressed, read, testLength - read)
}
assertArrayEquals("Output not equal to input", orig, uncompressed)
}
companion object {
private val rand = Random()
}
}

View File

@@ -1,57 +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.tests.utils;
import java.util.Locale;
import com.kunzisoft.keepass.utils.StringUtil;
import junit.framework.TestCase;
public class StringUtilTest extends TestCase {
private final String text = "AbCdEfGhIj";
private final String search = "BcDe";
private final String badSearch = "Ed";
public void testIndexOfIgnoreCase1() {
assertEquals(1, StringUtil.INSTANCE.indexOfIgnoreCase(text, search, Locale.ENGLISH));
}
public void testIndexOfIgnoreCase2() {
assertEquals(-1, StringUtil.INSTANCE.indexOfIgnoreCase(text, search, Locale.ENGLISH), 2);
}
public void testIndexOfIgnoreCase3() {
assertEquals(-1, StringUtil.INSTANCE.indexOfIgnoreCase(text, badSearch, Locale.ENGLISH));
}
private final String repText = "AbCtestingaBc";
private final String repSearch = "ABc";
private final String repSearchBad = "CCCCCC";
private final String repNew = "12345";
private final String repResult = "12345testing12345";
public void testReplaceAllIgnoresCase1() {
assertEquals(repResult, StringUtil.INSTANCE.replaceAllIgnoresCase(repText, repSearch, repNew, Locale.ENGLISH));
}
public void testReplaceAllIgnoresCase2() {
assertEquals(repText, StringUtil.INSTANCE.replaceAllIgnoresCase(repText, repSearchBad, repNew, Locale.ENGLISH));
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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.tests.utils
import java.util.Locale
import com.kunzisoft.keepass.utils.StringUtil
import junit.framework.TestCase
class StringUtilTest : TestCase() {
private val text = "AbCdEfGhIj"
private val search = "BcDe"
private val badSearch = "Ed"
private val repText = "AbCtestingaBc"
private val repSearch = "ABc"
private val repSearchBad = "CCCCCC"
private val repNew = "12345"
private val repResult = "12345testing12345"
fun testIndexOfIgnoreCase1() {
assertEquals(1, StringUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH))
}
fun testIndexOfIgnoreCase2() {
assertEquals(-1f, StringUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH).toFloat(), 2f)
}
fun testIndexOfIgnoreCase3() {
assertEquals(-1, StringUtil.indexOfIgnoreCase(text, badSearch, Locale.ENGLISH))
}
fun testReplaceAllIgnoresCase1() {
assertEquals(repResult, StringUtil.replaceAllIgnoresCase(repText, repSearch, repNew, Locale.ENGLISH))
}
fun testReplaceAllIgnoresCase2() {
assertEquals(repText, StringUtil.replaceAllIgnoresCase(repText, repSearchBad, repNew, Locale.ENGLISH))
}
}

View File

@@ -1,6 +1,5 @@
<?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
@@ -8,7 +7,8 @@
android:normalScreens="true"
android:largeScreens="true"
android:anyDensity="true" />
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<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"/>
@@ -48,7 +48,7 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:mimeType="application/octet-stream" />
<data android:mimeType="*/*" />
<data android:host="*" />
<data android:pathPattern=".*\\.kdb" />
<data android:pathPattern=".*\\..*\\.kdb" />
@@ -71,29 +71,28 @@
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
</intent-filter>
<intent-filter tools:ignore="AppLinkUrlError">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="file" />
<data android:scheme="content" />
<data android:mimeType="application/octet-stream" />
</intent-filter>
</activity>
<!-- Folder picker -->
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/nnf_provider_paths" />
</provider>
<activity
android:name=".activities.stylish.FilePickerStylishActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/x-kdb" />
<data android:mimeType="application/x-kdbx" />
<data android:mimeType="application/x-keepass" />
<data android:host="*" />
<data android:pathPattern=".*" />
<data android:pathPattern=".*\\.*" />
<data android:pathPattern=".*\\..*\\.*" />
<data android:pathPattern=".*\\..*\\..*\\.*" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.*" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.*" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.*" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.*" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.*" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.*" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.*" />
</intent-filter>
</activity>
<!-- Main Activity -->
@@ -129,9 +128,11 @@
<activity android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
android:configChanges="keyboardHidden" />
<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">

Binary file not shown.

Binary file not shown.

View File

@@ -1,2 +0,0 @@
v7<EFBFBD><07>gx<67><78><EFBFBD>"<04>Dm<44>]tIWRP<52>g<18>y<15>/˰1<CBB0><31><13>X <0B><>fW[<5B>F%<25><1E>\<5C>up4
<EFBFBD><EFBFBD>-t;<3B>z<EFBFBD>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<KeyFile>
<Meta>
<Version>1.00</Version>
</Meta>
<Key>
<Data>zaTWphVNtRbspnwkqjy8FGTy5IqCUx9+FNb5H+VdB24=</Data>
</Key>
</KeyFile>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.activities
import android.content.pm.PackageManager.NameNotFoundException
import android.os.Bundle
import android.support.v7.widget.Toolbar
import androidx.appcompat.widget.Toolbar
import android.util.Log
import android.view.MenuItem
import android.widget.TextView

View File

@@ -19,15 +19,14 @@
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.os.Handler
import android.support.design.widget.CollapsingToolbarLayout
import android.support.v7.app.AlertDialog
import android.support.v7.widget.Toolbar
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
@@ -45,22 +44,27 @@ import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.settings.PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields
import com.kunzisoft.keepass.settings.SettingsAutofillActivity
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.Util
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.EntryContentsView
import java.util.*
class EntryActivity : LockingHideActivity() {
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var titleIconView: ImageView? = null
private var historyView: View? = null
private var entryContentsView: EntryContentsView? = null
private var toolbar: Toolbar? = null
private var mDatabase: Database? = null
private var mEntry: EntryVersioned? = null
private var mIsHistory: Boolean = false
private var mShowPassword: Boolean = false
private var clipboardHelper: ClipboardHelper? = null
@@ -78,31 +82,14 @@ class EntryActivity : LockingHideActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
val currentDatabase = Database.getInstance()
readOnly = currentDatabase.isReadOnly || readOnly
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.textColorInverse))
iconColor = taIconColor.getColor(0, Color.WHITE)
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
iconColor = taIconColor.getColor(0, Color.BLACK)
taIconColor.recycle()
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
@@ -111,6 +98,7 @@ 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))
@@ -122,6 +110,29 @@ class EntryActivity : LockingHideActivity() {
override fun onResume() {
super.onResume()
// Get Entry from UUID
try {
val keyEntry: PwNodeId<UUID> = intent.getParcelableExtra(KEY_ENTRY)
mEntry = mDatabase?.getEntryById(keyEntry)
} catch (e: ClassCastException) {
Log.e(TAG, "Unable to retrieve the entry key")
}
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
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)
@@ -152,90 +163,126 @@ class EntryActivity : LockingHideActivity() {
titleIconView?.assignDatabaseIcon(database.drawFactory, entry.icon, iconColor)
// Assign title text
val entryTitle = entry.getVisualTitle()
val entryTitle = entry.title
collapsingToolbarLayout?.title = entryTitle
toolbar?.title = entryTitle
// Assign basic fields
entryContentsView?.assignUserName(entry.username)
entryContentsView?.assignUserNameCopyListener(View.OnClickListener {
database.startManageEntry(entry)
clipboardHelper?.timeoutCopyToClipboard(entry.username,
getString(R.string.copy_field,
getString(R.string.entry_user_name)))
database.stopManageEntry(entry)
})
val allowCopyPassword = PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
entryContentsView?.assignPassword(entry.password, allowCopyPassword)
if (allowCopyPassword) {
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
clipboardHelper?.timeoutCopyToClipboard(entry.password,
getString(R.string.copy_field,
getString(R.string.entry_password)))
})
} else {
// If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)) {
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
val message = getString(R.string.allow_copy_password_warning) +
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
val allowCopyPasswordAndProtectedFields =
PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
val showWarningClipboardDialogOnClickListener = View.OnClickListener {
AlertDialog.Builder(this@EntryActivity)
.setMessage(getString(R.string.allow_copy_password_warning) +
"\n\n" +
getString(R.string.clipboard_warning)
val warningDialog = AlertDialog.Builder(this@EntryActivity)
.setMessage(message).create()
warningDialog.setButton(AlertDialog.BUTTON_POSITIVE, getText(android.R.string.ok)
) { dialog, _ ->
getString(R.string.clipboard_warning))
.create().apply {
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) {dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
dialog.dismiss()
fillEntryDataInContentsView(entry)
}
warningDialog.setButton(AlertDialog.BUTTON_NEGATIVE, getText(android.R.string.cancel)
) { dialog, _ ->
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false)
dialog.dismiss()
fillEntryDataInContentsView(entry)
}
warningDialog.show()
show()
}
}
entryContentsView?.assignPassword(entry.password, allowCopyPasswordAndProtectedFields)
if (allowCopyPasswordAndProtectedFields) {
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
database.startManageEntry(entry)
clipboardHelper?.timeoutCopyToClipboard(entry.password,
getString(R.string.copy_field,
getString(R.string.entry_password)))
database.stopManageEntry(entry)
})
} else {
// If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
entryContentsView?.assignPasswordCopyListener(showWarningClipboardDialogOnClickListener)
} else {
entryContentsView?.assignPasswordCopyListener(null)
}
}
entryContentsView?.assignURL(entry.url)
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
entryContentsView?.assignComment(entry.notes)
// Assign custom fields
if (entry.allowExtraFields()) {
if (entry.allowCustomFields()) {
entryContentsView?.clearExtraFields()
entry.fields.doActionToAllCustomProtectedField { label, value ->
val showAction = !value.isProtected || PreferencesUtil.allowCopyPasswordAndProtectedFields(this@EntryActivity)
entryContentsView?.addExtraField(label, value, showAction, View.OnClickListener {
for (element in entry.customFields.entries) {
val label = element.key
val value = element.value
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
if (allowCopyProtectedField) {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, View.OnClickListener {
clipboardHelper?.timeoutCopyToClipboard(
value.toString(),
getString(R.string.copy_field, label)
)
})
} else {
// If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
} else {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
}
}
}
}
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
// 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))
}
// Assign special data
entryContentsView?.assignUUID(entry.nodeId.id)
// 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()
// isMainEntry = not an history
val showHistoryView = entryHistory.isNotEmpty()
entryContentsView?.showHistory(showHistoryView)
if (showHistoryView) {
entryContentsView?.assignHistory(entryHistory)
entryContentsView?.onHistoryClick { historyItem, position ->
launch(this, historyItem, true, position)
}
}
database.stopManageEntry(entry)
}
@@ -268,7 +315,7 @@ class EntryActivity : LockingHideActivity() {
inflater.inflate(R.menu.entry, menu)
inflater.inflate(R.menu.database_lock, menu)
if (readOnly) {
if (mReadOnly) {
menu.findItem(R.id.menu_edit)?.isVisible = false
}
@@ -303,7 +350,7 @@ class EntryActivity : LockingHideActivity() {
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
menu: Menu) {
if (entryContentsView?.isUserNamePresent == true
val entryCopyEducationPerformed = entryContentsView?.isUserNamePresent == true
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
findViewById(R.id.entry_user_name_action_image),
{
@@ -314,8 +361,11 @@ class EntryActivity : LockingHideActivity() {
{
// Launch autofill settings
startActivity(Intent(this@EntryActivity, SettingsAutofillActivity::class.java))
}))
else if (toolbar?.findViewById<View>(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
})
if (!entryCopyEducationPerformed) {
// entryEditEducationPerformed
toolbar?.findViewById<View>(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
toolbar!!.findViewById(R.id.menu_edit),
{
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
@@ -323,14 +373,17 @@ class EntryActivity : LockingHideActivity() {
{
// Open Keepass doc to create field references
startActivity(Intent(Intent.ACTION_VIEW,
Uri.parse(getString(R.string.field_references_url))))
}))
;
UriUtil.parse(getString(R.string.field_references_url))))
})
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_contribute -> return MenuUtil.onContributionItemSelected(this)
R.id.menu_contribute -> {
MenuUtil.onContributionItemSelected(this)
return true
}
R.id.menu_toggle_pass -> {
mShowPassword = !mShowPassword
@@ -354,11 +407,7 @@ class EntryActivity : LockingHideActivity() {
url = "http://$url"
}
try {
Util.gotoUrl(this, url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(this, R.string.no_url_handler, Toast.LENGTH_LONG).show()
}
UriUtil.gotoUrl(this, url)
return true
}
@@ -389,13 +438,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: EntryVersioned, 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,30 +22,30 @@ import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.support.v7.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 com.kunzisoft.keepass.R
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.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.education.EntryEditActivityEducation
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.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 java.util.*
class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPickerListener, GeneratePasswordDialogFragment.GeneratePasswordListener {
class EntryEditActivity : LockingHideActivity(),
IconPickerDialogFragment.IconPickerListener,
GeneratePasswordDialogFragment.GeneratePasswordListener {
private var mDatabase: Database? = null
@@ -58,11 +58,12 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
// Views
private var scrollView: ScrollView? = null
private var entryEditContentsView: EntryEditContentsView? = null
private var saveView: View? = null
// Dialog thread
private var progressDialogThread: ProgressDialogThread? = null
// Education
private var entryEditActivityEducation: EntryEditActivityEducation? = null
@@ -88,7 +89,7 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
mDatabase = Database.getInstance()
// Entry is retrieve, it's an entry to update
intent.getParcelableExtra<PwNodeId<*>>(KEY_ENTRY)?.let {
intent.getParcelableExtra<PwNodeId<UUID>>(KEY_ENTRY)?.let {
mIsNew = false
// Create an Entry copy to modify from the database entry
mEntry = mDatabase?.getEntryById(it)
@@ -103,16 +104,14 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
}
}
// 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 ->
// WARNING Remove the parent to keep memory with parcelable
newEntry.parent = null
newEntry.removeParent()
}
}
}
@@ -121,7 +120,11 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
// Parent is retrieve, it's a new entry to create
intent.getParcelableExtra<PwNodeId<*>>(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 ->
@@ -129,6 +132,12 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
}
}
// 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()
@@ -150,37 +159,21 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
saveView = findViewById(R.id.entry_edit_save)
saveView?.setOnClickListener { saveEntry() }
entryEditContentsView?.allowCustomField(mNewEntry?.allowExtraFields() == true) { addNewCustomField() }
entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) { addNewCustomField() }
// Verify the education views
entryEditActivityEducation = EntryEditActivityEducation(this)
entryEditActivityEducation?.let {
Handler().post { performedNextEducation(it) }
}
}
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
val passwordView = entryEditContentsView?.generatePasswordView
val addNewFieldView = entryEditContentsView?.addNewFieldView
if (passwordView != null
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
passwordView,
{
openPasswordGenerator()
},
{
performedNextEducation(entryEditActivityEducation)
// Create progress dialog
progressDialogThread = ProgressDialogThread(this) { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_CREATE_ENTRY_TASK,
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
if (result.isSuccess)
finish()
}
}
}
))
else if (mNewEntry != null && mNewEntry!!.allowExtraFields() && !mNewEntry!!.containsCustomFields()
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
addNewFieldView,
{
addNewCustomField()
}))
;
}
private fun populateViewsWithEntry(newEntry: EntryVersioned) {
@@ -193,12 +186,14 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
// 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
newEntry.fields.doActionToAllCustomProtectedField { key, value ->
addNewCustomField(key, value)
for (entry in newEntry.customFields.entries) {
post {
addNewCustomField(entry.key, entry.value)
}
}
}
}
@@ -210,6 +205,7 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
newEntry.apply {
// Build info from view
entryEditContentsView?.let { entryView ->
removeAllFields()
title = entryView.title
username = entryView.username
url = entryView.url
@@ -243,8 +239,6 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
*/
private fun addNewCustomField() {
entryEditContentsView?.addNewCustomField()
// Scroll bottom
scrollView?.post { scrollView?.fullScroll(ScrollView.FOCUS_DOWN) }
}
/**
@@ -255,7 +249,6 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
// 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
@@ -267,41 +260,39 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
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,
progressDialogThread?.startDatabaseCreateEntry(
newEntry,
parent,
afterActionNodeFinishRunnable,
!readOnly)
!mReadOnly
)
}
} else {
mEntry?.let { oldEntry ->
actionRunnable = UpdateEntryRunnable(this@EntryEditActivity,
database,
progressDialogThread?.startDatabaseUpdateEntry(
oldEntry,
newEntry,
afterActionNodeFinishRunnable,
!readOnly)
}
}
actionRunnable?.let { runnable ->
ProgressDialogSaveDatabaseThread(this@EntryEditActivity) { runnable }.start()
!mReadOnly
)
}
}
}
}
}
override fun onResume() {
super.onResume()
progressDialogThread?.registerProgressTask()
}
override fun onPause() {
progressDialogThread?.unregisterProgressTask()
super.onPause()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
@@ -309,9 +300,39 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
inflater.inflate(R.menu.database_lock, menu)
MenuUtil.contributionMenuInflater(inflater, menu)
entryEditActivityEducation?.let {
Handler().post { performedNextEducation(it) }
}
return true
}
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
val passwordView = entryEditContentsView?.generatePasswordView
val addNewFieldView = entryEditContentsView?.addNewFieldView
val generatePasswordEducationPerformed = passwordView != null
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
passwordView,
{
openPasswordGenerator()
},
{
performedNextEducation(entryEditActivityEducation)
}
)
if (!generatePasswordEducationPerformed) {
// entryNewFieldEducationPerformed
mNewEntry != null && mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
addNewFieldView,
{
addNewCustomField()
})
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_lock -> {
@@ -319,7 +340,10 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
return true
}
R.id.menu_contribute -> return MenuUtil.onContributionItemSelected(this)
R.id.menu_contribute -> {
MenuUtil.onContributionItemSelected(this)
return true
}
android.R.id.home -> finish()
}
@@ -334,7 +358,10 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putParcelable(KEY_NEW_ENTRY, mNewEntry)
mNewEntry?.let {
populateEntryWithViews(it)
outState.putParcelable(KEY_NEW_ENTRY, it)
}
super.onSaveInstanceState(outState)
}

View File

@@ -19,7 +19,7 @@
*/
package com.kunzisoft.keepass.activities
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.app.assist.AssistStructure
import android.content.Intent
@@ -29,81 +29,59 @@ import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.preference.PreferenceManager
import android.support.annotation.RequiresApi
import android.support.v7.app.AlertDialog
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.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 android.widget.Toast
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.CreateFileDialogFragment
import com.kunzisoft.keepass.activities.dialogs.FileInformationDialogFragment
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.KeyFileHelper
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.fileselect.DeleteFileHistoryAsyncTask
import com.kunzisoft.keepass.fileselect.FileDatabaseModel
import com.kunzisoft.keepass.fileselect.OpenFileHistoryAsyncTask
import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import net.cachapa.expandablelayout.ExpandableLayout
import permissions.dispatcher.*
import java.io.File
import com.kunzisoft.keepass.view.asError
import kotlinx.android.synthetic.main.activity_file_selection.*
import java.io.FileNotFoundException
import java.io.IOException
import java.lang.ref.WeakReference
import java.net.URLDecoder
import java.util.*
@RuntimePermissions
class FileDatabaseSelectActivity : StylishActivity(),
CreateFileDialogFragment.DefinePathDialogListener,
AssignMasterKeyDialogFragment.AssignPasswordDialogListener,
FileDatabaseHistoryAdapter.FileItemOpenListener,
FileDatabaseHistoryAdapter.FileSelectClearListener,
FileDatabaseHistoryAdapter.FileInformationShowListener {
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
// 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
private var mFileDatabaseHistory: FileDatabaseHistory? = null
private var mFileDatabaseHistoryAction: FileDatabaseHistoryAction? = null
private var mDatabaseFileUri: Uri? = null
private var mKeyFileHelper: KeyFileHelper? = null
private var mOpenFileHelper: OpenFileHelper? = null
private var mDefaultPath: String? = null
private var progressDialogThread: ProgressDialogThread? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mFileDatabaseHistory = FileDatabaseHistory.getInstance(WeakReference(applicationContext))
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
setContentView(R.layout.activity_file_selection)
fileListContainer = findViewById(R.id.container_file_list)
@@ -112,132 +90,122 @@ 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()
// Create button
createButtonView = findViewById(R.id.create_database_button)
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{
// There is an activity which can handle this intent.
createButtonView?.visibility = View.VISIBLE
}
createButtonView?.setOnClickListener { createNewFile() }
mOpenFileHelper = OpenFileHelper(this)
openDatabaseButtonView = findViewById(R.id.open_database_button)
openDatabaseButtonView?.setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
// History list
val databaseFileListView = findViewById<RecyclerView>(R.id.file_list)
databaseFileListView.layoutManager = LinearLayoutManager(this)
// Open button
openButtonView = findViewById(R.id.open_database)
openButtonView?.setOnClickListener { _ ->
var fileName = openFileNameView?.text?.toString() ?: ""
mDefaultPath?.let {
if (fileName.isEmpty())
fileName = it
}
launchPasswordActivityWithPath(fileName)
}
// Create button
createButtonView = findViewById(R.id.create_database)
createButtonView?.setOnClickListener { openCreateFileDialogFragmentWithPermissionCheck() }
mKeyFileHelper = KeyFileHelper(this)
browseButtonView = findViewById(R.id.browse_button)
browseButtonView?.setOnClickListener(mKeyFileHelper!!.getOpenFileOnClickViewListener {
Uri.parse("file://" + openFileNameView!!.text.toString())
})
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
fileDatabaseHistoryRecyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
// Removes blinks
(fileDatabaseHistoryRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
// Construct adapter with listeners
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this@FileDatabaseSelectActivity,
mFileDatabaseHistory?.databaseUriList ?: ArrayList())
mAdapterDatabaseHistory?.setOnItemClickListener(this)
mAdapterDatabaseHistory?.setFileSelectClearListener(this)
mAdapterDatabaseHistory?.setFileInformationShowListener(this)
databaseFileListView.adapter = mAdapterDatabaseHistory
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this)
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
UriUtil.parse(fileDatabaseHistoryEntityToOpen.databaseUri)?.let { databaseFileUri ->
launchPasswordActivity(
databaseFileUri,
UriUtil.parse(fileDatabaseHistoryEntityToOpen.keyFileUri))
}
updateFileListVisibility()
}
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
// Remove from app database
mFileDatabaseHistoryAction?.deleteFileDatabaseHistory(fileDatabaseHistoryToDelete) { fileHistoryDeleted ->
// Remove from adapter
fileHistoryDeleted?.let { databaseFileHistoryDeleted ->
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileHistoryDeleted)
mAdapterDatabaseHistory?.notifyDataSetChanged()
updateFileListVisibility()
}
}
true
}
mAdapterDatabaseHistory?.setOnSaveAliasListener { fileDatabaseHistoryWithNewAlias ->
mFileDatabaseHistoryAction?.addOrUpdateFileDatabaseHistory(fileDatabaseHistoryWithNewAlias)
}
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
// Load default database if not an orientation change
if (!(savedInstanceState != null
&& savedInstanceState.containsKey(EXTRA_STAY)
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val fileName = prefs.getString(PasswordActivity.KEY_DEFAULT_FILENAME, "")
val databasePath = prefs.getString(PasswordActivity.KEY_DEFAULT_DATABASE_PATH, "")
if (fileName != null && fileName.isNotEmpty()) {
val dbUri = UriUtil.parseUriFile(fileName)
var scheme: String? = null
if (dbUri != null)
scheme = dbUri.scheme
if (scheme != null && scheme.isNotEmpty() && scheme.equals("file", ignoreCase = true)) {
val path = dbUri!!.path
val db = File(path!!)
if (db.exists()) {
launchPasswordActivityWithPath(path)
UriUtil.parse(databasePath)?.let { databaseFileUri ->
launchPasswordActivityWithPath(databaseFileUri)
} ?: run {
Log.i(TAG, "Unable to launch Password Activity")
}
}
// Retrieve the database URI provided by file manager after an orientation change
if (savedInstanceState != null
&& savedInstanceState.containsKey(EXTRA_DATABASE_URI)) {
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
}
// Attach the dialog thread to this activity
progressDialogThread = ProgressDialogThread(this) { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_CREATE_TASK -> {
// TODO Check
// mAdapterDatabaseHistory?.notifyDataSetChanged()
// updateFileListVisibility()
GroupActivity.launch(this)
}
} else {
if (dbUri != null)
launchPasswordActivityWithPath(dbUri.toString())
}
}
}
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
}
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
// If no recent files
if (createButtonView != null
&& mFileDatabaseHistory != null
&& !mFileDatabaseHistory!!.hasRecentFiles() && fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
createButtonView!!,
{
openCreateFileDialogFragmentWithPermissionCheck()
/**
* Create a new file by calling the content provider
*/
@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))
},
{
// But if the user cancel, it can also select a database
performedNextEducation(fileDatabaseSelectActivityEducation)
}))
else if (browseButtonView != null
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
browseButtonView!!,
{tapTargetView ->
tapTargetView?.let {
mKeyFileHelper?.openFileOnClickViewListener?.onClick(it)
CREATE_FILE_REQUEST_CODE)
} catch (e: Exception) {
BrowserDialogFragment().show(supportFragmentManager, "browserDialog")
}
},
{
fileSelectExpandableButtonView?.let {
fileDatabaseSelectActivityEducation
.checkAndPerformedOpenLinkDatabaseEducation(it)
}
}
))
;
}
private fun fileNoFoundAction(e: FileNotFoundException) {
val error = getString(R.string.file_not_found_content)
Toast.makeText(this@FileDatabaseSelectActivity,
error, Toast.LENGTH_LONG).show()
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
Log.e(TAG, error, e)
}
private fun launchPasswordActivity(fileName: String, keyFile: String) {
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
EntrySelectionHelper.doEntrySelectionAction(intent,
{
try {
PasswordActivity.launch(this@FileDatabaseSelectActivity,
fileName, keyFile)
databaseUri, keyFile)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
@@ -245,7 +213,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
{
try {
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
fileName, keyFile)
databaseUri, keyFile)
finish()
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
@@ -255,7 +223,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
fileName, keyFile,
databaseUri, keyFile,
assistStructure)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
@@ -265,8 +233,25 @@ class FileDatabaseSelectActivity : StylishActivity(),
})
}
private fun launchPasswordActivityWithPath(path: String) {
launchPasswordActivity(path, "")
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 <=
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
overridePendingTransition(0, 0)
@@ -292,29 +277,41 @@ class FileDatabaseSelectActivity : StylishActivity(),
}
override fun onResume() {
val database = Database.getInstance()
if (database.loaded) {
launchGroupActivity(database.isReadOnly)
}
super.onResume()
updateExternalStorageWarning()
// Construct adapter with listeners
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
databaseFileHistoryList?.let {
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(it)
updateFileListVisibility()
mAdapterDatabaseHistory!!.notifyDataSetChanged()
mAdapterDatabaseHistory?.notifyDataSetChanged()
}
}
// Register progress task
progressDialogThread?.registerProgressTask()
}
override fun onPause() {
// Unregister progress task
progressDialogThread?.unregisterProgressTask()
super.onPause()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// only to keep the current activity
outState.putBoolean(EXTRA_STAY, true)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// NOTE: delegate the permission handling to generated method
onRequestPermissionsResult(requestCode, grantResults)
}
@NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
fun openCreateFileDialogFragment() {
val createFileDialogFragment = CreateFileDialogFragment()
createFileDialogFragment.show(supportFragmentManager, "createFileDialogFragment")
// to retrieve the URI of a created database after an orientation change
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
}
private fun updateFileListVisibility() {
@@ -324,133 +321,26 @@ class FileDatabaseSelectActivity : StylishActivity(),
fileListContainer?.visibility = View.VISIBLE
}
/**
* Create file for database
* @return If not created, return false
*/
private fun createDatabaseFile(path: Uri): Boolean {
val pathString = URLDecoder.decode(path.path, "UTF-8")
// Make sure file name exists
if (pathString.isEmpty()) {
Log.e(TAG, getString(R.string.error_filename_required))
Toast.makeText(this@FileDatabaseSelectActivity,
R.string.error_filename_required,
Toast.LENGTH_LONG).show()
return false
}
// Try to create the file
val file = File(pathString)
try {
if (file.exists()) {
Log.e(TAG, getString(R.string.error_database_exists) + " " + file)
Toast.makeText(this@FileDatabaseSelectActivity,
R.string.error_database_exists,
Toast.LENGTH_LONG).show()
return false
}
val parent = file.parentFile
if (parent == null || parent.exists() && !parent.isDirectory) {
Log.e(TAG, getString(R.string.error_invalid_path) + " " + file)
Toast.makeText(this@FileDatabaseSelectActivity,
R.string.error_invalid_path,
Toast.LENGTH_LONG).show()
return false
}
if (!parent.exists()) {
// Create parent directory
if (!parent.mkdirs()) {
Log.e(TAG, getString(R.string.error_could_not_create_parent) + " " + parent)
Toast.makeText(this@FileDatabaseSelectActivity,
R.string.error_could_not_create_parent,
Toast.LENGTH_LONG).show()
return false
}
}
return file.createNewFile()
} catch (e: IOException) {
Log.e(TAG, getString(R.string.error_could_not_create_parent) + " " + e.localizedMessage)
e.printStackTrace()
Toast.makeText(
this@FileDatabaseSelectActivity,
getText(R.string.error_file_not_create).toString() + " "
+ e.localizedMessage,
Toast.LENGTH_LONG).show()
return false
}
}
override fun onDefinePathDialogPositiveClick(pathFile: Uri?): Boolean {
mDatabaseFileUri = pathFile
if (pathFile == null)
return false
return if (createDatabaseFile(pathFile)) {
AssignMasterKeyDialogFragment().show(supportFragmentManager, "passwordDialog")
true
} else
false
}
override fun onDefinePathDialogNegativeClick(pathFile: Uri?): Boolean {
return true
}
override fun onAssignKeyDialogPositiveClick(
masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?) {
try {
UriUtil.parseUriFile(mDatabaseFileUri)?.let { databaseUri ->
mDatabaseFileUri?.let { databaseUri ->
// Create the new database
ProgressDialogThread(this@FileDatabaseSelectActivity,
{
CreateDatabaseRunnable(this@FileDatabaseSelectActivity,
progressDialogThread?.startDatabaseCreate(
databaseUri,
Database.getInstance(),
masterPasswordChecked,
masterPassword,
keyFileChecked,
keyFile,
true, // TODO get readonly
LaunchGroupActivityFinish(databaseUri)
keyFile
)
},
R.string.progress_create)
.start()
}
} catch (e: Exception) {
val error = "Unable to create database with this password and key file"
Toast.makeText(this, error, Toast.LENGTH_LONG).show()
Log.e(TAG, error + " " + e.message)
// TODO remove
e.printStackTrace()
}
}
private inner class LaunchGroupActivityFinish internal constructor(private val fileURI: Uri) : ActionRunnable() {
override fun run() {
finishRun(true, null)
}
override fun onFinishRun(result: Result) {
runOnUiThread {
if (result.isSuccess) {
// Add database to recent files
mFileDatabaseHistory?.addDatabaseUri(fileURI)
mAdapterDatabaseHistory?.notifyDataSetChanged()
updateFileListVisibility()
GroupActivity.launch(this@FileDatabaseSelectActivity)
} else {
Log.e(TAG, "Unable to open the database")
}
}
val error = getString(R.string.error_create_database_file)
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
Log.e(TAG, error, e)
}
}
@@ -460,29 +350,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
}
override fun onFileItemOpenListener(itemPosition: Int) {
OpenFileHistoryAsyncTask({ fileName, keyFile ->
if (fileName != null && keyFile != null)
launchPasswordActivity(fileName, keyFile)
updateFileListVisibility()
}, mFileDatabaseHistory).execute(itemPosition)
}
override fun onClickFileInformation(fileDatabaseModel: FileDatabaseModel) {
FileInformationDialogFragment.newInstance(fileDatabaseModel).show(supportFragmentManager, "fileInformation")
}
override fun onFileSelectClearListener(fileDatabaseModel: FileDatabaseModel): Boolean {
DeleteFileHistoryAsyncTask({
fileDatabaseModel.fileUri?.let {
mFileDatabaseHistory?.deleteDatabaseUri(it)
}
mAdapterDatabaseHistory?.notifyDataSetChanged()
updateFileListVisibility()
}, mFileDatabaseHistory, mAdapterDatabaseHistory).execute(fileDatabaseModel)
return true
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
@@ -490,44 +357,64 @@ class FileDatabaseSelectActivity : StylishActivity(),
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
if (uri != null) {
if (PreferencesUtil.autoOpenSelectedFile(this@FileDatabaseSelectActivity)) {
launchPasswordActivityWithPath(uri.toString())
} else {
fileSelectExpandableLayout?.expand(false)
openFileNameView?.setText(uri.toString())
}
}
launchPasswordActivityWithPath(uri)
}
}
@OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showRationaleForExternalStorage(request: PermissionRequest) {
AlertDialog.Builder(this)
.setMessage(R.string.permission_external_storage_rationale_write_database)
.setPositiveButton(R.string.allow) { _, _ -> request.proceed() }
.setNegativeButton(R.string.cancel) { _, _ -> request.cancel() }
.show()
// Retrieve the created URI from the file manager
if (requestCode == CREATE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
mDatabaseFileUri = data?.data
if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment.getInstance(true)
.show(supportFragmentManager, "passwordDialog")
}
@OnPermissionDenied(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showDeniedForExternalStorage() {
Toast.makeText(this, R.string.permission_external_storage_denied, Toast.LENGTH_SHORT).show()
// else {
// TODO Show error
// }
}
@OnNeverAskAgain(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showNeverAskForExternalStorage() {
Toast.makeText(this, R.string.permission_external_storage_never_ask, Toast.LENGTH_SHORT).show()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
MenuUtil.defaultMenuInflater(menuInflater, menu)
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
return true
}
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
// If no recent files
val createDatabaseEducationPerformed = createButtonView != null && createButtonView!!.visibility == View.VISIBLE
&& mAdapterDatabaseHistory != null
&& mAdapterDatabaseHistory!!.itemCount > 0
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
createButtonView!!,
{
createNewFile()
},
{
// But if the user cancel, it can also select a database
performedNextEducation(fileDatabaseSelectActivityEducation)
})
if (!createDatabaseEducationPerformed) {
// selectDatabaseEducationPerformed
openDatabaseButtonView != null
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
openDatabaseButtonView!!,
{tapTargetView ->
tapTargetView?.let {
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
}
},
{}
)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
}
@@ -536,6 +423,9 @@ class FileDatabaseSelectActivity : StylishActivity(),
private const val TAG = "FileDbSelectActivity"
private const val EXTRA_STAY = "EXTRA_STAY"
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
private const val CREATE_FILE_REQUEST_CODE = 3853
/*
* -------------------------
@@ -550,7 +440,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
*/
fun launchForKeyboardSelection(activity: Activity) {
KeyboardHelper.startActivityForKeyboardSelection(activity, Intent(activity, FileDatabaseSelectActivity::class.java))
EntrySelectionHelper.startActivityForEntrySelection(activity, Intent(activity, FileDatabaseSelectActivity::class.java))
}
/*

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,17 +28,19 @@ import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.preference.PreferenceManager
import android.support.annotation.RequiresApi
import android.support.v4.app.FragmentManager
import android.support.v7.widget.SearchView
import android.support.v7.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.GroupEditDialogFragment
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
@@ -48,38 +49,44 @@ 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.ProgressDialogThread
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
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.LOCK_ACTION
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,
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
@@ -89,12 +96,14 @@ class GroupActivity : LockingActivity(),
private var mListNodesFragment: ListNodesFragment? = null
private var mCurrentGroupIsASearch: Boolean = false
private var progressDialogThread: ProgressDialogThread? = null
// 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
// TODO private var mNodeToCopy: NodeVersioned? = null
// TODO private var mNodeToMove: NodeVersioned? = null
private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null
@@ -112,15 +121,27 @@ 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)
/*
toolbarAction?.setNavigationOnClickListener {
toolbarAction?.collapse()
mNodeToCopy = null
mNodeToMove = null
}
*/
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(addNodeButtonView)
@@ -128,13 +149,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 {
@@ -155,17 +169,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)
@@ -178,12 +181,12 @@ class GroupActivity : LockingActivity(),
// Initialize the fragment with the list
mListNodesFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as ListNodesFragment?
if (mListNodesFragment == null)
mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, readOnly, mCurrentGroupIsASearch)
mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, mReadOnly, mCurrentGroupIsASearch)
// Attach fragment to content view
supportFragmentManager.beginTransaction().replace(
R.id.nodes_list_fragment_container,
mListNodesFragment,
mListNodesFragment!!,
fragmentTag)
.commit()
@@ -199,25 +202,92 @@ class GroupActivity : LockingActivity(),
}
})
// Search suggestion
mDatabase?.let { database ->
// Search suggestion
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, database)
// Init dialog thread
progressDialogThread = ProgressDialogThread(this) { actionTask, result ->
var oldNodes: List<NodeVersioned> = ArrayList()
result.data?.getBundle(OLD_NODES_KEY)?.let { oldNodesBundle ->
oldNodes = getListNodesFromBundle(database, oldNodesBundle)
}
var newNodes: List<NodeVersioned> = 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 the avoid bug when delete node from db sort
if (PreferencesUtil.getListSort(this@GroupActivity) == SortNodeEnum.DB) {
mListNodesFragment?.rebuildList()
} else {
// Use the old Nodes / entries unchanged with the old parent
mListNodesFragment?.removeNodes(oldNodes)
}
// Add trash in views list if it doesn't exists
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)
}
}
}
}
}
if (!result.isSuccess) {
result.exception?.errorId?.let { errorId ->
coordinatorLayout?.let { coordinatorLayout ->
Snackbar.make(coordinatorLayout, errorId, Snackbar.LENGTH_LONG).asError().show()
}
}
}
finishNodeAction()
}
}
Log.i(TAG, "Finished creating tree")
}
override fun onNewIntent(intent: Intent) {
Log.d(TAG, "setNewIntent: $intent")
setIntent(intent)
mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intent.action) {
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intent?.let { intentNotNull ->
Log.d(TAG, "setNewIntent: $intentNotNull")
setIntent(intentNotNull)
mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intentNotNull.action) {
// only one instance of search in backstack
openSearchGroup(retrieveCurrentGroup(intent, null))
openSearchGroup(retrieveCurrentGroup(intentNotNull, null))
true
} else {
false
}
}
}
private fun openSearchGroup(group: GroupVersioned?) {
// Delete the previous search fragment
@@ -239,7 +309,7 @@ class GroupActivity : LockingActivity(),
// Check TimeoutHelper
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
// Open a group in a new fragment
val newListNodeFragment = ListNodesFragment.newInstance(group, readOnly, isASearch)
val newListNodeFragment = ListNodesFragment.newInstance(group, mReadOnly, isASearch)
val fragmentTransaction = supportFragmentManager.beginTransaction()
// Different animation
val fragmentTag: String
@@ -272,17 +342,14 @@ 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? {
// Force read only if the database is like that
mReadOnly = mDatabase?.isReadOnly == true || mReadOnly
// If it's a search
if (Intent.ACTION_SEARCH == intent.action) {
return mDatabase?.search(intent.getStringExtra(SearchManager.QUERY).trim { it <= ' ' })
@@ -297,8 +364,6 @@ class GroupActivity : LockingActivity(),
pwGroupId = intent.getParcelableExtra(GROUP_ID_KEY)
}
readOnly = mDatabase?.isReadOnly == true || readOnly // Force read only if the database is like that
Log.w(TAG, "Creating tree view")
val currentGroup: GroupVersioned?
currentGroup = if (pwGroupId == null) {
@@ -356,8 +421,18 @@ class GroupActivity : LockingActivity(),
}
}
// Assign number of children
numberChildrenView?.apply {
if (PreferencesUtil.showNumberEntries(context)) {
text = mCurrentGroup?.getChildEntries(true)?.size?.toString() ?: ""
visibility = View.VISIBLE
} else {
visibility = View.GONE
}
}
// Show selection mode message if needed
if (selectionMode) {
if (mSelectionMode) {
modeTitleView?.visibility = View.VISIBLE
} else {
modeTitleView?.visibility = View.GONE
@@ -367,8 +442,8 @@ class GroupActivity : LockingActivity(),
addNodeButtonView?.apply {
// To enable add button
val addGroupEnabled = !readOnly && !mCurrentGroupIsASearch
var addEntryEnabled = !readOnly && !mCurrentGroupIsASearch
val addGroupEnabled = !mReadOnly && !mCurrentGroupIsASearch
var addEntryEnabled = !mReadOnly && !mCurrentGroupIsASearch
mCurrentGroup?.let {
val isRoot = it == mRootGroup
if (!it.allowAddEntryIfIsRoot())
@@ -401,7 +476,7 @@ class GroupActivity : LockingActivity(),
val entryVersioned = node as EntryVersioned
EntrySelectionHelper.doEntrySelectionAction(intent,
{
EntryActivity.launch(this@GroupActivity, entryVersioned, readOnly)
EntryActivity.launch(this@GroupActivity, entryVersioned, mReadOnly)
},
{
// Populate Magikeyboard with entry
@@ -427,12 +502,36 @@ class GroupActivity : LockingActivity(),
}
}
private var actionNodeMode: ActionMode? = null
private fun finishNodeAction() {
actionNodeMode?.finish()
actionNodeMode = null
}
override fun onNodeSelected(nodes: List<NodeVersioned>): 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: NodeVersioned): Boolean {
finishNodeAction()
onNodeClick(node)
return true
}
override fun onEditMenuClick(node: NodeVersioned): Boolean {
finishNodeAction()
when (node.type) {
Type.GROUP -> {
mOldGroupToUpdate = node as GroupVersioned
@@ -445,132 +544,56 @@ class GroupActivity : LockingActivity(),
return true
}
override fun onCopyMenuClick(node: NodeVersioned): Boolean {
toolbarPasteExpandableLayout?.expand()
mNodeToCopy = node
toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener())
return false
}
override fun onCopyMenuClick(nodes: List<NodeVersioned>): 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<NodeVersioned>): 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<NodeVersioned>): Boolean {
when (pasteMode) {
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
// Copy
mCurrentGroup?.let { newParent ->
progressDialogThread?.startDatabaseCopyNodes(
nodes,
newParent,
AfterAddNodeRunnable(),
!readOnly)
}.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
)
}
}
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 ->
progressDialogThread?.startDatabaseMoveNodes(
nodes,
newParent,
AfterAddNodeRunnable(),
!readOnly)
}.start()
!mReadOnly
)
}
private fun moveEntry(entryToMove: EntryVersioned, newParent: GroupVersioned) {
ProgressDialogSaveDatabaseThread(this) {
MoveEntryRunnable(
this,
Database.getInstance(),
entryToMove,
newParent,
AfterAddNodeRunnable(),
!readOnly)
}.start()
}
override fun onDeleteMenuClick(node: NodeVersioned): Boolean {
when (node.type) {
Type.GROUP -> deleteGroup(node as GroupVersioned)
Type.ENTRY -> deleteEntry(node as EntryVersioned)
else -> {}
}
finishNodeAction()
return true
}
private fun deleteGroup(group: GroupVersioned) {
//TODO Verify trash recycle bin
ProgressDialogSaveDatabaseThread(this) {
DeleteGroupRunnable(
this,
Database.getInstance(),
group,
AfterDeleteNodeRunnable(),
!readOnly)
}.start()
}
private fun deleteEntry(entry: EntryVersioned) {
ProgressDialogSaveDatabaseThread(this) {
DeleteEntryRunnable(
this,
Database.getInstance(),
entry,
AfterDeleteNodeRunnable(),
!readOnly)
}.start()
override fun onDeleteMenuClick(nodes: List<NodeVersioned>): Boolean {
progressDialogThread?.startDatabaseDeleteNodes(
nodes,
!mReadOnly
)
finishNodeAction()
return true
}
override fun onResume() {
@@ -579,6 +602,16 @@ class GroupActivity : LockingActivity(),
assignGroupViewElements()
// Refresh suggestions to change preferences
mSearchSuggestionAdapter?.reInit(this)
progressDialogThread?.registerProgressTask()
}
override fun onPause() {
progressDialogThread?.unregisterProgressTask()
super.onPause()
finishNodeAction()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -586,7 +619,7 @@ class GroupActivity : LockingActivity(),
val inflater = menuInflater
inflater.inflate(R.menu.search, menu)
inflater.inflate(R.menu.database_lock, menu)
if (!selectionMode) {
if (!mSelectionMode) {
inflater.inflate(R.menu.default_menu, menu)
MenuUtil.contributionMenuInflater(inflater, menu)
}
@@ -628,8 +661,9 @@ class GroupActivity : LockingActivity(),
private fun performedNextEducation(groupActivityEducation: GroupActivityEducation,
menu: Menu) {
// If no node, show education to add new one
if (mListNodesFragment != null
val addNodeButtonEducationPerformed = mListNodesFragment != null
&& mListNodesFragment!!.isEmpty
&& addNodeButtonView?.addButtonView != null
&& addNodeButtonView!!.isEnable
@@ -641,8 +675,10 @@ class GroupActivity : LockingActivity(),
{
performedNextEducation(groupActivityEducation, menu)
}
))
else if (toolbar != null
)
if (!addNodeButtonEducationPerformed) {
val searchMenuEducationPerformed = toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_search) != null
&& groupActivityEducation.checkAndPerformedSearchMenuEducation(
toolbar!!.findViewById(R.id.menu_search),
@@ -651,8 +687,11 @@ class GroupActivity : LockingActivity(),
},
{
performedNextEducation(groupActivityEducation, menu)
}))
else if (toolbar != null
})
if (!searchMenuEducationPerformed) {
val sortMenuEducationPerformed = toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_sort) != null
&& groupActivityEducation.checkAndPerformedSortMenuEducation(
toolbar!!.findViewById(R.id.menu_sort),
@@ -661,8 +700,11 @@ class GroupActivity : LockingActivity(),
},
{
performedNextEducation(groupActivityEducation, menu)
}))
else if (toolbar != null
})
if (!sortMenuEducationPerformed) {
// lockMenuEducationPerformed
toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_lock) != null
&& groupActivityEducation.checkAndPerformedLockMenuEducation(
toolbar!!.findViewById(R.id.menu_lock),
@@ -671,41 +713,9 @@ class GroupActivity : LockingActivity(),
},
{
performedNextEducation(groupActivityEducation, menu)
}))
;
}
override fun startActivity(intent: Intent) {
// Get the intent, verify the action and get the query
if (Intent.ACTION_SEARCH == intent.action) {
val query = intent.getStringExtra(SearchManager.QUERY)
// manually launch the real search activity
val searchIntent = Intent(applicationContext, GroupActivity::class.java)
// add query to the Intent Extras
searchIntent.action = Intent.ACTION_SEARCH
searchIntent.putExtra(SearchManager.QUERY, query)
EntrySelectionHelper.doEntrySelectionAction(intent,
{
super@GroupActivity.startActivity(intent)
},
{
KeyboardHelper.startActivityForKeyboardSelection(
this@GroupActivity,
searchIntent)
finish()
},
{ assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.startActivityForAutofillResult(
this@GroupActivity,
searchIntent,
assistStructure)
}
})
} else {
super.startActivity(intent)
}
}
}
}
@@ -724,7 +734,7 @@ class GroupActivity : LockingActivity(),
}
else -> {
// Check the time lock before launching settings
MenuUtil.onDefaultMenuOptionsItemSelected(this, item, readOnly, true)
MenuUtil.onDefaultMenuOptionsItemSelected(this, item, mReadOnly, true)
return super.onOptionsItemSelected(item)
}
}
@@ -733,7 +743,6 @@ class GroupActivity : LockingActivity(),
override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
name: String?,
icon: PwIcon?) {
val database = Database.getInstance()
if (name != null && name.isNotEmpty() && icon != null) {
when (action) {
@@ -741,94 +750,37 @@ 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(),
newGroup,
currentGroup,
AfterAddNodeRunnable(),
!readOnly)
}.start()
progressDialogThread?.startDatabaseCreateGroup(
newGroup, currentGroup, !mReadOnly)
}
}
}
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
updateGroup.apply {
// WARNING remove parent and children to keep memory
removeParent()
removeChildren() // TODO concurrent exception
mListNodesFragment?.removeNode(oldGroupToUpdate)
title = name
this.icon = icon // TODO custom icon
}
// If group updated save it in the database
ProgressDialogSaveDatabaseThread(this) {
UpdateGroupRunnable(this,
Database.getInstance(),
oldGroupToUpdate,
updateGroup,
AfterUpdateNodeRunnable(),
!readOnly)
}.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) {
actionNodeValues.oldNode?.let { oldNode ->
mListNodesFragment?.removeNode(oldNode)
// TODO Move trash view
// 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) {
mListNodesFragment?.addNode(recycleBin)
}
progressDialogThread?.startDatabaseUpdateGroup(
oldGroupToUpdate, updateGroup, !mReadOnly)
}
}
}
else -> {}
}
}
}
@@ -847,11 +799,9 @@ class GroupActivity : LockingActivity(),
}
private fun showWarnings() {
// TODO Preferences
if (Database.getInstance().isReadOnly) {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
if (prefs.getBoolean(getString(R.string.show_read_only_warning), true)) {
ReadOnlyDialog(this).show()
if (PreferencesUtil.showReadOnlyWarning(this)) {
ReadOnlyDialog().show(supportFragmentManager, "readOnlyDialog")
}
}
}
@@ -860,6 +810,40 @@ class GroupActivity : LockingActivity(),
mListNodesFragment?.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom)
}
override fun startActivity(intent: Intent) {
// Get the intent, verify the action and get the query
if (Intent.ACTION_SEARCH == intent.action) {
// manually launch the real search activity
val searchIntent = Intent(applicationContext, GroupActivity::class.java).apply {
// Add bundle of current intent
putExtras(this@GroupActivity.intent)
// add query to the Intent Extras
action = Intent.ACTION_SEARCH
putExtra(SearchManager.QUERY, intent.getStringExtra(SearchManager.QUERY))
}
super.startActivity(searchIntent)
} else {
super.startActivity(intent)
}
}
override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) {
/*
* ACTION_SEARCH automatically forces a new task. This occurs when you open a kdb file in
* another app such as Files or GoogleDrive and then Search for an entry. Here we remove the
* FLAG_ACTIVITY_NEW_TASK flag bit allowing search to open it's activity in the current task.
*/
if (Intent.ACTION_SEARCH == intent.action) {
var flags = intent.flags
flags = flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv()
intent.flags = flags
}
super.startActivityForResult(intent, requestCode, options)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
@@ -871,24 +855,6 @@ class GroupActivity : LockingActivity(),
mListNodesFragment?.rebuildList()
}
@SuppressLint("RestrictedApi")
override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) {
/*
* ACTION_SEARCH automatically forces a new task. This occurs when you open a kdb file in
* another app such as Files or GoogleDrive and then Search for an entry. Here we remove the
* FLAG_ACTIVITY_NEW_TASK flag bit allowing search to open it's activity in the current task.
*/
if (Intent.ACTION_SEARCH == intent.action) {
var flags = intent.flags
flags = flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv()
intent.flags = flags
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
super.startActivityForResult(intent, requestCode, options)
}
}
private fun removeSearchInIntent(intent: Intent) {
if (Intent.ACTION_SEARCH == intent.action) {
mCurrentGroupIsASearch = false
@@ -898,6 +864,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()
@@ -918,6 +887,7 @@ class GroupActivity : LockingActivity(),
removeSearchInIntent(intent)
assignGroupViewElements()
}
}
companion object {
@@ -927,13 +897,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: GroupVersioned?, 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)
}
@@ -949,10 +921,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)
}
}
@@ -963,10 +935,10 @@ class GroupActivity : LockingActivity(),
*/
// TODO implement pre search to directly open the direct group
fun launchForKeyboardSelection(activity: Activity, readOnly: Boolean) {
TimeoutHelper.recordTime(activity)
buildAndLaunchIntent(activity, null, readOnly) { intent ->
KeyboardHelper.startActivityForKeyboardSelection(activity, intent)
fun launchForKeyboardSelection(context: Context, readOnly: Boolean) {
TimeoutHelper.recordTime(context)
buildAndLaunchIntent(context, null, readOnly) { intent ->
EntrySelectionHelper.startActivityForEntrySelection(context, intent)
}
}

View File

@@ -5,8 +5,8 @@ import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
@@ -14,6 +14,7 @@ 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
@@ -25,11 +26,13 @@ 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.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
@@ -37,6 +40,13 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
private set
private var mAdapter: NodeAdapter? = null
var nodeActionSelectionMode = false
private set
var nodeActionPasteMode: PasteMode = PasteMode.UNDEFINED
private set
private val listActionNodes = LinkedList<NodeVersioned>()
private val listPasteNodes = LinkedList<NodeVersioned>()
private var notFoundView: View? = null
private var isASearchResult: Boolean = false
@@ -52,31 +62,22 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
val isEmpty: Boolean
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
override fun onAttach(context: Context?) {
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()
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?
onScrollListener = context as OnScrollListener
} catch (e: ClassCastException) {
onScrollListener = null
// Context menu can be omit
Log.w(TAG, context?.toString()
Log.w(TAG, context.toString()
+ " must implement " + RecyclerView.OnScrollListener::class.java.name)
}
}
@@ -84,7 +85,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.let { currentActivity ->
setHasOptionsMenu(true)
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments)
@@ -100,18 +100,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: NodeVersioned) {
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: NodeVersioned): 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)
@@ -129,7 +155,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
onScrollListener?.let { onScrollListener ->
listView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
onScrollListener.onScrolled(dy)
}
@@ -147,10 +173,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()
@@ -194,35 +216,31 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
inflater?.inflate(R.menu.tree, menu)
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.tree, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_sort -> {
context?.let { context ->
val sortDialogFragment: SortDialogFragment
/*
// TODO Recycle bin bottom
if (database.isRecycleBinAvailable() && database.isRecycleBinEnabled()) {
sortDialogFragment =
val sortDialogFragment: SortDialogFragment =
if (Database.getInstance().allowRecycleBin
&& Database.getInstance().isRecycleBinEnabled) {
SortDialogFragment.getInstance(
PrefsUtil.getListSort(this),
PrefsUtil.getAscendingSort(this),
PrefsUtil.getGroupsBeforeSort(this),
PrefsUtil.getRecycleBinBottomSort(this));
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context),
PreferencesUtil.getRecycleBinBottomSort(context))
} else {
*/
sortDialogFragment = SortDialogFragment.getInstance(
SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context))
//}
}
sortDialogFragment.show(childFragmentManager, "sortDialog")
}
@@ -233,6 +251,102 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}
}
fun actionNodesCallback(nodes: List<NodeVersioned>,
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 || 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 == database.recycleBin }
|| 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 || 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)
@@ -255,18 +369,66 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}
}
fun contains(node: NodeVersioned): Boolean {
return mAdapter?.contains(node) ?: false
}
fun addNode(newNode: NodeVersioned) {
mAdapter?.addNode(newNode)
}
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned) {
mAdapter?.updateNode(oldNode, newNode)
fun addNodes(newNodes: List<NodeVersioned>) {
mAdapter?.addNodes(newNodes)
}
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned? = null) {
mAdapter?.updateNode(oldNode, newNode ?: oldNode)
}
fun updateNodes(oldNodes: List<NodeVersioned>, newNodes: List<NodeVersioned>) {
mAdapter?.updateNodes(oldNodes, newNodes)
}
fun removeNode(pwNode: NodeVersioned) {
mAdapter?.removeNode(pwNode)
}
fun removeNodes(nodes: List<NodeVersioned>) {
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: NodeVersioned)
fun onNodeSelected(nodes: List<NodeVersioned>): Boolean
}
/**
* Menu listener to redefine to do an action in menu
*/
interface NodesActionMenuListener {
fun onOpenMenuClick(node: NodeVersioned): Boolean
fun onEditMenuClick(node: NodeVersioned): Boolean
fun onCopyMenuClick(nodes: List<NodeVersioned>): Boolean
fun onMoveMenuClick(nodes: List<NodeVersioned>): Boolean
fun onDeleteMenuClick(nodes: List<NodeVersioned>): Boolean
fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List<NodeVersioned>): Boolean
}
enum class PasteMode {
UNDEFINED, PASTE_FROM_COPY, PASTE_FROM_MOVE
}
interface OnScrollListener {
/**

View File

@@ -19,61 +19,69 @@
*/
package com.kunzisoft.keepass.activities
import android.Manifest
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.hardware.fingerprint.FingerprintManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.preference.PreferenceManager
import android.support.annotation.RequiresApi
import android.support.v7.app.AlertDialog
import android.support.v7.widget.Toolbar
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.widget.*
import android.widget.Button
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar
import androidx.biometric.BiometricManager
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.*
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
import com.kunzisoft.keepass.activities.dialogs.FingerPrintExplanationDialog
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.App
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
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.LoadDatabaseDuplicateUuidException
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.fingerprint.FingerPrintHelper
import com.kunzisoft.keepass.fingerprint.FingerPrintViewsManager
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
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.FingerPrintInfoView
import permissions.dispatcher.*
import java.io.File
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
@RuntimePermissions
class PasswordActivity : StylishActivity(),
UriIntentInitTaskCallback {
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
@@ -81,26 +89,29 @@ class PasswordActivity : StylishActivity(),
private var checkboxPasswordView: CompoundButton? = null
private var checkboxKeyFileView: CompoundButton? = null
private var checkboxDefaultDatabaseView: CompoundButton? = null
private var fingerPrintInfoView: FingerPrintInfoView? = null
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
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 mKeyFileHelper: KeyFileHelper? = null
private var mOpenFileHelper: OpenFileHelper? = null
private var readOnly: Boolean = false
private var fingerPrintViewsManager: FingerPrintViewsManager? = null
private var progressDialogThread: ProgressDialogThread? = null
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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)
@@ -110,6 +121,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)
@@ -117,13 +129,13 @@ class PasswordActivity : StylishActivity(),
checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
fingerPrintInfoView = findViewById(R.id.fingerprint_info)
advancedUnlockInfoView = findViewById(R.id.fingerprint_info)
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
val browseView = findViewById<View>(R.id.browse_button)
mKeyFileHelper = KeyFileHelper(this@PasswordActivity)
browseView.setOnClickListener(mKeyFileHelper!!.openFileOnClickViewListener)
val browseView = findViewById<View>(R.id.open_database_button)
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
browseView.setOnClickListener(mOpenFileHelper!!.openFileOnClickViewListener)
passwordView?.setOnEditorActionListener(onEditorActionListener)
passwordView?.addTextChangedListener(object : TextWatcher {
@@ -148,11 +160,91 @@ class PasswordActivity : StylishActivity(),
}
})
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, isChecked ->
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
confirmButtonView?.isEnabled = isChecked
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
enableOrNotTheConfirmationButton()
}
progressDialogThread = ProgressDialogThread(this) { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> {
// 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()
}
}
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 ->
// 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)
if (resultException is LoadDatabaseDuplicateUuidException)
showLoadDatabaseDuplicateUuidMessage {
showProgressDialogAndLoadDatabase(
databaseFileUri,
masterPassword,
keyFileUri,
readOnly,
cipherEntity,
true)
}
}
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 +258,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,17 +270,9 @@ class PasswordActivity : StylishActivity(),
// For check shutdown
super.onResume()
// Enable or not the open button
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
checkboxPasswordView?.let {
confirmButtonView?.isEnabled = it.isChecked
}
} else {
confirmButtonView?.isEnabled = true
}
progressDialogThread?.registerProgressTask()
UriIntentInitTask(WeakReference(this), this, mRememberKeyFile)
.execute(intent)
initUriFromIntent()
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -193,26 +280,45 @@ class PasswordActivity : StylishActivity(),
super.onSaveInstanceState(outState)
}
override fun onPostInitTask(databaseFileUri: Uri?, keyFileUri: Uri?, errorStringId: Int?) {
mDatabaseFileUri = databaseFileUri
private fun initUriFromIntent() {
if (errorStringId != null) {
Toast.makeText(this@PasswordActivity, errorStringId, Toast.LENGTH_LONG).show()
finish()
return
val databaseUri: Uri?
val keyFileUri: Uri?
// If is a view intent
val action = intent.action
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)
}
// Verify permission to read file
if (databaseFileUri != null && !databaseFileUri.scheme!!.contains("content"))
doNothingWithPermissionCheck()
// Post init uri with KeyFile if needed
if (mRememberKeyFile && (keyFileUri == null || keyFileUri.toString().isEmpty())) {
// Retrieve KeyFile in a thread
databaseUri?.let { databaseUriNotNull ->
FileDatabaseHistoryAction.getInstance(applicationContext)
.getKeyFileUriByDatabaseUri(databaseUriNotNull) {
onPostInitUri(databaseUri, it)
}
}
} else {
onPostInitUri(databaseUri, keyFileUri)
}
}
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
mDatabaseFileUri = databaseFileUri
mDatabaseKeyFileUri = keyFileUri
// Define title
val dbUriString = databaseFileUri?.toString() ?: ""
if (dbUriString.isNotEmpty()) {
if (PreferencesUtil.isFullFilePathEnable(this))
filenameView?.text = dbUriString
else
filenameView?.text = File(databaseFileUri!!.path!!).name // TODO Encapsulate
databaseFileUri?.let {
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
filenameView?.text = title
}
}
// Define Key File text
@@ -223,13 +329,17 @@ class PasswordActivity : StylishActivity(),
// Define listeners for default database checkbox and validate button
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
var newDefaultFileName = ""
var newDefaultFileName: Uri? = null
if (isChecked) {
newDefaultFileName = databaseFileUri?.toString() ?: newDefaultFileName
newDefaultFileName = databaseFileUri ?: newDefaultFileName
}
prefs?.edit()?.apply() {
putString(KEY_DEFAULT_FILENAME, newDefaultFileName)
prefs?.edit()?.apply {
newDefaultFileName?.let {
putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString())
} ?: kotlin.run {
remove(KEY_DEFAULT_DATABASE_PATH)
}
apply()
}
@@ -239,10 +349,10 @@ class PasswordActivity : StylishActivity(),
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
// Retrieve settings for default database
val defaultFilename = prefs?.getString(KEY_DEFAULT_FILENAME, "")
val defaultFilename = prefs?.getString(KEY_DEFAULT_DATABASE_PATH, "")
if (databaseFileUri != null
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
&& databaseFileUri == UriUtil.parseUriFile(defaultFilename)) {
&& databaseFileUri == UriUtil.parse(defaultFilename)) {
checkboxDefaultDatabaseView?.isChecked = true
}
@@ -259,35 +369,62 @@ class PasswordActivity : StylishActivity(),
// Init FingerPrint elements
var fingerPrintInit = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (PreferencesUtil.isFingerprintEnable(this)) {
if (fingerPrintViewsManager == null) {
fingerPrintViewsManager = FingerPrintViewsManager(this,
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
advancedUnlockInfoView?.setOnClickListener {
FingerPrintExplanationDialog().show(supportFragmentManager, "fingerPrintExplanationDialog")
}
if (advancedUnlockedManager == null && databaseFileUri != null) {
advancedUnlockedManager = AdvancedUnlockedManager(this,
databaseFileUri,
fingerPrintInfoView,
advancedUnlockInfoView,
checkboxPasswordView,
enableButtonOnCheckedChangeListener,
passwordView) { passwordRetrieve ->
// Load the database if password is registered or retrieve
passwordRetrieve?.let {
passwordView,
{ passwordEncrypted, ivSpec ->
// Load the database if password is registered with biometric
if (passwordEncrypted != null && ivSpec != null) {
verifyCheckboxesAndLoadDatabase(
CipherDatabaseEntity(
databaseFileUri.toString(),
passwordEncrypted,
ivSpec)
)
}
},
{ passwordDecrypted ->
// Load the database if password is retrieve from biometric
passwordDecrypted?.let {
// Retrieve from fingerprint
verifyKeyFileCheckboxAndLoadDatabase(it)
} ?: run {
// Register with fingerprint
verifyCheckboxesAndLoadDatabase()
}
})
}
}
fingerPrintViewsManager?.initFingerprint()
// checks if fingerprint is available, will also start listening for fingerprints when available
fingerPrintViewsManager?.checkFingerprintAvailability()
advancedUnlockedManager?.initBiometric()
fingerPrintInit = true
} else {
fingerPrintViewsManager?.destroy()
advancedUnlockedManager?.destroy()
}
}
if (!fingerPrintInit) {
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
}
checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
}
enableOrNotTheConfirmationButton()
}
private fun enableOrNotTheConfirmationButton() {
// Enable or not the open button if setting is checked
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
checkboxPasswordView?.let {
confirmButtonView?.isEnabled = (checkboxPasswordView?.isChecked == true
|| checkboxKeyFileView?.isChecked == true)
}
} else {
confirmButtonView?.isEnabled = true
}
}
@@ -325,29 +462,43 @@ class PasswordActivity : StylishActivity(),
override fun onPause() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
fingerPrintViewsManager?.stopListening()
advancedUnlockedManager?.pause()
}
progressDialogThread?.unregisterProgressTask()
super.onPause()
}
override fun onDestroy() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
fingerPrintViewsManager?.destroy()
advancedUnlockedManager?.destroy()
}
super.onDestroy()
}
private fun verifyCheckboxesAndLoadDatabase(password: String? = passwordView?.text?.toString(),
keyFile: Uri? = UriUtil.parseUriFile(keyFileView?.text?.toString())) {
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
loadDatabase(keyPassword, keyFileUri)
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
val password: String? = passwordView?.text?.toString()
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity)
}
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String? = passwordView?.text?.toString(),
keyFile: Uri? = UriUtil.parseUriFile(keyFileView?.text?.toString())) {
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
loadDatabase(password, keyFileUri)
private fun verifyCheckboxesAndLoadDatabase(password: String?,
keyFile: Uri?,
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
verifyKeyFileCheckbox(keyFile)
loadDatabase(mDatabaseFileUri, keyPassword, mDatabaseKeyFileUri, cipherDatabaseEntity)
}
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
verifyKeyFileCheckbox(keyFile)
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
}
private fun verifyKeyFileCheckbox(keyFile: Uri?) {
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
}
private fun removePassword() {
@@ -355,89 +506,51 @@ class PasswordActivity : StylishActivity(),
checkboxPasswordView?.isChecked = false
}
private fun loadDatabase(password: String?, keyFile: Uri?) {
private fun loadDatabase(databaseFileUri: Uri?,
password: String?,
keyFileUri: Uri?,
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
removePassword()
}
// Clear before we load
val database = Database.getInstance()
database.closeAndClear(applicationContext.filesDir)
mDatabaseFileUri?.let { databaseUri ->
databaseFileUri?.let { databaseUri ->
// Show the progress dialog and load the database
ProgressDialogThread(this,
{ progressTaskUpdater ->
LoadDatabaseRunnable(
WeakReference(this@PasswordActivity),
database,
showProgressDialogAndLoadDatabase(
databaseUri,
password,
keyFileUri,
readOnly,
cipherDatabaseEntity,
false)
}
}
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
password: String?,
keyFile: Uri?,
readOnly: Boolean,
cipherDatabaseEntity: CipherDatabaseEntity?,
fixDuplicateUUID: Boolean) {
progressDialogThread?.startDatabaseLoad(
databaseUri,
password,
keyFile,
progressTaskUpdater,
AfterLoadingDatabase(database, password))
},
R.string.loading_database).start()
}
readOnly,
cipherDatabaseEntity,
fixDuplicateUUID
)
}
/**
* Called after verify and try to opening the database
*/
private inner class AfterLoadingDatabase internal constructor(var database: Database,
val password: String?)
: ActionRunnable() {
override fun onFinishRun(result: Result) {
runOnUiThread {
// Recheck fingerprint if error
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (PreferencesUtil.isFingerprintEnable(this@PasswordActivity)) {
// Stay with the same mode
fingerPrintViewsManager?.reInitWithFingerprintMode()
}
private fun showLoadDatabaseDuplicateUuidMessage(loadDatabaseWithFix: (() -> Unit)? = null) {
DuplicateUuidDialog().apply {
positiveAction = loadDatabaseWithFix
}.show(supportFragmentManager, "duplicateUUIDDialog")
}
if (result.isSuccess) {
// Remove the password in view in all cases
removePassword()
if (database.validatePasswordEncoding(password)) {
launchGroupActivity()
} else {
PasswordEncodingDialogFragment().apply {
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
launchGroupActivity()
}
show(supportFragmentManager, "passwordEncodingTag")
}
}
} else {
if (result.message != null && result.message!!.isNotEmpty()) {
Toast.makeText(this@PasswordActivity, result.message, Toast.LENGTH_LONG).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)
}
})
}
// To fix multiple view education
private var performedEductionInProgress = false
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
@@ -449,45 +562,58 @@ class PasswordActivity : StylishActivity(),
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Fingerprint menu
fingerPrintViewsManager?.inflateOptionsMenu(inflater, menu)
advancedUnlockedManager?.inflateOptionsMenu(inflater, menu)
}
super.onCreateOptionsMenu(menu)
if (!performedEductionInProgress) {
performedEductionInProgress = true
// Show education views
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) }
}
return true
}
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
menu: Menu) {
if (toolbar != null
&& passwordActivityEducation.checkAndPerformedFingerprintUnlockEducation(
toolbar!!,
val educationContainerView = containerView
val unlockEducationPerformed = educationContainerView != null
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
educationContainerView,
{
performedNextEducation(passwordActivityEducation, menu)
},
{
performedNextEducation(passwordActivityEducation, menu)
}))
else if (toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
})
if (!unlockEducationPerformed) {
val educationToolbar = toolbar
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)
}))
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& PreferencesUtil.isFingerprintEnable(applicationContext)
&& FingerPrintHelper.isFingerprintSupported(getSystemService(FingerprintManager::class.java))
&& fingerPrintInfoView != null && fingerPrintInfoView?.fingerPrintImageView != null
&& passwordActivityEducation.checkAndPerformedFingerprintEducation(fingerPrintInfoView?.fingerPrintImageView!!))
;
})
if (!readOnlyEducationPerformed) {
val biometricCanAuthenticate = BiometricManager.from(this).canAuthenticate()
// fingerprintEducationPerformed
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!!)
}
}
}
private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
@@ -509,7 +635,7 @@ class PasswordActivity : StylishActivity(),
changeOpenFileReadIcon(item)
}
R.id.menu_fingerprint_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
fingerPrintViewsManager?.deleteEntryKey()
advancedUnlockedManager?.deleteEntryKey()
}
else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
}
@@ -517,12 +643,6 @@ class PasswordActivity : StylishActivity(),
return super.onOptionsItemSelected(item)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// NOTE: delegate the permission handling to generated method
onRequestPermissionsResult(requestCode, grantResults)
}
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
@@ -535,7 +655,7 @@ class PasswordActivity : StylishActivity(),
}
var keyFileResult = false
mKeyFileHelper?.let {
mOpenFileHelper?.let {
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
if (uri != null) {
@@ -554,64 +674,28 @@ class PasswordActivity : StylishActivity(),
}
}
@NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
fun doNothing() {
}
@OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showRationaleForExternalStorage(request: PermissionRequest) {
AlertDialog.Builder(this)
.setMessage(R.string.permission_external_storage_rationale_read_database)
.setPositiveButton(R.string.allow) { _, _ -> request.proceed() }
.setNegativeButton(R.string.cancel) { _, _ -> request.cancel() }
.show()
}
@OnPermissionDenied(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showDeniedForExternalStorage() {
Toast.makeText(this, R.string.permission_external_storage_denied, Toast.LENGTH_SHORT).show()
finish()
}
@OnNeverAskAgain(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showNeverAskForExternalStorage() {
Toast.makeText(this, R.string.permission_external_storage_never_ask, Toast.LENGTH_SHORT).show()
finish()
}
companion object {
private val TAG = PasswordActivity::class.java.name
const val KEY_DEFAULT_FILENAME = "defaultFileName"
const val KEY_DEFAULT_DATABASE_PATH = "KEY_DEFAULT_DATABASE_PATH"
private const val KEY_FILENAME = "fileName"
private const val KEY_KEYFILE = "keyFile"
private const val VIEW_INTENT = "android.intent.action.VIEW"
private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private fun buildAndLaunchIntent(activity: Activity, fileName: String, keyFile: String,
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
intentBuildLauncher: (Intent) -> Unit) {
val intent = Intent(activity, PasswordActivity::class.java)
intent.putExtra(UriIntentInitTask.KEY_FILENAME, fileName)
intent.putExtra(UriIntentInitTask.KEY_KEYFILE, keyFile)
intent.putExtra(KEY_FILENAME, databaseFile)
if (keyFile != null)
intent.putExtra(KEY_KEYFILE, keyFile)
intentBuildLauncher.invoke(intent)
}
@Throws(FileNotFoundException::class)
private fun verifyFileNameUriFromLaunch(fileName: String) {
if (fileName.isEmpty()) {
throw FileNotFoundException()
}
val uri = UriUtil.parseUriFile(fileName)
val scheme = uri?.scheme
if (scheme != null && scheme.isNotEmpty() && scheme.equals("file", ignoreCase = true)) {
val dbFile = File(uri.path!!)
if (!dbFile.exists()) {
throw FileNotFoundException()
}
}
}
/*
* -------------------------
* Standard Launch
@@ -621,10 +705,9 @@ class PasswordActivity : StylishActivity(),
@Throws(FileNotFoundException::class)
fun launch(
activity: Activity,
fileName: String,
keyFile: String) {
verifyFileNameUriFromLaunch(fileName)
buildAndLaunchIntent(activity, fileName, keyFile) { intent ->
databaseFile: Uri,
keyFile: Uri?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
activity.startActivity(intent)
}
}
@@ -638,12 +721,10 @@ class PasswordActivity : StylishActivity(),
@Throws(FileNotFoundException::class)
fun launchForKeyboardResult(
activity: Activity,
fileName: String,
keyFile: String) {
verifyFileNameUriFromLaunch(fileName)
buildAndLaunchIntent(activity, fileName, keyFile) { intent ->
KeyboardHelper.startActivityForKeyboardSelection(activity, intent)
databaseFile: Uri,
keyFile: Uri?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
EntrySelectionHelper.startActivityForEntrySelection(activity, intent)
}
}
@@ -657,20 +738,18 @@ class PasswordActivity : StylishActivity(),
@Throws(FileNotFoundException::class)
fun launchForAutofillResult(
activity: Activity,
fileName: String,
keyFile: String,
databaseFile: Uri,
keyFile: Uri?,
assistStructure: AssistStructure?) {
verifyFileNameUriFromLaunch(fileName)
if (assistStructure != null) {
buildAndLaunchIntent(activity, fileName, keyFile) { intent ->
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
AutofillHelper.startActivityForAutofillResult(
activity,
intent,
assistStructure)
}
} else {
launch(activity, fileName, keyFile)
launch(activity, databaseFile, keyFile)
}
}
}

View File

@@ -25,16 +25,16 @@ import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import com.google.android.material.textfield.TextInputLayout
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.widget.CompoundButton
import android.widget.TextView
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.KeyFileHelper
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
import com.kunzisoft.keepass.utils.UriUtil
class AssignMasterKeyDialogFragment : DialogFragment() {
@@ -43,15 +43,41 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
private var mKeyFile: Uri? = null
private var rootView: View? = null
private var passwordCheckBox: CompoundButton? = null
private var passView: TextView? = null
private var passConfView: TextView? = null
private var passwordTextInputLayout: TextInputLayout? = null
private var passwordView: TextView? = null
private var passwordRepeatTextInputLayout: TextInputLayout? = null
private var passwordRepeatView: TextView? = null
private var keyFileTextInputLayout: TextInputLayout? = null
private var keyFileCheckBox: CompoundButton? = null
private var keyFileView: TextView? = null
private var mListener: AssignPasswordDialogListener? = null
private var mKeyFileHelper: KeyFileHelper? = null
private var mOpenFileHelper: OpenFileHelper? = null
private val passwordTextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
passwordCheckBox?.isChecked = true
}
}
private val keyFileTextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
keyFileCheckBox?.isChecked = true
}
}
interface AssignPasswordDialogListener {
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
@@ -60,18 +86,25 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
keyFileChecked: Boolean, keyFile: Uri?)
}
override fun onAttach(activity: Context?) {
override fun onAttach(activity: Context) {
super.onAttach(activity)
try {
mListener = activity as AssignPasswordDialogListener?
mListener = activity as AssignPasswordDialogListener
} catch (e: ClassCastException) {
throw ClassCastException(activity?.toString()
throw ClassCastException(activity.toString()
+ " must implement " + AssignPasswordDialogListener::class.java.name)
}
}
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
@@ -80,36 +113,21 @@ 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)
passView = rootView?.findViewById(R.id.pass_password)
passView?.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
passwordCheckBox?.isChecked = true
}
})
passConfView = rootView?.findViewById(R.id.pass_conf_password)
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)
keyFileTextInputLayout = rootView?.findViewById(R.id.keyfile_input_layout)
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
keyFileView = rootView?.findViewById(R.id.pass_keyfile)
keyFileView?.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
keyFileCheckBox?.isChecked = true
}
})
mKeyFileHelper = KeyFileHelper(this)
rootView?.findViewById<View>(R.id.browse_button)?.setOnClickListener { view ->
mKeyFileHelper?.openFileOnClickViewListener?.onClick(view) }
mOpenFileHelper = OpenFileHelper(this)
rootView?.findViewById<View>(R.id.open_database_button)?.setOnClickListener { view ->
mOpenFileHelper?.openFileOnClickViewListener?.onClick(view) }
val dialog = builder.create()
@@ -124,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(
@@ -149,20 +171,35 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
return super.onCreateDialog(savedInstanceState)
}
override fun onResume() {
super.onResume()
// To check checkboxes if a text is present
passwordView?.addTextChangedListener(passwordTextWatcher)
keyFileView?.addTextChangedListener(keyFileTextWatcher)
}
override fun onPause() {
super.onPause()
passwordView?.removeTextChangedListener(passwordTextWatcher)
keyFileView?.removeTextChangedListener(keyFileTextWatcher)
}
private fun verifyPassword(): Boolean {
var error = false
if (passwordCheckBox != null
&& passwordCheckBox!!.isChecked
&& passView != null
&& passConfView != null) {
mMasterPassword = passView!!.text.toString()
val confPassword = passConfView!!.text.toString()
&& passwordView != null
&& passwordRepeatView != null) {
mMasterPassword = passwordView!!.text.toString()
val confPassword = passwordRepeatView!!.text.toString()
// Verify that passwords match
if (mMasterPassword != confPassword) {
error = true
// Passwords do not match
Toast.makeText(context, R.string.error_pass_match, Toast.LENGTH_LONG).show()
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
}
if (mMasterPassword == null || mMasterPassword!!.isEmpty()) {
@@ -170,6 +207,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
showEmptyPasswordConfirmationDialog()
}
}
return error
}
@@ -177,13 +215,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
var error = false
if (keyFileCheckBox != null
&& keyFileCheckBox!!.isChecked) {
val keyFile = UriUtil.parseUriFile(keyFileView?.text?.toString())
mKeyFile = keyFile
// Verify that a keyfile is set
if (keyFile == null || keyFile.toString().isEmpty()) {
UriUtil.parse(keyFileView?.text?.toString())?.let { uri ->
mKeyFile = uri
} ?: run {
error = true
Toast.makeText(context, R.string.error_nokeyfile, Toast.LENGTH_LONG).show()
keyFileTextInputLayout?.error = getString(R.string.error_nokeyfile)
}
}
return error
@@ -201,7 +238,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
this@AssignMasterKeyDialogFragment.dismiss()
}
}
.setNegativeButton(R.string.cancel) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
builder.create().show()
}
}
@@ -216,7 +253,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
keyFileCheckBox!!.isChecked, mKeyFile)
this@AssignMasterKeyDialogFragment.dismiss()
}
.setNegativeButton(R.string.cancel) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
builder.create().show()
}
}
@@ -224,13 +261,26 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
UriUtil.parseUriFile(uri)?.let { pathUri ->
uri?.let { pathUri ->
keyFileCheckBox?.isChecked = true
keyFileView?.text = pathUri.toString()
}
}
}
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

@@ -21,11 +21,12 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.widget.Button
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.Util
import com.kunzisoft.keepass.utils.UriUtil
class BrowserDialogFragment : DialogFragment() {
@@ -35,17 +36,20 @@ 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 market = root.findViewById<Button>(R.id.install_market)
val textDescription = root.findViewById<TextView>(R.id.file_manager_install_description)
textDescription.text = getString(R.string.file_manager_install_description)
val market = root.findViewById<Button>(R.id.file_manager_install_play_store)
market.setOnClickListener {
Util.gotoUrl(context!!, R.string.filemanager_play_store)
UriUtil.gotoUrl(context!!, R.string.file_manager_play_store)
dismiss()
}
val web = root.findViewById<Button>(R.id.install_web)
val web = root.findViewById<Button>(R.id.file_manager_install_f_droid)
web.setOnClickListener {
Util.gotoUrl(context!!, R.string.filemanager_f_droid)
UriUtil.gotoUrl(context!!, R.string.file_manager_f_droid)
dismiss()
}

View File

@@ -1,213 +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.Activity
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.EditText
import android.widget.Spinner
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.FilePickerStylishActivity
import com.kunzisoft.keepass.utils.UriUtil
import com.nononsenseapps.filepicker.FilePickerActivity
import com.nononsenseapps.filepicker.Utils
class CreateFileDialogFragment : DialogFragment(), AdapterView.OnItemSelectedListener {
private val FILE_CODE = 3853
private var folderPathView: EditText? = null
private var fileNameView: EditText? = null
private var positiveButton: Button? = null
private var negativeButton: Button? = null
private var mDefinePathDialogListener: DefinePathDialogListener? = null
private var mDatabaseFileExtension: String? = null
private var mUriPath: Uri? = null
interface DefinePathDialogListener {
fun onDefinePathDialogPositiveClick(pathFile: Uri?): Boolean
fun onDefinePathDialogNegativeClick(pathFile: Uri?): Boolean
}
override fun onAttach(activity: Context?) {
super.onAttach(activity)
try {
mDefinePathDialogListener = activity as DefinePathDialogListener?
} catch (e: ClassCastException) {
throw ClassCastException(activity?.toString()
+ " must implement " + DefinePathDialogListener::class.java.name)
}
}
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_file_creation, null)
builder.setView(rootView)
.setTitle(R.string.create_keepass_file)
// Add action buttons
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(R.string.cancel) { _, _ -> }
// To prevent crash issue #69 https://github.com/Kunzisoft/KeePassDX/issues/69
val actionCopyBarCallback = object : ActionMode.Callback {
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
positiveButton?.isEnabled = false
negativeButton?.isEnabled = false
return true
}
override fun onDestroyActionMode(mode: ActionMode) {
positiveButton?.isEnabled = true
negativeButton?.isEnabled = true
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
return true
}
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
return true
}
}
// Folder selection
val browseView = rootView.findViewById<View>(R.id.browse_button)
folderPathView = rootView.findViewById(R.id.folder_path)
folderPathView?.customSelectionActionModeCallback = actionCopyBarCallback
fileNameView = rootView.findViewById(R.id.filename)
fileNameView?.customSelectionActionModeCallback = actionCopyBarCallback
val defaultPath = Environment.getExternalStorageDirectory().path + getString(R.string.database_file_path_default)
folderPathView?.setText(defaultPath)
browseView.setOnClickListener { _ ->
Intent(context, FilePickerStylishActivity::class.java).apply {
putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
putExtra(FilePickerActivity.EXTRA_START_PATH,
Environment.getExternalStorageDirectory().path)
startActivityForResult(this, FILE_CODE)
}
}
// Init path
mUriPath = null
// Extension
mDatabaseFileExtension = getString(R.string.database_file_extension_default)
val spinner = rootView.findViewById<Spinner>(R.id.file_types)
spinner.onItemSelectedListener = this
// Spinner Drop down elements
val fileTypes = resources.getStringArray(R.array.file_types)
val dataAdapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, fileTypes)
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = dataAdapter
// Or text if only one item https://github.com/Kunzisoft/KeePassDX/issues/105
if (fileTypes.size == 1) {
val params = spinner.layoutParams
spinner.visibility = View.GONE
val extensionTextView = TextView(context)
extensionTextView.text = mDatabaseFileExtension
extensionTextView.layoutParams = params
val parentView = spinner.parent as ViewGroup
parentView.addView(extensionTextView)
}
val dialog = builder.create()
dialog.setOnShowListener { _ ->
positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
positiveButton?.setOnClickListener { _ ->
mDefinePathDialogListener?.let {
if (it.onDefinePathDialogPositiveClick(buildPath()))
dismiss()
}
}
negativeButton?.setOnClickListener { _->
mDefinePathDialogListener?.let {
if (it.onDefinePathDialogNegativeClick(buildPath())) {
dismiss()
}
}
}
}
return dialog
}
return super.onCreateDialog(savedInstanceState)
}
private fun buildPath(): Uri? {
if (folderPathView != null && fileNameView != null && mDatabaseFileExtension != null) {
var path = Uri.Builder().path(folderPathView!!.text.toString())
.appendPath(fileNameView!!.text.toString() + mDatabaseFileExtension!!)
.build()
context?.let { context ->
path = UriUtil.translateUri(context, path)
}
return path
}
return null
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == FILE_CODE && resultCode == Activity.RESULT_OK) {
mUriPath = data?.data
mUriPath?.let {
val file = Utils.getFileForUri(it)
folderPathView?.setText(file.path)
}
}
}
override fun onItemSelected(adapterView: AdapterView<*>, view: View, position: Int, id: Long) {
mDatabaseFileExtension = adapterView.getItemAtPosition(position).toString()
}
override fun onNothingSelected(adapterView: AdapterView<*>) {
// Do nothing
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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.os.Bundle
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
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).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,99 +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.net.Uri
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.view.View
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.fileselect.FileDatabaseModel
import java.text.DateFormat
class FileInformationDialogFragment : DialogFragment() {
private var fileSizeContainerView: View? = null
private var fileModificationContainerView: View? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater
val root = inflater.inflate(R.layout.fragment_file_selection_information, null)
val fileNameView = root.findViewById<TextView>(R.id.file_filename)
val filePathView = root.findViewById<TextView>(R.id.file_path)
fileSizeContainerView = root.findViewById(R.id.file_size_container)
val fileSizeView = root.findViewById<TextView>(R.id.file_size)
fileModificationContainerView = root.findViewById(R.id.file_modification_container)
val fileModificationView = root.findViewById<TextView>(R.id.file_modification)
arguments?.apply {
if (containsKey(FILE_SELECT_BEEN_ARG)) {
(getSerializable(FILE_SELECT_BEEN_ARG) as FileDatabaseModel?)?.let { fileDatabaseModel ->
fileDatabaseModel.fileUri?.let { fileUri ->
filePathView.text = Uri.decode(fileUri.toString())
}
fileNameView.text = fileDatabaseModel.fileName
if (fileDatabaseModel.notFound()) {
hideFileInfo()
} else {
showFileInfo()
fileSizeView.text = fileDatabaseModel.size.toString()
fileModificationView.text = DateFormat.getDateTimeInstance()
.format(fileDatabaseModel.lastModification)
}
} ?: hideFileInfo()
}
}
builder.setView(root)
builder.setPositiveButton(android.R.string.ok) { _, _ -> }
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun showFileInfo() {
fileSizeContainerView?.visibility = View.VISIBLE
fileModificationContainerView?.visibility = View.VISIBLE
}
private fun hideFileInfo() {
fileSizeContainerView?.visibility = View.GONE
fileModificationContainerView?.visibility = View.GONE
}
companion object {
private const val FILE_SELECT_BEEN_ARG = "FILE_SELECT_BEEN_ARG"
fun newInstance(fileDatabaseModel: FileDatabaseModel): FileInformationDialogFragment {
val fileInformationDialogFragment = FileInformationDialogFragment()
val args = Bundle()
args.putSerializable(FILE_SELECT_BEEN_ARG, fileDatabaseModel)
fileInformationDialogFragment.arguments = args
return fileInformationDialogFragment
}
}
}

View File

@@ -17,17 +17,19 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.fingerprint
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.support.annotation.RequiresApi
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
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() {
@@ -41,11 +43,16 @@ class FingerPrintExplanationDialog : DialogFragment() {
val rootView = inflater.inflate(R.layout.fragment_fingerprint_explanation, null)
val fingerprintSettingWayTextView = rootView.findViewById<View>(R.id.fingerprint_setting_way_text)
fingerprintSettingWayTextView.setOnClickListener { startActivity(Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS)) }
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.fingerprint_image))
rootView.findViewById(R.id.biometric_image))
builder.setView(rootView)
.setPositiveButton(android.R.string.ok) { _, _ -> }

View File

@@ -22,14 +22,18 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import com.google.android.material.textfield.TextInputLayout
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.view.View
import android.widget.*
import android.widget.Button
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.SeekBar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.applyFontVisibility
import com.kunzisoft.keepass.view.applyFontVisibility
class GeneratePasswordDialogFragment : DialogFragment() {
@@ -37,6 +41,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
private var root: View? = null
private var lengthTextView: EditText? = null
private var passwordInputLayoutView: TextInputLayout? = null
private var passwordView: EditText? = null
private var uppercaseBox: CompoundButton? = null
@@ -49,12 +54,12 @@ class GeneratePasswordDialogFragment : DialogFragment() {
private var bracketsBox: CompoundButton? = null
private var extendedBox: CompoundButton? = null
override fun onAttach(context: Context?) {
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as GeneratePasswordListener?
mListener = context as GeneratePasswordListener
} catch (e: ClassCastException) {
throw ClassCastException(context?.toString()
throw ClassCastException(context.toString()
+ " must implement " + GeneratePasswordListener::class.java.name)
}
}
@@ -65,6 +70,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
val inflater = activity.layoutInflater
root = inflater.inflate(R.layout.fragment_generate_password, null)
passwordInputLayoutView = root?.findViewById(R.id.password_input_layout)
passwordView = root?.findViewById(R.id.password)
passwordView?.applyFontVisibility()
@@ -108,7 +114,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
dismiss()
}
.setNegativeButton(R.string.cancel) { _, _ ->
.setNegativeButton(android.R.string.cancel) { _, _ ->
val bundle = Bundle()
mListener?.cancelPassword(bundle)
@@ -162,8 +168,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
try {
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
val generator = PasswordGenerator(resources)
password = generator.generatePassword(length,
password = PasswordGenerator(resources).generatePassword(length,
uppercaseBox?.isChecked == true,
lowercaseBox?.isChecked == true,
digitsBox?.isChecked == true,
@@ -174,9 +179,9 @@ class GeneratePasswordDialogFragment : DialogFragment() {
bracketsBox?.isChecked == true,
extendedBox?.isChecked == true)
} catch (e: NumberFormatException) {
Toast.makeText(context, R.string.error_wrong_length, Toast.LENGTH_LONG).show()
passwordInputLayoutView?.error = getString(R.string.error_wrong_length)
} catch (e: IllegalArgumentException) {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
passwordInputLayoutView?.error = e.message
}
return password

View File

@@ -23,9 +23,9 @@ import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.support.design.widget.TextInputLayout
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import com.google.android.material.textfield.TextInputLayout
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
@@ -62,15 +62,15 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
}
}
override fun onAttach(context: Context?) {
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
editGroupListener = context as EditGroupListener?
editGroupListener = context as EditGroupListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context?.toString()
throw ClassCastException(context.toString()
+ " must implement " + GroupEditDialogFragment::class.java.name)
}
@@ -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(),

View File

@@ -24,9 +24,9 @@ import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v4.widget.ImageViewCompat
import android.support.v7.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.core.widget.ImageViewCompat
import androidx.appcompat.app.AlertDialog
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -45,13 +45,13 @@ class IconPickerDialogFragment : DialogFragment() {
private var iconPickerListener: IconPickerListener? = null
private var iconPack: IconPack? = null
override fun onAttach(context: Context?) {
override fun onAttach(context: Context) {
super.onAttach(context)
try {
iconPickerListener = context as IconPickerListener?
iconPickerListener = context as IconPickerListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context!!.toString()
throw ClassCastException(context.toString()
+ " must implement " + IconPickerListener::class.java.name)
}
}
@@ -77,7 +77,7 @@ class IconPickerDialogFragment : DialogFragment() {
dismiss()
}
builder.setNegativeButton(R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog.cancel() }
builder.setNegativeButton(android.R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() }
return builder.create()
}

View File

@@ -24,12 +24,12 @@ import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.os.Bundle
import android.provider.Settings
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
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.Util
import com.kunzisoft.keepass.utils.UriUtil
class KeyboardExplanationDialogFragment : DialogFragment() {
@@ -40,16 +40,14 @@ class KeyboardExplanationDialogFragment : DialogFragment() {
val rootView = inflater.inflate(R.layout.fragment_keyboard_explanation, null)
rootView.findViewById<View>(R.id.keyboards_activate_setting_path1_text)
.setOnClickListener { launchActivateKeyboardSetting() }
rootView.findViewById<View>(R.id.keyboards_activate_setting_path2_text)
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 { Util.gotoUrl(context!!, R.string.keyboard_switcher_play_store) }
containerKeyboardSwitcher.setOnClickListener { UriUtil.gotoUrl(context!!, R.string.keyboard_switcher_play_store) }
} else {
containerKeyboardSwitcher.setOnClickListener { Util.gotoUrl(context!!, R.string.keyboard_switcher_f_droid) }
containerKeyboardSwitcher.setOnClickListener { UriUtil.gotoUrl(context!!, R.string.keyboard_switcher_f_droid) }
}
builder.setView(rootView)

View File

@@ -23,7 +23,7 @@ import android.app.AlertDialog
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.support.v4.app.DialogFragment
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
class PasswordEncodingDialogFragment : DialogFragment() {
@@ -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

@@ -20,17 +20,15 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.ActivityNotFoundException
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.text.Html
import android.text.SpannableStringBuilder
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.text.HtmlCompat
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.Util
import com.kunzisoft.keepass.utils.UriUtil
/**
* Custom Dialog that asks the user to download the pro version or make a donation.
@@ -44,25 +42,16 @@ class ProFeatureDialogFragment : DialogFragment() {
val stringBuilder = SpannableStringBuilder()
if (BuildConfig.CLOSED_STORE) {
// TODO HtmlCompat with androidX
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_ad_free))).append("\n\n")
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_buy_pro)))
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.download) { _, _ ->
try {
Util.gotoUrl(context!!, R.string.app_pro_url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
}
UriUtil.gotoUrl(context!!, R.string.app_pro_url)
}
} else {
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_feature_generosity))).append("\n\n")
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_donation)))
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n")
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.contribute) { _, _ ->
try {
Util.gotoUrl(context!!, R.string.contribution_url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
}
UriUtil.gotoUrl(context!!, R.string.contribution_url)
}
}
builder.setMessage(stringBuilder)

View File

@@ -19,33 +19,38 @@
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.AlertDialog
import android.content.Context
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(context: Context) : AlertDialog(context) {
class ReadOnlyDialog : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle) {
val ctx = context
var warning = ctx.getString(R.string.read_only_warning)
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" + context.getString(R.string.read_only_kitkat_warning)
warning = warning + "\n\n" + getString(R.string.read_only_kitkat_warning)
}
setMessage(warning)
builder.setMessage(warning)
setButton(BUTTON_POSITIVE, ctx.getText(android.R.string.ok)) { _, _ -> dismiss() }
setButton(BUTTON_NEGATIVE, ctx.getText(R.string.beta_dontask)) { _, _ ->
val prefs = PreferenceManager.getDefaultSharedPreferences(ctx)
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(ctx.getString(R.string.show_read_only_warning), false)
edit.putBoolean(getString(R.string.show_read_only_warning), false)
edit.apply()
dismiss()
}
super.onCreate(savedInstanceState)
// Create the AlertDialog object and return it
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
}

View File

@@ -22,9 +22,9 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.support.annotation.IdRes
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import androidx.annotation.IdRes
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.view.View
import android.widget.CompoundButton
import android.widget.RadioGroup
@@ -35,33 +35,30 @@ class SortDialogFragment : DialogFragment() {
private var mListener: SortSelectionListener? = null
private var mSortNodeEnum: SortNodeEnum? = null
private var mSortNodeEnum: SortNodeEnum = SortNodeEnum.DB
@IdRes
private var mCheckedId: Int = 0
private var mGroupsBefore: Boolean = false
private var mAscending: Boolean = false
private var mRecycleBinBottom: Boolean = false
private var mGroupsBefore: Boolean = true
private var mAscending: Boolean = true
private var mRecycleBinBottom: Boolean = true
override fun onAttach(context: Context?) {
private var recycleBinBottomView: CompoundButton? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as SortSelectionListener?
mListener = context as SortSelectionListener
} catch (e: ClassCastException) {
throw ClassCastException(context!!.toString()
throw ClassCastException(context.toString()
+ " must implement " + SortSelectionListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
mSortNodeEnum = SortNodeEnum.TITLE
mAscending = true
mGroupsBefore = true
var recycleBinAllowed = false
mRecycleBinBottom = true
arguments?.apply {
if (containsKey(SORT_NODE_ENUM_BUNDLE_KEY))
@@ -78,15 +75,15 @@ class SortDialogFragment : DialogFragment() {
}
}
mCheckedId = retrieveViewFromEnum(mSortNodeEnum!!)
mCheckedId = retrieveViewFromEnum(mSortNodeEnum)
val rootView = activity.layoutInflater.inflate(R.layout.fragment_sort_selection, null)
builder.setTitle(R.string.sort_menu)
builder.setView(rootView)
// Add action buttons
.setPositiveButton(android.R.string.ok
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum!!, mAscending, mGroupsBefore, mRecycleBinBottom) }
.setNegativeButton(R.string.cancel) { _, _ -> }
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
// Check if is ascending or descending
@@ -98,25 +95,35 @@ class SortDialogFragment : DialogFragment() {
groupsBeforeView.isChecked = mGroupsBefore
groupsBeforeView.setOnCheckedChangeListener { _, isChecked -> mGroupsBefore = isChecked }
val recycleBinBottomView = rootView.findViewById<CompoundButton>(R.id.sort_selection_recycle_bin_bottom)
recycleBinBottomView = rootView.findViewById(R.id.sort_selection_recycle_bin_bottom)
if (!recycleBinAllowed) {
recycleBinBottomView.visibility = View.GONE
recycleBinBottomView?.visibility = View.GONE
} else {
// Check if recycle bin at the bottom
recycleBinBottomView.isChecked = mRecycleBinBottom
recycleBinBottomView.setOnCheckedChangeListener { _, isChecked -> mRecycleBinBottom = isChecked }
recycleBinBottomView?.isChecked = mRecycleBinBottom
recycleBinBottomView?.setOnCheckedChangeListener { _, isChecked -> mRecycleBinBottom = isChecked }
disableRecycleBinBottomOptionIfNaturalOrder()
}
val sortSelectionRadioGroupView = rootView.findViewById<RadioGroup>(R.id.sort_selection_radio_group)
// Check value by default
sortSelectionRadioGroupView.check(mCheckedId)
sortSelectionRadioGroupView.setOnCheckedChangeListener { _, checkedId -> mSortNodeEnum = retrieveSortEnumFromViewId(checkedId) }
sortSelectionRadioGroupView.setOnCheckedChangeListener { _, checkedId ->
mSortNodeEnum = retrieveSortEnumFromViewId(checkedId)
disableRecycleBinBottomOptionIfNaturalOrder()
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun disableRecycleBinBottomOptionIfNaturalOrder() {
// Disable recycle bin if natural order
recycleBinBottomView?.isEnabled = mSortNodeEnum != SortNodeEnum.DB
}
@IdRes
private fun retrieveViewFromEnum(sortNodeEnum: SortNodeEnum): Int {
return when (sortNodeEnum) {

View File

@@ -22,12 +22,13 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.os.Build
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.text.Html
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.widget.TextView
import androidx.core.text.HtmlCompat
import com.kunzisoft.keepass.R
class UnavailableFeatureDialogFragment : DialogFragment() {
@@ -53,7 +54,7 @@ class UnavailableFeatureDialogFragment : DialogFragment() {
androidNameFromApiNumber(Build.VERSION.SDK_INT, Build.VERSION.RELEASE),
androidNameFromApiNumber(minVersionRequired)))
message.append("\n\n")
.append(Html.fromHtml("<a href=\"https://source.android.com/setup/build-numbers\">CodeNames</a>"))
.append(HtmlCompat.fromHtml("<a href=\"https://source.android.com/setup/build-numbers\">CodeNames</a>", HtmlCompat.FROM_HTML_MODE_LEGACY))
} else
message.append(getString(R.string.unavailable_feature_hardware))

View File

@@ -20,17 +20,14 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.ActivityNotFoundException
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.text.Html
import android.text.SpannableStringBuilder
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.text.HtmlCompat
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.Util
import com.kunzisoft.keepass.utils.UriUtil
/**
* Custom Dialog that asks the user to download the pro version or make a donation.
@@ -45,34 +42,26 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
val stringBuilder = SpannableStringBuilder()
if (BuildConfig.CLOSED_STORE) {
if (BuildConfig.FULL_VERSION) {
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature_thanks))).append("\n\n")
.append(Html.fromHtml(getString(R.string.html_rose))).append("\n\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_work_hard))).append("\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_upgrade))).append(" ")
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_work_hard), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
} else {
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature))).append("\n\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_buy_pro))).append("\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_buy_pro), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.download) { _, _ ->
try {
Util.gotoUrl(context!!, R.string.app_pro_url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
}
UriUtil.gotoUrl(context!!, R.string.app_pro_url)
}
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
}
} else {
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature))).append("\n\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_contibute))).append(" ")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.contribute) { _, _ ->
try {
Util.gotoUrl(context!!, R.string.contribution_url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
}
UriUtil.gotoUrl(context!!, R.string.contribution_url)
}
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
}

View File

@@ -1,84 +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.helpers;
import android.content.Intent;
import android.net.Uri;
import java.lang.reflect.Method;
public class ClipDataCompat {
private static Method getClipDataFromIntent;
private static Method getDescription;
private static Method getItemCount;
private static Method getLabel;
private static Method getItemAt;
private static Method getUri;
private static boolean initSucceded;
static {
try {
Class clipData = Class.forName("android.content.ClipData");
getDescription = clipData.getMethod("getDescription", (Class[])null);
getItemCount = clipData.getMethod("getItemCount", (Class[])null);
getItemAt = clipData.getMethod("getItemAt", new Class[]{int.class});
Class clipDescription = Class.forName("android.content.ClipDescription");
getLabel = clipDescription.getMethod("getLabel", (Class[])null);
Class clipDataItem = Class.forName("android.content.ClipData$Item");
getUri = clipDataItem.getMethod("getUri", (Class[])null);
getClipDataFromIntent = Intent.class.getMethod("getClipData", (Class[])null);
initSucceded = true;
} catch (Exception e) {
initSucceded = false;
}
}
public static Uri getUriFromIntent(Intent i, String key) {
if (initSucceded) {
try {
Object clip = getClipDataFromIntent.invoke(i);
if (clip != null) {
Object clipDescription = getDescription.invoke(clip);
CharSequence label = (CharSequence)getLabel.invoke(clipDescription);
if (label.equals(key)) {
int itemCount = (int) getItemCount.invoke(clip);
if (itemCount == 1) {
Object clipItem = getItemAt.invoke(clip,0);
if (clipItem != null) {
return (Uri)getUri.invoke(clipItem);
}
}
}
}
return null;
} catch (Exception e) {
// Fall through below to backup method if reflection fails
}
}
return i.getParcelableExtra(key);
}
}

View File

@@ -1,6 +1,7 @@
package com.kunzisoft.keepass.activities.helpers
import android.app.assist.AssistStructure
import android.content.Context
import android.content.Intent
import android.os.Build
import com.kunzisoft.keepass.autofill.AutofillHelper
@@ -10,6 +11,12 @@ 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(context: Context, intent: Intent) {
addEntrySelectionModeExtraInIntent(intent)
// only to avoid visible flickering when redirecting
context.startActivity(intent)
}
fun addEntrySelectionModeExtraInIntent(intent: Intent) {
intent.putExtra(EXTRA_ENTRY_SELECTION_MODE, true)
}

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,21 +27,20 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.support.v4.app.Fragment
import android.support.v4.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.fileselect.StorageAF
import com.kunzisoft.keepass.utils.UriUtil
class KeyFileHelper {
class OpenFileHelper {
private var activity: Activity? = null
private var fragment: Fragment? = null
val openFileOnClickViewListener: OpenFileOnClickViewListener
get() = OpenFileOnClickViewListener(null)
get() = OpenFileOnClickViewListener()
constructor(context: Activity) {
this.activity = context
@@ -52,69 +52,55 @@ class KeyFileHelper {
this.fragment = context
}
inner class OpenFileOnClickViewListener(private val dataUri: (() -> Uri)?) : View.OnClickListener {
inner class OpenFileOnClickViewListener : View.OnClickListener {
override fun onClick(v: View) {
try {
if (activity != null && StorageAF.useStorageFramework(activity!!)) {
try {
openActivityWithActionOpenDocument()
} else {
} catch(e: Exception) {
openActivityWithActionGetContent()
}
} 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(StorageAF.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
val intentOpenDocument = Intent(APP_ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
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
}
if (fragment != null)
fragment?.startActivityForResult(i, OPEN_DOC)
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
else
activity?.startActivityForResult(i, OPEN_DOC)
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
}
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 = "*/*"
}
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
@@ -158,7 +144,7 @@ class KeyFileHelper {
val browserDialogFragment = BrowserDialogFragment()
if (fragment != null && fragment!!.fragmentManager != null)
browserDialogFragment.show(fragment!!.fragmentManager!!, "browserDialog")
else if (activity!!.fragmentManager != null)
else
browserDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
} catch (e: Exception) {
Log.e(TAG, "Can't open BrowserDialog", e)
@@ -182,7 +168,7 @@ class KeyFileHelper {
val filename = data?.dataString
var keyUri: Uri? = null
if (filename != null) {
keyUri = UriUtil.parseUriFile(filename)
keyUri = UriUtil.parse(filename)
}
keyFileCallback?.invoke(keyUri)
}
@@ -191,24 +177,19 @@ class KeyFileHelper {
GET_CONTENT, OPEN_DOC -> {
if (resultCode == RESULT_OK) {
if (data != null) {
var uri = data.data
val uri = data.data
if (uri != null) {
if (StorageAF.useStorageFramework(activity!!)) {
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)
}
}
@@ -221,7 +202,13 @@ class KeyFileHelper {
companion object {
private const val TAG = "KeyFileHelper"
private const val TAG = "OpenFileHelper"
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,88 +0,0 @@
package com.kunzisoft.keepass.activities.helpers
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.AsyncTask
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory
import com.kunzisoft.keepass.utils.UriUtil
import java.io.File
import java.lang.ref.WeakReference
class UriIntentInitTask(private val weakContext: WeakReference<Context>,
private val uriIntentInitTaskCallback: UriIntentInitTaskCallback,
private val isKeyFileNeeded: Boolean)
: AsyncTask<Intent, Void, Int>() {
private var databaseUri: Uri? = null
private var keyFileUri: Uri? = null
override fun doInBackground(vararg args: Intent): Int? {
val intent = args[0]
val action = intent.action
// If is a view intent
if (action != null && action == VIEW_INTENT) {
val incoming = intent.data
databaseUri = incoming
keyFileUri = ClipDataCompat.getUriFromIntent(intent, KEY_KEYFILE)
if (incoming == null) {
return R.string.error_can_not_handle_uri
} else if (incoming.scheme == "file") {
val fileName = incoming.path
if (fileName?.isNotEmpty() == true) {
// No file name
return R.string.file_not_found
}
val dbFile = File(fileName)
if (!dbFile.exists()) {
// File does not exist
return R.string.file_not_found
}
if (keyFileUri == null) {
keyFileUri = getKeyFileUri(databaseUri)
}
} else if (incoming.scheme == "content") {
if (keyFileUri == null) {
keyFileUri = getKeyFileUri(databaseUri)
}
} else {
return R.string.error_can_not_handle_uri
}
} else {
databaseUri = UriUtil.parseUriFile(intent.getStringExtra(KEY_FILENAME))
keyFileUri = UriUtil.parseUriFile(intent.getStringExtra(KEY_KEYFILE))
if (keyFileUri == null || keyFileUri!!.toString().isEmpty()) {
keyFileUri = getKeyFileUri(databaseUri)
}
}
return null
}
public override fun onPostExecute(result: Int?) {
uriIntentInitTaskCallback.onPostInitTask(databaseUri, keyFileUri, result)
}
private fun getKeyFileUri(databaseUri: Uri?): Uri? {
return if (isKeyFileNeeded) {
FileDatabaseHistory.getInstance(weakContext).getKeyFileUriByDatabaseUri(databaseUri!!)
} else {
null
}
}
companion object {
const val KEY_FILENAME = "fileName"
const val KEY_KEYFILE = "keyFile"
private const val VIEW_INTENT = "android.intent.action.VIEW"
}
}

View File

@@ -1,26 +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.helpers
import android.net.Uri
interface UriIntentInitTaskCallback {
fun onPostInitTask(databaseFileUri: Uri?, keyFileUri: Uri?, errorStringId: Int?)
}

View File

@@ -51,46 +51,46 @@ abstract class LockingActivity : StylishActivity() {
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
}
protected var timeoutEnable: Boolean = true
protected var mTimeoutEnable: Boolean = true
private var lockReceiver: LockReceiver? = null
private var exitLock: Boolean = false
private var mLockReceiver: LockReceiver? = null
private var mExitLock: Boolean = false
// Force readOnly if Entry Selection mode
protected var readOnly: Boolean = false
protected var mReadOnly: Boolean = false
get() {
return field || selectionMode
return field || mSelectionMode
}
protected var selectionMode: Boolean = false
protected var mSelectionMode: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null
&& savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY)) {
timeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
mTimeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
} else {
if (intent != null)
timeoutEnable = intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT)
mTimeoutEnable = intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT)
}
if (timeoutEnable) {
lockReceiver = LockReceiver()
if (mTimeoutEnable) {
mLockReceiver = LockReceiver()
val intentFilter = IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(LOCK_ACTION)
}
registerReceiver(lockReceiver, intentFilter)
registerReceiver(mLockReceiver, intentFilter)
}
exitLock = false
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState, intent)
mExitLock = false
mReadOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState, intent)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_EXIT_LOCK) {
exitLock = true
mExitLock = true
if (Database.getInstance().loaded) {
lockAndExit()
}
@@ -101,9 +101,9 @@ abstract class LockingActivity : StylishActivity() {
super.onResume()
// To refresh when back to normal workflow from selection workflow
selectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
mSelectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
if (timeoutEnable) {
if (mTimeoutEnable) {
// End activity if database not loaded
if (!Database.getInstance().loaded) {
finish()
@@ -115,7 +115,7 @@ abstract class LockingActivity : StylishActivity() {
// If the time is out -> close the Activity
TimeoutHelper.checkTimeAndLockIfTimeout(this)
// If onCreate already record time
if (!exitLock)
if (!mExitLock)
TimeoutHelper.recordTime(this)
}
@@ -123,15 +123,15 @@ abstract class LockingActivity : StylishActivity() {
}
override fun onSaveInstanceState(outState: Bundle) {
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
outState.putBoolean(TIMEOUT_ENABLE_KEY, timeoutEnable)
ReadOnlyHelper.onSaveInstanceState(outState, mReadOnly)
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
super.onSaveInstanceState(outState)
}
override fun onPause() {
super.onPause()
if (timeoutEnable) {
if (mTimeoutEnable) {
// If the time is out during our navigation in activity -> close the Activity
TimeoutHelper.checkTimeAndLockIfTimeout(this)
}
@@ -139,8 +139,8 @@ abstract class LockingActivity : StylishActivity() {
override fun onDestroy() {
super.onDestroy()
if (lockReceiver != null)
unregisterReceiver(lockReceiver)
if (mLockReceiver != null)
unregisterReceiver(mLockReceiver)
}
inner class LockReceiver : BroadcastReceiver() {
@@ -184,7 +184,7 @@ abstract class LockingActivity : StylishActivity() {
}
override fun onBackPressed() {
if (timeoutEnable) {
if (mTimeoutEnable) {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
super.onBackPressed()
}

View File

@@ -1,49 +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.stylish
import android.os.Bundle
import android.support.annotation.StyleRes
import android.util.Log
import com.nononsenseapps.filepicker.FilePickerActivity
/**
* FilePickerActivity class with a style compatibility
*/
class FilePickerStylishActivity : FilePickerActivity() {
@StyleRes
private var themeId: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
this.themeId = Stylish.getFilePickerThemeId(this)
setTheme(themeId)
super.onCreate(savedInstanceState)
}
override fun onResume() {
super.onResume()
if (Stylish.getFilePickerThemeId(this) != this.themeId) {
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
this.recreate()
}
}
}

View File

@@ -20,8 +20,8 @@
package com.kunzisoft.keepass.activities.stylish
import android.content.Context
import android.support.annotation.StyleRes
import android.support.v7.preference.PreferenceManager
import androidx.annotation.StyleRes
import androidx.preference.PreferenceManager
import android.util.Log
import com.kunzisoft.keepass.R
@@ -68,15 +68,4 @@ object Stylish {
else -> R.style.KeepassDXStyle_Light
}
}
@StyleRes
fun getFilePickerThemeId(context: Context): Int {
return when {
themeString.equals(context.getString(R.string.list_style_name_dark)) -> R.style.KeepassDXStyle_FilePickerStyle_Dark
themeString.equals(context.getString(R.string.list_style_name_blue)) -> R.style.KeepassDXStyle_FilePickerStyle_Blue
themeString.equals(context.getString(R.string.list_style_name_red)) -> R.style.KeepassDXStyle_FilePickerStyle_Red
themeString.equals(context.getString(R.string.list_style_name_purple)) -> R.style.KeepassDXStyle_FilePickerStyle_Purple
else -> R.style.KeepassDXStyle_FilePickerStyle
}
}
}

View File

@@ -20,8 +20,8 @@
package com.kunzisoft.keepass.activities.stylish
import android.os.Bundle
import android.support.annotation.StyleRes
import android.support.v7.app.AppCompatActivity
import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity
import android.util.Log
abstract class StylishActivity : AppCompatActivity() {

View File

@@ -23,9 +23,9 @@ import android.content.Context
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.support.annotation.StyleRes
import android.support.v4.app.Fragment
import android.support.v7.view.ContextThemeWrapper
import androidx.annotation.StyleRes
import androidx.fragment.app.Fragment
import androidx.appcompat.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -36,11 +36,9 @@ abstract class StylishFragment : Fragment() {
protected var themeId: Int = 0
protected var contextThemed: Context? = null
override fun onAttach(context: Context?) {
override fun onAttach(context: Context) {
super.onAttach(context)
if (context != null) {
this.themeId = Stylish.getThemeId(context)
}
contextThemed = ContextThemeWrapper(context, themeId)
}

View File

@@ -0,0 +1,50 @@
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.EntryVersioned
class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
var entryHistoryList: MutableList<EntryVersioned> = ArrayList()
var onItemClickListener: ((item: EntryVersioned, 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,7 +1,7 @@
package com.kunzisoft.keepass.magikeyboard.adapter
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.support.v7.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

View File

@@ -20,24 +20,31 @@
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.net.Uri
import android.support.annotation.ColorInt
import android.support.v7.widget.RecyclerView
import androidx.annotation.ColorInt
import androidx.recyclerview.widget.RecyclerView
import android.util.TypedValue
import android.view.*
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import android.widget.ViewSwitcher
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.fileselect.FileDatabaseModel
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryEntity
import com.kunzisoft.keepass.utils.FileDatabaseInfo
import com.kunzisoft.keepass.utils.UriUtil
class FileDatabaseHistoryAdapter(private val context: Context, private val listFiles: List<String>)
class FileDatabaseHistoryAdapter(private val context: Context)
: RecyclerView.Adapter<FileDatabaseHistoryAdapter.FileDatabaseHistoryViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var fileItemOpenListener: FileItemOpenListener? = null
private var fileSelectClearListener: FileSelectClearListener? = null
private var fileInformationShowListener: FileInformationShowListener? = null
private var fileItemOpenListener: ((FileDatabaseHistoryEntity)->Unit)? = null
private var fileSelectClearListener: ((FileDatabaseHistoryEntity)->Boolean)? = null
private var saveAliasListener: ((FileDatabaseHistoryEntity)->Unit)? = null
private val listDatabaseFiles = ArrayList<FileDatabaseHistoryEntity>()
private var mExpandedPosition = -1
private var mPreviousExpandedPosition = -1
@ColorInt
private val defaultColor: Int
@@ -45,7 +52,6 @@ class FileDatabaseHistoryAdapter(private val context: Context, private val listF
private val warningColor: Int
init {
val typedValue = TypedValue()
val theme = context.theme
theme.resolveAttribute(R.attr.colorAccent, typedValue, true)
@@ -60,91 +66,120 @@ class FileDatabaseHistoryAdapter(private val context: Context, private val listF
}
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
val fileDatabaseModel = FileDatabaseModel(context, listFiles[position])
// Context menu creation
holder.fileContainer.setOnCreateContextMenuListener(ContextMenuBuilder(fileDatabaseModel))
// Get info from position
val fileHistoryEntity = listDatabaseFiles[position]
val fileDatabaseInfo = FileDatabaseInfo(context, fileHistoryEntity.databaseUri)
// Click item to open file
if (fileItemOpenListener != null)
holder.fileContainer.setOnClickListener(FileItemClickListener(position))
// Assign file name
if (PreferencesUtil.isFullFilePathEnable(context))
holder.fileName.text = Uri.decode(fileDatabaseModel.fileUri.toString())
else
holder.fileName.text = fileDatabaseModel.fileName
holder.fileName.textSize = PreferencesUtil.getListTextSize(context)
holder.fileContainer.setOnClickListener {
fileItemOpenListener?.invoke(fileHistoryEntity)
}
// File alias
holder.fileAlias.text = fileDatabaseInfo.retrieveDatabaseAlias(fileHistoryEntity.databaseAlias)
// File path
holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString())
holder.filePreciseInfoContainer.visibility = if (fileDatabaseInfo.found()) {
// Modification
holder.fileModification.text = fileDatabaseInfo.getModificationString()
// Size
holder.fileSize.text = fileDatabaseInfo.getSizeString()
View.VISIBLE
} else
View.GONE
// Click on information
if (fileInformationShowListener != null)
holder.fileInformation.setOnClickListener(FileInformationClickListener(fileDatabaseModel))
val isExpanded = position == mExpandedPosition
//This line hides or shows the layout in question
holder.fileExpandContainer.visibility = if (isExpanded) View.VISIBLE else View.GONE
// Save alias modification
holder.fileAliasCloseButton.setOnClickListener {
// Change the alias
fileHistoryEntity.databaseAlias = holder.fileAliasEdit.text.toString()
saveAliasListener?.invoke(fileHistoryEntity)
// Finish save mode
holder.fileMainSwitcher.showPrevious()
// Refresh current position to show alias
notifyItemChanged(position)
}
// Open alias modification
holder.fileModifyButton.setOnClickListener {
holder.fileAliasEdit.setText(holder.fileAlias.text)
holder.fileMainSwitcher.showNext()
}
holder.fileDeleteButton.setOnClickListener {
fileSelectClearListener?.invoke(fileHistoryEntity)
}
if (isExpanded) {
mPreviousExpandedPosition = position
}
holder.fileInformation.setOnClickListener {
mExpandedPosition = if (isExpanded) -1 else position
// Notify change
if (mPreviousExpandedPosition < itemCount)
notifyItemChanged(mPreviousExpandedPosition)
notifyItemChanged(position)
}
// Refresh View / Close alias modification if not contains fileAlias
if (holder.fileMainSwitcher.currentView.findViewById<View>(R.id.file_alias)
!= holder.fileAlias)
holder.fileMainSwitcher.showPrevious()
}
override fun getItemCount(): Int {
return listFiles.size
return listDatabaseFiles.size
}
fun setOnItemClickListener(fileItemOpenListener: FileItemOpenListener) {
this.fileItemOpenListener = fileItemOpenListener
fun addDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<FileDatabaseHistoryEntity>) {
listDatabaseFiles.clear()
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
}
fun setFileSelectClearListener(fileSelectClearListener: FileSelectClearListener) {
this.fileSelectClearListener = fileSelectClearListener
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: FileDatabaseHistoryEntity) {
listDatabaseFiles.remove(fileDatabaseHistoryToDelete)
}
fun setFileInformationShowListener(fileInformationShowListener: FileInformationShowListener) {
this.fileInformationShowListener = fileInformationShowListener
fun setOnFileDatabaseHistoryOpenListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
this.fileItemOpenListener = listener
}
interface FileItemOpenListener {
fun onFileItemOpenListener(itemPosition: Int)
fun setOnFileDatabaseHistoryDeleteListener(listener : ((FileDatabaseHistoryEntity)->Boolean)?) {
this.fileSelectClearListener = listener
}
interface FileSelectClearListener {
fun onFileSelectClearListener(fileDatabaseModel: FileDatabaseModel): Boolean
}
interface FileInformationShowListener {
fun onClickFileInformation(fileDatabaseModel: FileDatabaseModel)
}
private inner class FileItemClickListener(private val position: Int) : View.OnClickListener {
override fun onClick(v: View) {
fileItemOpenListener?.onFileItemOpenListener(position)
}
}
private inner class FileInformationClickListener(private val fileDatabaseModel: FileDatabaseModel) : View.OnClickListener {
override fun onClick(view: View) {
fileInformationShowListener?.onClickFileInformation(fileDatabaseModel)
}
}
private inner class ContextMenuBuilder(private val fileDatabaseModel: FileDatabaseModel) : View.OnCreateContextMenuListener {
private val mOnMyActionClickListener = MenuItem.OnMenuItemClickListener { item ->
if (fileSelectClearListener == null)
return@OnMenuItemClickListener false
when (item.itemId) {
MENU_CLEAR -> fileSelectClearListener!!.onFileSelectClearListener(fileDatabaseModel)
else -> false
}
}
override fun onCreateContextMenu(contextMenu: ContextMenu?, view: View?, contextMenuInfo: ContextMenu.ContextMenuInfo?) {
contextMenu?.add(Menu.NONE, MENU_CLEAR, Menu.NONE, R.string.remove_from_filelist)
?.setOnMenuItemClickListener(mOnMyActionClickListener)
}
fun setOnSaveAliasListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
this.saveAliasListener = listener
}
inner class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var fileContainer: View = itemView.findViewById(R.id.file_container)
var fileName: TextView = itemView.findViewById(R.id.file_filename)
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)
var fileAlias: TextView = itemView.findViewById(R.id.file_alias)
var fileInformation: ImageView = itemView.findViewById(R.id.file_information)
}
companion object {
var fileMainSwitcher: ViewSwitcher = itemView.findViewById(R.id.file_main_switcher)
var fileAliasEdit: EditText = itemView.findViewById(R.id.file_alias_edit)
var fileAliasCloseButton: ImageView = itemView.findViewById(R.id.file_alias_save)
private const val MENU_CLEAR = 1
var fileExpandContainer: ViewGroup = itemView.findViewById(R.id.file_expand_container)
var fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
var filePath: TextView = itemView.findViewById(R.id.file_path)
var filePreciseInfoContainer: ViewGroup = itemView.findViewById(R.id.file_precise_info_container)
var fileModification: TextView = itemView.findViewById(R.id.file_modification)
var fileSize: TextView = itemView.findViewById(R.id.file_size)
}
}

View File

@@ -21,44 +21,52 @@ package com.kunzisoft.keepass.adapters
import android.content.Context
import android.graphics.Color
import android.support.v7.util.SortedList
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.util.SortedListAdapterCallback
import android.graphics.Paint
import android.util.Log
import android.view.*
import android.util.TypedValue
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.app.App
import com.kunzisoft.keepass.database.SortNodeEnum
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil
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 inflater: LayoutInflater = LayoutInflater.from(context)
private var textSize: Float = 0.toFloat()
private var subtextSize: Float = 0.toFloat()
private var iconSize: Float = 0.toFloat()
private var listSort: SortNodeEnum? = null
private var groupsBeforeSort: Boolean = false
private var ascendingSort: Boolean = false
private var showUserNames: Boolean = false
private var calculateViewTypeTextSize = Array(2) { true} // number of view type
private var textSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
private var prefTextSize: Float = 0F
private var subtextSize: Float = 0F
private var infoTextSize: Float = 0F
private var numberChildrenTextSize: Float = 0F
private var iconSize: Float = 0F
private var listSort: SortNodeEnum = SortNodeEnum.DB
private var ascendingSort: Boolean = true
private var groupsBeforeSort: Boolean = true
private var recycleBinBottomSort: Boolean = true
private var showUserNames: Boolean = true
private var showNumberEntries: Boolean = true
private var actionNodesList = LinkedList<NodeVersioned>()
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
@@ -74,17 +82,16 @@ 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 {
return listSort?.getNodeComparator(ascendingSort, groupsBeforeSort)?.compare(item1, item2) ?: 0
return listSort.getNodeComparator(ascendingSort, groupsBeforeSort, recycleBinBottomSort).compare(item1, item2)
}
override fun areContentsTheSame(oldItem: NodeVersioned, newItem: NodeVersioned): Boolean {
return oldItem.title == newItem.title && oldItem.icon == newItem.icon
return oldItem.type == newItem.type
&& oldItem.title == newItem.title
&& oldItem.icon == newItem.icon
}
override fun areItemsTheSame(item1: NodeVersioned, item2: NodeVersioned): Boolean {
@@ -105,29 +112,22 @@ 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() {
val textSizeDefault = java.lang.Float.parseFloat(context.getString(R.string.list_size_default))
this.textSize = PreferencesUtil.getListTextSize(context)
this.subtextSize = context.resources.getInteger(R.integer.list_small_size_default) * textSize / textSizeDefault
// Retrieve the icon size
val iconDefaultSize = context.resources.getDimension(R.dimen.list_icon_size_default)
this.iconSize = iconDefaultSize * textSize / textSizeDefault
this.prefTextSize = PreferencesUtil.getListTextSize(context)
this.infoTextSize = context.resources.getDimension(R.dimen.list_medium_size_default) * prefTextSize
this.subtextSize = context.resources.getDimension(R.dimen.list_small_size_default) * prefTextSize
this.numberChildrenTextSize = context.resources.getDimension(R.dimen.list_tiny_size_default) * prefTextSize
this.iconSize = context.resources.getDimension(R.dimen.list_icon_size_default) * prefTextSize
this.listSort = PreferencesUtil.getListSort(context)
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context)
this.ascendingSort = PreferencesUtil.getAscendingSort(context)
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context)
this.recycleBinBottomSort = PreferencesUtil.getRecycleBinBottomSort(context)
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
this.showNumberEntries = PreferencesUtil.showNumberEntries(context)
// Reinit textSize for all view type
calculateViewTypeTextSize.forEachIndexed { index, _ -> calculateViewTypeTextSize[index] = true }
}
/**
@@ -136,14 +136,17 @@ class NodeAdapter
fun rebuildList(group: GroupVersioned) {
this.nodeSortedList.clear()
assignPreferences()
// TODO verify sort
try {
this.nodeSortedList.addAll(group.getChildrenWithoutMetaStream())
this.nodeSortedList.addAll(group.getChildren())
} 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 {
return nodeSortedList.indexOf(node) != SortedList.INVALID_POSITION
}
/**
@@ -154,6 +157,14 @@ class NodeAdapter
nodeSortedList.add(node)
}
/**
* Add nodes to the list
* @param nodes Nodes to add
*/
fun addNodes(nodes: List<NodeVersioned>) {
nodeSortedList.addAll(nodes)
}
/**
* Remove a node in the list
* @param node Node to delete
@@ -162,6 +173,37 @@ class NodeAdapter
nodeSortedList.remove(node)
}
/**
* Remove nodes in the list
* @param nodes Nodes to delete
*/
fun removeNodes(nodes: List<NodeVersioned>) {
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)
}
}
/**
* Update a node in the list
* @param oldNode Node before the update
@@ -174,6 +216,40 @@ class NodeAdapter
nodeSortedList.endBatchedUpdates()
}
/**
* Update nodes in the list
* @param oldNodes Nodes before the update
* @param newNodes Node after the update
*/
fun updateNodes(oldNodes: List<NodeVersioned>, newNodes: List<NodeVersioned>) {
nodeSortedList.beginBatchedUpdates()
oldNodes.forEach { oldNode ->
nodeSortedList.remove(oldNode)
}
nodeSortedList.addAll(newNodes)
nodeSortedList.endBatchedUpdates()
}
fun notifyNodeChanged(node: NodeVersioned) {
notifyItemChanged(nodeSortedList.indexOf(node))
}
fun setActionNodes(actionNodes: List<NodeVersioned>) {
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
*/
@@ -203,20 +279,41 @@ class NodeAdapter
Type.GROUP -> iconGroupColor
Type.ENTRY -> iconEntryColor
}
holder.icon.assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
holder.icon.apply {
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
// Relative size of the icon
layoutParams?.apply {
height = iconSize.toInt()
width = iconSize.toInt()
}
}
// Assign text
holder.text.text = subNode.title
holder.text.apply {
text = subNode.title
setTextSize(textSizeUnit, infoTextSize)
paintFlags = if (subNode.isCurrentlyExpires)
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
else
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG
}
// Assign click
holder.container.setOnClickListener { nodeClickCallback?.onNodeClick(subNode) }
// Context menu
if (activateContextMenu) {
holder.container.setOnCreateContextMenuListener(
ContextMenuBuilder(menuInflater, subNode, readOnly, isASearchResult, nodeMenuListener))
holder.container.setOnClickListener {
nodeClickCallback?.onNodeClick(subNode)
}
holder.container.setOnLongClickListener {
nodeClickCallback?.onNodeLongClick(subNode) ?: false
}
// Add username
holder.subText.text = ""
holder.subText.visibility = View.GONE
holder.container.isSelected = actionNodesList.contains(subNode)
// Add subText with username
holder.subText.apply {
text = ""
paintFlags = if (subNode.isCurrentlyExpires)
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
else
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG
visibility = View.GONE
if (subNode.type == Type.ENTRY) {
val entry = subNode as EntryVersioned
@@ -226,19 +323,27 @@ class NodeAdapter
val username = entry.username
if (showUserNames && username.isNotEmpty()) {
holder.subText.visibility = View.VISIBLE
holder.subText.text = username
visibility = View.VISIBLE
text = username
setTextSize(textSizeUnit, subtextSize)
}
mDatabase.stopManageEntry(entry)
}
}
// Assign image and text size
// Relative size of the icon
holder.icon.layoutParams?.height = iconSize.toInt()
holder.icon.layoutParams?.width = iconSize.toInt()
holder.text.textSize = textSize
holder.subText.textSize = subtextSize
// Add number of entries in groups
if (subNode.type == Type.GROUP) {
if (showNumberEntries) {
holder.numberChildren?.apply {
text = (subNode as GroupVersioned).getChildEntries(true).size.toString()
setTextSize(textSizeUnit, numberChildrenTextSize)
visibility = View.VISIBLE
}
} else {
holder.numberChildren?.visibility = View.GONE
}
}
}
override fun getItemCount(): Int {
@@ -252,103 +357,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 onNodeLongClick(node: NodeVersioned): Boolean
}
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
@@ -356,6 +370,7 @@ class NodeAdapter
var icon: ImageView = itemView.findViewById(R.id.node_icon)
var text: TextView = itemView.findViewById(R.id.node_text)
var subText: TextView = itemView.findViewById(R.id.node_subtext)
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
}
companion object {

View File

@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.adapters
import android.content.Context
import android.database.Cursor
import android.graphics.Color
import android.support.v4.widget.CursorAdapter
import androidx.cursoradapter.widget.CursorAdapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -38,7 +38,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.*
class SearchEntryCursorAdapter(context: Context, private val database: Database)
: CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
private val cursorInflater: LayoutInflater = context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater

View File

@@ -19,7 +19,7 @@
*/
package com.kunzisoft.keepass.app
import android.support.multidex.MultiDexApplication
import androidx.multidex.MultiDexApplication
import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.database.element.Database

View File

@@ -0,0 +1,20 @@
package com.kunzisoft.keepass.app.database
import android.os.AsyncTask
/**
* Private class to invoke each method in a separate thread
*/
class ActionDatabaseAsyncTask<T>(
private val action: () -> T ,
private val afterActionDatabaseListener: ((T?) -> Unit)? = null
) : AsyncTask<Void, Void, T>() {
override fun doInBackground(vararg args: Void?): T? {
return action.invoke()
}
override fun onPostExecute(result: T?) {
afterActionDatabaseListener?.invoke(result)
}
}

View File

@@ -0,0 +1,22 @@
package com.kunzisoft.keepass.app.database
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import android.content.Context
@Database(version = 1, entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class])
abstract class AppDatabase : RoomDatabase() {
abstract fun fileDatabaseHistoryDao(): FileDatabaseHistoryDao
abstract fun cipherDatabaseDao(): CipherDatabaseDao
companion object {
fun getDatabase(applicationContext: Context): AppDatabase {
return Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "com.kunzisoft.keepass.database"
).build()
}
}
}

View File

@@ -0,0 +1,73 @@
package com.kunzisoft.keepass.app.database
import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.utils.SingletonHolderParameter
class CipherDatabaseAction(applicationContext: Context) {
private val cipherDatabaseDao =
AppDatabase
.getDatabase(applicationContext)
.cipherDatabaseDao()
fun getCipherDatabase(databaseUri: Uri,
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
ActionDatabaseAsyncTask(
{
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
},
{
cipherDatabaseResultListener.invoke(it)
}
).execute()
}
fun containsCipherDatabase(databaseUri: Uri,
contains: (Boolean) -> Unit) {
getCipherDatabase(databaseUri) {
contains.invoke(it != null)
}
}
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
cipherDatabaseResultListener: (() -> Unit)? = null) {
ActionDatabaseAsyncTask(
{
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
// Update values if element not yet in the database
if (cipherDatabaseRetrieve == null) {
cipherDatabaseDao.add(cipherDatabaseEntity)
} else {
cipherDatabaseDao.update(cipherDatabaseEntity)
}
},
{
cipherDatabaseResultListener?.invoke()
}
).execute()
}
fun deleteByDatabaseUri(databaseUri: Uri,
cipherDatabaseResultListener: (() -> Unit)? = null) {
ActionDatabaseAsyncTask(
{
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
},
{
cipherDatabaseResultListener?.invoke()
}
).execute()
}
fun deleteAll() {
ActionDatabaseAsyncTask(
{
cipherDatabaseDao.deleteAll()
}
).execute()
}
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction)
}

View File

@@ -0,0 +1,22 @@
package com.kunzisoft.keepass.app.database
import androidx.room.*
@Dao
interface CipherDatabaseDao {
@Query("SELECT * FROM cipher_database WHERE database_uri = :databaseUriString")
fun getByDatabaseUri(databaseUriString: String): CipherDatabaseEntity?
@Insert
fun add(vararg fileDatabaseHistory: CipherDatabaseEntity)
@Update
fun update(vararg fileDatabaseHistory: CipherDatabaseEntity)
@Query("DELETE FROM cipher_database WHERE database_uri = :databaseUriString")
fun deleteByDatabaseUri(databaseUriString: String)
@Query("DELETE FROM cipher_database")
fun deleteAll()
}

View File

@@ -0,0 +1,61 @@
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
@Entity(tableName = "cipher_database")
data class CipherDatabaseEntity(
@PrimaryKey
@ColumnInfo(name = "database_uri")
val databaseUri: String,
@ColumnInfo(name = "encrypted_value")
var encryptedValue: String,
@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
other as CipherDatabaseEntity
if (databaseUri != other.databaseUri) return false
return true
}
override fun hashCode(): Int {
return databaseUri.hashCode()
}
}

View File

@@ -0,0 +1,132 @@
/*
* 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.app.database
import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.utils.SingletonHolderParameter
import com.kunzisoft.keepass.utils.UriUtil
class FileDatabaseHistoryAction(applicationContext: Context) {
private val databaseFileHistoryDao =
AppDatabase
.getDatabase(applicationContext)
.fileDatabaseHistoryDao()
fun getFileDatabaseHistory(databaseUri: Uri,
fileHistoryResultListener: (fileDatabaseHistoryResult: FileDatabaseHistoryEntity?) -> Unit) {
ActionDatabaseAsyncTask(
{
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
},
{
fileHistoryResultListener.invoke(it)
}
).execute()
}
fun getKeyFileUriByDatabaseUri(databaseUri: Uri,
keyFileUriResultListener: (Uri?) -> Unit) {
ActionDatabaseAsyncTask(
{
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
},
{
it?.let { fileHistoryEntity ->
fileHistoryEntity.keyFileUri?.let { keyFileUri ->
keyFileUriResultListener.invoke(UriUtil.parse(keyFileUri))
}
} ?: keyFileUriResultListener.invoke(null)
}
).execute()
}
fun getAllFileDatabaseHistories(fileHistoryResultListener: (fileDatabaseHistoryResult: List<FileDatabaseHistoryEntity>?) -> Unit) {
ActionDatabaseAsyncTask(
{
databaseFileHistoryDao.getAll()
},
{
fileHistoryResultListener.invoke(it)
}
).execute()
}
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null) {
addOrUpdateFileDatabaseHistory(FileDatabaseHistoryEntity(
databaseUri.toString(),
"",
keyFileUri?.toString(),
System.currentTimeMillis()
), true)
}
fun addOrUpdateFileDatabaseHistory(fileDatabaseHistory: FileDatabaseHistoryEntity, unmodifiedAlias: Boolean = false) {
ActionDatabaseAsyncTask(
{
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(fileDatabaseHistory.databaseUri)
if (unmodifiedAlias) {
fileDatabaseHistory.databaseAlias = fileDatabaseHistoryRetrieve?.databaseAlias ?: ""
}
// Update values if history element not yet in the database
if (fileDatabaseHistoryRetrieve == null) {
databaseFileHistoryDao.add(fileDatabaseHistory)
} else {
databaseFileHistoryDao.update(fileDatabaseHistory)
}
}
).execute()
}
fun deleteFileDatabaseHistory(fileDatabaseHistory: FileDatabaseHistoryEntity,
fileHistoryDeletedResult: (FileDatabaseHistoryEntity?) -> Unit) {
ActionDatabaseAsyncTask(
{
databaseFileHistoryDao.delete(fileDatabaseHistory)
},
{
if (it != null && it > 0)
fileHistoryDeletedResult.invoke(fileDatabaseHistory)
else
fileHistoryDeletedResult.invoke(null)
}
).execute()
}
fun deleteAllKeyFiles() {
ActionDatabaseAsyncTask(
{
databaseFileHistoryDao.deleteAllKeyFiles()
}
).execute()
}
fun deleteAll() {
ActionDatabaseAsyncTask(
{
databaseFileHistoryDao.deleteAll()
}
).execute()
}
companion object : SingletonHolderParameter<FileDatabaseHistoryAction, Context>(::FileDatabaseHistoryAction)
}

View File

@@ -0,0 +1,27 @@
package com.kunzisoft.keepass.app.database
import androidx.room.*
@Dao
interface FileDatabaseHistoryDao {
@Query("SELECT * FROM file_database_history ORDER BY updated DESC")
fun getAll(): List<FileDatabaseHistoryEntity>
@Query("SELECT * FROM file_database_history WHERE database_uri = :databaseUriString")
fun getByDatabaseUri(databaseUriString: String): FileDatabaseHistoryEntity?
@Insert
fun add(vararg fileDatabaseHistory: FileDatabaseHistoryEntity)
@Update
fun update(vararg fileDatabaseHistory: FileDatabaseHistoryEntity)
@Delete
fun delete(fileDatabaseHistory: FileDatabaseHistoryEntity): Int
@Query("UPDATE file_database_history SET keyfile_uri=null")
fun deleteAllKeyFiles()
@Query("DELETE FROM file_database_history")
fun deleteAll()
}

View File

@@ -0,0 +1,36 @@
package com.kunzisoft.keepass.app.database
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "file_database_history")
data class FileDatabaseHistoryEntity(
@PrimaryKey
@ColumnInfo(name = "database_uri")
val databaseUri: String,
@ColumnInfo(name = "database_alias")
var databaseAlias: String,
@ColumnInfo(name = "keyfile_uri")
var keyFileUri: String?,
@ColumnInfo(name = "updated")
val updated: Long
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as FileDatabaseHistoryEntity
if (databaseUri != other.databaseUri) return false
return true
}
override fun hashCode(): Int {
return databaseUri.hashCode()
}
}

View File

@@ -26,7 +26,7 @@ import android.content.Intent
import android.os.Build
import android.service.autofill.Dataset
import android.service.autofill.FillResponse
import android.support.annotation.RequiresApi
import androidx.annotation.RequiresApi
import android.util.Log
import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue

View File

@@ -26,8 +26,8 @@ import android.content.Intent
import android.content.IntentSender
import android.os.Build
import android.os.Bundle
import android.support.annotation.RequiresApi
import android.support.v7.app.AppCompatActivity
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.database.element.Database
@@ -56,6 +56,7 @@ class AutofillLauncherActivity : AppCompatActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
super.onActivityResult(requestCode, resultCode, data)
}
companion object {

View File

@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.autofill
import android.os.Build
import android.os.CancellationSignal
import android.service.autofill.*
import android.support.annotation.RequiresApi
import androidx.annotation.RequiresApi
import android.util.Log
import android.widget.RemoteViews
import com.kunzisoft.keepass.R

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