Compare commits

...

193 Commits

Author SHA1 Message Date
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
354 changed files with 8277 additions and 8538 deletions

View File

@@ -1,3 +1,27 @@
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

1
app/.gitignore vendored
View File

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

View File

@@ -4,15 +4,15 @@ apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 27
compileSdkVersion 28
buildToolsVersion '28.0.3'
defaultConfig {
applicationId "com.kunzisoft.keepass"
minSdkVersion 14
targetSdkVersion 27
versionCode = 20
versionName = "2.5.0.0beta20"
targetSdkVersion 28
versionCode = 23
versionName = "2.5.0.0beta23"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
@@ -79,42 +79,35 @@ android {
}
}
def supportVersion = "27.1.1"
def spongycastleVersion = "1.58.0.0"
def permissionDispatcherVersion = "3.3.1"
def room_version = "2.1.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-beta01'
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'
// 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: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:scheme="file" />
<data android:scheme="content" />
<data android:mimeType="application/octet-stream" />
<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,6 +128,7 @@
<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">

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,13 @@
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.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,12 +43,11 @@ 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
class EntryActivity : LockingHideActivity() {
@@ -79,7 +76,7 @@ class EntryActivity : LockingHideActivity() {
supportActionBar?.setDisplayShowHomeEnabled(true)
val currentDatabase = Database.getInstance()
readOnly = currentDatabase.isReadOnly || readOnly
mReadOnly = currentDatabase.isReadOnly || mReadOnly
mShowPassword = !PreferencesUtil.isPasswordMask(this)
@@ -101,8 +98,8 @@ class EntryActivity : LockingHideActivity() {
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
@@ -152,72 +149,93 @@ 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) {
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))
.create().apply {
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) {dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
dialog.dismiss()
fillEntryDataInContentsView(entry)
}
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false)
dialog.dismiss()
fillEntryDataInContentsView(entry)
}
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(this)) {
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
val message = 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, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
dialog.dismiss()
fillEntryDataInContentsView(entry)
}
warningDialog.setButton(AlertDialog.BUTTON_NEGATIVE, getText(android.R.string.cancel)
) { dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false)
dialog.dismiss()
fillEntryDataInContentsView(entry)
}
warningDialog.show()
})
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 {
clipboardHelper?.timeoutCopyToClipboard(
value.toString(),
getString(R.string.copy_field, label)
)
})
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 {
@@ -236,6 +254,9 @@ class EntryActivity : LockingHideActivity() {
entryContentsView?.assignExpiresDate(getString(R.string.never))
}
// Assign special data
entryContentsView?.assignUUID(entry.nodeId.id)
database.stopManageEntry(entry)
}
@@ -268,7 +289,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 +324,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,23 +335,29 @@ 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(
toolbar!!.findViewById(R.id.menu_edit),
{
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
},
{
// Open Keepass doc to create field references
startActivity(Intent(Intent.ACTION_VIEW,
Uri.parse(getString(R.string.field_references_url))))
}))
;
})
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))
},
{
// Open Keepass doc to create field references
startActivity(Intent(Intent.ACTION_VIEW,
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 +381,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
}

View File

@@ -22,7 +22,7 @@ import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.support.v7.widget.Toolbar
import androidx.appcompat.widget.Toolbar
import android.util.Log
import android.view.Menu
import android.view.MenuItem
@@ -45,7 +45,9 @@ import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.view.EntryEditContentsView
class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPickerListener, GeneratePasswordDialogFragment.GeneratePasswordListener {
class EntryEditActivity : LockingHideActivity(),
IconPickerDialogFragment.IconPickerListener,
GeneratePasswordDialogFragment.GeneratePasswordListener {
private var mDatabase: Database? = null
@@ -150,37 +152,10 @@ 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)
}
))
else if (mNewEntry != null && mNewEntry!!.allowExtraFields() && !mNewEntry!!.containsCustomFields()
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
addNewFieldView,
{
addNewCustomField()
}))
;
}
private fun populateViewsWithEntry(newEntry: EntryVersioned) {
@@ -197,8 +172,8 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
url = newEntry.url
password = newEntry.password
notes = newEntry.notes
newEntry.fields.doActionToAllCustomProtectedField { key, value ->
addNewCustomField(key, value)
for (entry in newEntry.customFields.entries) {
addNewCustomField(entry.key, entry.value)
}
}
}
@@ -281,7 +256,7 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
newEntry,
parent,
afterActionNodeFinishRunnable,
!readOnly)
!mReadOnly)
}
} else {
@@ -291,7 +266,7 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
oldEntry,
newEntry,
afterActionNodeFinishRunnable,
!readOnly)
!mReadOnly)
}
}
actionRunnable?.let { runnable ->
@@ -309,9 +284,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 +324,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()
}

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,56 +29,42 @@ 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 androidx.annotation.RequiresApi
import com.google.android.material.snackbar.Snackbar
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.appcompat.widget.Toolbar
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
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.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.asError
import kotlinx.android.synthetic.main.activity_file_selection.*
import net.cachapa.expandablelayout.ExpandableLayout
import permissions.dispatcher.*
import java.io.File
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
@@ -92,18 +78,18 @@ class FileDatabaseSelectActivity : StylishActivity(),
// 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
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)
@@ -131,10 +117,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
fileSelectExpandableLayout?.expand()
}
// History list
val databaseFileListView = findViewById<RecyclerView>(R.id.file_list)
databaseFileListView.layoutManager = LinearLayoutManager(this)
// Open button
openButtonView = findViewById(R.id.open_database)
openButtonView?.setOnClickListener { _ ->
@@ -143,101 +125,121 @@ class FileDatabaseSelectActivity : StylishActivity(),
if (fileName.isEmpty())
fileName = it
}
launchPasswordActivityWithPath(fileName)
UriUtil.parse(fileName)?.let { fileNameUri ->
launchPasswordActivityWithPath(fileNameUri)
} ?: run {
Log.e(TAG, "Unable to open the database link")
Snackbar.make(activity_file_selection_coordinator_layout, getString(R.string.error_can_not_handle_uri), Snackbar.LENGTH_LONG).asError().show()
null
}
}
// Create button
createButtonView = findViewById(R.id.create_database)
createButtonView?.setOnClickListener { openCreateFileDialogFragmentWithPermissionCheck() }
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
}
mKeyFileHelper = KeyFileHelper(this)
createButtonView?.setOnClickListener { createNewFile() }
mOpenFileHelper = OpenFileHelper(this)
browseButtonView = findViewById(R.id.browse_button)
browseButtonView?.setOnClickListener(mKeyFileHelper!!.getOpenFileOnClickViewListener {
Uri.parse("file://" + openFileNameView!!.text.toString())
browseButtonView?.setOnClickListener(mOpenFileHelper!!.getOpenFileOnClickViewListener {
UriUtil.parse(openFileNameView?.text?.toString())
})
// History list
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)
}
} else {
if (dbUri != null)
launchPasswordActivityWithPath(dbUri.toString())
}
UriUtil.parse(databasePath)?.let { databaseFileUri ->
launchPasswordActivityWithPath(databaseFileUri)
} ?: run {
Log.i(TAG, "Unable to launch Password Activity")
}
}
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
// 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)
}
}
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
// If no recent files
if (createButtonView != null
&& mFileDatabaseHistory != null
&& !mFileDatabaseHistory!!.hasRecentFiles() && fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
createButtonView!!,
{
openCreateFileDialogFragmentWithPermissionCheck()
},
{
// 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)
}
},
{
fileSelectExpandableButtonView?.let {
fileDatabaseSelectActivityEducation
.checkAndPerformedOpenLinkDatabaseEducation(it)
}
}
))
;
/**
* 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))
},
CREATE_FILE_REQUEST_CODE)
} catch (e: Exception) {
BrowserDialogFragment().show(supportFragmentManager, "browserDialog")
}
}
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 +247,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
{
try {
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
fileName, keyFile)
databaseUri, keyFile)
finish()
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
@@ -255,7 +257,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 +267,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
})
}
private fun launchPasswordActivityWithPath(path: String) {
launchPasswordActivity(path, "")
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
launchPasswordActivity(databaseUri, null)
// Delete flickering for kitkat <=
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
overridePendingTransition(0, 0)
@@ -295,26 +297,23 @@ class FileDatabaseSelectActivity : StylishActivity(),
super.onResume()
updateExternalStorageWarning()
updateFileListVisibility()
mAdapterDatabaseHistory!!.notifyDataSetChanged()
// Construct adapter with listeners
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
databaseFileHistoryList?.let {
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(it)
updateFileListVisibility()
mAdapterDatabaseHistory?.notifyDataSetChanged()
}
}
}
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,88 +323,12 @@ 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,
@@ -418,22 +341,21 @@ class FileDatabaseSelectActivity : StylishActivity(),
keyFileChecked,
keyFile,
true, // TODO get readonly
LaunchGroupActivityFinish(databaseUri)
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()
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)
}
}
private inner class LaunchGroupActivityFinish internal constructor(private val fileURI: Uri) : ActionRunnable() {
private inner class LaunchGroupActivityFinish(private val databaseFileUri: Uri,
private val keyFileUri: Uri?) : ActionRunnable() {
override fun run() {
finishRun(true, null)
@@ -443,7 +365,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
runOnUiThread {
if (result.isSuccess) {
// Add database to recent files
mFileDatabaseHistory?.addDatabaseUri(fileURI)
mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(databaseFileUri, keyFileUri)
mAdapterDatabaseHistory?.notifyDataSetChanged()
updateFileListVisibility()
GroupActivity.launch(this@FileDatabaseSelectActivity)
@@ -460,29 +382,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 +389,73 @@ 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())
launchPasswordActivityWithPath(uri)
} else {
fileSelectExpandableLayout?.expand(false)
openFileNameView?.setText(uri.toString())
}
}
}
}
@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()
}
@OnPermissionDenied(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showDeniedForExternalStorage() {
Toast.makeText(this, R.string.permission_external_storage_denied, Toast.LENGTH_SHORT).show()
}
@OnNeverAskAgain(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showNeverAskForExternalStorage() {
Toast.makeText(this, R.string.permission_external_storage_never_ask, Toast.LENGTH_SHORT).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().show(supportFragmentManager, "passwordDialog")
}
// else {
// TODO Show error
// }
}
}
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
browseButtonView != null
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
browseButtonView!!,
{tapTargetView ->
tapTargetView?.let {
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
}
},
{
fileSelectExpandableButtonView?.let {
fileDatabaseSelectActivityEducation
.checkAndPerformedOpenLinkDatabaseEducation(it)
}
}
)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
}
@@ -536,6 +464,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 +481,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

@@ -29,11 +29,10 @@ 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 androidx.annotation.RequiresApi
import androidx.fragment.app.FragmentManager
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import android.util.Log
import android.view.Menu
import android.view.MenuItem
@@ -54,14 +53,13 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.SortNodeEnum
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
import com.kunzisoft.keepass.database.action.node.*
import com.kunzisoft.keepass.database.action.node.ActionNodeDatabaseRunnable.Companion.NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
import com.kunzisoft.keepass.magikeyboard.MagikIME
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
@@ -178,12 +176,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()
@@ -207,15 +205,19 @@ class GroupActivity : LockingActivity(),
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) {
// only one instance of search in backstack
openSearchGroup(retrieveCurrentGroup(intent, null))
true
} else {
false
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(intentNotNull, null))
true
} else {
false
}
}
}
@@ -239,7 +241,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
@@ -283,6 +285,9 @@ class GroupActivity : LockingActivity(),
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 +302,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) {
@@ -357,7 +360,7 @@ class GroupActivity : LockingActivity(),
}
// Show selection mode message if needed
if (selectionMode) {
if (mSelectionMode) {
modeTitleView?.visibility = View.VISIBLE
} else {
modeTitleView?.visibility = View.GONE
@@ -367,8 +370,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 +404,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
@@ -481,7 +484,7 @@ class GroupActivity : LockingActivity(),
entryToCopy,
newParent,
AfterAddNodeRunnable(),
!readOnly)
!mReadOnly)
}.start()
}
@@ -523,10 +526,10 @@ class GroupActivity : LockingActivity(),
MoveGroupRunnable(
this,
Database.getInstance(),
groupToMove,
newParent,
AfterAddNodeRunnable(),
!readOnly)
groupToMove,
newParent,
AfterAddNodeRunnable(),
!mReadOnly)
}.start()
}
@@ -538,7 +541,7 @@ class GroupActivity : LockingActivity(),
entryToMove,
newParent,
AfterAddNodeRunnable(),
!readOnly)
!mReadOnly)
}.start()
}
@@ -558,7 +561,7 @@ class GroupActivity : LockingActivity(),
Database.getInstance(),
group,
AfterDeleteNodeRunnable(),
!readOnly)
!mReadOnly)
}.start()
}
@@ -569,7 +572,7 @@ class GroupActivity : LockingActivity(),
Database.getInstance(),
entry,
AfterDeleteNodeRunnable(),
!readOnly)
!mReadOnly)
}.start()
}
@@ -586,7 +589,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 +631,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,71 +645,47 @@ class GroupActivity : LockingActivity(),
{
performedNextEducation(groupActivityEducation, menu)
}
))
else if (toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_search) != null
&& groupActivityEducation.checkAndPerformedSearchMenuEducation(
toolbar!!.findViewById(R.id.menu_search),
{
menu.findItem(R.id.menu_search).expandActionView()
},
{
performedNextEducation(groupActivityEducation, menu)
}))
else if (toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_sort) != null
&& groupActivityEducation.checkAndPerformedSortMenuEducation(
)
if (!addNodeButtonEducationPerformed) {
val searchMenuEducationPerformed = toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_search) != null
&& groupActivityEducation.checkAndPerformedSearchMenuEducation(
toolbar!!.findViewById(R.id.menu_search),
{
menu.findItem(R.id.menu_search).expandActionView()
},
{
performedNextEducation(groupActivityEducation, menu)
})
if (!searchMenuEducationPerformed) {
val sortMenuEducationPerformed = toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_sort) != null
&& groupActivityEducation.checkAndPerformedSortMenuEducation(
toolbar!!.findViewById(R.id.menu_sort),
{
onOptionsItemSelected(menu.findItem(R.id.menu_sort))
},
{
performedNextEducation(groupActivityEducation, menu)
}))
else if (toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_lock) != null
&& groupActivityEducation.checkAndPerformedLockMenuEducation(
toolbar!!.findViewById(R.id.menu_lock),
{
onOptionsItemSelected(menu.findItem(R.id.menu_lock))
},
{
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)
if (!sortMenuEducationPerformed) {
// lockMenuEducationPerformed
toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_lock) != null
&& groupActivityEducation.checkAndPerformedLockMenuEducation(
toolbar!!.findViewById(R.id.menu_lock),
{
onOptionsItemSelected(menu.findItem(R.id.menu_lock))
},
{
performedNextEducation(groupActivityEducation, menu)
})
}
}
}
}
@@ -724,7 +704,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)
}
}
@@ -754,7 +734,7 @@ class GroupActivity : LockingActivity(),
newGroup,
currentGroup,
AfterAddNodeRunnable(),
!readOnly)
!mReadOnly)
}.start()
}
}
@@ -776,7 +756,7 @@ class GroupActivity : LockingActivity(),
oldGroupToUpdate,
updateGroup,
AfterUpdateNodeRunnable(),
!readOnly)
!mReadOnly)
}.start()
}
}
@@ -812,20 +792,30 @@ class GroupActivity : LockingActivity(),
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
runOnUiThread {
if (actionNodeValues.result.isSuccess) {
actionNodeValues.oldNode?.let { oldNode ->
mListNodesFragment?.removeNode(oldNode)
// If the action register the position, use it to remove the entry view
val positionNode = actionNodeValues.result.data?.getInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY)
if (PreferencesUtil.getListSort(this@GroupActivity) == SortNodeEnum.DB
&& positionNode != null) {
mListNodesFragment?.removeNodeAt(positionNode)
} else {
// else use the old Node that was the entry unchanged with the old parent
actionNodeValues.oldNode?.let { oldNode ->
mListNodesFragment?.removeNode(oldNode)
}
}
// 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
// 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)
}
if (mListNodesFragment?.contains(recycleBin) == true)
mListNodesFragment?.updateNode(recycleBin)
else
mListNodesFragment?.addNode(recycleBin)
}
}
}
@@ -847,11 +837,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,18 +848,25 @@ class GroupActivity : LockingActivity(),
mListNodesFragment?.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
override fun startActivity(intent: Intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
// 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)
}
// Not directly get the entry from intent data but from database
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
@@ -884,9 +879,18 @@ class GroupActivity : LockingActivity(),
intent.flags = flags
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
super.startActivityForResult(intent, requestCode, options)
super.startActivityForResult(intent, requestCode, options)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
// Not directly get the entry from intent data but from database
mListNodesFragment?.rebuildList()
}
private fun removeSearchInIntent(intent: Intent) {
@@ -963,10 +967,10 @@ class GroupActivity : LockingActivity(),
*/
// TODO implement pre search to directly open the direct group
fun launchForKeyboardSelection(activity: Activity, readOnly: Boolean) {
fun launchForKeyboarSelection(activity: Activity, readOnly: Boolean) {
TimeoutHelper.recordTime(activity)
buildAndLaunchIntent(activity, null, readOnly) { intent ->
KeyboardHelper.startActivityForKeyboardSelection(activity, intent)
EntrySelectionHelper.startActivityForEntrySelection(activity, 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
@@ -25,6 +25,7 @@ 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
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
@@ -52,31 +53,31 @@ 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?
nodeClickCallback = context as NodeAdapter.NodeClickCallback
} 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?
nodeMenuListener = context as NodeAdapter.NodeMenuListener
} catch (e: ClassCastException) {
nodeMenuListener = null
// Context menu can be omit
Log.w(TAG, context?.toString()
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)
}
}
@@ -129,7 +130,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)
}
@@ -194,35 +195,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().isRecycleBinAvailable
&& Database.getInstance().isRecycleBinEnabled) {
SortDialogFragment.getInstance(
PrefsUtil.getListSort(this),
PrefsUtil.getAscendingSort(this),
PrefsUtil.getGroupsBeforeSort(this),
PrefsUtil.getRecycleBinBottomSort(this));
} else {
*/
sortDialogFragment = SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context))
//}
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context),
PreferencesUtil.getRecycleBinBottomSort(context))
} else {
SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context))
}
sortDialogFragment.show(childFragmentManager, "sortDialog")
}
@@ -255,18 +252,26 @@ 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 updateNode(oldNode: NodeVersioned, newNode: NodeVersioned? = null) {
mAdapter?.updateNode(oldNode, newNode ?: oldNode)
}
fun removeNode(pwNode: NodeVersioned) {
mAdapter?.removeNode(pwNode)
}
fun removeNodeAt(position: Int) {
mAdapter?.removeNodeAt(position)
}
interface OnScrollListener {
/**

View File

@@ -19,57 +19,59 @@
*/
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 androidx.annotation.RequiresApi
import com.google.android.material.snackbar.Snackbar
import androidx.appcompat.widget.Toolbar
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
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 androidx.biometric.BiometricManager
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.FingerPrintExplanationDialog
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.*
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.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.utils.FileDatabaseInfo
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable
import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.element.Database
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.biometric.AdvancedUnlockedManager
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.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
@@ -81,18 +83,18 @@ 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 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 advancedUnlockedManager: AdvancedUnlockedManager? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -117,13 +119,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)
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
browseView.setOnClickListener(mOpenFileHelper!!.openFileOnClickViewListener)
passwordView?.setOnEditorActionListener(onEditorActionListener)
passwordView?.addTextChangedListener(object : TextWatcher {
@@ -148,10 +150,8 @@ class PasswordActivity : StylishActivity(),
}
})
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, isChecked ->
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
confirmButtonView?.isEnabled = isChecked
}
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
enableOrNotTheConfirmationButton()
}
}
@@ -175,17 +175,7 @@ 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
}
UriIntentInitTask(WeakReference(this), this, mRememberKeyFile)
.execute(intent)
initUriFromIntent()
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -193,26 +183,51 @@ 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) {
val databaseUriRetrieve = intent.data
// Stop activity here if we can't verify database URI
if (!UriUtil.verifyFileUri(databaseUriRetrieve)) {
Log.e(TAG, "File URI not validate")
finish()
}
databaseUri = databaseUriRetrieve
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
// 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,14 +238,16 @@ 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)
apply()
newDefaultFileName?.let {
prefs?.edit()?.apply {
putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString())
apply()
}
}
val backupManager = BackupManager(this@PasswordActivity)
@@ -239,10 +256,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 +276,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,27 +369,34 @@ class PasswordActivity : StylishActivity(),
override fun onPause() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
fingerPrintViewsManager?.stopListening()
advancedUnlockedManager?.pause()
}
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())) {
private fun verifyCheckboxesAndLoadDatabase(password: String?,
keyFile: Uri?,
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
loadDatabase(keyPassword, keyFileUri, cipherDatabaseEntity)
}
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
loadDatabase(password, keyFileUri)
}
@@ -355,10 +406,12 @@ class PasswordActivity : StylishActivity(),
checkboxPasswordView?.isChecked = false
}
private fun loadDatabase(password: String?, keyFile: Uri?) {
private fun loadDatabase(password: String?, keyFile: Uri?, cipherDatabaseEntity: CipherDatabaseEntity? = null) {
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
removePassword()
runOnUiThread {
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
removePassword()
}
}
// Clear before we load
@@ -376,7 +429,7 @@ class PasswordActivity : StylishActivity(),
password,
keyFile,
progressTaskUpdater,
AfterLoadingDatabase(database, password))
AfterLoadingDatabase(database, password, cipherDatabaseEntity))
},
R.string.loading_database).start()
}
@@ -385,17 +438,17 @@ class PasswordActivity : StylishActivity(),
/**
* Called after verify and try to opening the database
*/
private inner class AfterLoadingDatabase internal constructor(var database: Database,
val password: String?)
private inner class AfterLoadingDatabase(val database: Database, val password: String?,
val cipherDatabaseEntity: CipherDatabaseEntity? = null)
: ActionRunnable() {
override fun onFinishRun(result: Result) {
runOnUiThread {
// Recheck fingerprint if error
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (PreferencesUtil.isFingerprintEnable(this@PasswordActivity)) {
// Stay with the same mode
fingerPrintViewsManager?.reInitWithFingerprintMode()
if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) {
// Stay with the same mode and init it
advancedUnlockedManager?.initBiometricMode()
}
}
@@ -403,32 +456,45 @@ class PasswordActivity : StylishActivity(),
// Remove the password in view in all cases
removePassword()
if (database.validatePasswordEncoding(password)) {
launchGroupActivity()
// Register the biometric
if (cipherDatabaseEntity != null) {
CipherDatabaseAction.getInstance(this@PasswordActivity)
.addOrUpdateCipherDatabase(cipherDatabaseEntity) {
checkAndLaunchGroupActivity(database, password)
}
} else {
PasswordEncodingDialogFragment().apply {
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
launchGroupActivity()
}
show(supportFragmentManager, "passwordEncodingTag")
}
checkAndLaunchGroupActivity(database, password)
}
} else {
if (result.message != null && result.message!!.isNotEmpty()) {
Toast.makeText(this@PasswordActivity, result.message, Toast.LENGTH_LONG).show()
Snackbar.make(activity_password_coordinator_layout, result.message!!, Snackbar.LENGTH_LONG).asError().show()
}
}
}
}
}
private fun checkAndLaunchGroupActivity(database: Database, password: String?) {
if (database.validatePasswordEncoding(password)) {
launchGroupActivity()
} else {
PasswordEncodingDialogFragment().apply {
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
launchGroupActivity()
}
show(supportFragmentManager, "passwordEncodingTag")
}
}
}
private fun launchGroupActivity() {
EntrySelectionHelper.doEntrySelectionAction(intent,
{
GroupActivity.launch(this@PasswordActivity, readOnly)
},
{
GroupActivity.launchForKeyboardSelection(this@PasswordActivity, readOnly)
GroupActivity.launchForKeyboarSelection(this@PasswordActivity, readOnly)
// Do not keep history
finish()
},
@@ -449,7 +515,7 @@ 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)
@@ -462,32 +528,41 @@ class PasswordActivity : StylishActivity(),
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
menu: Menu) {
if (toolbar != null
&& passwordActivityEducation.checkAndPerformedFingerprintUnlockEducation(
val unlockEducationPerformed = toolbar != null
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
toolbar!!,
{
performedNextEducation(passwordActivityEducation, menu)
},
{
performedNextEducation(passwordActivityEducation, menu)
}))
else if (toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
toolbar!!.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 (!unlockEducationPerformed) {
val readOnlyEducationPerformed = toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
toolbar!!.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)
})
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 +584,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 +592,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 +604,7 @@ class PasswordActivity : StylishActivity(),
}
var keyFileResult = false
mKeyFileHelper?.let {
mOpenFileHelper?.let {
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
if (uri != null) {
@@ -554,64 +623,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 +654,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 +670,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 +687,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,39 @@ 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 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,12 +84,12 @@ 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)
}
}
@@ -83,33 +107,17 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
.setNegativeButton(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)
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)
mOpenFileHelper = OpenFileHelper(this)
rootView?.findViewById<View>(R.id.browse_button)?.setOnClickListener { view ->
mKeyFileHelper?.openFileOnClickViewListener?.onClick(view) }
mOpenFileHelper?.openFileOnClickViewListener?.onClick(view) }
val dialog = builder.create()
@@ -149,20 +157,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()) {
@@ -177,13 +200,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
@@ -224,9 +246,9 @@ 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()

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() {
@@ -37,15 +38,18 @@ class BrowserDialogFragment : DialogFragment() {
builder.setView(root)
.setNegativeButton(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

@@ -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()
@@ -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)
}

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(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() {

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)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
warning = warning + "\n\n" + context.getString(R.string.read_only_kitkat_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" + getString(R.string.read_only_kitkat_warning)
}
builder.setMessage(warning)
builder.setPositiveButton(getString(android.R.string.ok)) { _, _ -> dismiss() }
builder.setNegativeButton(getString(R.string.beta_dontask)) { _, _ ->
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val edit = prefs.edit()
edit.putBoolean(getString(R.string.show_read_only_warning), false)
edit.apply()
dismiss()
}
// Create the AlertDialog object and return it
return builder.create()
}
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)
val edit = prefs.edit()
edit.putBoolean(ctx.getString(R.string.show_read_only_warning), false)
edit.apply()
dismiss()
}
super.onCreate(savedInstanceState)
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,14 +75,14 @@ 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) }
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) }
.setNegativeButton(R.string.cancel) { _, _ -> }
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
@@ -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,5 +1,6 @@
package com.kunzisoft.keepass.activities.helpers
import android.app.Activity
import android.app.assist.AssistStructure
import android.content.Intent
import android.os.Build
@@ -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(activity: Activity, intent: Intent) {
addEntrySelectionModeExtraInIntent(intent)
// only to avoid visible flickering when redirecting
activity.startActivity(intent)
}
fun addEntrySelectionModeExtraInIntent(intent: Intent) {
intent.putExtra(EXTRA_ENTRY_SELECTION_MODE, true)
}

View File

@@ -26,15 +26,14 @@ 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 androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import android.util.Log
import android.view.View
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
@@ -52,13 +51,13 @@ class KeyFileHelper {
this.fragment = context
}
inner class OpenFileOnClickViewListener(private val dataUri: (() -> Uri)?) : View.OnClickListener {
inner class OpenFileOnClickViewListener(private val dataUri: (() -> Uri?)?) : View.OnClickListener {
override fun onClick(v: View) {
try {
if (activity != null && StorageAF.useStorageFramework(activity!!)) {
try {
openActivityWithActionOpenDocument()
} else {
} catch(e: Exception) {
openActivityWithActionGetContent()
}
} catch (e: Exception) {
@@ -72,7 +71,7 @@ class KeyFileHelper {
}
private fun openActivityWithActionOpenDocument() {
val i = Intent(StorageAF.ACTION_OPEN_DOCUMENT)
val i = Intent(ACTION_OPEN_DOCUMENT)
i.addCategory(Intent.CATEGORY_OPENABLE)
i.type = "*/*"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
@@ -98,7 +97,7 @@ class KeyFileHelper {
activity?.startActivityForResult(i, GET_CONTENT)
}
fun getOpenFileOnClickViewListener(dataUri: () -> Uri): OpenFileOnClickViewListener {
fun getOpenFileOnClickViewListener(dataUri: () -> Uri?): OpenFileOnClickViewListener {
return OpenFileOnClickViewListener(dataUri)
}
@@ -158,7 +157,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 +181,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)
}
@@ -193,18 +192,16 @@ class KeyFileHelper {
if (data != null) {
var 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)
}
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)
}
} catch (e: Exception) {
// nop
}
} catch (e: Exception) {
// nop
}
if (requestCode == GET_CONTENT) {
uri = UriUtil.translateUri(activity!!, uri)
@@ -221,7 +218,18 @@ class KeyFileHelper {
companion object {
private const val TAG = "KeyFileHelper"
private const val TAG = "OpenFileHelper"
private var ACTION_OPEN_DOCUMENT: String
init {
ACTION_OPEN_DOCUMENT = try {
val openDocument = Intent::class.java.getField("ACTION_OPEN_DOCUMENT")
openDocument.get(null) as String
} 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)
}
this.themeId = Stylish.getThemeId(context)
contextThemed = ContextThemeWrapper(context, themeId)
}

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,16 +21,16 @@ 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 androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedListAdapterCallback
import android.util.Log
import android.util.TypedValue
import android.view.*
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
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
@@ -46,13 +46,20 @@ class NodeAdapter
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 nodeClickCallback: NodeClickCallback? = null
private var nodeMenuListener: NodeMenuListener? = null
@@ -80,11 +87,13 @@ class NodeAdapter
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 {
@@ -118,16 +127,21 @@ class NodeAdapter
}
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 +150,16 @@ 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()
}
}
fun contains(node: NodeVersioned): Boolean {
return nodeSortedList.indexOf(node) != SortedList.INVALID_POSITION
}
/**
@@ -162,6 +178,13 @@ class NodeAdapter
nodeSortedList.remove(node)
}
/**
* Remove a node at [position] in the list
*/
fun removeNodeAt(position: Int) {
nodeSortedList.removeItemAt(position)
}
/**
* Update a node in the list
* @param oldNode Node before the update
@@ -203,9 +226,19 @@ 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)
}
// Assign click
holder.container.setOnClickListener { nodeClickCallback?.onNodeClick(subNode) }
// Context menu
@@ -214,31 +247,40 @@ class NodeAdapter
ContextMenuBuilder(menuInflater, subNode, readOnly, isASearchResult, nodeMenuListener))
}
// Add username
holder.subText.text = ""
holder.subText.visibility = View.GONE
if (subNode.type == Type.ENTRY) {
val entry = subNode as EntryVersioned
// Add subText with username
holder.subText.apply {
text = ""
visibility = View.GONE
if (subNode.type == Type.ENTRY) {
val entry = subNode as EntryVersioned
mDatabase.startManageEntry(entry)
mDatabase.startManageEntry(entry)
holder.text.text = entry.getVisualTitle()
holder.text.text = entry.getVisualTitle()
val username = entry.username
if (showUserNames && username.isNotEmpty()) {
holder.subText.visibility = View.VISIBLE
holder.subText.text = username
val username = entry.username
if (showUserNames && username.isNotEmpty()) {
visibility = View.VISIBLE
text = username
setTextSize(textSizeUnit, subtextSize)
}
mDatabase.stopManageEntry(entry)
}
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 {
@@ -356,6 +398,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,33 @@
package com.kunzisoft.keepass.app.database
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
) {
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("REPLACE INTO file_database_history(keyfile_uri) VALUES(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

View File

@@ -20,7 +20,7 @@ package com.kunzisoft.keepass.autofill
import android.app.assist.AssistStructure
import android.os.Build
import android.support.annotation.RequiresApi
import androidx.annotation.RequiresApi
import android.text.InputType
import android.util.Log
import android.view.View

View File

@@ -26,6 +26,8 @@ import android.app.backup.SharedPreferencesBackupHelper
@SuppressLint("NewApi")
class SettingsBackupAgent : BackupAgentHelper() {
//TODO Backup
override fun onCreate() {
val defaultPrefs = this.packageName + "_preferences"
val prefHelper = SharedPreferencesBackupHelper(this, defaultPrefs)

View File

@@ -0,0 +1,316 @@
package com.kunzisoft.keepass.biometric
import android.net.Uri
import android.os.Build
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
import android.widget.CompoundButton
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
@RequiresApi(api = Build.VERSION_CODES.M)
class AdvancedUnlockedManager(var context: FragmentActivity,
var databaseFileUri: Uri,
var advancedUnlockInfoView: AdvancedUnlockInfoView?,
var checkboxPasswordView: CompoundButton?,
var onCheckedPasswordChangeListener: CompoundButton.OnCheckedChangeListener? = null,
var passwordView: TextView?,
var loadDatabaseAfterRegisterCredentials: (encryptedPassword: String?, ivSpec: String?) -> Unit,
var loadDatabaseAfterRetrieveCredentials: (decryptedPassword: String?) -> Unit)
: BiometricUnlockDatabaseHelper.BiometricUnlockCallback {
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
private var biometricMode: Mode = Mode.UNAVAILABLE
private var isBiometricPromptAutoOpenEnable = PreferencesUtil.isBiometricPromptAutoOpenEnable(context)
private var cipherDatabaseAction = CipherDatabaseAction.getInstance(context.applicationContext)
// fingerprint related code here
fun initBiometric() {
// Check if fingerprint well init (be called the first time the fingerprint is configured
// and the activity still active)
if (biometricUnlockDatabaseHelper == null || !biometricUnlockDatabaseHelper!!.isFingerprintInitialized) {
biometricUnlockDatabaseHelper = BiometricUnlockDatabaseHelper(context, this)
// callback for fingerprint findings
biometricUnlockDatabaseHelper?.setAuthenticationCallback(biometricCallback)
}
// Add a check listener to change fingerprint mode
checkboxPasswordView?.setOnCheckedChangeListener { compoundButton, checked ->
checkBiometricAvailability()
// Add old listener to enable the button, only be call here because of onCheckedChange bug
onCheckedPasswordChangeListener?.onCheckedChanged(compoundButton, checked)
}
checkBiometricAvailability()
}
@Synchronized
fun checkBiometricAvailability() {
// fingerprint not supported (by API level or hardware) so keep option hidden
// or manually disable
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate()
if (!PreferencesUtil.isBiometricUnlockEnable(context)
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
toggleMode(Mode.UNAVAILABLE)
} else {
// fingerprint is available but not configured, show icon but in disabled state with some information
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
toggleMode(Mode.NOT_CONFIGURED)
} else {
if (checkboxPasswordView?.isChecked == true) {
// listen for encryption
toggleMode(Mode.STORE)
} else {
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) {
// fingerprint available but no stored password found yet for this DB so show info don't listen
toggleMode( if (it) {
// listen for decryption
Mode.OPEN
} else {
// wait for typing
Mode.WAIT_CREDENTIAL
})
}
}
}
}
}
private fun toggleMode(newBiometricMode: Mode) {
if (newBiometricMode != biometricMode) {
biometricMode = newBiometricMode
initBiometricMode()
}
}
private val biometricCallback = object : BiometricPrompt.AuthenticationCallback () {
override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence) {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
setAdvancedUnlockedMessageView(errString.toString())
}
override fun onAuthenticationFailed() {
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
setAdvancedUnlockedMessageView(R.string.biometric_not_recognized)
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
when (biometricMode) {
Mode.UNAVAILABLE -> {}
Mode.PAUSE -> {}
Mode.NOT_CONFIGURED -> {}
Mode.WAIT_CREDENTIAL -> {}
Mode.STORE -> {
// newly store the entered password in encrypted way
biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString())
}
Mode.OPEN -> {
// retrieve the encrypted value from preferences
cipherDatabaseAction.getCipherDatabase(databaseFileUri) {
it?.encryptedValue?.let { value ->
biometricUnlockDatabaseHelper?.decryptData(value)
}
}
}
}
}
}
private fun initNotAvailable() {
showFingerPrintViews(false)
advancedUnlockInfoView?.setIconViewClickListener(null)
}
private fun initPause() {
advancedUnlockInfoView?.setIconViewClickListener(null)
}
private fun initNotConfigured() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.configure_biometric)
setAdvancedUnlockedMessageView("")
advancedUnlockInfoView?.setIconViewClickListener(null)
}
private fun initWaitData() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
setAdvancedUnlockedMessageView("")
advancedUnlockInfoView?.setIconViewClickListener(null)
}
private fun initEncryptData() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.open_biometric_prompt_store_credential)
setAdvancedUnlockedMessageView("")
biometricUnlockDatabaseHelper?.initEncryptData { biometricPrompt, cryptoObject, promptInfo ->
cryptoObject?.let { crypto ->
// Set listener to open the biometric dialog and save credential
advancedUnlockInfoView?.setIconViewClickListener { _ ->
context.runOnUiThread {
biometricPrompt?.authenticate(promptInfo, crypto)
}
}
}
}
}
private fun initDecryptData() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.open_biometric_prompt_unlock_database)
setAdvancedUnlockedMessageView("")
if (biometricUnlockDatabaseHelper != null) {
cipherDatabaseAction.getCipherDatabase(databaseFileUri) {
it?.specParameters?.let { specs ->
biometricUnlockDatabaseHelper?.initDecryptData(specs) { biometricPrompt, cryptoObject, promptInfo ->
cryptoObject?.let { crypto ->
// Set listener to open the biometric dialog and check credential
advancedUnlockInfoView?.setIconViewClickListener { _ ->
context.runOnUiThread {
biometricPrompt?.authenticate(promptInfo, crypto)
}
}
// Auto open the biometric prompt
if (isBiometricPromptAutoOpenEnable) {
isBiometricPromptAutoOpenEnable = false
context.runOnUiThread {
biometricPrompt?.authenticate(promptInfo, crypto)
}
}
}
}
}
}
}
}
@Synchronized
fun initBiometricMode() {
when (biometricMode) {
Mode.UNAVAILABLE -> initNotAvailable()
Mode.PAUSE -> initPause()
Mode.NOT_CONFIGURED -> initNotConfigured()
Mode.WAIT_CREDENTIAL -> initWaitData()
Mode.STORE -> initEncryptData()
Mode.OPEN -> initDecryptData()
}
// Show fingerprint key deletion
context.invalidateOptionsMenu()
}
fun pause() {
biometricMode = Mode.PAUSE
initBiometricMode()
}
fun destroy() {
// Restore the checked listener
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
biometricMode = Mode.UNAVAILABLE
initBiometricMode()
biometricUnlockDatabaseHelper = null
}
fun inflateOptionsMenu(menuInflater: MenuInflater, menu: Menu) {
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) {
if ((biometricMode != Mode.UNAVAILABLE
&& biometricMode != Mode.NOT_CONFIGURED) && it)
menuInflater.inflate(R.menu.advanced_unlock, menu)
}
}
fun deleteEntryKey() {
biometricUnlockDatabaseHelper?.deleteEntryKey()
cipherDatabaseAction.deleteByDatabaseUri(databaseFileUri)
biometricMode = Mode.NOT_CONFIGURED
checkBiometricAvailability()
}
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
loadDatabaseAfterRegisterCredentials.invoke(encryptedValue, ivSpec)
// TODO setAdvancedUnlockedMessageView(R.string.encrypted_value_stored)
}
override fun handleDecryptedResult(decryptedValue: String) {
// Load database directly with password retrieve
loadDatabaseAfterRetrieveCredentials.invoke(decryptedValue)
}
override fun onInvalidKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.biometric_invalid_key)
}
override fun onBiometricException(e: Exception) {
if (e.localizedMessage != null)
setAdvancedUnlockedMessageView(e.localizedMessage)
}
private fun showFingerPrintViews(show: Boolean) {
context.runOnUiThread { advancedUnlockInfoView?.hide = !show }
}
private fun setAdvancedUnlockedTitleView(textId: Int) {
context.runOnUiThread {
advancedUnlockInfoView?.setTitle(textId)
}
}
private fun setAdvancedUnlockedMessageView(textId: Int) {
context.runOnUiThread {
advancedUnlockInfoView?.setMessage(textId)
}
}
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
context.runOnUiThread {
advancedUnlockInfoView?.message = text
}
}
enum class Mode {
UNAVAILABLE, PAUSE, NOT_CONFIGURED, WAIT_CREDENTIAL, STORE, OPEN
}
companion object {
private val TAG = AdvancedUnlockedManager::class.java.name
}
}

View File

@@ -0,0 +1,311 @@
/*
* 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.biometric
import android.app.KeyguardManager
import android.content.Context
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties
import android.util.Base64
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import java.security.KeyStore
import java.security.UnrecoverableKeyException
import java.util.concurrent.Executors
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
@RequiresApi(api = Build.VERSION_CODES.M)
class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
private val biometricUnlockCallback: BiometricUnlockCallback?) {
private var biometricPrompt: BiometricPrompt? = null
private var keyStore: KeyStore? = null
private var keyGenerator: KeyGenerator? = null
private var cipher: Cipher? = null
private var keyguardManager: KeyguardManager? = null
private var cryptoObject: BiometricPrompt.CryptoObject? = null
private var isBiometricInit = false
private var authenticationCallback: BiometricPrompt.AuthenticationCallback? = null
private val promptInfoStoreCredential = BiometricPrompt.PromptInfo.Builder()
.setTitle(context.getString(R.string.biometric_prompt_store_credential_title))
.setDescription(context.getString(R.string.biometric_prompt_store_credential_message))
//.setDeviceCredentialAllowed(true) TODO device credential
.setNegativeButtonText(context.getString(android.R.string.cancel))
.build()
private val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder()
.setTitle(context.getString(R.string.biometric_prompt_extract_credential_title))
.setDescription(context.getString(R.string.biometric_prompt_extract_credential_message))
//.setDeviceCredentialAllowed(true)
.setNegativeButtonText(context.getString(android.R.string.cancel))
.build()
val isFingerprintInitialized: Boolean
get() {
if (!isBiometricInit && biometricUnlockCallback != null) {
biometricUnlockCallback.onBiometricException(Exception("FingerPrint not initialized"))
}
return isBiometricInit
}
init {
if (BiometricManager.from(context).canAuthenticate() != BiometricManager.BIOMETRIC_SUCCESS) {
// really not much to do when no fingerprint support found
isBiometricInit = false
} else {
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
try {
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)
this.keyGenerator = KeyGenerator.getInstance(BIOMETRIC_KEY_ALGORITHM, BIOMETRIC_KEYSTORE)
this.cipher = Cipher.getInstance(
BIOMETRIC_KEY_ALGORITHM + "/"
+ BIOMETRIC_BLOCKS_MODES + "/"
+ BIOMETRIC_ENCRYPTION_PADDING)
this.cryptoObject = BiometricPrompt.CryptoObject(cipher!!)
isBiometricInit = true
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize the keystore", e)
isBiometricInit = false
biometricUnlockCallback?.onBiometricException(e)
}
}
}
private fun getSecretKey(): SecretKey? {
if (!isFingerprintInitialized) {
return null
}
try {
// Create new key if needed
keyStore?.let { keyStore ->
keyStore.load(null)
try {
if (!keyStore.containsAlias(BIOMETRIC_KEYSTORE_KEY)) {
// Set the alias of the entry in Android KeyStore where the key will appear
// and the constrains (purposes) in the constructor of the Builder
keyGenerator?.init(
KeyGenParameterSpec.Builder(
BIOMETRIC_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
// Require the user to authenticate with a fingerprint to authorize every use
// of the key
.setUserAuthenticationRequired(true)
.build())
keyGenerator?.generateKey()
}
} catch (e: Exception) {
Log.e(TAG, "Unable to create a key in keystore", e)
biometricUnlockCallback?.onBiometricException(e)
}
return keyStore.getKey(BIOMETRIC_KEYSTORE_KEY, null) as SecretKey?
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve the key in keystore", e)
biometricUnlockCallback?.onBiometricException(e)
}
return null
}
fun initEncryptData(actionIfCypherInit
: (biometricPrompt: BiometricPrompt?,
cryptoObject: BiometricPrompt.CryptoObject?,
promptInfo: BiometricPrompt.PromptInfo)->Unit) {
if (!isFingerprintInitialized) {
return
}
try {
getSecretKey()?.let { secretKey ->
cipher?.init(Cipher.ENCRYPT_MODE, secretKey)
initBiometricPrompt()
actionIfCypherInit.invoke(biometricPrompt, cryptoObject, promptInfoStoreCredential)
}
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
deleteEntryKey()
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
biometricUnlockCallback?.onInvalidKeyException(invalidKeyException)
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize encrypt data", e)
biometricUnlockCallback?.onBiometricException(e)
}
}
fun encryptData(value: String) {
if (!isFingerprintInitialized) {
return
}
try {
val encrypted = cipher?.doFinal(value.toByteArray())
val encryptedBase64 = Base64.encodeToString(encrypted, Base64.NO_WRAP)
// passes updated iv spec on to callback so this can be stored for decryption
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
val ivSpecValue = Base64.encodeToString(spec.iv, Base64.NO_WRAP)
biometricUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to encrypt data", e)
biometricUnlockCallback?.onBiometricException(e)
}
}
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
: (biometricPrompt: BiometricPrompt?,
cryptoObject: BiometricPrompt.CryptoObject?,
promptInfo: BiometricPrompt.PromptInfo)->Unit) {
if (!isFingerprintInitialized) {
return
}
try {
// important to restore spec here that was used for decryption
val iv = Base64.decode(ivSpecValue, Base64.NO_WRAP)
val spec = IvParameterSpec(iv)
getSecretKey()?.let { secretKey ->
cipher?.init(Cipher.DECRYPT_MODE, secretKey, spec)
initBiometricPrompt()
actionIfCypherInit.invoke(biometricPrompt, cryptoObject, promptInfoExtractCredential)
}
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
deleteEntryKey()
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
biometricUnlockCallback?.onInvalidKeyException(invalidKeyException)
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize decrypt data", e)
biometricUnlockCallback?.onBiometricException(e)
}
}
fun decryptData(encryptedValue: String) {
if (!isFingerprintInitialized) {
return
}
try {
// actual decryption here
val encrypted = Base64.decode(encryptedValue, Base64.NO_WRAP)
cipher?.doFinal(encrypted)?.let { decrypted ->
biometricUnlockCallback?.handleDecryptedResult(String(decrypted))
}
} catch (badPaddingException: BadPaddingException) {
Log.e(TAG, "Unable to decrypt data", badPaddingException)
biometricUnlockCallback?.onInvalidKeyException(badPaddingException)
} catch (e: Exception) {
Log.e(TAG, "Unable to decrypt data", e)
biometricUnlockCallback?.onBiometricException(e)
}
}
fun deleteEntryKey() {
try {
keyStore?.load(null)
keyStore?.deleteEntry(BIOMETRIC_KEYSTORE_KEY)
} catch (e: Exception) {
Log.e(TAG, "Unable to delete entry key in keystore", e)
biometricUnlockCallback?.onBiometricException(e)
}
}
fun setAuthenticationCallback(authenticationCallback: BiometricPrompt.AuthenticationCallback) {
this.authenticationCallback = authenticationCallback
}
@Synchronized
fun initBiometricPrompt() {
if (biometricPrompt == null) {
authenticationCallback?.let {
biometricPrompt = BiometricPrompt(context, Executors.newSingleThreadExecutor(), it)
}
}
}
interface BiometricUnlockErrorCallback {
fun onInvalidKeyException(e: Exception)
fun onBiometricException(e: Exception)
}
interface BiometricUnlockCallback : BiometricUnlockErrorCallback {
fun handleEncryptedResult(encryptedValue: String, ivSpec: String)
fun handleDecryptedResult(decryptedValue: String)
}
companion object {
private val TAG = BiometricUnlockDatabaseHelper::class.java.name
private const val BIOMETRIC_KEYSTORE = "AndroidKeyStore"
private const val BIOMETRIC_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
private const val BIOMETRIC_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
private const val BIOMETRIC_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
private const val BIOMETRIC_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
/**
* Remove entry key in keystore
*/
fun deleteEntryKeyInKeystoreForBiometric(context: FragmentActivity,
biometricUnlockCallback: BiometricUnlockErrorCallback) {
val fingerPrintHelper = BiometricUnlockDatabaseHelper(context, object : BiometricUnlockCallback {
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {}
override fun handleDecryptedResult(decryptedValue: String) {}
override fun onInvalidKeyException(e: Exception) {
biometricUnlockCallback.onInvalidKeyException(e)
}
override fun onBiometricException(e: Exception) {
biometricUnlockCallback.onBiometricException(e)
}
})
fingerPrintHelper.deleteEntryKey()
}
}
}

View File

@@ -17,14 +17,14 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.fingerprint
package com.kunzisoft.keepass.biometric
import android.content.Context
import android.graphics.drawable.Animatable2
import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import android.support.annotation.RequiresApi
import androidx.annotation.RequiresApi
import android.widget.ImageView
import com.kunzisoft.keepass.R
@@ -39,19 +39,23 @@ class FingerPrintAnimatedVector(context: Context, imageView: ImageView) {
imageView.setImageDrawable(scanFingerprint)
}
private var animationCallback = object : Animatable2.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable) {
if (!scanFingerprint.isRunning)
scanFingerprint.start()
}
}
fun startScan() {
scanFingerprint.registerAnimationCallback(object : Animatable2.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable) {
if (!scanFingerprint.isRunning)
scanFingerprint.start()
}
})
scanFingerprint.registerAnimationCallback(animationCallback)
if (!scanFingerprint.isRunning)
scanFingerprint.start()
}
fun stopScan() {
scanFingerprint.unregisterAnimationCallback(animationCallback)
if (scanFingerprint.isRunning)
scanFingerprint.stop()
}

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