Compare commits

...

244 Commits

Author SHA1 Message Date
J-Jamet
7182c2e66d Merge branch 'release/2.5RC1' 2020-04-12 20:32:03 +02:00
J-Jamet
503316bc70 Fix navigation bar color for light theme 2020-04-12 20:08:39 +02:00
J-Jamet
d5af59f2c7 Merge branch 'translations' into release/2.5RC1 2020-04-12 20:03:44 +02:00
J-Jamet
cb3ac1ad3a version 2.5RC1 2020-04-12 19:36:51 +02:00
J-Jamet
e161080e4c Fix special button 2020-04-11 17:36:24 +02:00
J-Jamet
ceab7f917b Upgrade CHANGELOG 2020-04-11 16:21:27 +02:00
J-Jamet
41d8066c4c Fix autofill bad recognition for key-value format #513 2020-04-11 16:16:50 +02:00
J-Jamet
e373cbd776 File manager explanation link 2020-04-11 12:23:46 +02:00
J-Jamet
05ea6b6b10 Change background color and fix bottom button 2020-04-10 20:37:56 +02:00
J-Jamet
28eed3ae71 New file selection layout 2020-04-10 20:15:08 +02:00
Mert Sezer
535c67ac9b Translated using Weblate (Turkish)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-10 16:09:45 +02:00
J-Jamet
68027a6e15 Linkfy notes #426 2020-04-10 13:22:26 +02:00
J-Jamet
12dea6b499 Add new autofill hint for username 2020-04-10 12:32:38 +02:00
J-Jamet
d8bd078a02 Fix custom fields duplication #257 2020-04-10 11:32:23 +02:00
J-Jamet
439bc109b0 Update CHANGELOG 2020-04-09 21:42:16 +02:00
J-Jamet
b1ec93ceb5 Add navigation bar color #510 2020-04-09 21:38:32 +02:00
J-Jamet
44b9aa0e48 Style black as separate file 2020-04-09 21:31:48 +02:00
J-Jamet
0a59063027 Add scrollbar in nodes list 2020-04-09 20:33:12 +02:00
Oğuz Ersen
5db4608abd Translated using Weblate (Turkish)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-09 06:45:37 +02:00
J-Jamet
6ece2aa6cb Add autofill web domain and app id 2020-04-08 20:50:28 +02:00
J-Jamet
95ee45f666 Merge branch 'feature/Autofill_Domain' into develop 2020-04-08 17:55:42 +02:00
J-Jamet
556e90b8d8 Fix autofill response 2020-04-08 17:17:29 +02:00
J-Jamet
42841e6247 Setting for autofill automatic search 2020-04-08 16:55:30 +02:00
J-Jamet
324c82248a Refactor magikeyboard settings and add autofill settings 2020-04-08 15:44:14 +02:00
J-Jamet
94f5a47918 Catch exception during autofill parse 2020-04-08 14:11:18 +02:00
J-Jamet
3f70990956 Better autofill workflow and add autofill icon for each entry 2020-04-08 13:02:15 +02:00
J-Jamet
d77635e572 Fix autofill auto search 2020-04-07 21:01:26 +02:00
solokot
0f26f1b751 Translated using Weblate (Russian)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-04-07 01:36:50 +02:00
J-Jamet
456bc22138 Encapsulate search methods and search without UI 2020-04-06 21:43:55 +02:00
J-Jamet
b6fe91e396 Fix small warning 2020-04-06 21:36:50 +02:00
J-Jamet
0fb4d26949 Add auto search info 2020-04-06 17:36:51 +02:00
J-Jamet
3b3583a416 Retrieve application id and domain 2020-04-06 11:41:16 +02:00
J-Jamet
cac1d576c8 Upgrade Autofill algorithm 2020-04-06 10:33:44 +02:00
J-Jamet
9cba0d0a48 Message to indicate file revoked 2020-04-06 10:12:09 +02:00
J-Jamet
3bf11e9dc0 Remove unused import 2020-04-06 09:41:02 +02:00
Fabio Iacovino
edbb160ac6 Translated using Weblate (Italian)
Currently translated at 90.4% (388 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-04-06 00:22:25 +02:00
WaldiS
ee111dc63c Translated using Weblate (Polish)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-04-05 17:58:26 +02:00
Filippo De Bortoli
b06edb756a Translated using Weblate (Italian)
Currently translated at 89.7% (385 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-04-05 17:13:40 +02:00
anonymous
fd745494e0 Translated using Weblate (Italian)
Currently translated at 89.7% (385 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-04-05 17:13:40 +02:00
Fabio Iacovino
702bf3f479 Translated using Weblate (Italian)
Currently translated at 89.7% (385 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-04-05 17:13:40 +02:00
Fabio Iacovino
6adc02a91f Translated using Weblate (Italian)
Currently translated at 85.3% (366 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-04-05 17:07:29 +02:00
anonymous
55bae5a130 Translated using Weblate (Italian)
Currently translated at 85.3% (366 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-04-05 17:07:29 +02:00
Filippo De Bortoli
1aae817b17 Translated using Weblate (Italian)
Currently translated at 85.3% (366 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-04-05 17:07:29 +02:00
Oğuz Ersen
8afc8c23fb Translated using Weblate (Turkish)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-05 03:01:39 +02:00
Allan Nordhøy
36e473b139 Translated using Weblate (Norwegian Bokmål)
Currently translated at 76.6% (329 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-04-05 03:01:38 +02:00
Allan Nordhøy
e529723f86 Translated using Weblate (English)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-04-05 03:01:38 +02:00
Dwhite Reeves
6c5112c142 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-04-05 03:01:38 +02:00
Allan Nordhøy
77ac68a603 Translated using Weblate (Dutch)
Currently translated at 77.6% (333 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2020-04-05 03:01:37 +02:00
Éfrit
e816f40872 Translated using Weblate (French)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-04-05 03:01:37 +02:00
zeritti
65313f114b Translated using Weblate (Czech)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-04-05 03:01:37 +02:00
J-Jamet
ef1f1342f5 Upgrade CHANGELOG 2020-04-04 18:49:22 +02:00
J-Jamet
9205fe6c08 Merge branch 'feature/Lock_Button' into develop 2020-04-04 18:43:03 +02:00
J-Jamet
75df3e81fe Fix style 2020-04-04 18:42:35 +02:00
J-Jamet
47fffbadb5 Setting to show the lock button 2020-04-04 18:25:48 +02:00
J-Jamet
78354473fa Fix lock button color and education screen 2020-04-04 17:53:04 +02:00
J-Jamet
8d7efb44b5 Change lock button color 2020-04-03 22:21:50 +02:00
J-Jamet
cab00b3d8c Change lock button color 2020-04-03 22:18:33 +02:00
Mert Sezer
8efab01336 Translated using Weblate (Turkish)
Currently translated at 99.5% (427 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-03 21:41:26 +02:00
Oğuz Ersen
66feb8beb4 Translated using Weblate (Turkish)
Currently translated at 99.5% (427 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-03 21:41:26 +02:00
Mert Sezer
ca4cccffeb Translated using Weblate (Turkish)
Currently translated at 99.3% (426 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-03 21:40:34 +02:00
Oğuz Ersen
51b1760c50 Translated using Weblate (Turkish)
Currently translated at 99.3% (426 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-03 21:40:33 +02:00
J-Jamet
42e1bda365 Change lock button color 2020-04-03 21:32:32 +02:00
J-Jamet
194b6b557a Add lock button elevation and border 2020-04-03 21:17:38 +02:00
Dwhite Reeves
e7bc439997 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-04-03 15:44:41 +02:00
Allan Nordhøy
ef76cce0ac Translated using Weblate (Romanian)
Currently translated at 95.3% (409 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2020-04-03 13:26:34 +02:00
Allan Nordhøy
63eec6d969 Translated using Weblate (Turkish)
Currently translated at 96.9% (416 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-03 13:26:34 +02:00
WaldiS
719ae74c06 Translated using Weblate (Polish)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-04-03 13:26:33 +02:00
Mert Sezer
37cf424eb8 Translated using Weblate (Turkish)
Currently translated at 96.9% (416 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-01 17:46:51 +02:00
anonymous
3a87f7ba9d Translated using Weblate (Turkish)
Currently translated at 96.9% (416 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-01 17:46:50 +02:00
J-Jamet
275428d825 First commit for new lock button 2020-04-01 14:57:31 +02:00
Pavel Borecki
7b9dac86ca Translated using Weblate (Czech)
Currently translated at 96.7% (415 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-04-01 03:28:42 +02:00
J-Jamet
46496ee2cc Merge branch 'damoasda-translation-update' into develop 2020-03-31 21:58:21 +02:00
J-Jamet
110cc402cc Add keyfile selection view 2020-03-31 21:48:51 +02:00
J-Jamet
587f006259 Long click to use GET_CONTENT 2020-03-31 19:46:43 +02:00
Moasda
ec45c0df81 Translated using Weblate (German)
Currently translated at 99.7% (428 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-30 22:24:51 +02:00
jan madsen
dc575aeca4 Translated using Weblate (Danish)
Currently translated at 98.3% (422 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-03-30 22:24:51 +02:00
Herbert Reiter
a3f790f000 Fix inconsistencies with translation arguments (e.g. %1$s), escape % character 2020-03-30 22:08:50 +02:00
Herbert Reiter
4e9e188d02 Add and fix German translations 2020-03-30 22:03:09 +02:00
J-Jamet
abb17efae4 Merge branch 'feature/File_Manager_Access' into develop 2020-03-30 18:17:24 +02:00
Hosted Weblate
16be990502 Merge branch 'origin/master' into Weblate. 2020-03-30 08:48:35 +02:00
anonymous
d1a496f9a3 Translated using Weblate (German)
Currently translated at 99.7% (428 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-30 03:17:18 +02:00
Allan Nordhøy
cd730fcfef Translated using Weblate (Norwegian Bokmål)
Currently translated at 76.9% (330 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-03-29 23:06:21 +02:00
Moasda
1b65bf665b Translated using Weblate (German)
Currently translated at 99.7% (428 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-29 23:06:21 +02:00
J-Jamet
6f4735790c Fix autofill for Android app #502 2020-03-29 22:00:24 +02:00
J-Jamet
040666f89d Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2020-03-29 20:19:42 +02:00
J-Jamet
227cb800c3 Merge branch 'damoasda-readme-fix' 2020-03-29 16:15:44 +02:00
Herbert Reiter
0052569a14 Fix FAQ link to wiki page 2020-03-29 15:41:49 +02:00
J-Jamet
f7561c4888 Upgrade to version 2.5beta31 2020-03-29 15:22:34 +02:00
Destiny Li
92200f19e7 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-03-29 14:09:36 +02:00
Éfrit
4c01f18a62 Translated using Weblate (French)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-03-29 14:09:36 +02:00
anonymous
a6ce3e49fe Translated using Weblate (German)
Currently translated at 99.5% (427 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-29 14:09:36 +02:00
Oliver
1b3a5d1bf6 Translated using Weblate (German)
Currently translated at 99.5% (427 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-29 14:09:35 +02:00
J-Jamet
db7bdc63c8 Fix #501 2020-03-28 18:13:09 +01:00
J-Jamet
7aca550f02 Merge branch 'develop' into feature/File_Manager_Access 2020-03-28 17:47:14 +01:00
J-Jamet
b60980b3fd Upgrade to 2.5beta31 2020-03-28 17:46:50 +01:00
J-Jamet
b578c2c584 Add write permission 2020-03-28 17:30:57 +01:00
solokot
fc45bd624e Translated using Weblate (Russian)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-27 20:06:43 +01:00
anonymous
17be3d9d2c Translated using Weblate (Russian)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-27 20:03:48 +01:00
solokot
5b3a38a7bc Translated using Weblate (Russian)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-27 20:03:47 +01:00
Hosted Weblate
4403835d50 Merge branch 'origin/master' into Weblate. 2020-03-27 18:36:55 +01:00
C. Rüdinger
b7a3d3eb46 Translated using Weblate (German)
Currently translated at 99.5% (424 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-27 18:36:54 +01:00
J-Jamet
29846b22fe Merge tag '2.5beta30' into develop
2.5beta30
2020-03-27 18:13:28 +01:00
J-Jamet
32d235e8c7 Merge branch 'release/2.5beta30' 2020-03-27 18:13:21 +01:00
J-Jamet
cb982b3513 Fix strong tag 2020-03-27 17:53:42 +01:00
J-Jamet
d7ed6c26dd Merge branch 'develop' into translations 2020-03-27 17:49:29 +01:00
J-Jamet
8ff19f7e68 First string pass according to #460 2020-03-27 17:43:31 +01:00
J-Jamet
729e062c3a Add error when create database file 2020-03-27 17:12:54 +01:00
J-Jamet
7d0340ac07 Upgrade CHANGELOG 2020-03-27 16:57:35 +01:00
J-Jamet
01960e74c1 Fix check file ANR #494 2020-03-27 16:36:57 +01:00
anonymous
8e40250985 Translated using Weblate (German)
Currently translated at 96.4% (411 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-27 00:46:37 +01:00
C. Rüdinger
8ae2edb61a Translated using Weblate (German)
Currently translated at 96.4% (411 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-27 00:46:37 +01:00
J-Jamet
0baa7bcbf1 Change discard button 2020-03-26 16:36:17 +01:00
J-Jamet
fffee48918 Upgrade version to 2.5beta30 2020-03-26 14:50:20 +01:00
J-Jamet
515abb6e14 Better UUID view 2020-03-26 14:45:51 +01:00
J-Jamet
6c1c3ff87f New method to wait 1.5 seconds after screen turns off 2020-03-26 13:38:20 +01:00
J-Jamet
5b65575c7a Add comment to ignore autocomplete off 2020-03-26 12:57:24 +01:00
J-Jamet
0f258fc5f8 Upgrade autofill algorithm 2020-03-26 12:47:32 +01:00
anonymous
206bc661dc Translated using Weblate (German)
Currently translated at 96.2% (410 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-25 23:44:32 +01:00
J-Jamet
d0e35b109e Revert : Fix Nextcloud File Upload conflict #497 2020-03-25 22:48:15 +01:00
J-Jamet
61769c4f20 Fix Nextcloud File Upload conflict #497 2020-03-25 21:07:36 +01:00
Hosted Weblate
95778ee5f4 Merge branch 'origin/master' into Weblate. 2020-03-25 12:21:35 +01:00
anonymous
155030fdca Translated using Weblate (Italian)
Currently translated at 79.5% (339 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-25 12:21:35 +01:00
Filippo De Bortoli
98237ef76c Translated using Weblate (Italian)
Currently translated at 79.5% (339 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-25 12:21:34 +01:00
J-Jamet
f0a7b38199 Merge tag '2.5beta29' into develop
2.5beta29
2020-03-25 12:06:20 +01:00
J-Jamet
9fc5e6751b Merge branch 'release/2.5beta29' 2020-03-25 12:06:08 +01:00
J-Jamet
4c1630312b Add UUID color 2020-03-25 11:46:43 +01:00
J-Jamet
d397c5c996 Add UUID ref for entry 2020-03-25 11:22:19 +01:00
J-Jamet
f6c41b5a60 Replace the strong nodes 2020-03-24 20:49:33 +01:00
J-Jamet
06eb5c01c3 Update versions 2020-03-24 20:44:50 +01:00
J-Jamet
7df49f91e8 Merge branch 'release/2.5beta29' of github.com:Kunzisoft/KeePassDX into release/2.5beta29 2020-03-24 20:10:27 +01:00
J-Jamet
96dcbb0ce7 Merge branch 'develop' into release/2.5beta29 2020-03-24 20:09:47 +01:00
J-Jamet
5f828fb986 Fix setting 2020-03-24 20:09:30 +01:00
J-Jamet
533d663938 Merge branch 'translations' into develop 2020-03-24 19:44:38 +01:00
J-Jamet
ae788503a9 Fix html_about licence 2020-03-24 19:44:15 +01:00
anonymous
cf0acd9c73 Translated using Weblate (Italian)
Currently translated at 78.4% (334 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-24 11:42:45 +01:00
Filippo De Bortoli
0857f2f1cf Translated using Weblate (Italian)
Currently translated at 78.4% (334 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-24 11:42:43 +01:00
Ema Panz
c05d412bdb Translated using Weblate (Italian)
Currently translated at 71.1% (303 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-24 11:18:14 +01:00
anonymous
c8e0ce717d Translated using Weblate (Italian)
Currently translated at 71.1% (303 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-24 11:18:14 +01:00
J-Jamet
cc3485b201 Update Readme 2020-03-23 21:10:28 +01:00
J-Jamet
81ea7080c2 Show UUID as KeePass UUID 2020-03-21 20:25:37 +01:00
J-Jamet
76ff6f5ae0 Fix reference loop 2020-03-21 19:01:05 +01:00
J-Jamet
0c0d0b7a6f Encapsulate readonly for launch method 2020-03-21 17:23:17 +01:00
J-Jamet
ec8cf1f6b7 Move autofill setting up 2020-03-21 17:16:49 +01:00
J-Jamet
da44310d1b Fix focus validation button 2020-03-21 17:03:36 +01:00
J-Jamet
4bd9c84bb0 Add dialog to validate discard entry changes 2020-03-21 16:52:50 +01:00
J-Jamet
3b1269a770 Fix bind listeners 2020-03-21 16:12:07 +01:00
J-Jamet
7c986ccee8 Fix card_view_margin issue 2020-03-21 16:08:23 +01:00
J-Jamet
903bad8f36 Fix launch of open db service exception 2020-03-21 14:36:33 +01:00
J-Jamet
4b9577437c Hide add button when nodes are selected 2020-03-21 12:27:18 +01:00
J-Jamet
c316011fbc Show toast on invalid key exception 2020-03-21 12:16:45 +01:00
J-Jamet
3fb1f18c22 Education screen for OTP and entry edit menu 2020-03-19 19:47:51 +01:00
J-Jamet
53935058f5 Add OTP icon 2020-03-19 19:16:38 +01:00
J-Jamet
a3860c9581 Generate password icon as dice 2020-03-19 18:34:14 +01:00
J-Jamet
dc20899d26 Update CHANGELOG 2020-03-19 17:48:56 +01:00
J-Jamet
62ac3ddb75 Merge branch 'feature/Autofill_Improvement' into develop 2020-03-19 17:23:24 +01:00
J-Jamet
b792a61bf9 Improve hint "on" and "off" recognition 2020-03-19 15:18:06 +01:00
J-Jamet
aae9f9e1cb Warning when cancel autofill 2020-03-19 14:59:33 +01:00
J-Jamet
d098bf5e6a Upgrade Autofill algorithm 2020-03-19 14:35:22 +01:00
J-Jamet
b0e8a3ecd9 Add expiration datetime 2020-03-18 12:03:12 +01:00
J-Jamet
4efa684022 Merge branch 'feature/Edit_Expired_Date' into develop 2020-03-18 11:53:46 +01:00
J-Jamet
f2c8082990 Fix date picker kitkat issue 2020-03-18 11:53:21 +01:00
J-Jamet
1abba80045 Fix dialog theme 2020-03-18 11:45:52 +01:00
J-Jamet
68564a2b75 Merge branch 'develop' into feature/Edit_Expired_Date 2020-03-18 09:46:28 +01:00
J-Jamet
385b701b38 Fix cardview margin 2020-03-18 09:42:36 +01:00
Éfrit
11c9a1d707 Translated using Weblate (French)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-03-17 00:42:55 +01:00
J-Jamet
c5aef6b561 New entry edit tool menu style 2020-03-15 12:21:26 +01:00
J-Jamet
dfcf73cfd0 Merge branch 'develop' into feature/Edit_Expired_Date 2020-03-14 21:15:58 +01:00
J-Jamet
7fc9389700 Always show menu 2020-03-14 21:15:37 +01:00
J-Jamet
d1af7349bc Edit button as validation 2020-03-14 20:51:47 +01:00
J-Jamet
0fd955197d Expires selection 2020-03-14 20:16:36 +01:00
Destiny Li
cbf33507d1 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-03-14 17:37:29 +01:00
WaldiS
bc60a5d97e Translated using Weblate (Polish)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-03-14 17:37:27 +01:00
zeritti
57596b2991 Translated using Weblate (Czech)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-03-14 17:37:23 +01:00
solokot
43b3602a52 Translated using Weblate (Russian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-14 17:37:22 +01:00
J-Jamet
c09ec961b8 Merge branch 'feature/Entry_Edit_BottomBar' into develop 2020-03-14 11:00:47 +01:00
J-Jamet
d140b453b2 Merge branch 'feature/Entry_Edit_BottomBar' into develop 2020-03-14 10:36:52 +01:00
J-Jamet
9eb42636ec Change read icons 2020-03-13 21:58:32 +01:00
J-Jamet
ee2d663fce New icons for entry edit tools 2020-03-13 21:33:38 +01:00
J-Jamet
8e83615a22 Add Edit Toolbar menu on top 2020-03-13 17:06:09 +01:00
J-Jamet
0ff129c5ca Edit Entry with Card View 2020-03-13 11:06:56 +01:00
WaldiS
e49858439f Translated using Weblate (Polish)
Currently translated at 99.5% (424 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-03-12 20:33:30 +01:00
zeritti
3df07f7f47 Translated using Weblate (Czech)
Currently translated at 99.5% (424 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-03-12 20:33:30 +01:00
Aurel F
ff9e179593 Translated using Weblate (Romanian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2020-03-12 20:33:28 +01:00
J-Jamet
7539fee04b Add BottomBar in EntryEdit 2020-03-12 19:15:03 +01:00
J-Jamet
71a339a58f Suppress small warning 2020-03-12 17:15:17 +01:00
J-Jamet
9ef2ea016b Suppress deprecation for Keyboard 2020-03-12 17:14:46 +01:00
J-Jamet
de4936a16d Set nullable getSystemService 2020-03-12 17:05:53 +01:00
J-Jamet
574d2b8904 Wait 3 seconds before the lock after the screen turns off #59 2020-03-12 16:56:46 +01:00
J-Jamet
1f3f7634e7 Allow empty title in entries #423 2020-03-12 14:41:02 +01:00
J-Jamet
3c0725baff Fix remember key file option 2020-03-12 14:24:33 +01:00
J-Jamet
b0e1411012 Update CHANGELOG 2020-03-12 14:20:48 +01:00
J-Jamet
39daf4714d Merge branch 'feature/Delete_Registered_Keyfile_New_Credentials' into develop 2020-03-12 14:17:35 +01:00
J-Jamet
4706afa823 Fix credentials options 2020-03-12 14:16:30 +01:00
J-Jamet
25977d389d Add DAO command to delete registered keyfile 2020-03-12 13:18:36 +01:00
J-Jamet
f760110569 default_database_path_key in strings.xml 2020-03-12 09:28:46 +01:00
J-Jamet
133e78fe97 Update CHANGELOG 2020-03-12 09:20:29 +01:00
J-Jamet
d92e0c8620 Fix magikeyboard lock 2020-03-11 19:17:51 +01:00
J-Jamet
62fdb69d6b Fix small element 2020-03-11 18:56:45 +01:00
J-Jamet
def57c9fb2 Merge branch 'feature/Lock_Database' issue 483 2020-03-11 18:42:34 +01:00
J-Jamet
21c9c898c3 Add lock timer in service, notification remains lock to capture the broadcast 2020-03-11 18:30:11 +01:00
J-Jamet
1f03c922c2 Encapsulate lock broadcast 2020-03-11 16:46:44 +01:00
J-Jamet
3f6ae6bdac Fix node update #487 2020-03-11 12:42:18 +01:00
jan madsen
60615ee1eb Translated using Weblate (Danish)
Currently translated at 98.5% (420 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-03-10 20:23:08 +01:00
J-Jamet
92b0d1bfa9 Upgrade to version 2.5beta29 2020-03-10 19:02:37 +01:00
J-Jamet
237988dc1f Merge tag '2.5beta28' into develop
2.5beta28
2020-03-10 19:00:08 +01:00
J-Jamet
a846ec29ca Merge branch 'release/2.5beta28' 2020-03-10 18:59:59 +01:00
J-Jamet
4533e96bff Upgrade CHANGELOG 2020-03-10 18:48:57 +01:00
J-Jamet
0a401c3ac9 Fix strings 2020-03-10 18:41:35 +01:00
J-Jamet
468abaf077 Fix strings 2020-03-10 18:29:19 +01:00
J-Jamet
4ccf2f641c Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into develop 2020-03-10 18:14:14 +01:00
Dominik Baláž
34eb2785cf Translated using Weblate (Slovak)
Currently translated at 21.5% (92 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2020-03-10 17:21:26 +01:00
anonymous
09dbfe323e Translated using Weblate (Slovak)
Currently translated at 21.5% (92 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2020-03-10 17:21:26 +01:00
Destiny Li
1f06c5b425 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-03-10 11:33:21 +01:00
Destiny Li
b98e089f7a Translated using Weblate (Chinese (Traditional))
Currently translated at 48.3% (206 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2020-03-10 11:33:20 +01:00
Aurel F
a0ad06ed0a Translated using Weblate (Romanian)
Currently translated at 40.8% (174 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2020-03-10 11:33:18 +01:00
solokot
ec63365429 Translated using Weblate (Russian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-10 11:33:10 +01:00
J-Jamet
2cb85e4346 Update items after appearance change #452 2020-03-10 08:58:22 +01:00
J-Jamet
0d7c479c51 Merge branch 'feature/Migration_SDK_29' into develop 2020-03-10 08:40:53 +01:00
J-Jamet
5a6c21e662 Try to fix read only bug #480 2020-03-10 08:38:49 +01:00
J-Jamet
d6cadac98f Fix small warning 2020-03-08 13:22:11 +01:00
J-Jamet
dac2fc2c37 Use PreferenceManager from AndroidX 2020-03-08 13:10:16 +01:00
J-Jamet
0fb45cef0d Encapsulate preferences 2020-03-08 13:04:25 +01:00
anonymous
5ebdbd4003 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-03-08 12:08:21 +01:00
Destiny Li
b30f1023cb Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-03-08 12:08:21 +01:00
J-Jamet
e5f65a4d1e Fix % issue in string 2020-03-08 11:48:47 +01:00
J-Jamet
ab42a65aa4 Fix null issues 2020-03-08 11:41:13 +01:00
J-Jamet
e351456bfe First pass migration SDK 29 2020-03-08 11:03:21 +01:00
J-Jamet
452e68b08f Fix C cast warning 2020-03-08 10:52:29 +01:00
J-Jamet
d65beed7a1 Fix small warnings 2020-03-08 10:49:06 +01:00
J-Jamet
f5a5a0e8cb Add generated JSON for database 2020-03-08 10:41:52 +01:00
solokot
98380a0906 Translated using Weblate (Russian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-08 10:20:37 +01:00
J-Jamet
22fe7508f3 Fix doForEachChild warning 2020-03-08 10:16:31 +01:00
J-Jamet
c8e241fc76 Fix Room incremental warning 2020-03-08 09:50:32 +01:00
J-Jamet
2c943e00d0 Fix writeEnum warning 2020-03-08 09:50:07 +01:00
J-Jamet
7ddb83b72d Fix DateFormatter warning 2020-03-08 09:36:45 +01:00
Kunzisoft
50bac01699 Translated using Weblate (French)
Currently translated at 99.7% (425 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-03-08 09:33:23 +01:00
Aurel F
e0a92dfadd Translated using Weblate (Romanian)
Currently translated at 12.2% (52 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2020-03-08 09:33:22 +01:00
solokot
b50c951091 Translated using Weblate (Russian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-08 09:33:17 +01:00
Kunzisoft
2f589a95a9 Translated using Weblate (English)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-03-08 09:33:15 +01:00
J-Jamet
53eac86a95 Update AndroidX dependencies and fix small warning 2020-03-08 09:17:18 +01:00
J-Jamet
9412f8955e Update kotlin to 1.3.61 2020-03-08 09:07:14 +01:00
J-Jamet
71c98d82b1 Update gradle to 5.6.4 2020-03-08 09:00:56 +01:00
J-Jamet
42c1a925b4 Upgrade app to 2.54beta28 2020-03-08 08:59:41 +01:00
J-Jamet
1e84534ffd Merge tag '2.5beta27' into develop
2.5beta27
2020-03-07 14:41:37 +01:00
190 changed files with 5041 additions and 2273 deletions

View File

@@ -1,4 +1,32 @@
KeepassDX (2.5beta27) KeePassDX(2.5beta31)
* Add write permission to keep compatibility with old file managers
* Fix autofill for apps
* Auto search for autofill
* New keyfile input
* Icon to hide keyfile input
* New lock button
* Setting to hide lock button in user interface
* Clickable links in notes
* Fix autofill for key-value pairs
KeePassDX(2.5beta30)
* Fix Lock after screen off (wait 1.5 seconds)
* Upgrade autofill algorithm
* Fix ANR during file verifications
KeePassDX(2.5beta29)
* Upgrade autofill algorithm
* Delete registered KeyFile after save new credentials
* Fix title and username entry view refresh after an update
* Fix database lock request (open notification always active)
* Allow empty title in entries
* Add expiration datetime
KeePassDX(2.5beta28)
* Fix read only database
* Upgrade to Android SDK 29
KeePassDX (2.5beta27)
* New setting to hide broken links * New setting to hide broken links
* Show URL when title is empty * Show URL when title is empty
* Setting to open search field at database opening * Setting to open search field at database opening
@@ -7,7 +35,7 @@ KeepassDX (2.5beta27)
* Fix appearance refresh settings * Fix appearance refresh settings
* Sort optimization * Sort optimization
KeepassDX (2.5.0.0beta26) KeePassDX (2.5.0.0beta26)
* Download attachments * Download attachments
* Change file size string format * Change file size string format
* Prevent screenshot for all screen * Prevent screenshot for all screen
@@ -20,7 +48,7 @@ KeepassDX (2.5.0.0beta26)
* Fix dates * Fix dates
* Fix UUID message for Database v1 * Fix UUID message for Database v1
KeepassDX (2.5.0.0beta25) KeePassDX (2.5.0.0beta25)
* Setting for Recycle Bin * Setting for Recycle Bin
* Fix Recycle bin issues * Fix Recycle bin issues
* Fix TOTP * Fix TOTP
@@ -28,7 +56,7 @@ KeepassDX (2.5.0.0beta25)
* Fix update group * Fix update group
* Fix OOM * Fix OOM
KeepassDX (2.5.0.0beta24) KeePassDX (2.5.0.0beta24)
* Add OTP (HOTP / TOTP) * Add OTP (HOTP / TOTP)
* Add settings (Color, Security, Master Key) * Add settings (Color, Security, Master Key)
* Show history of each entry * Show history of each entry
@@ -38,7 +66,7 @@ KeepassDX (2.5.0.0beta24)
* Open/Save database as service / Add persistent notification * Open/Save database as service / Add persistent notification
* Fix settings / edit group / small bugs * Fix settings / edit group / small bugs
KeepassDX (2.5.0.0beta23) KeePassDX (2.5.0.0beta23)
* New, more secure database creation workflow * New, more secure database creation workflow
* Recognize more database files * Recognize more database files
* Add alias for history files (WARNING: history is erased) * Add alias for history files (WARNING: history is erased)
@@ -47,14 +75,14 @@ KeepassDX (2.5.0.0beta23)
* Fix OOM with KeyFile * Fix OOM with KeyFile
* Fix small issues * Fix small issues
KeepassDX (2.5.0.0beta22) KeePassDX (2.5.0.0beta22)
* Rebuild code for actions * Rebuild code for actions
* Add UUID as entry view * Add UUID as entry view
* Fix bug with natural order * Fix bug with natural order
* Fix number of entries in databaseV1 * Fix number of entries in databaseV1
* New entry views * New entry views
KeepassDX (2.5.0.0beta21) KeePassDX (2.5.0.0beta21)
* Fix nested groups no longer visible in V1 databases * Fix nested groups no longer visible in V1 databases
* Improved data import algorithm for V1 databases * Improved data import algorithm for V1 databases
* Add natural database sort * Add natural database sort
@@ -62,10 +90,10 @@ KeepassDX (2.5.0.0beta21)
* Fix button disabled with only KeyFile * Fix button disabled with only KeyFile
* Show the number of entries in a group * Show the number of entries in a group
KeepassDX (2.5.0.0beta20) KeePassDX (2.5.0.0beta20)
* Fix a major bug that displays an entry history * Fix a major bug that displays an entry history
KeepassDX (2.5.0.0beta19) KeePassDX (2.5.0.0beta19)
* Add lock button always visible * Add lock button always visible
* New connection workflow * New connection workflow
* Code refactored in Kotlin * Code refactored in Kotlin
@@ -76,7 +104,7 @@ KeepassDX (2.5.0.0beta19)
* Fix memory when load database * Fix memory when load database
* Fix small bugs * Fix small bugs
KeepassDX (2.5.0.0beta18) KeePassDX (2.5.0.0beta18)
* New recent databases views * New recent databases views
* New information dialog * New information dialog
* Custom fields for the Magikeyboard * Custom fields for the Magikeyboard
@@ -85,10 +113,10 @@ KeepassDX (2.5.0.0beta18)
* Fix memory when opening the database * Fix memory when opening the database
* Memory management for attachments * Memory management for attachments
KeepassDX (2.5.0.0beta17) KeePassDX (2.5.0.0beta17)
* Fix font and search * Fix font and search
KeepassDX (2.5.0.0beta16) KeePassDX (2.5.0.0beta16)
* New search in a single fragment * New search in a single fragment
* Search suggestions * Search suggestions
* Added the display of usernames * Added the display of usernames
@@ -96,20 +124,20 @@ KeepassDX (2.5.0.0beta16)
* Fix read-only mode * Fix read-only mode
* Fix parcelable / toolbar / back * Fix parcelable / toolbar / back
KeepassDX (2.5.0.0beta15) KeePassDX (2.5.0.0beta15)
* Read only mode * Read only mode
* Best group recovery for the navigation fragment * Best group recovery for the navigation fragment
* Fix copies in notifications * Fix copies in notifications
* Fix orientation * Fix orientation
* Added translations * Added translations
KeepassDX (2.5.0.0beta14) KeePassDX (2.5.0.0beta14)
* Optimize all the memory with parcelables / fix search * Optimize all the memory with parcelables / fix search
KeepassDX (2.5.0.0beta13) KeePassDX (2.5.0.0beta13)
* Fix memory issue with parcelable (crash in beta12 version) * Fix memory issue with parcelable (crash in beta12 version)
KeepassDX (2.5.0.0beta12) KeePassDX (2.5.0.0beta12)
* Added the Magikeyboard to fill the forms (settings still in development) * Added the Magikeyboard to fill the forms (settings still in development)
* Added move and copy for groups and entries * Added move and copy for groups and entries
* New navigation in a single screen / new animations between activities * New navigation in a single screen / new animations between activities
@@ -122,10 +150,10 @@ KeepassDX (2.5.0.0beta12)
* Fix the fingerprint recognition (WARNING : The keystore is reinit, you must delete the old keys) * Fix the fingerprint recognition (WARNING : The keystore is reinit, you must delete the old keys)
* Fix small bugs * Fix small bugs
KeepassDX (2.5.0.0beta11) KeePassDX (2.5.0.0beta11)
* Fix crash in beta10 version * Fix crash in beta10 version
KeepassDX (2.5.0.0beta10) KeePassDX (2.5.0.0beta10)
* Dynamically change Algorithm and Key Derivation Function in settings * Dynamically change Algorithm and Key Derivation Function in settings
* Upgrade translations * Upgrade translations
* New red volcano theme, fix classic dark theme * New red volcano theme, fix classic dark theme
@@ -133,7 +161,7 @@ KeepassDX (2.5.0.0beta10)
* Update fingerprint state with checkbox * Update fingerprint state with checkbox
* Fix bugs * Fix bugs
KeepassDX (2.5.0.0beta9) KeePassDX (2.5.0.0beta9)
* Education Screens to learn how to use the app * Education Screens to learn how to use the app
* New designs * New designs
* New custom font for character visibility * New custom font for character visibility
@@ -142,9 +170,9 @@ KeepassDX (2.5.0.0beta9)
* Change setting organisation * Change setting organisation
* Pro version * Pro version
KeepassDX (2.5.0.0beta8) KeePassDX (2.5.0.0beta8)
* Hide custom entries protected * Hide custom entries protected
* Best management of field references (https://keepass.info/help/base/fieldrefs.html) * Best management of field references (https://KeePass.info/help/base/fieldrefs.html)
* Change database / default settings * Change database / default settings
* Add Autofill for search * Add Autofill for search
* Add sorting by last access and by creation time * Add sorting by last access and by creation time
@@ -152,7 +180,7 @@ KeepassDX (2.5.0.0beta8)
* Refactor old code * Refactor old code
* Fix bugs * Fix bugs
KeepassDX (2.5.0.0beta7) KeePassDX (2.5.0.0beta7)
* Rebuild Notifications * Rebuild Notifications
* Change links to https * Change links to https
* Add extended Ascii (ñæËÌÂÝÜ...) * Add extended Ascii (ñæËÌÂÝÜ...)
@@ -161,10 +189,10 @@ KeepassDX (2.5.0.0beta7)
* Add setting to prevent the password copy * Add setting to prevent the password copy
* Fix bugs * Fix bugs
KeepassDX (2.5.0.0beta6) KeePassDX (2.5.0.0beta6)
* Fix crash * Fix crash
KeepassDX (2.5.0.0beta5) KeePassDX (2.5.0.0beta5)
* Autofill (Android O) * Autofill (Android O)
* Deletion for group * Deletion for group
* New sorts with (Asc/Dsc, Groups before or after) * New sorts with (Asc/Dsc, Groups before or after)
@@ -185,7 +213,7 @@ KeepassDX (2.5.0.0beta5)
* Fix many small bugs * Fix many small bugs
* Add recycle bin setting (not yet accessible) * Add recycle bin setting (not yet accessible)
KeepassDX (2.5.0.0beta4) KeePassDX (2.5.0.0beta4)
* Show only file name * Show only file name
* Setting for full path * Setting for full path
* Add information for each database file * Add information for each database file
@@ -194,7 +222,7 @@ KeepassDX (2.5.0.0beta4)
* Delete view assignment for fingerprint opening * Delete view assignment for fingerprint opening
* Merge KeePassDroid 2.2.1 * Merge KeePassDroid 2.2.1
KeepassDX (2.5.0.0beta3) KeePassDX (2.5.0.0beta3)
* New database workflow with new screens and folder selection * New database workflow with new screens and folder selection
* Settings for default password generation * Settings for default password generation
* Fingerprint dialog for explanations * Fingerprint dialog for explanations
@@ -205,17 +233,17 @@ KeepassDX (2.5.0.0beta3)
* Merge KeePassDroid 2.2.0.9 * Merge KeePassDroid 2.2.0.9
* Add corruption fix mode * Add corruption fix mode
KeepassDX (2.5.0.0beta2) KeePassDX (2.5.0.0beta2)
* Remove libs for F-Droid * Remove libs for F-Droid
KeepassDX (2.5.0.0beta1) KeePassDX (2.5.0.0beta1)
* Fork KeepassDroid * Fork KeePassDroid
* Add Material Design * Add Material Design
* Add Light and Night theme * Add Light and Night theme
* Min API is 14 * Min API is 14
* Solve bug for fingerprint * Solve bug for fingerprint
* Update French translation * Update French translation
* Change donation (see KeepassDroid to contribute on both projects) * Change donation (see KeePassDroid to contribute on both projects)
KeePassDroid (2.2.1) KeePassDroid (2.2.1)
* Fix kdbx4 date corruption * Fix kdbx4 date corruption
@@ -476,7 +504,7 @@ KeePassDroid (1.9.10)
KeePassDroid (1.9.9) KeePassDroid (1.9.9)
* Go back to explicitly storing blank fields in the database * Go back to explicitly storing blank fields in the database
(works around bug in keepassx) (works around bug in KeePassx)
* Add support for native code on MIPS architectures * Add support for native code on MIPS architectures
* Adding Vibrate permission. On some devices notifications fail * Adding Vibrate permission. On some devices notifications fail
without the vibrate permission. without the vibrate permission.

View File

@@ -30,7 +30,7 @@ KeePassDX is a **free open source password manager for Android**, which helps yo
Yes, KeePassDX is under **free license (OSI certified)** and **without advertising**. You can have a look at its full source and check whether the encryption algorithms are implemented correctly. Yes, KeePassDX is under **free license (OSI certified)** and **without advertising**. You can have a look at its full source and check whether the encryption algorithms are implemented correctly.
*Note : If you access the application from a store, visual features may not be available to incentivize the contribution to the work of open source projects. These optional visuals are accessible after a donation (and a small congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free. If you contribute to the project and you do not have access to the themes, do not hesitate to contact me at [contact@kunzisoft.com](contact@kunzisoft.com), I will give you the procedure.* *Note : If you access the application from a store, visual styles may not be available to encourage contribution to the work of open source projects. These optional styles are accessible after a contribution (and a congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free. If you contribute to the project and you do not have access to the themes, do not hesitate to contact me at [contact@kunzisoft.com](contact@kunzisoft.com), I will give you the procedure.*
## Contributions ## Contributions
@@ -53,9 +53,9 @@ You can contribute in different ways to help us on our work.
alt="Get it on Google Play" alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free) height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
## F.A.Q. ## Frequently Asked Questions
Other questions? You can read the [F.A.Q.](https://github.com/Kunzisoft/KeePassDX/wiki/F.A.Q.) Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ)
## Other devices ## Other devices

View File

@@ -4,21 +4,28 @@ apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
android { android {
compileSdkVersion 28 compileSdkVersion 29
buildToolsVersion '28.0.3' buildToolsVersion '29.0.3'
defaultConfig { defaultConfig {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 14 minSdkVersion 14
targetSdkVersion 28 targetSdkVersion 29
versionCode = 27 versionCode = 31
versionName = "2.5beta27" versionName = "2.5RC1"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"
testInstrumentationRunner = "android.test.InstrumentationTestRunner" testInstrumentationRunner = "android.test.InstrumentationTestRunner"
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}" buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
kapt {
arguments {
arg("room.incremental", "true")
arg("room.schemaLocation", "$projectDir/schemas".toString())
}
}
} }
externalNativeBuild { externalNativeBuild {
@@ -79,7 +86,7 @@ android {
} }
def spongycastleVersion = "1.58.0.0" def spongycastleVersion = "1.58.0.0"
def room_version = "2.2.1" def room_version = "2.2.5"
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@@ -88,7 +95,10 @@ dependencies {
implementation 'androidx.legacy:legacy-preference-v14:1.0.0' implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.biometric:biometric:1.0.0' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.0.1'
implementation "androidx.core:core-ktx:1.2.0"
// To upgrade with style
implementation 'com.google.android.material:material:1.0.0' implementation 'com.google.android.material:material:1.0.0'
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"

View File

@@ -0,0 +1,84 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "56438e5f7372ef3e36e33b782aed245d",
"entities": [
{
"tableName": "file_database_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `database_alias` TEXT NOT NULL, `keyfile_uri` TEXT, `updated` INTEGER NOT NULL, PRIMARY KEY(`database_uri`))",
"fields": [
{
"fieldPath": "databaseUri",
"columnName": "database_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "databaseAlias",
"columnName": "database_alias",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "keyFileUri",
"columnName": "keyfile_uri",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "updated",
"columnName": "updated",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"database_uri"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "cipher_database",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `encrypted_value` TEXT NOT NULL, `specs_parameters` TEXT NOT NULL, PRIMARY KEY(`database_uri`))",
"fields": [
{
"fieldPath": "databaseUri",
"columnName": "database_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "encryptedValue",
"columnName": "encrypted_value",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "specParameters",
"columnName": "specs_parameters",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"database_uri"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '56438e5f7372ef3e36e33b782aed245d')"
]
}
}

View File

@@ -15,7 +15,6 @@
<uses-permission <uses-permission
android:name="android.permission.VIBRATE"/> android:name="android.permission.VIBRATE"/>
<uses-permission <uses-permission
android:maxSdkVersion="18"
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application <application
@@ -138,12 +137,12 @@
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity" <activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
android:configChanges="keyboardHidden" /> android:configChanges="keyboardHidden" />
<activity android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" /> <activity android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
<activity android:name="com.kunzisoft.keepass.settings.SettingsAutofillActivity" /> <activity android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
<activity android:name="com.kunzisoft.keepass.magikeyboard.KeyboardLauncherActivity" <activity android:name="com.kunzisoft.keepass.magikeyboard.KeyboardLauncherActivity"
android:label="@string/keyboard_name" android:label="@string/keyboard_name"
android:exported="true"> android:exported="true">
</activity> </activity>
<activity android:name="com.kunzisoft.keepass.settings.MagikIMESettings" <activity android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
android:label="@string/keyboard_setting_label"> android:label="@string/keyboard_setting_label">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>

View File

@@ -52,7 +52,6 @@ import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.settings.SettingsAutofillActivity
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.ClipboardHelper import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
@@ -73,6 +72,7 @@ class EntryActivity : LockingActivity() {
private var historyView: View? = null private var historyView: View? = null
private var entryContentsView: EntryContentsView? = null private var entryContentsView: EntryContentsView? = null
private var entryProgress: ProgressBar? = null private var entryProgress: ProgressBar? = null
private var lockView: View? = null
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
private var mDatabase: Database? = null private var mDatabase: Database? = null
@@ -124,6 +124,11 @@ class EntryActivity : LockingActivity() {
entryContentsView = findViewById(R.id.entry_contents) entryContentsView = findViewById(R.id.entry_contents)
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this)) entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
entryProgress = findViewById(R.id.entry_progress) entryProgress = findViewById(R.id.entry_progress)
lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener {
lockAndExit()
}
// Init the clipboard helper // Init the clipboard helper
clipboardHelper = ClipboardHelper(this) clipboardHelper = ClipboardHelper(this)
@@ -148,11 +153,20 @@ class EntryActivity : LockingActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// Show the lock button
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
View.VISIBLE
} else {
View.GONE
}
// Get Entry from UUID // Get Entry from UUID
try { try {
val keyEntry: NodeId<UUID> = intent.getParcelableExtra(KEY_ENTRY) val keyEntry: NodeId<UUID>? = intent.getParcelableExtra(KEY_ENTRY)
mEntry = mDatabase?.getEntryById(keyEntry) if (keyEntry != null) {
mEntryLastVersion = mEntry mEntry = mDatabase?.getEntryById(keyEntry)
mEntryLastVersion = mEntry
}
} catch (e: ClassCastException) { } catch (e: ClassCastException) {
Log.e(TAG, "Unable to retrieve the entry key") Log.e(TAG, "Unable to retrieve the entry key")
} }
@@ -460,8 +474,7 @@ class EntryActivity : LockingActivity() {
getString(R.string.entry_user_name))) getString(R.string.entry_user_name)))
}, },
{ {
// Launch autofill settings performedNextEducation(entryActivityEducation, menu)
startActivity(Intent(this@EntryActivity, SettingsAutofillActivity::class.java))
}) })
if (!entryCopyEducationPerformed) { if (!entryCopyEducationPerformed) {
@@ -524,10 +537,6 @@ class EntryActivity : LockingActivity() {
!mReadOnly && mAutoSaveEnable) !mReadOnly && mAutoSaveEnable)
} }
} }
R.id.menu_lock -> {
lockAndExit()
return true
}
R.id.menu_save_database -> { R.id.menu_save_database -> {
mProgressDialogThread?.startDatabaseSave(!mReadOnly) mProgressDialogThread?.startDatabaseSave(!mReadOnly)
} }
@@ -539,12 +548,10 @@ class EntryActivity : LockingActivity() {
override fun finish() { override fun finish() {
// Transit data in previous Activity after an update // Transit data in previous Activity after an update
/* Intent().apply {
TODO Slowdown when add entry as result putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry)
Intent intent = new Intent(); setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this)
intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry); }
onFinish(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
*/
super.finish() super.finish()
} }

View File

@@ -19,6 +19,8 @@
package com.kunzisoft.keepass.activities package com.kunzisoft.keepass.activities
import android.app.Activity import android.app.Activity
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
@@ -26,13 +28,15 @@ import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ScrollView import android.widget.DatePicker
import android.widget.TimePicker
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.ActionMenuView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.widget.NestedScrollView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
@@ -52,12 +56,15 @@ import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.view.EntryEditContentsView import com.kunzisoft.keepass.view.EntryEditContentsView
import com.kunzisoft.keepass.view.showActionError import com.kunzisoft.keepass.view.showActionError
import org.joda.time.DateTime
import java.util.* import java.util.*
class EntryEditActivity : LockingActivity(), class EntryEditActivity : LockingActivity(),
IconPickerDialogFragment.IconPickerListener, IconPickerDialogFragment.IconPickerListener,
GeneratePasswordDialogFragment.GeneratePasswordListener, GeneratePasswordDialogFragment.GeneratePasswordListener,
SetOTPDialogFragment.CreateOtpListener { SetOTPDialogFragment.CreateOtpListener,
DatePickerDialog.OnDateSetListener,
TimePickerDialog.OnTimeSetListener {
private var mDatabase: Database? = null private var mDatabase: Database? = null
@@ -70,9 +77,11 @@ class EntryEditActivity : LockingActivity(),
// Views // Views
private var coordinatorLayout: CoordinatorLayout? = null private var coordinatorLayout: CoordinatorLayout? = null
private var scrollView: ScrollView? = null private var scrollView: NestedScrollView? = null
private var entryEditContentsView: EntryEditContentsView? = null private var entryEditContentsView: EntryEditContentsView? = null
private var entryEditAddToolBar: ActionMenuView? = null
private var saveView: View? = null private var saveView: View? = null
private var lockView: View? = null
// Education // Education
private var entryEditActivityEducation: EntryEditActivityEducation? = null private var entryEditActivityEducation: EntryEditActivityEducation? = null
@@ -94,6 +103,22 @@ class EntryEditActivity : LockingActivity(),
entryEditContentsView = findViewById(R.id.entry_edit_contents) entryEditContentsView = findViewById(R.id.entry_edit_contents)
entryEditContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this)) entryEditContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
entryEditContentsView?.onDateClickListener = View.OnClickListener {
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
val dateTime = DateTime(expiresDate)
val defaultYear = dateTime.year
val defaultMonth = dateTime.monthOfYear-1
val defaultDay = dateTime.dayOfMonth
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
.show(supportFragmentManager, "DatePickerFragment")
}
}
lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener {
lockAndExit()
}
// Focus view to reinitialize timeout // Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(entryEditContentsView) resetAppTimeoutWhenViewFocusedOrChanged(entryEditContentsView)
@@ -167,17 +192,46 @@ class EntryEditActivity : LockingActivity(),
// Add listener to the icon // Add listener to the icon
entryEditContentsView?.setOnIconViewClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) } entryEditContentsView?.setOnIconViewClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) }
// Generate password button // Bottom Bar
entryEditContentsView?.setOnPasswordGeneratorClickListener { openPasswordGenerator() } entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
entryEditAddToolBar?.apply {
menuInflater.inflate(R.menu.entry_edit, menu)
menu.findItem(R.id.menu_add_field).apply {
val allowCustomField = mNewEntry?.allowCustomFields() == true
isEnabled = allowCustomField
isVisible = allowCustomField
}
menu.findItem(R.id.menu_add_otp).apply {
val allowOTP = mDatabase?.allowOTP == true
isEnabled = allowOTP
isVisible = allowOTP
}
setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.menu_generate_password -> {
openPasswordGenerator()
true
}
R.id.menu_add_field -> {
addNewCustomField()
true
}
R.id.menu_add_otp -> {
setupOTP()
true
}
else -> true
}
}
}
// Save button // Save button
saveView = findViewById(R.id.entry_edit_save) saveView = findViewById(R.id.entry_edit_validate)
saveView?.setOnClickListener { saveEntry() } saveView?.setOnClickListener { saveEntry() }
entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) {
addNewCustomField()
}
// Verify the education views // Verify the education views
entryEditActivityEducation = EntryEditActivityEducation(this) entryEditActivityEducation = EntryEditActivityEducation(this)
@@ -194,6 +248,16 @@ class EntryEditActivity : LockingActivity(),
} }
} }
override fun onResume() {
super.onResume()
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
View.VISIBLE
} else {
View.GONE
}
}
private fun populateViewsWithEntry(newEntry: Entry) { private fun populateViewsWithEntry(newEntry: Entry) {
// Don't start the field reference manager, we want to see the raw ref // Don't start the field reference manager, we want to see the raw ref
mDatabase?.stopManageEntry(newEntry) mDatabase?.stopManageEntry(newEntry)
@@ -207,6 +271,9 @@ class EntryEditActivity : LockingActivity(),
username = if (newEntry.username.isEmpty()) mDatabase?.defaultUsername ?:"" else newEntry.username username = if (newEntry.username.isEmpty()) mDatabase?.defaultUsername ?:"" else newEntry.username
url = newEntry.url url = newEntry.url
password = newEntry.password password = newEntry.password
expires = newEntry.expires
if (expires)
expiresDate = newEntry.expiryTime
notes = newEntry.notes notes = newEntry.notes
for (entry in newEntry.customFields.entries) { for (entry in newEntry.customFields.entries) {
post { post {
@@ -228,7 +295,11 @@ class EntryEditActivity : LockingActivity(),
username = entryView.username username = entryView.username
url = entryView.url url = entryView.url
password = entryView.password password = entryView.password
notes = entryView.notes expires = entryView.expires
if (entryView.expires) {
expiryTime = entryView.expiresDate
}
notes = entryView. notes
entryView.customFields.forEach { customField -> entryView.customFields.forEach { customField ->
putExtraField(customField.name, customField.protectedValue) putExtraField(customField.name, customField.protectedValue)
} }
@@ -259,6 +330,13 @@ class EntryEditActivity : LockingActivity(),
entryEditContentsView?.addEmptyCustomField() entryEditContentsView?.addEmptyCustomField()
} }
private fun setupOTP() {
// Retrieve the current otpElement if exists
// and open the dialog to set up the OTP
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
.show(supportFragmentManager, "addOTPDialog")
}
/** /**
* Saves the new entry or update an existing entry in the database * Saves the new entry or update an existing entry in the database
*/ */
@@ -307,8 +385,6 @@ class EntryEditActivity : LockingActivity(),
// Save database not needed here // Save database not needed here
menu.findItem(R.id.menu_save_database)?.isVisible = false menu.findItem(R.id.menu_save_database)?.isVisible = false
MenuUtil.contributionMenuInflater(inflater, menu) MenuUtil.contributionMenuInflater(inflater, menu)
if (mDatabase?.allowOTP == true)
inflater.inflate(R.menu.entry_otp, menu)
entryEditActivityEducation?.let { entryEditActivityEducation?.let {
Handler().post { performedNextEducation(it) } Handler().post { performedNextEducation(it) }
@@ -318,12 +394,10 @@ class EntryEditActivity : LockingActivity(),
} }
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) { private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
val passwordView = entryEditContentsView?.generatePasswordView val passwordGeneratorView: View? = entryEditAddToolBar?.findViewById(R.id.menu_generate_password)
val addNewFieldView = entryEditContentsView?.addNewFieldButton val generatePasswordEducationPerformed = passwordGeneratorView != null
val generatePasswordEducationPerformed = passwordView != null
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation( && entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
passwordView, passwordGeneratorView,
{ {
openPasswordGenerator() openPasswordGenerator()
}, },
@@ -332,23 +406,33 @@ class EntryEditActivity : LockingActivity(),
} }
) )
if (!generatePasswordEducationPerformed) { if (!generatePasswordEducationPerformed) {
// entryNewFieldEducationPerformed val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field)
mNewEntry != null && mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty() val addNewFieldEducationPerformed = mNewEntry != null
&& mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE && addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation( && entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
addNewFieldView, addNewFieldView,
{ {
addNewCustomField() addNewCustomField()
}) },
{
performedNextEducation(entryEditActivityEducation)
}
)
if (!addNewFieldEducationPerformed) {
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
setupOtpView != null && setupOtpView.visibility == View.VISIBLE
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
setupOtpView,
{
setupOTP()
})
}
} }
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_lock -> {
lockAndExit()
return true
}
R.id.menu_save_database -> { R.id.menu_save_database -> {
mProgressDialogThread?.startDatabaseSave(!mReadOnly) mProgressDialogThread?.startDatabaseSave(!mReadOnly)
} }
@@ -356,14 +440,9 @@ class EntryEditActivity : LockingActivity(),
MenuUtil.onContributionItemSelected(this) MenuUtil.onContributionItemSelected(this)
return true return true
} }
R.id.menu_add_otp -> { android.R.id.home -> {
// Retrieve the current otpElement if exists onBackPressed()
// and open the dialog to set up the OTP
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
.show(supportFragmentManager, "addOTPDialog")
return true
} }
android.R.id.home -> finish()
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
@@ -383,6 +462,39 @@ class EntryEditActivity : LockingActivity(),
} }
} }
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
// To fix android 4.4 issue
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
if (datePicker?.isShown == true) {
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
// Save the date
entryEditContentsView?.expiresDate =
DateInstant(DateTime(expiresDate)
.withYear(year)
.withMonthOfYear(month + 1)
.withDayOfMonth(day)
.toDate())
// Launch the time picker
val dateTime = DateTime(expiresDate)
val defaultHour = dateTime.hourOfDay
val defaultMinute = dateTime.minuteOfHour
TimePickerFragment.getInstance(defaultHour, defaultMinute)
.show(supportFragmentManager, "TimePickerFragment")
}
}
}
override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) {
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
// Save the date
entryEditContentsView?.expiresDate =
DateInstant(DateTime(expiresDate)
.withHourOfDay(hours)
.withMinuteOfHour(minutes)
.toDate())
}
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
mNewEntry?.let { mNewEntry?.let {
populateEntryWithViews(it) populateEntryWithViews(it)
@@ -406,6 +518,15 @@ class EntryEditActivity : LockingActivity(),
// Do nothing here // Do nothing here
} }
override fun onBackPressed() {
AlertDialog.Builder(this)
.setMessage(R.string.discard_changes)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.discard) { _, _ ->
super@EntryEditActivity.onBackPressed()
}.create().show()
}
override fun finish() { override fun finish() {
// Assign entry callback as a result in all case // Assign entry callback as a result in all case
try { try {

View File

@@ -26,16 +26,14 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.os.Handler import android.os.Handler
import android.preference.PreferenceManager
import android.util.Log import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.TextView
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
@@ -48,9 +46,11 @@ import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.autofill.AutofillHelper.KEY_SEARCH_INFO
import com.kunzisoft.keepass.database.action.ProgressDialogThread import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.utils.*
@@ -62,7 +62,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
AssignMasterKeyDialogFragment.AssignPasswordDialogListener { AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
// Views // Views
private var fileListContainer: View? = null private var coordinatorLayout: CoordinatorLayout? = null
private var fileManagerExplanationButton: View? = null
private var createButtonView: View? = null private var createButtonView: View? = null
private var openDatabaseButtonView: View? = null private var openDatabaseButtonView: View? = null
@@ -83,12 +84,17 @@ class FileDatabaseSelectActivity : StylishActivity(),
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext) mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
setContentView(R.layout.activity_file_selection) setContentView(R.layout.activity_file_selection)
fileListContainer = findViewById(R.id.container_file_list) coordinatorLayout = findViewById(R.id.activity_file_selection_coordinator_layout)
val toolbar = findViewById<Toolbar>(R.id.toolbar) val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.title = "" toolbar.title = ""
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
fileManagerExplanationButton = findViewById(R.id.file_manager_explanation_button)
fileManagerExplanationButton?.setOnClickListener {
UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
}
// Create button // Create button
createButtonView = findViewById(R.id.create_database_button) createButtonView = findViewById(R.id.create_database_button)
if (allowCreateDocumentByStorageAccessFramework(packageManager)) { if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
@@ -103,8 +109,13 @@ class FileDatabaseSelectActivity : StylishActivity(),
createButtonView?.setOnClickListener { createNewFile() } createButtonView?.setOnClickListener { createNewFile() }
mOpenFileHelper = OpenFileHelper(this) mOpenFileHelper = OpenFileHelper(this)
openDatabaseButtonView = findViewById(R.id.open_database_button) openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
openDatabaseButtonView?.setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener) openDatabaseButtonView?.apply {
mOpenFileHelper?.openFileOnClickViewListener?.let {
setOnClickListener(it)
setOnLongClickListener(it)
}
}
// History list // History list
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list) val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
@@ -119,7 +130,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
databaseFileUri, databaseFileUri,
UriUtil.parse(fileDatabaseHistoryEntityToOpen.keyFileUri)) UriUtil.parse(fileDatabaseHistoryEntityToOpen.keyFileUri))
} }
updateFileListVisibility()
} }
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete -> mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
// Remove from app database // Remove from app database
@@ -128,7 +138,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
fileHistoryDeleted?.let { databaseFileHistoryDeleted -> fileHistoryDeleted?.let { databaseFileHistoryDeleted ->
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileHistoryDeleted) mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileHistoryDeleted)
mAdapterDatabaseHistory?.notifyDataSetChanged() mAdapterDatabaseHistory?.notifyDataSetChanged()
updateFileListVisibility()
} }
} }
true true
@@ -142,8 +151,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
if (!(savedInstanceState != null if (!(savedInstanceState != null
&& savedInstanceState.containsKey(EXTRA_STAY) && savedInstanceState.containsKey(EXTRA_STAY)
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) { && savedInstanceState.getBoolean(EXTRA_STAY, false))) {
val prefs = PreferenceManager.getDefaultSharedPreferences(this) val databasePath = PreferencesUtil.getDefaultDatabasePath(this)
val databasePath = prefs.getString(PasswordActivity.KEY_DEFAULT_DATABASE_PATH, "")
UriUtil.parse(databasePath)?.let { databaseFileUri -> UriUtil.parse(databasePath)?.let { databaseFileUri ->
launchPasswordActivityWithPath(databaseFileUri) launchPasswordActivityWithPath(databaseFileUri)
@@ -163,9 +171,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
onActionFinish = { actionTask, _ -> onActionFinish = { actionTask, _ ->
when (actionTask) { when (actionTask) {
ACTION_DATABASE_CREATE_TASK -> { ACTION_DATABASE_CREATE_TASK -> {
// TODO Check
// mAdapterDatabaseHistory?.notifyDataSetChanged()
// updateFileListVisibility()
GroupActivity.launch(this@FileDatabaseSelectActivity) GroupActivity.launch(this@FileDatabaseSelectActivity)
} }
} }
@@ -184,7 +189,9 @@ class FileDatabaseSelectActivity : StylishActivity(),
private fun fileNoFoundAction(e: FileNotFoundException) { private fun fileNoFoundAction(e: FileNotFoundException) {
val error = getString(R.string.file_not_found_content) val error = getString(R.string.file_not_found_content)
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show() coordinatorLayout?.let {
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
}
Log.e(TAG, error, e) Log.e(TAG, error, e)
} }
@@ -212,7 +219,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
try { try {
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity, PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
databaseUri, keyFile, databaseUri, keyFile,
assistStructure) assistStructure,
intent.getParcelableExtra(KEY_SEARCH_INFO))
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
fileNoFoundAction(e) fileNoFoundAction(e)
} }
@@ -224,16 +232,21 @@ class FileDatabaseSelectActivity : StylishActivity(),
private fun launchGroupActivity(readOnly: Boolean) { private fun launchGroupActivity(readOnly: Boolean) {
EntrySelectionHelper.doEntrySelectionAction(intent, EntrySelectionHelper.doEntrySelectionAction(intent,
{ {
GroupActivity.launch(this@FileDatabaseSelectActivity, readOnly) GroupActivity.launch(this@FileDatabaseSelectActivity,
readOnly)
}, },
{ {
GroupActivity.launchForKeyboardSelection(this@FileDatabaseSelectActivity, readOnly) GroupActivity.launchForKeyboardSelection(this@FileDatabaseSelectActivity,
readOnly)
// Do not keep history // Do not keep history
finish() finish()
}, },
{ assistStructure -> { assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity, assistStructure, readOnly) GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
assistStructure,
intent.getParcelableExtra(KEY_SEARCH_INFO),
readOnly)
} }
}) })
} }
@@ -245,25 +258,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
overridePendingTransition(0, 0) overridePendingTransition(0, 0)
} }
private fun updateExternalStorageWarning() {
// To show errors
var warning = -1
val state = Environment.getExternalStorageState()
if (state == Environment.MEDIA_MOUNTED_READ_ONLY) {
warning = R.string.read_only_warning
} else if (state != Environment.MEDIA_MOUNTED) {
warning = R.string.warning_unmounted
}
val labelWarningView = findViewById<TextView>(R.id.label_warning)
if (warning != -1) {
labelWarningView.setText(warning)
labelWarningView.visibility = View.VISIBLE
} else {
labelWarningView.visibility = View.INVISIBLE
}
}
override fun onResume() { override fun onResume() {
val database = Database.getInstance() val database = Database.getInstance()
if (database.loaded) { if (database.loaded) {
@@ -272,8 +266,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
super.onResume() super.onResume()
updateExternalStorageWarning()
// Construct adapter with listeners // Construct adapter with listeners
if (PreferencesUtil.showRecentFiles(this)) { if (PreferencesUtil.showRecentFiles(this)) {
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList -> mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
@@ -283,20 +275,17 @@ class FileDatabaseSelectActivity : StylishActivity(),
// Show only uri accessible // Show only uri accessible
historyList.filter { historyList.filter {
if (hideBrokenLocations) { if (hideBrokenLocations) {
UriUtil.parse(it.databaseUri)?.let { historyUri -> FileDatabaseInfo(this@FileDatabaseSelectActivity,
UriUtil.isUriAccessible(contentResolver, historyUri) it.databaseUri).exists
} ?: false
} else } else
true true
}) })
mAdapterDatabaseHistory?.notifyDataSetChanged() mAdapterDatabaseHistory?.notifyDataSetChanged()
updateFileListVisibility()
} }
} }
} else { } else {
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList() mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
mAdapterDatabaseHistory?.notifyDataSetChanged() mAdapterDatabaseHistory?.notifyDataSetChanged()
updateFileListVisibility()
} }
// Register progress task // Register progress task
@@ -318,13 +307,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri) outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
} }
private fun updateFileListVisibility() {
if (mAdapterDatabaseHistory?.itemCount == 0)
fileListContainer?.visibility = View.INVISIBLE
else
fileListContainer?.visibility = View.VISIBLE
}
override fun onAssignKeyDialogPositiveClick( override fun onAssignKeyDialogPositiveClick(
masterPasswordChecked: Boolean, masterPassword: String?, masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?) { keyFileChecked: Boolean, keyFile: Uri?) {
@@ -374,10 +356,13 @@ class FileDatabaseSelectActivity : StylishActivity(),
if (mDatabaseFileUri != null) { if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment.getInstance(true) AssignMasterKeyDialogFragment.getInstance(true)
.show(supportFragmentManager, "passwordDialog") .show(supportFragmentManager, "passwordDialog")
} else {
val error = getString(R.string.error_create_database)
coordinatorLayout?.let {
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
}
Log.e(TAG, error)
} }
// else {
// TODO Show error
// }
} }
} }
@@ -452,10 +437,13 @@ class FileDatabaseSelectActivity : StylishActivity(),
*/ */
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure) { fun launchForAutofillResult(activity: Activity,
assistStructure: AssistStructure,
searchInfo: SearchInfo?) {
AutofillHelper.startActivityForAutofillResult(activity, AutofillHelper.startActivityForAutofillResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java), Intent(activity, FileDatabaseSelectActivity::class.java),
assistStructure) assistStructure,
searchInfo)
} }
} }
} }

View File

@@ -62,6 +62,7 @@ import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.education.GroupActivityEducation import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.magikeyboard.MagikIME import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
@@ -89,6 +90,7 @@ class GroupActivity : LockingActivity(),
// Views // Views
private var coordinatorLayout: CoordinatorLayout? = null private var coordinatorLayout: CoordinatorLayout? = null
private var lockView: View? = null
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
private var searchTitleView: View? = null private var searchTitleView: View? = null
private var toolbarAction: ToolbarAction? = null private var toolbarAction: ToolbarAction? = null
@@ -134,6 +136,11 @@ class GroupActivity : LockingActivity(),
groupNameView = findViewById(R.id.group_name) groupNameView = findViewById(R.id.group_name)
toolbarAction = findViewById(R.id.toolbar_action) toolbarAction = findViewById(R.id.toolbar_action)
modeTitleView = findViewById(R.id.mode_title_view) modeTitleView = findViewById(R.id.mode_title_view)
lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener {
lockAndExit()
}
toolbar?.title = "" toolbar?.title = ""
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
@@ -346,7 +353,8 @@ class GroupActivity : LockingActivity(),
// If it's a search // If it's a search
if (Intent.ACTION_SEARCH == intent.action) { if (Intent.ACTION_SEARCH == intent.action) {
return mDatabase?.search(intent.getStringExtra(SearchManager.QUERY).trim { it <= ' ' }) val searchString = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
return mDatabase?.createVirtualGroupFromSearch(searchString)
} }
// else a real group // else a real group
else { else {
@@ -438,8 +446,7 @@ class GroupActivity : LockingActivity(),
enableAddGroup(addGroupEnabled) enableAddGroup(addGroupEnabled)
enableAddEntry(addEntryEnabled) enableAddEntry(addEntryEnabled)
if (isEnable) showButton()
showButton()
} }
} }
@@ -486,7 +493,7 @@ class GroupActivity : LockingActivity(),
// Build response with the entry selected // Build response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
mDatabase?.let { database -> mDatabase?.let { database ->
AutofillHelper.buildResponseWhenEntrySelected(this@GroupActivity, AutofillHelper.buildResponse(this@GroupActivity,
entryVersioned.getEntryInfo(database)) entryVersioned.getEntryInfo(database))
} }
} }
@@ -503,6 +510,7 @@ class GroupActivity : LockingActivity(),
private fun finishNodeAction() { private fun finishNodeAction() {
actionNodeMode?.finish() actionNodeMode?.finish()
actionNodeMode = null actionNodeMode = null
addNodeButtonView?.showButton()
} }
override fun onNodeSelected(nodes: List<Node>): Boolean { override fun onNodeSelected(nodes: List<Node>): Boolean {
@@ -514,6 +522,7 @@ class GroupActivity : LockingActivity(),
} else { } else {
actionNodeMode?.invalidate() actionNodeMode?.invalidate()
} }
addNodeButtonView?.hideButton()
} else { } else {
finishNodeAction() finishNodeAction()
} }
@@ -630,6 +639,13 @@ class GroupActivity : LockingActivity(),
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// Show the lock button
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
View.VISIBLE
} else {
View.GONE
}
// Refresh the elements // Refresh the elements
assignGroupViewElements() assignGroupViewElements()
// Refresh suggestions to change preferences // Refresh suggestions to change preferences
@@ -663,13 +679,15 @@ class GroupActivity : LockingActivity(),
} }
// Get the SearchView and set the searchable configuration // Get the SearchView and set the searchable configuration
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager?
menu.findItem(R.id.menu_search)?.let { menu.findItem(R.id.menu_search)?.let {
val searchView = it.actionView as SearchView? val searchView = it.actionView as SearchView?
searchView?.apply { searchView?.apply {
setSearchableInfo(searchManager.getSearchableInfo( (searchManager?.getSearchableInfo(
ComponentName(this@GroupActivity, GroupActivity::class.java))) ComponentName(this@GroupActivity, GroupActivity::class.java)))?.let { searchableInfo ->
setSearchableInfo(searchableInfo)
}
setIconifiedByDefault(false) // Do not iconify the widget; expand it by default setIconifiedByDefault(false) // Do not iconify the widget; expand it by default
suggestionsAdapter = mSearchSuggestionAdapter suggestionsAdapter = mSearchSuggestionAdapter
setOnSuggestionListener(object : SearchView.OnSuggestionListener { setOnSuggestionListener(object : SearchView.OnSuggestionListener {
@@ -749,12 +767,11 @@ class GroupActivity : LockingActivity(),
if (!sortMenuEducationPerformed) { if (!sortMenuEducationPerformed) {
// lockMenuEducationPerformed // lockMenuEducationPerformed
toolbar != null val lockButtonView = findViewById<View>(R.id.lock_button_icon)
&& toolbar!!.findViewById<View>(R.id.menu_lock) != null lockButtonView != null
&& groupActivityEducation.checkAndPerformedLockMenuEducation( && groupActivityEducation.checkAndPerformedLockMenuEducation(lockButtonView,
toolbar!!.findViewById(R.id.menu_lock),
{ {
onOptionsItemSelected(menu.findItem(R.id.menu_lock)) lockAndExit()
}, },
{ {
performedNextEducation(groupActivityEducation, menu) performedNextEducation(groupActivityEducation, menu)
@@ -773,10 +790,6 @@ class GroupActivity : LockingActivity(),
R.id.menu_search -> R.id.menu_search ->
//onSearchRequested(); //onSearchRequested();
return true return true
R.id.menu_lock -> {
lockAndExit()
return true
}
R.id.menu_save_database -> { R.id.menu_save_database -> {
mProgressDialogThread?.startDatabaseSave(!mReadOnly) mProgressDialogThread?.startDatabaseSave(!mReadOnly)
return true return true
@@ -859,8 +872,8 @@ class GroupActivity : LockingActivity(),
.iconPicked(bundle) .iconPicked(bundle)
} }
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) { override fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters) {
mListNodesFragment?.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom) mListNodesFragment?.onSortSelected(sortNodeEnum, sortNodeParameters)
} }
override fun startActivity(intent: Intent) { override fun startActivity(intent: Intent) {
@@ -904,8 +917,8 @@ class GroupActivity : LockingActivity(),
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data) AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
} }
// Not directly get the entry from intent data but from database // Directly used the onActivityResult in fragment
mListNodesFragment?.rebuildList() mListNodesFragment?.onActivityResult(requestCode, resultCode, data)
} }
private fun removeSearchInIntent(intent: Intent) { private fun removeSearchInIntent(intent: Intent) {
@@ -952,19 +965,41 @@ class GroupActivity : LockingActivity(),
private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG" private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY" private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"
private fun buildAndLaunchIntent(context: Context, group: Group?, readOnly: Boolean, private fun buildIntent(context: Context,
intentBuildLauncher: (Intent) -> Unit) { group: Group?,
val checkTime = if (context is Activity) searchInfo: SearchInfo?,
TimeoutHelper.checkTimeAndLockIfTimeout(context) readOnly: Boolean,
else intentBuildLauncher: (Intent) -> Unit) {
TimeoutHelper.checkTime(context) val intent = Intent(context, GroupActivity::class.java)
if (checkTime) { if (group != null) {
val intent = Intent(context, GroupActivity::class.java) intent.putExtra(GROUP_ID_KEY, group.nodeId)
if (group != null) { }
intent.putExtra(GROUP_ID_KEY, group.nodeId) if (searchInfo != null) {
} intent.action = Intent.ACTION_SEARCH
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly) val searchQuery = searchInfo.webDomain ?: searchInfo.applicationId
intentBuildLauncher.invoke(intent) intent.putExtra(SearchManager.QUERY, searchQuery)
}
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
intentBuildLauncher.invoke(intent)
}
private fun checkTimeAndBuildIntent(activity: Activity,
group: Group?,
searchInfo: SearchInfo?,
readOnly: Boolean,
intentBuildLauncher: (Intent) -> Unit) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
buildIntent(activity, group, searchInfo, readOnly, intentBuildLauncher)
}
}
private fun checkTimeAndBuildIntent(context: Context,
group: Group?,
searchInfo: SearchInfo?,
readOnly: Boolean,
intentBuildLauncher: (Intent) -> Unit) {
if (TimeoutHelper.checkTime(context)) {
buildIntent(context, group, searchInfo, readOnly, intentBuildLauncher)
} }
} }
@@ -973,11 +1008,9 @@ class GroupActivity : LockingActivity(),
* Standard Launch * Standard Launch
* ------------------------- * -------------------------
*/ */
fun launch(context: Context,
@JvmOverloads readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
fun launch(context: Context, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) { checkTimeAndBuildIntent(context, null, null, readOnly) { intent ->
TimeoutHelper.recordTime(context)
buildAndLaunchIntent(context, null, readOnly) { intent ->
context.startActivity(intent) context.startActivity(intent)
} }
} }
@@ -988,10 +1021,9 @@ class GroupActivity : LockingActivity(),
* ------------------------- * -------------------------
*/ */
// TODO implement pre search to directly open the direct group // TODO implement pre search to directly open the direct group
fun launchForKeyboardSelection(context: Context,
fun launchForKeyboardSelection(context: Context, readOnly: Boolean) { readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
TimeoutHelper.recordTime(context) checkTimeAndBuildIntent(context, null, null, readOnly) { intent ->
buildAndLaunchIntent(context, null, readOnly) { intent ->
EntrySelectionHelper.startActivityForEntrySelection(context, intent) EntrySelectionHelper.startActivityForEntrySelection(context, intent)
} }
} }
@@ -1001,13 +1033,13 @@ class GroupActivity : LockingActivity(),
* Autofill Launch * Autofill Launch
* ------------------------- * -------------------------
*/ */
// TODO implement pre search to directly open the direct group
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure, readOnly: Boolean) { fun launchForAutofillResult(activity: Activity,
TimeoutHelper.recordTime(activity) assistStructure: AssistStructure,
buildAndLaunchIntent(activity, null, readOnly) { intent -> searchInfo: SearchInfo? = null,
AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure) readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) {
checkTimeAndBuildIntent(activity, null, searchInfo, readOnly) { intent ->
AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure, searchInfo)
} }
} }
} }

View File

@@ -21,9 +21,7 @@ package com.kunzisoft.keepass.activities
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.util.Log import android.util.Log
@@ -69,8 +67,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
private var notFoundView: View? = null private var notFoundView: View? = null
private var isASearchResult: Boolean = false private var isASearchResult: Boolean = false
// Preferences for sorting
private var prefs: SharedPreferences? = null
private var readOnly: Boolean = false private var readOnly: Boolean = false
get() { get() {
@@ -155,7 +151,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}) })
} }
} }
prefs = PreferenceManager.getDefaultSharedPreferences(context)
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@@ -213,26 +208,26 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
fun rebuildList() { fun rebuildList() {
// Add elements to the list // Add elements to the list
mainGroup?.let { mainGroup -> mainGroup?.let { mainGroup ->
mAdapter?.rebuildList(mainGroup) mAdapter?.apply {
rebuildList(mainGroup)
// To visually change the elements
if (PreferencesUtil.APPEARANCE_CHANGED) {
notifyDataSetChanged()
PreferencesUtil.APPEARANCE_CHANGED = false
}
}
} }
} }
override fun onSortSelected(sortNodeEnum: SortNodeEnum, override fun onSortSelected(sortNodeEnum: SortNodeEnum,
ascending: Boolean, sortNodeParameters: SortNodeEnum.SortNodeParameters) {
groupsBefore: Boolean, // Save setting
recycleBinBottom: Boolean) { context?.let {
// Toggle setting PreferencesUtil.saveNodeSort(it, sortNodeEnum, sortNodeParameters)
prefs?.edit()?.apply {
putString(getString(R.string.sort_node_key), sortNodeEnum.name)
putBoolean(getString(R.string.sort_ascending_key), ascending)
putBoolean(getString(R.string.sort_group_before_key), groupsBefore)
putBoolean(getString(R.string.sort_recycle_bin_bottom_key), recycleBinBottom)
apply()
} }
// Tell the adapter to refresh it's list // Tell the adapter to refresh it's list
mAdapter?.notifyChangeSort(sortNodeEnum, mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
SortNodeEnum.SortNodeParameters(ascending, groupsBefore, recycleBinBottom))
rebuildList() rebuildList()
} }
@@ -374,13 +369,11 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> { EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) { || resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { newNode -> data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { changedNode ->
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE) if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
mAdapter?.addNode(newNode) addNode(changedNode)
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) { if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE)
//mAdapter.updateLastNodeRegister(newNode); mAdapter?.notifyDataSetChanged()
rebuildList()
}
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result") } ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
} }
} }

View File

@@ -23,24 +23,21 @@ import android.app.Activity
import android.app.assist.AssistStructure import android.app.assist.AssistStructure
import android.app.backup.BackupManager import android.app.backup.BackupManager
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.preference.PreferenceManager
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.util.Log import android.util.Log
import android.view.* import android.view.*
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.widget.Button import android.widget.*
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.TextView
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.core.app.ActivityCompat
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
@@ -52,11 +49,13 @@ import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.autofill.AutofillHelper.KEY_SEARCH_INFO
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
import com.kunzisoft.keepass.database.action.ProgressDialogThread import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
@@ -68,6 +67,7 @@ import com.kunzisoft.keepass.utils.FileDatabaseInfo
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import kotlinx.android.synthetic.main.activity_password.* import kotlinx.android.synthetic.main.activity_password.*
import java.io.FileNotFoundException import java.io.FileNotFoundException
@@ -79,7 +79,7 @@ open class PasswordActivity : StylishActivity() {
private var containerView: View? = null private var containerView: View? = null
private var filenameView: TextView? = null private var filenameView: TextView? = null
private var passwordView: EditText? = null private var passwordView: EditText? = null
private var keyFileView: EditText? = null private var keyFileSelectionView: KeyFileSelectionView? = null
private var confirmButtonView: Button? = null private var confirmButtonView: Button? = null
private var checkboxPasswordView: CompoundButton? = null private var checkboxPasswordView: CompoundButton? = null
private var checkboxKeyFileView: CompoundButton? = null private var checkboxKeyFileView: CompoundButton? = null
@@ -91,11 +91,10 @@ open class PasswordActivity : StylishActivity() {
private var mDatabaseFileUri: Uri? = null private var mDatabaseFileUri: Uri? = null
private var mDatabaseKeyFileUri: Uri? = null private var mDatabaseKeyFileUri: Uri? = null
private var mSharedPreferences: SharedPreferences? = null
private var mRememberKeyFile: Boolean = false private var mRememberKeyFile: Boolean = false
private var mOpenFileHelper: OpenFileHelper? = null private var mOpenFileHelper: OpenFileHelper? = null
private var mPermissionAsked = false
private var readOnly: Boolean = false private var readOnly: Boolean = false
private var mForceReadOnly: Boolean = false private var mForceReadOnly: Boolean = false
set(value) { set(value) {
@@ -115,10 +114,6 @@ open class PasswordActivity : StylishActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
setContentView(R.layout.activity_password) setContentView(R.layout.activity_password)
toolbar = findViewById(R.id.toolbar) toolbar = findViewById(R.id.toolbar)
@@ -131,18 +126,23 @@ open class PasswordActivity : StylishActivity() {
confirmButtonView = findViewById(R.id.activity_password_open_button) confirmButtonView = findViewById(R.id.activity_password_open_button)
filenameView = findViewById(R.id.filename) filenameView = findViewById(R.id.filename)
passwordView = findViewById(R.id.password) passwordView = findViewById(R.id.password)
keyFileView = findViewById(R.id.pass_keyfile) keyFileSelectionView = findViewById(R.id.keyfile_selection)
checkboxPasswordView = findViewById(R.id.password_checkbox) checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox) checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
checkboxDefaultDatabaseView = findViewById(R.id.default_database) checkboxDefaultDatabaseView = findViewById(R.id.default_database)
advancedUnlockInfoView = findViewById(R.id.biometric_info) advancedUnlockInfoView = findViewById(R.id.biometric_info)
infoContainerView = findViewById(R.id.activity_password_info_container) infoContainerView = findViewById(R.id.activity_password_info_container)
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState) readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
val browseView = findViewById<View>(R.id.open_database_button)
mOpenFileHelper = OpenFileHelper(this@PasswordActivity) mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
browseView.setOnClickListener(mOpenFileHelper!!.openFileOnClickViewListener) keyFileSelectionView?.apply {
mOpenFileHelper?.openFileOnClickViewListener?.let {
setOnClickListener(it)
setOnLongClickListener(it)
}
}
passwordView?.setOnEditorActionListener(onEditorActionListener) passwordView?.setOnEditorActionListener(onEditorActionListener)
passwordView?.addTextChangedListener(object : TextWatcher { passwordView?.addTextChangedListener(object : TextWatcher {
@@ -155,22 +155,17 @@ open class PasswordActivity : StylishActivity() {
checkboxPasswordView?.isChecked = true checkboxPasswordView?.isChecked = true
} }
}) })
keyFileView?.setOnEditorActionListener(onEditorActionListener)
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) {
if (editable.toString().isNotEmpty() && checkboxKeyFileView?.isChecked != true)
checkboxKeyFileView?.isChecked = true
}
})
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ -> enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
enableOrNotTheConfirmationButton() enableOrNotTheConfirmationButton()
} }
// If is a view intent
getUriFromIntent(intent)
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
}
mProgressDialogThread = ProgressDialogThread(this).apply { mProgressDialogThread = ProgressDialogThread(this).apply {
onActionFinish = { actionTask, result -> onActionFinish = { actionTask, result ->
when (actionTask) { when (actionTask) {
@@ -183,11 +178,9 @@ open class PasswordActivity : StylishActivity() {
} }
} }
// Remove the password in view in all cases
removePassword()
if (result.isSuccess) { if (result.isSuccess) {
setEmptyViews() mDatabaseKeyFileUri = null
clearCredentialsViews(true)
launchGroupActivity() launchGroupActivity()
} else { } else {
var resultError = "" var resultError = ""
@@ -243,19 +236,59 @@ open class PasswordActivity : StylishActivity() {
} }
} }
private fun getUriFromIntent(intent: Intent?) {
// If is a view intent
val action = intent?.action
if (action != null
&& action == VIEW_INTENT) {
mDatabaseFileUri = intent.data
mDatabaseKeyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
} else {
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
}
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
getUriFromIntent(intent)
}
private fun launchGroupActivity() { private fun launchGroupActivity() {
EntrySelectionHelper.doEntrySelectionAction(intent, EntrySelectionHelper.doEntrySelectionAction(intent,
{ {
GroupActivity.launch(this@PasswordActivity, readOnly) GroupActivity.launch(this@PasswordActivity,
readOnly)
}, },
{ {
GroupActivity.launchForKeyboardSelection(this@PasswordActivity, readOnly) GroupActivity.launchForKeyboardSelection(this@PasswordActivity,
readOnly)
// Do not keep history // Do not keep history
finish() finish()
}, },
{ assistStructure -> { assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
GroupActivity.launchForAutofillResult(this@PasswordActivity, assistStructure, readOnly) val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
AutofillHelper.checkAutoSearchInfo(this,
Database.getInstance(),
searchInfo,
{ items ->
// Response is build
AutofillHelper.buildResponse(this, items)
finish()
},
{
// Here no search info found
GroupActivity.launchForAutofillResult(this@PasswordActivity,
assistStructure,
null,
readOnly)
},
{
// Simply close if database not opened, normally not happened
finish()
}
)
} }
}) })
} }
@@ -271,13 +304,16 @@ open class PasswordActivity : StylishActivity() {
} }
override fun onResume() { override fun onResume() {
if (Database.getInstance().loaded) if (Database.getInstance().loaded)
launchGroupActivity() launchGroupActivity()
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
// If the database isn't accessible make sure to clear the password field, if it // If the database isn't accessible make sure to clear the password field, if it
// was saved in the instance state // was saved in the instance state
if (Database.getInstance().loaded) { if (Database.getInstance().loaded) {
setEmptyViews() clearCredentialsViews()
} }
// For check shutdown // For check shutdown
@@ -289,46 +325,40 @@ open class PasswordActivity : StylishActivity() {
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
mDatabaseKeyFileUri?.let {
outState.putString(KEY_KEYFILE, it.toString())
}
ReadOnlyHelper.onSaveInstanceState(outState, readOnly) ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
private fun initUriFromIntent() { private fun initUriFromIntent() {
/*
val databaseUri: Uri? // "canXrite" doesn't work with Google Drive, don't really know why?
val keyFileUri: Uri? mForceReadOnly = mDatabaseFileUri?.let {
!FileDatabaseInfo(this, it).canWrite
// If is a view intent } ?: false
val action = intent.action */
if (action != null mForceReadOnly = mDatabaseFileUri?.let {
&& action == VIEW_INTENT) { !FileDatabaseInfo(this, it).exists
databaseUri = intent.data } ?: true
keyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
} else {
databaseUri = intent.getParcelableExtra(KEY_FILENAME)
keyFileUri = intent.getParcelableExtra(KEY_KEYFILE)
}
mForceReadOnly = UriUtil.isUriNotWritable(contentResolver, databaseUri)
// Post init uri with KeyFile if needed // Post init uri with KeyFile if needed
if (mRememberKeyFile && (keyFileUri == null || keyFileUri.toString().isEmpty())) { if (mRememberKeyFile && (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
// Retrieve KeyFile in a thread // Retrieve KeyFile in a thread
databaseUri?.let { databaseUriNotNull -> mDatabaseFileUri?.let { databaseUri ->
FileDatabaseHistoryAction.getInstance(applicationContext) FileDatabaseHistoryAction.getInstance(applicationContext)
.getKeyFileUriByDatabaseUri(databaseUriNotNull) { .getKeyFileUriByDatabaseUri(databaseUri) {
onPostInitUri(databaseUri, it) onPostInitUri(databaseUri, it)
} }
} }
} else { } else {
onPostInitUri(databaseUri, keyFileUri) onPostInitUri(mDatabaseFileUri, mDatabaseKeyFileUri)
} }
} }
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) { private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
mDatabaseFileUri = databaseFileUri
mDatabaseKeyFileUri = keyFileUri
// Define title // Define title
databaseFileUri?.let { databaseFileUri?.let {
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title -> FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
@@ -337,26 +367,18 @@ open class PasswordActivity : StylishActivity() {
} }
// Define Key File text // Define Key File text
val keyUriString = keyFileUri?.toString() ?: "" if (mRememberKeyFile) {
if (keyUriString.isNotEmpty() && mRememberKeyFile) { // Bug KeepassDX #18 populateKeyFileTextView(keyFileUri)
populateKeyFileTextView(keyUriString)
} }
// Define listeners for default database checkbox and validate button // Define listeners for default database checkbox and validate button
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked -> checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
var newDefaultFileName: Uri? = null var newDefaultFileUri: Uri? = null
if (isChecked) { if (isChecked) {
newDefaultFileName = databaseFileUri ?: newDefaultFileName newDefaultFileUri = databaseFileUri ?: newDefaultFileUri
} }
mSharedPreferences?.edit()?.apply { PreferencesUtil.saveDefaultDatabasePath(this, newDefaultFileUri)
newDefaultFileName?.let {
putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString())
} ?: kotlin.run {
remove(KEY_DEFAULT_DATABASE_PATH)
}
apply()
}
val backupManager = BackupManager(this@PasswordActivity) val backupManager = BackupManager(this@PasswordActivity)
backupManager.dataChanged() backupManager.dataChanged()
@@ -364,7 +386,7 @@ open class PasswordActivity : StylishActivity() {
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() } confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
// Retrieve settings for default database // Retrieve settings for default database
val defaultFilename = mSharedPreferences?.getString(KEY_DEFAULT_DATABASE_PATH, "") val defaultFilename = PreferencesUtil.getDefaultDatabasePath(this)
if (databaseFileUri != null if (databaseFileUri != null
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty() && databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
&& databaseFileUri == UriUtil.parse(defaultFilename)) { && databaseFileUri == UriUtil.parse(defaultFilename)) {
@@ -441,10 +463,9 @@ open class PasswordActivity : StylishActivity() {
} }
} }
private fun setEmptyViews() { private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
populatePasswordTextView(null) populatePasswordTextView(null)
// Bug KeepassDX #18 if (clearKeyFile) {
if (!mRememberKeyFile) {
populateKeyFileTextView(null) populateKeyFileTextView(null)
} }
} }
@@ -461,13 +482,13 @@ open class PasswordActivity : StylishActivity() {
} }
} }
private fun populateKeyFileTextView(text: String?) { private fun populateKeyFileTextView(uri: Uri?) {
if (text == null || text.isEmpty()) { if (uri == null || uri.toString().isEmpty()) {
keyFileView?.setText("") keyFileSelectionView?.uri = null
if (checkboxKeyFileView?.isChecked == true) if (checkboxKeyFileView?.isChecked == true)
checkboxKeyFileView?.isChecked = false checkboxKeyFileView?.isChecked = false
} else { } else {
keyFileView?.setText(text) keyFileSelectionView?.uri = uri
if (checkboxKeyFileView?.isChecked != true) if (checkboxKeyFileView?.isChecked != true)
checkboxKeyFileView?.isChecked = true checkboxKeyFileView?.isChecked = true
} }
@@ -488,7 +509,7 @@ open class PasswordActivity : StylishActivity() {
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) { private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
val password: String? = passwordView?.text?.toString() val password: String? = passwordView?.text?.toString()
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString()) val keyFile: Uri? = keyFileSelectionView?.uri
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity) verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity)
} }
@@ -501,7 +522,7 @@ open class PasswordActivity : StylishActivity() {
} }
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) { private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString()) val keyFile: Uri? = keyFileSelectionView?.uri
verifyKeyFileCheckbox(keyFile) verifyKeyFileCheckbox(keyFile)
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri) loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
} }
@@ -510,18 +531,13 @@ open class PasswordActivity : StylishActivity() {
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
} }
private fun removePassword() {
passwordView?.setText("")
checkboxPasswordView?.isChecked = false
}
private fun loadDatabase(databaseFileUri: Uri?, private fun loadDatabase(databaseFileUri: Uri?,
password: String?, password: String?,
keyFileUri: Uri?, keyFileUri: Uri?,
cipherDatabaseEntity: CipherDatabaseEntity? = null) { cipherDatabaseEntity: CipherDatabaseEntity? = null) {
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) { if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
removePassword() clearCredentialsViews()
} }
databaseFileUri?.let { databaseUri -> databaseFileUri?.let { databaseUri ->
@@ -578,11 +594,42 @@ open class PasswordActivity : StylishActivity() {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
launchEducation(menu) launchEducation(menu) {
launchCheckPermission()
}
return true return true
} }
// Check permission
private fun launchCheckPermission() {
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
val permissions = arrayOf(writePermission)
if (Build.VERSION.SDK_INT >= 23
&& !readOnly
&& !mPermissionAsked) {
mPermissionAsked = true
// Check self permission to show or not the dialog
if (toolbar != null
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
WRITE_EXTERNAL_STORAGE_REQUEST -> {
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
Toast.makeText(this, R.string.read_only_warning, Toast.LENGTH_LONG).show()
}
}
}
}
// To fix multiple view education // To fix multiple view education
private var performedEductionInProgress = false private var performedEductionInProgress = false
private fun launchEducation(menu: Menu, onEducationFinished: (()-> Unit)? = null) { private fun launchEducation(menu: Menu, onEducationFinished: (()-> Unit)? = null) {
@@ -684,7 +731,8 @@ open class PasswordActivity : StylishActivity() {
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
) { uri -> ) { uri ->
if (uri != null) { if (uri != null) {
populateKeyFileTextView(uri.toString()) mDatabaseKeyFileUri = uri
populateKeyFileTextView(uri)
} }
} }
} }
@@ -692,7 +740,7 @@ open class PasswordActivity : StylishActivity() {
// this block if not a key file response // this block if not a key file response
when (resultCode) { when (resultCode) {
LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> { LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> {
setEmptyViews() clearCredentialsViews()
Database.getInstance().closeAndClear(applicationContext.filesDir) Database.getInstance().closeAndClear(applicationContext.filesDir)
} }
} }
@@ -703,14 +751,14 @@ open class PasswordActivity : StylishActivity() {
private val TAG = PasswordActivity::class.java.name private val TAG = PasswordActivity::class.java.name
const val KEY_DEFAULT_DATABASE_PATH = "KEY_DEFAULT_DATABASE_PATH"
private const val KEY_FILENAME = "fileName" private const val KEY_FILENAME = "fileName"
private const val KEY_KEYFILE = "keyFile" private const val KEY_KEYFILE = "keyFile"
private const val VIEW_INTENT = "android.intent.action.VIEW" private const val VIEW_INTENT = "android.intent.action.VIEW"
private const val KEY_PASSWORD = "password" private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately" private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?, private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
intentBuildLauncher: (Intent) -> Unit) { intentBuildLauncher: (Intent) -> Unit) {
@@ -765,13 +813,15 @@ open class PasswordActivity : StylishActivity() {
activity: Activity, activity: Activity,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?, keyFile: Uri?,
assistStructure: AssistStructure?) { assistStructure: AssistStructure?,
searchInfo: SearchInfo?) {
if (assistStructure != null) { if (assistStructure != null) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
AutofillHelper.startActivityForAutofillResult( AutofillHelper.startActivityForAutofillResult(
activity, activity,
intent, intent,
assistStructure) assistStructure,
searchInfo)
} }
} else { } else {
launch(activity, databaseFile, keyFile) launch(activity, databaseFile, keyFile)

View File

@@ -35,7 +35,7 @@ import android.widget.CompoundButton
import android.widget.TextView import android.widget.TextView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.view.KeyFileSelectionView
class AssignMasterKeyDialogFragment : DialogFragment() { class AssignMasterKeyDialogFragment : DialogFragment() {
@@ -51,9 +51,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
private var passwordRepeatTextInputLayout: TextInputLayout? = null private var passwordRepeatTextInputLayout: TextInputLayout? = null
private var passwordRepeatView: TextView? = null private var passwordRepeatView: TextView? = null
private var keyFileTextInputLayout: TextInputLayout? = null
private var keyFileCheckBox: CompoundButton? = null private var keyFileCheckBox: CompoundButton? = null
private var keyFileView: TextView? = null private var keyFileSelectionView: KeyFileSelectionView? = null
private var mListener: AssignPasswordDialogListener? = null private var mListener: AssignPasswordDialogListener? = null
@@ -69,16 +68,6 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
} }
} }
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 { interface AssignPasswordDialogListener {
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?, fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?) keyFileChecked: Boolean, keyFile: Uri?)
@@ -121,13 +110,14 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout) passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password) passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
keyFileTextInputLayout = rootView?.findViewById(R.id.keyfile_input_layout)
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox) keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
keyFileView = rootView?.findViewById(R.id.pass_keyfile) keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
mOpenFileHelper = OpenFileHelper(this) mOpenFileHelper = OpenFileHelper(this)
rootView?.findViewById<View>(R.id.open_database_button)?.setOnClickListener { view -> keyFileSelectionView?.apply {
mOpenFileHelper?.openFileOnClickViewListener?.onClick(view) } setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
setOnLongClickListener(mOpenFileHelper?.openFileOnClickViewListener)
}
val dialog = builder.create() val dialog = builder.create()
@@ -176,14 +166,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
// To check checkboxes if a text is present // To check checkboxes if a text is present
passwordView?.addTextChangedListener(passwordTextWatcher) passwordView?.addTextChangedListener(passwordTextWatcher)
keyFileView?.addTextChangedListener(keyFileTextWatcher)
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
passwordView?.removeTextChangedListener(passwordTextWatcher) passwordView?.removeTextChangedListener(passwordTextWatcher)
keyFileView?.removeTextChangedListener(keyFileTextWatcher)
} }
private fun verifyPassword(): Boolean { private fun verifyPassword(): Boolean {
@@ -216,11 +204,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
if (keyFileCheckBox != null if (keyFileCheckBox != null
&& keyFileCheckBox!!.isChecked) { && keyFileCheckBox!!.isChecked) {
UriUtil.parse(keyFileView?.text?.toString())?.let { uri -> keyFileSelectionView?.uri?.let { uri ->
mKeyFile = uri mKeyFile = uri
} ?: run { } ?: run {
error = true error = true
keyFileTextInputLayout?.error = getString(R.string.error_nokeyfile) keyFileSelectionView?.error = getString(R.string.error_nokeyfile)
} }
} }
return error return error
@@ -265,8 +253,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
) { uri -> ) { uri ->
uri?.let { pathUri -> uri?.let { pathUri ->
keyFileCheckBox?.isChecked = true keyFileCheckBox?.isChecked = true
keyFileView?.text = pathUri.toString() keyFileSelectionView?.uri = pathUri
} }
} }
} }

View File

@@ -0,0 +1,61 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.DatePickerDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.DialogFragment
class DatePickerFragment : DialogFragment() {
private var mDefaultYear: Int = 2000
private var mDefaultMonth: Int = 1
private var mDefaultDay: Int = 1
private var mListener: DatePickerDialog.OnDateSetListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as DatePickerDialog.OnDateSetListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// Create a new instance of DatePickerDialog and return it
return context?.let {
arguments?.apply {
if (containsKey(DEFAULT_YEAR_BUNDLE_KEY))
mDefaultYear = getInt(DEFAULT_YEAR_BUNDLE_KEY)
if (containsKey(DEFAULT_MONTH_BUNDLE_KEY))
mDefaultMonth = getInt(DEFAULT_MONTH_BUNDLE_KEY)
if (containsKey(DEFAULT_DAY_BUNDLE_KEY))
mDefaultDay = getInt(DEFAULT_DAY_BUNDLE_KEY)
}
DatePickerDialog(it, mListener, mDefaultYear, mDefaultMonth, mDefaultDay)
} ?: super.onCreateDialog(savedInstanceState)
}
companion object {
private const val DEFAULT_YEAR_BUNDLE_KEY = "DEFAULT_YEAR_BUNDLE_KEY"
private const val DEFAULT_MONTH_BUNDLE_KEY = "DEFAULT_MONTH_BUNDLE_KEY"
private const val DEFAULT_DAY_BUNDLE_KEY = "DEFAULT_DAY_BUNDLE_KEY"
fun getInstance(defaultYear: Int,
defaultMonth: Int,
defaultDay: Int): DatePickerFragment {
return DatePickerFragment().apply {
arguments = Bundle().apply {
putInt(DEFAULT_YEAR_BUNDLE_KEY, defaultYear)
putInt(DEFAULT_MONTH_BUNDLE_KEY, defaultMonth)
putInt(DEFAULT_DAY_BUNDLE_KEY, defaultDay)
}
}
}
}
}

View File

@@ -112,7 +112,7 @@ class IconPickerDialogFragment : DialogFragment() {
// Retrieve the textColor to tint the icon // Retrieve the textColor to tint the icon
val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK))) ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK)))
ta?.recycle() ta.recycle()
} }
} }

View File

@@ -82,7 +82,9 @@ class SortDialogFragment : DialogFragment() {
builder.setView(rootView) builder.setView(rootView)
// Add action buttons // Add action buttons
.setPositiveButton(android.R.string.ok .setPositiveButton(android.R.string.ok
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) } ) { _, _ -> mListener?.onSortSelected(mSortNodeEnum,
SortNodeEnum.SortNodeParameters(mAscending, mGroupsBefore, mRecycleBinBottom))
}
.setNegativeButton(android.R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending) val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
@@ -150,10 +152,7 @@ class SortDialogFragment : DialogFragment() {
} }
interface SortSelectionListener { interface SortSelectionListener {
fun onSortSelected(sortNodeEnum: SortNodeEnum, fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters)
ascending: Boolean,
groupsBefore: Boolean,
recycleBinBottom: Boolean)
} }
companion object { companion object {

View File

@@ -0,0 +1,57 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.DatePickerDialog
import android.app.Dialog
import android.app.TimePickerDialog
import android.content.Context
import android.os.Bundle
import android.text.format.DateFormat
import androidx.fragment.app.DialogFragment
class TimePickerFragment : DialogFragment() {
private var defaultHour: Int = 0
private var defaultMinute: Int = 0
private var mListener: TimePickerDialog.OnTimeSetListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as TimePickerDialog.OnTimeSetListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// Create a new instance of DatePickerDialog and return it
return context?.let {
arguments?.apply {
if (containsKey(DEFAULT_HOUR_BUNDLE_KEY))
defaultHour = getInt(DEFAULT_HOUR_BUNDLE_KEY)
if (containsKey(DEFAULT_MINUTE_BUNDLE_KEY))
defaultMinute = getInt(DEFAULT_MINUTE_BUNDLE_KEY)
}
TimePickerDialog(it, mListener, defaultHour, defaultMinute, DateFormat.is24HourFormat(activity))
} ?: super.onCreateDialog(savedInstanceState)
}
companion object {
private const val DEFAULT_HOUR_BUNDLE_KEY = "DEFAULT_HOUR_BUNDLE_KEY"
private const val DEFAULT_MINUTE_BUNDLE_KEY = "DEFAULT_MINUTE_BUNDLE_KEY"
fun getInstance(defaultHour: Int,
defaultMinute: Int): TimePickerFragment {
return TimePickerFragment().apply {
arguments = Bundle().apply {
putInt(DEFAULT_HOUR_BUNDLE_KEY, defaultHour)
putInt(DEFAULT_MINUTE_BUNDLE_KEY, defaultMinute)
}
}
}
}
}

View File

@@ -52,14 +52,22 @@ class OpenFileHelper {
this.fragment = context this.fragment = context
} }
inner class OpenFileOnClickViewListener : View.OnClickListener { inner class OpenFileOnClickViewListener : View.OnClickListener, View.OnLongClickListener {
override fun onClick(v: View) { private fun onAbstractClick(longClick: Boolean = false) {
try { try {
try { if (longClick) {
openActivityWithActionOpenDocument() try {
} catch(e: Exception) { openActivityWithActionGetContent()
openActivityWithActionGetContent() } catch (e: Exception) {
openActivityWithActionOpenDocument()
}
} else {
try {
openActivityWithActionOpenDocument()
} catch (e: Exception) {
openActivityWithActionGetContent()
}
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Enable to start the file picker activity", e) Log.e(TAG, "Enable to start the file picker activity", e)
@@ -68,6 +76,15 @@ class OpenFileHelper {
showBrowserDialog() showBrowserDialog()
} }
} }
override fun onClick(v: View) {
onAbstractClick()
}
override fun onLongClick(v: View?): Boolean {
onAbstractClick(true)
return true
}
} }
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")

View File

@@ -20,11 +20,7 @@
package com.kunzisoft.keepass.activities.lock package com.kunzisoft.keepass.activities.lock
import android.app.Activity import android.app.Activity
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View import android.view.View
@@ -34,12 +30,9 @@ import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.stylish.StylishActivity import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.action.ProgressDialogThread import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.*
abstract class LockingActivity : StylishActivity() { abstract class LockingActivity : StylishActivity() {
@@ -81,12 +74,10 @@ abstract class LockingActivity : StylishActivity() {
} }
if (mTimeoutEnable) { if (mTimeoutEnable) {
mLockReceiver = LockReceiver() mLockReceiver = LockReceiver {
val intentFilter = IntentFilter().apply { lockAndExit()
addAction(Intent.ACTION_SCREEN_OFF)
addAction(LOCK_ACTION)
} }
registerReceiver(mLockReceiver, intentFilter) registerLockReceiver(mLockReceiver)
} }
mExitLock = false mExitLock = false
@@ -151,29 +142,12 @@ abstract class LockingActivity : StylishActivity() {
} }
override fun onDestroy() { override fun onDestroy() {
unregisterLockReceiver(mLockReceiver)
super.onDestroy() super.onDestroy()
if (mLockReceiver != null)
unregisterReceiver(mLockReceiver)
}
inner class LockReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// If allowed, lock and exit
if (!TimeoutHelper.temporarilyDisableTimeout) {
intent.action?.let {
when (it) {
Intent.ACTION_SCREEN_OFF -> if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(this@LockingActivity)) {
lockAndExit()
}
LOCK_ACTION -> lockAndExit()
}
}
}
}
} }
protected fun lockAndExit() { protected fun lockAndExit() {
sendBroadcast(Intent(LOCK_ACTION))
lock() lock()
} }
@@ -208,20 +182,8 @@ abstract class LockingActivity : StylishActivity() {
} }
fun Activity.lock() { fun Activity.lock() {
// Stop the Magikeyboard service closeDatabase()
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
MagikIME.removeEntry(this)
// Stop the notification service
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
Log.i(Activity::class.java.name, "Shutdown " + localClassName +
" after inactivity or manual lock")
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {
cancelAll()
}
// Clear data
Database.getInstance().closeAndClear(applicationContext.filesDir)
// Add onActivityForResult response // Add onActivityForResult response
setResult(LockingActivity.RESULT_EXIT_LOCK) setResult(LockingActivity.RESULT_EXIT_LOCK)
finish() finish()

View File

@@ -84,24 +84,25 @@ class FileDatabaseHistoryAdapter(private val context: Context)
// File path // File path
holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString()) holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString())
if (fileDatabaseInfo.dataAccessible()) { if (fileDatabaseInfo.exists) {
holder.fileInformation.clearColorFilter() holder.fileInformation.clearColorFilter()
} else { } else {
holder.fileInformation.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY) holder.fileInformation.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
} }
// Modification // Modification
if (fileDatabaseInfo.lastModificationAccessible()) { fileDatabaseInfo.getModificationString()?.let {
holder.fileModification.text = fileDatabaseInfo.getModificationString() holder.fileModification.text = it
holder.fileModification.visibility = View.VISIBLE holder.fileModification.visibility = View.VISIBLE
} else { } ?: run {
holder.fileModification.visibility = View.GONE holder.fileModification.visibility = View.GONE
} }
// Size // Size
if (fileDatabaseInfo.sizeAccessible()) { fileDatabaseInfo.getSizeString()?.let {
holder.fileSize.text = fileDatabaseInfo.getSizeString() holder.fileSize.text = it
holder.fileSize.visibility = View.VISIBLE holder.fileSize.visibility = View.VISIBLE
} else { } ?: run {
holder.fileSize.visibility = View.GONE holder.fileSize.visibility = View.GONE
} }

View File

@@ -28,8 +28,14 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.cursor.EntryCursorKDB
import com.kunzisoft.keepass.database.cursor.EntryCursorKDBX
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.strikeOut import com.kunzisoft.keepass.view.strikeOut
@@ -38,8 +44,8 @@ class SearchEntryCursorAdapter(private val context: Context,
private val database: Database) private val database: Database)
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) { : androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
private val cursorInflater: LayoutInflater = context.getSystemService( private val cursorInflater: LayoutInflater? = context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
private var displayUsername: Boolean = false private var displayUsername: Boolean = false
private val iconColor: Int private val iconColor: Int
@@ -58,7 +64,7 @@ class SearchEntryCursorAdapter(private val context: Context,
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View { override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
val view = cursorInflater.inflate(R.layout.item_search_entry, parent, false) val view = cursorInflater!!.inflate(R.layout.item_search_entry, parent, false)
val viewHolder = ViewHolder() val viewHolder = ViewHolder()
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon) viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
viewHolder.textViewTitle = view.findViewById(R.id.entry_text) viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
@@ -69,8 +75,7 @@ class SearchEntryCursorAdapter(private val context: Context,
} }
override fun bindView(view: View, context: Context, cursor: Cursor) { override fun bindView(view: View, context: Context, cursor: Cursor) {
getEntryFrom(cursor)?.let { currentEntry ->
database.getEntryFrom(cursor)?.let { currentEntry ->
val viewHolder = view.tag as ViewHolder val viewHolder = view.tag as ViewHolder
// Assign image // Assign image
@@ -98,14 +103,46 @@ class SearchEntryCursorAdapter(private val context: Context,
} }
} }
private class ViewHolder { private fun getEntryFrom(cursor: Cursor): Entry? {
internal var imageViewIcon: ImageView? = null return database.createEntry()?.apply {
internal var textViewTitle: TextView? = null database.startManageEntry(this)
internal var textViewSubTitle: TextView? = null entryKDB?.let { entryKDB ->
(cursor as EntryCursorKDB).populateEntry(entryKDB, database.iconFactory)
}
entryKDBX?.let { entryKDBX ->
(cursor as EntryCursorKDBX).populateEntry(entryKDBX, database.iconFactory)
}
database.stopManageEntry(this)
}
} }
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? { override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
return database.searchEntries(context, constraint.toString()) return searchEntries(context, constraint.toString())
}
private fun searchEntries(context: Context, query: String): Cursor? {
var cursorKDB: EntryCursorKDB? = null
var cursorKDBX: EntryCursorKDBX? = null
if (database.type == DatabaseKDB.TYPE)
cursorKDB = EntryCursorKDB()
if (database.type == DatabaseKDBX.TYPE)
cursorKDBX = EntryCursorKDBX()
val searchGroup = database.createVirtualGroupFromSearch(query, SearchHelper.MAX_SEARCH_ENTRY)
if (searchGroup != null) {
// Search in hide entries but not meta-stream
for (entry in searchGroup.getFilteredChildEntries(*Group.ChildFilter.getDefaults(context))) {
entry.entryKDB?.let {
cursorKDB?.addEntry(it)
}
entry.entryKDBX?.let {
cursorKDBX?.addEntry(it)
}
}
}
return cursorKDB ?: cursorKDBX
} }
fun getEntryFromPosition(position: Int): Entry? { fun getEntryFromPosition(position: Int): Entry? {
@@ -113,9 +150,14 @@ class SearchEntryCursorAdapter(private val context: Context,
val cursor = this.cursor val cursor = this.cursor
if (cursor.moveToFirst() && cursor.move(position)) { if (cursor.moveToFirst() && cursor.move(position)) {
pwEntry = database.getEntryFrom(cursor) pwEntry = getEntryFrom(cursor)
} }
return pwEntry return pwEntry
} }
private class ViewHolder {
internal var imageViewIcon: ImageView? = null
internal var textViewTitle: TextView? = null
internal var textViewSubTitle: TextView? = null
}
} }

View File

@@ -112,6 +112,14 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
).execute() ).execute()
} }
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
ActionDatabaseAsyncTask(
{
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
}
).execute()
}
fun deleteAllKeyFiles() { fun deleteAllKeyFiles() {
ActionDatabaseAsyncTask( ActionDatabaseAsyncTask(
{ {

View File

@@ -38,6 +38,9 @@ interface FileDatabaseHistoryDao {
@Delete @Delete
fun delete(fileDatabaseHistory: FileDatabaseHistoryEntity): Int fun delete(fileDatabaseHistory: FileDatabaseHistoryEntity): Int
@Query("UPDATE file_database_history SET keyfile_uri=null WHERE database_uri = :databaseUriString")
fun deleteKeyFileByDatabaseUri(databaseUriString: String)
@Query("UPDATE file_database_history SET keyfile_uri=null") @Query("UPDATE file_database_history SET keyfile_uri=null")
fun deleteAllKeyFiles() fun deleteAllKeyFiles()

View File

@@ -26,15 +26,22 @@ import android.content.Intent
import android.os.Build import android.os.Build
import android.service.autofill.Dataset import android.service.autofill.Dataset
import android.service.autofill.FillResponse import android.service.autofill.FillResponse
import androidx.annotation.RequiresApi
import android.util.Log import android.util.Log
import android.view.autofill.AutofillManager import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue import android.view.autofill.AutofillValue
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import java.util.* import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
@@ -43,6 +50,7 @@ object AutofillHelper {
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165 private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
private const val ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE private const val ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
fun retrieveAssistStructure(intent: Intent?): AssistStructure? { fun retrieveAssistStructure(intent: Intent?): AssistStructure? {
intent?.let { intent?.let {
@@ -56,27 +64,44 @@ object AutofillHelper {
return String.format("%s (%s)", entryInfo.title, entryInfo.username) return String.format("%s (%s)", entryInfo.title, entryInfo.username)
if (entryInfo.title.isNotEmpty()) if (entryInfo.title.isNotEmpty())
return entryInfo.title return entryInfo.title
if (entryInfo.username.isNotEmpty())
return entryInfo.username
if (entryInfo.url.isNotEmpty()) if (entryInfo.url.isNotEmpty())
return entryInfo.url return entryInfo.url
if (entryInfo.username.isNotEmpty())
return entryInfo.username
return "" return ""
} }
private fun buildDataset(context: Context, internal fun addHeader(responseBuilder: FillResponse.Builder,
entryInfo: EntryInfo, packageName: String,
struct: StructureParser.Result): Dataset? { webDomain: String?,
applicationId: String?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (webDomain != null) {
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_web_domain).apply {
setTextViewText(R.id.autofill_web_domain_text, webDomain)
})
} else if (applicationId != null) {
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_app_id).apply {
setTextViewText(R.id.autofill_app_id_text, applicationId)
})
}
}
}
internal fun buildDataset(context: Context,
entryInfo: EntryInfo,
struct: StructureParser.Result): Dataset? {
val title = makeEntryTitle(entryInfo) val title = makeEntryTitle(entryInfo)
val views = newRemoteViews(context.packageName, title) val views = newRemoteViews(context, title, entryInfo.icon)
val builder = Dataset.Builder(views) val builder = Dataset.Builder(views)
builder.setId(entryInfo.id) builder.setId(entryInfo.id)
struct.password.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.password)) } struct.usernameId?.let { usernameId ->
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
val ids = ArrayList(struct.username) }
if (entryInfo.username.contains("@") || struct.username.isEmpty()) struct.passwordId?.let { password ->
ids.addAll(struct.email) builder.setValue(password, AutofillValue.forText(entryInfo.password))
ids.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.username)) } }
return try { return try {
builder.build() builder.build()
@@ -87,9 +112,16 @@ object AutofillHelper {
} }
/** /**
* Method to hit when right key is selected * Build the Autofill response for one entry
*/ */
fun buildResponseWhenEntrySelected(activity: Activity, entryInfo: EntryInfo) { fun buildResponse(activity: Activity, entryInfo: EntryInfo) {
buildResponse(activity, ArrayList<EntryInfo>().apply { add(entryInfo) })
}
/**
* Build the Autofill response for many entry
*/
fun buildResponse(activity: Activity, entriesInfo: List<EntryInfo>) {
var setResultOk = false var setResultOk = false
activity.intent?.extras?.let { extras -> activity.intent?.extras?.let { extras ->
if (extras.containsKey(ASSIST_STRUCTURE)) { if (extras.containsKey(ASSIST_STRUCTURE)) {
@@ -97,8 +129,9 @@ object AutofillHelper {
StructureParser(structure).parse()?.let { result -> StructureParser(structure).parse()?.let { result ->
// New Response // New Response
val responseBuilder = FillResponse.Builder() val responseBuilder = FillResponse.Builder()
val dataset = buildDataset(activity, entryInfo, result) entriesInfo.forEach {
responseBuilder.addDataset(dataset) responseBuilder.addDataset(buildDataset(activity, it, result))
}
val mReplyIntent = Intent() val mReplyIntent = Intent()
Log.d(activity.javaClass.name, "Successed Autofill auth.") Log.d(activity.javaClass.name, "Successed Autofill auth.")
mReplyIntent.putExtra( mReplyIntent.putExtra(
@@ -116,12 +149,48 @@ object AutofillHelper {
} }
} }
/**
* Utility method to perform actions if item is found or not after an auto search in [database]
*/
fun checkAutoSearchInfo(context: Context,
database: Database,
searchInfo: SearchInfo?,
onItemsFound: (items: List<EntryInfo>) -> Unit,
onItemNotFound: () -> Unit,
onDatabaseClosed: () -> Unit) {
if (database.loaded && TimeoutHelper.checkTime(context)) {
var searchWithoutUI = false
if (PreferencesUtil.isAutofillAutoSearchEnable(context)
&& searchInfo != null) {
// If search provide results
database.createVirtualGroupFromSearch(searchInfo, SearchHelper.MAX_SEARCH_ENTRY)?.let { searchGroup ->
if (searchGroup.getNumberOfChildEntries() > 0) {
searchWithoutUI = true
onItemsFound.invoke(
searchGroup.getChildEntriesInfo(database))
}
}
}
if (!searchWithoutUI) {
onItemNotFound.invoke()
}
} else {
onDatabaseClosed.invoke()
}
}
/** /**
* Utility method to start an activity with an Autofill for result * Utility method to start an activity with an Autofill for result
*/ */
fun startActivityForAutofillResult(activity: Activity, intent: Intent, assistStructure: AssistStructure) { fun startActivityForAutofillResult(activity: Activity,
intent: Intent,
assistStructure: AssistStructure,
searchInfo: SearchInfo?) {
EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent) EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent)
intent.putExtra(ASSIST_STRUCTURE, assistStructure) intent.putExtra(ASSIST_STRUCTURE, assistStructure)
searchInfo?.let {
intent.putExtra(KEY_SEARCH_INFO, it)
}
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE) activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
} }
@@ -140,9 +209,18 @@ object AutofillHelper {
} }
} }
private fun newRemoteViews(packageName: String, remoteViewsText: String): RemoteViews { private fun newRemoteViews(context: Context,
val presentation = RemoteViews(packageName, R.layout.item_autofill_service) remoteViewsText: String,
presentation.setTextViewText(R.id.text, remoteViewsText) remoteViewsIcon: IconImage? = null): RemoteViews {
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
if (remoteViewsIcon != null) {
presentation.assignDatabaseIcon(context,
R.id.autofill_entry_icon,
Database.getInstance().drawFactory,
remoteViewsIcon,
ContextCompat.getColor(context, R.color.green))
}
return presentation return presentation
} }
} }

View File

@@ -31,8 +31,7 @@ import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.timeout.TimeoutHelper
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : AppCompatActivity() { class AutofillLauncherActivity : AppCompatActivity() {
@@ -41,11 +40,31 @@ class AutofillLauncherActivity : AppCompatActivity() {
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE) // Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val assistStructure = AutofillHelper.retrieveAssistStructure(intent) val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
if (assistStructure != null) { if (assistStructure != null) {
if (Database.getInstance().loaded && TimeoutHelper.checkTime(this)) // Build search param
GroupActivity.launchForAutofillResult(this, assistStructure, PreferencesUtil.enableReadOnlyDatabase(this)) val searchInfo = SearchInfo().apply {
else { applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
FileDatabaseSelectActivity.launchForAutofillResult(this, assistStructure) webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
} }
// If database is open
AutofillHelper.checkAutoSearchInfo(this,
Database.getInstance(),
searchInfo,
{ items ->
// Items found
AutofillHelper.buildResponse(this, items)
finish()
},
{
// Show the database UI to select the entry
GroupActivity.launchForAutofillResult(this,
assistStructure)
},
{
// If database not open
FileDatabaseSelectActivity.launchForAutofillResult(this,
assistStructure, searchInfo)
}
)
} else { } else {
setResult(Activity.RESULT_CANCELED) setResult(Activity.RESULT_CANCELED)
finish() finish()
@@ -61,10 +80,20 @@ class AutofillLauncherActivity : AppCompatActivity() {
companion object { companion object {
fun getAuthIntentSenderForResponse(context: Context): IntentSender { private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
val intent = Intent(context, AutofillLauncherActivity::class.java) private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
fun getAuthIntentSenderForResponse(context: Context,
searchInfo: SearchInfo? = null): IntentSender {
return PendingIntent.getActivity(context, 0, return PendingIntent.getActivity(context, 0,
intent, PendingIntent.FLAG_CANCEL_CURRENT).intentSender // Doesn't work with Parcelable (don't know why?)
Intent(context, AutofillLauncherActivity::class.java).apply {
searchInfo?.let {
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
}
},
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
} }
} }
} }

View File

@@ -22,31 +22,78 @@ package com.kunzisoft.keepass.autofill
import android.os.Build import android.os.Build
import android.os.CancellationSignal import android.os.CancellationSignal
import android.service.autofill.* import android.service.autofill.*
import androidx.annotation.RequiresApi
import android.util.Log import android.util.Log
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.SearchInfo
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class KeeAutofillService : AutofillService() { class KeeAutofillService : AutofillService() {
override fun onFillRequest(request: FillRequest, cancellationSignal: CancellationSignal, override fun onFillRequest(request: FillRequest,
cancellationSignal: CancellationSignal,
callback: FillCallback) { callback: FillCallback) {
val fillContexts = request.fillContexts val fillContexts = request.fillContexts
val latestStructure = fillContexts[fillContexts.size - 1].structure val latestStructure = fillContexts[fillContexts.size - 1].structure
cancellationSignal.setOnCancelListener { Log.e(TAG, "Cancel autofill not implemented in this sample.") } cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
val responseBuilder = FillResponse.Builder()
// Check user's settings for authenticating Responses and Datasets. // Check user's settings for authenticating Responses and Datasets.
val parseResult = StructureParser(latestStructure).parse() StructureParser(latestStructure).parse()?.let { parseResult ->
parseResult?.allAutofillIds()?.let { autofillIds ->
if (listOf(*autofillIds).isNotEmpty()) { val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.domain
}
AutofillHelper.checkAutoSearchInfo(this,
Database.getInstance(),
searchInfo,
{ items ->
val responseBuilder = FillResponse.Builder()
AutofillHelper.addHeader(responseBuilder, packageName,
parseResult.domain, parseResult.applicationId)
items.forEach {
responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult))
}
callback.onSuccess(responseBuilder.build())
},
{
// Show UI if no search result
showUIForEntrySelection(parseResult, searchInfo, callback)
},
{
// Show UI if database not open
showUIForEntrySelection(parseResult, searchInfo, callback)
}
)
}
}
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
searchInfo: SearchInfo,
callback: FillCallback) {
parseResult.allAutofillIds().let { autofillIds ->
if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used // If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response. // to generate Response.
val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this) val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this,
val presentation = RemoteViews(packageName, R.layout.item_autofill_service_unlock) searchInfo)
responseBuilder.setAuthentication(autofillIds, sender, presentation) val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (!parseResult.domain.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
setTextViewText(R.id.autofill_web_domain_text, parseResult.domain)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
setTextViewText(R.id.autofill_app_id_text, parseResult.applicationId)
}
} else {
RemoteViews(packageName, R.layout.item_autofill_unlock)
}
responseBuilder.setAuthentication(autofillIds, sender, remoteViewsUnlock)
callback.onSuccess(responseBuilder.build()) callback.onSuccess(responseBuilder.build())
} }
} }

View File

@@ -20,8 +20,8 @@ package com.kunzisoft.keepass.autofill
import android.app.assist.AssistStructure import android.app.assist.AssistStructure
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi
import android.text.InputType import android.text.InputType
import androidx.annotation.RequiresApi
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.view.autofill.AutofillId import android.view.autofill.AutofillId
@@ -37,72 +37,196 @@ internal class StructureParser(private val structure: AssistStructure) {
private var usernameCandidate: AutofillId? = null private var usernameCandidate: AutofillId? = null
fun parse(): Result? { fun parse(): Result? {
result = Result() try {
result?.apply { result = Result()
usernameCandidate = null result?.apply {
for (i in 0 until structure.windowNodeCount) { usernameCandidate = null
val windowNode = structure.getWindowNodeAt(i) mainLoop@ for (i in 0 until structure.windowNodeCount) {
title.add(windowNode.title) val windowNode = structure.getWindowNodeAt(i)
windowNode.rootViewNode.webDomain?.let { applicationId = windowNode.title.toString().split("/")[0]
webDomain.add(it) Log.d(TAG, "Autofill applicationId: $applicationId")
}
parseViewNode(windowNode.rootViewNode)
}
// If not explicit username field found, add the field just before password field.
if (username.isEmpty() && email.isEmpty()
&& password.isNotEmpty() && usernameCandidate != null)
username.add(usernameCandidate!!)
}
return result if (parseViewNode(windowNode.rootViewNode))
break@mainLoop
}
// If not explicit username field found, add the field just before password field.
if (usernameId == null && passwordId != null && usernameCandidate != null)
usernameId = usernameCandidate
}
// Return the result only if password field is retrieved
return if (result?.usernameId != null
&& result?.passwordId != null)
result
else
null
} catch (e: Exception) {
return null
}
} }
private fun parseViewNode(node: AssistStructure.ViewNode) { private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
val hints = node.autofillHints // Get the domain of a web app
node.webDomain?.let {
result?.domain = it
Log.d(TAG, "Autofill domain: $it")
}
// Only parse visible nodes
if (node.visibility == View.VISIBLE) {
if (node.autofillId != null
&& node.autofillType == View.AUTOFILL_TYPE_TEXT) {
// Parse methods
val hints = node.autofillHints
if (hints != null && hints.isNotEmpty()) {
if (parseNodeByAutofillHint(node))
return true
} else if (parseNodeByHtmlAttributes(node))
return true
else if (parseNodeByAndroidInput(node))
return true
}
// Recursive method to process each node
for (i in 0 until node.childCount) {
if (parseViewNode(node.getChildAt(i)))
return true
}
}
return false
}
private fun parseNodeByAutofillHint(node: AssistStructure.ViewNode): Boolean {
val autofillId = node.autofillId val autofillId = node.autofillId
if (autofillId != null) { node.autofillHints?.forEach {
if (hints != null && hints.isNotEmpty()) { when {
when { it.equals(View.AUTOFILL_HINT_USERNAME, true)
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_USERNAME == it } -> result?.username?.add(autofillId) || it.equals(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_EMAIL_ADDRESS == it } -> result?.email?.add(autofillId) || it.equals(View.AUTOFILL_HINT_PHONE, true)
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_PASSWORD == it } -> result?.password?.add(autofillId) || it.equals("usernameOrEmail", true)-> {
else -> Log.d(TAG, "unsupported hints") result?.usernameId = autofillId
Log.d(TAG, "Autofill username hint")
} }
} else if (node.autofillType == View.AUTOFILL_TYPE_TEXT) { it.equals(View.AUTOFILL_HINT_PASSWORD, true)
val inputType = node.inputType || it.contains("password", true) -> {
when { result?.passwordId = autofillId
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS > 0 -> result?.email?.add(autofillId) Log.d(TAG, "Autofill password hint")
inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD > 0 -> result?.password?.add(autofillId) return true
result?.password?.isEmpty() == true -> usernameCandidate = autofillId }
// Ignore autocomplete="off"
// https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
it.equals("off", true) ||
it.equals("on", true) -> {
Log.d(TAG, "Autofill web hint")
return parseNodeByHtmlAttributes(node)
}
else -> Log.d(TAG, "Autofill unsupported hint $it")
}
}
return false
}
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
val autofillId = node.autofillId
val nodHtml = node.htmlInfo
when (nodHtml?.tag?.toLowerCase(Locale.ENGLISH)) {
"input" -> {
nodHtml.attributes?.forEach { pairAttribute ->
when (pairAttribute.first.toLowerCase(Locale.ENGLISH)) {
"type" -> {
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
"tel", "email" -> {
result?.usernameId = autofillId
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
"text" -> {
usernameCandidate = autofillId
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
"password" -> {
result?.passwordId = autofillId
Log.d(TAG, "Autofill password web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
return true
}
}
}
}
} }
} }
} }
return false
}
for (i in 0 until node.childCount) private fun parseNodeByAndroidInput(node: AssistStructure.ViewNode): Boolean {
parseViewNode(node.getChildAt(i)) val autofillId = node.autofillId
val inputType = node.inputType
if (inputType and InputType.TYPE_CLASS_TEXT != 0) {
when {
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS != 0 -> {
result?.usernameId = autofillId
Log.d(TAG, "Autofill username android type: $inputType")
}
inputType and InputType.TYPE_TEXT_VARIATION_NORMAL != 0 ||
inputType and InputType.TYPE_NUMBER_VARIATION_NORMAL != 0 ||
inputType and InputType.TYPE_TEXT_VARIATION_PERSON_NAME != 0 -> {
usernameCandidate = autofillId
Log.d(TAG, "Autofill username candidate android type: $inputType")
}
inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD != 0 ||
inputType and InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD != 0 ||
inputType and InputType.TYPE_NUMBER_VARIATION_PASSWORD != 0 -> {
result?.passwordId = autofillId
Log.d(TAG, "Autofill password android type: $inputType")
return true
}
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT != 0 ||
inputType and InputType.TYPE_TEXT_VARIATION_FILTER != 0 ||
inputType and InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE != 0 ||
inputType and InputType.TYPE_TEXT_VARIATION_PHONETIC != 0 ||
inputType and InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS != 0 ||
inputType and InputType.TYPE_TEXT_VARIATION_URI != 0 ||
inputType and InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT != 0 ||
inputType and InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS != 0 ||
inputType and InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD != 0 -> {
// Type not used
}
else -> {
Log.d(TAG, "Autofill unknown android type: $inputType")
usernameCandidate = autofillId
}
}
}
return false
} }
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
internal class Result { internal class Result {
val title: MutableList<CharSequence> var applicationId: String? = null
val webDomain: MutableList<String> var domain: String? = null
val username: MutableList<AutofillId> set(value) {
val email: MutableList<AutofillId> if (field == null)
val password: MutableList<AutofillId> field = value
}
init { var usernameId: AutofillId? = null
title = ArrayList() set(value) {
webDomain = ArrayList() if (field == null)
username = ArrayList() field = value
email = ArrayList() }
password = ArrayList()
} var passwordId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
fun allAutofillIds(): Array<AutofillId> { fun allAutofillIds(): Array<AutofillId> {
val all = ArrayList<AutofillId>() val all = ArrayList<AutofillId>()
all.addAll(username) usernameId?.let {
all.addAll(email) all.add(it)
all.addAll(password) }
passwordId?.let {
all.add(it)
}
return all.toTypedArray() return all.toTypedArray()
} }
} }

View File

@@ -313,8 +313,9 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
} }
override fun onBiometricException(e: Exception) { override fun onBiometricException(e: Exception) {
if (e.localizedMessage != null) e.localizedMessage?.let {
setAdvancedUnlockedMessageView(e.localizedMessage) setAdvancedUnlockedMessageView(it)
}
} }
private fun showFingerPrintViews(show: Boolean) { private fun showFingerPrintViews(show: Boolean) {

View File

@@ -95,7 +95,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
// really not much to do when no fingerprint support found // really not much to do when no fingerprint support found
isKeyManagerInit = false isKeyManagerInit = false
} else { } else {
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
try { try {
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE) this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
@@ -70,6 +71,9 @@ open class AssignPasswordInDatabaseRunnable (
// Erase the biometric // Erase the biometric
CipherDatabaseAction.getInstance(context) CipherDatabaseAction.getInstance(context)
.deleteByDatabaseUri(mDatabaseUri) .deleteByDatabaseUri(mDatabaseUri)
// Erase the register keyfile
FileDatabaseHistoryAction.getInstance(context)
.deleteKeyFileByDatabaseUri(mDatabaseUri)
if (!result.isSuccess) { if (!result.isSuccess) {
// Erase the current master key // Erase the current master key

View File

@@ -86,8 +86,11 @@ class LoadDatabaseRunnable(private val context: Context,
.addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called .addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called
} }
// Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context)
// Start the opening notification // Start the opening notification
DatabaseOpenNotificationService.startIfAllowed(context) DatabaseOpenNotificationService.start(context)
} else { } else {
mDatabase.closeAndClear(cacheDirectory) mDatabase.closeAndClear(cacheDirectory)
} }

View File

@@ -27,6 +27,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.*
@@ -35,6 +36,7 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
@@ -85,12 +87,17 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener { private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) { override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
TimeoutHelper.temporarilyDisableTimeout(activity) TimeoutHelper.temporarilyDisableTimeout()
// Stop the opening notification
DatabaseOpenNotificationService.stop(activity)
startOrUpdateDialog(titleId, messageId, warningId) startOrUpdateDialog(titleId, messageId, warningId)
} }
override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) { override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) {
TimeoutHelper.temporarilyDisableTimeout(activity) TimeoutHelper.temporarilyDisableTimeout()
// Stop the opening notification
DatabaseOpenNotificationService.stop(activity)
startOrUpdateDialog(titleId, messageId, warningId) startOrUpdateDialog(titleId, messageId, warningId)
} }
@@ -98,7 +105,18 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
onActionFinish?.invoke(actionTask, result) onActionFinish?.invoke(actionTask, result)
// Remove the progress task // Remove the progress task
ProgressTaskDialogFragment.stop(activity) ProgressTaskDialogFragment.stop(activity)
TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(activity) TimeoutHelper.releaseTemporarilyDisableTimeout()
val inTime = if (activity is LockingActivity) {
TimeoutHelper.checkTimeAndLockIfTimeout(activity)
} else {
TimeoutHelper.checkTime(activity)
}
// Start the opening notification if in time
// (databaseOpenService is open manually in Action Open Task)
if (actionTask != ACTION_DATABASE_LOAD_TASK && inTime) {
DatabaseOpenNotificationService.start(activity)
}
} }
} }
@@ -126,7 +144,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
if (serviceConnection == null) { if (serviceConnection == null) {
serviceConnection = object : ServiceConnection { serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) { override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder).apply { mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
addActionTaskListener(actionTaskListener) addActionTaskListener(actionTaskListener)
getService().checkAction() getService().checkAction()
} }

View File

@@ -20,15 +20,11 @@
package com.kunzisoft.keepass.database.element package com.kunzisoft.keepass.database.element
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.action.node.NodeHandler import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.cursor.EntryCursorKDB
import com.kunzisoft.keepass.database.cursor.EntryCursorKDBX
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDB import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
@@ -49,6 +45,7 @@ import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.icons.IconDrawableFactory import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.stream.readBytes4ToInt import com.kunzisoft.keepass.stream.readBytes4ToInt
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.SingletonHolder import com.kunzisoft.keepass.utils.SingletonHolder
@@ -137,6 +134,9 @@ class Database {
val version: String val version: String
get() = mDatabaseKDB?.version ?: mDatabaseKDBX?.version ?: "-" get() = mDatabaseKDB?.version ?: mDatabaseKDBX?.version ?: "-"
val type: Class<*>?
get() = mDatabaseKDB?.javaClass ?: mDatabaseKDBX?.javaClass
val allowDataCompression: Boolean val allowDataCompression: Boolean
get() = mDatabaseKDBX != null get() = mDatabaseKDBX != null
@@ -397,54 +397,17 @@ class Database {
false false
} }
@JvmOverloads fun createVirtualGroupFromSearch(searchQuery: String, max: Int = Integer.MAX_VALUE): Group? {
fun search(str: String, max: Int = Integer.MAX_VALUE): Group? { return mSearchHelper?.createVirtualGroupWithSearchResult(this, searchQuery, max)
return mSearchHelper?.search(this, str, max)
} }
fun searchEntries(context: Context, query: String): Cursor? { fun createVirtualGroupFromSearch(searchInfo: SearchInfo, max: Int = Integer.MAX_VALUE): Group? {
val query = (if (searchInfo.webDomain != null)
var cursorKDB: EntryCursorKDB? = null searchInfo.webDomain
var cursorKDBX: EntryCursorKDBX? = null else
searchInfo.applicationId)
if (mDatabaseKDB != null) ?: return null
cursorKDB = EntryCursorKDB() return mSearchHelper?.createVirtualGroupWithSearchResult(this, query, max)
if (mDatabaseKDBX != null)
cursorKDBX = EntryCursorKDBX()
val searchResult = search(query, SearchHelper.MAX_SEARCH_ENTRY)
if (searchResult != null) {
// Search in hide entries but not meta-stream
for (entry in searchResult.getFilteredChildEntries(*Group.ChildFilter.getDefaults(context))) {
entry.entryKDB?.let {
cursorKDB?.addEntry(it)
}
entry.entryKDBX?.let {
cursorKDBX?.addEntry(it)
}
}
}
return cursorKDB ?: cursorKDBX
}
fun getEntryFrom(cursor: Cursor): Entry? {
val iconFactory = mDatabaseKDB?.iconFactory ?: mDatabaseKDBX?.iconFactory ?: IconImageFactory()
return createEntry()?.apply {
startManageEntry(this)
mDatabaseKDB?.let {
entryKDB?.let { entryKDB ->
(cursor as EntryCursorKDB).populateEntry(entryKDB, iconFactory)
}
}
mDatabaseKDBX?.let {
entryKDBX?.let { entryKDBX ->
(cursor as EntryCursorKDBX).populateEntry(entryKDBX, iconFactory)
}
}
stopManageEntry(this)
}
} }
@Throws(DatabaseOutputException::class) @Throws(DatabaseOutputException::class)
@@ -493,10 +456,11 @@ class Database {
var outputStream: OutputStream? = null var outputStream: OutputStream? = null
try { try {
outputStream = contentResolver.openOutputStream(uri) outputStream = contentResolver.openOutputStream(uri)
val pmo = outputStream?.let { definedOutputStream ->
mDatabaseKDB?.let { DatabaseOutputKDB(it, outputStream) } val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, outputStream) } ?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
pmo?.output() databaseOutput?.output()
}
} catch (e: Exception) { } catch (e: Exception) {
throw IOException(e) throw IOException(e)
} finally { } finally {
@@ -723,7 +687,7 @@ class Database {
fun canRecycle(entry: Entry): Boolean { fun canRecycle(entry: Entry): Boolean {
var canRecycle: Boolean? = null var canRecycle: Boolean? = null
entry.entryKDB?.let { entry.entryKDB?.let {
canRecycle = mDatabaseKDB?.canRecycle(it) canRecycle = mDatabaseKDB?.canRecycle()
} }
entry.entryKDBX?.let { entry.entryKDBX?.let {
canRecycle = mDatabaseKDBX?.canRecycle(it) canRecycle = mDatabaseKDBX?.canRecycle(it)
@@ -734,7 +698,7 @@ class Database {
fun canRecycle(group: Group): Boolean { fun canRecycle(group: Group): Boolean {
var canRecycle: Boolean? = null var canRecycle: Boolean? = null
group.groupKDB?.let { group.groupKDB?.let {
canRecycle = mDatabaseKDB?.canRecycle(it) canRecycle = mDatabaseKDB?.canRecycle()
} }
group.groupKDBX?.let { group.groupKDBX?.let {
canRecycle = mDatabaseKDBX?.canRecycle(it) canRecycle = mDatabaseKDBX?.canRecycle(it)

View File

@@ -46,7 +46,7 @@ class DateInstant : Parcelable {
} }
constructor(string: String) { constructor(string: String) {
jDate = dateFormat.parse(string) jDate = dateFormat.parse(string) ?: jDate
} }
constructor() { constructor() {
@@ -121,7 +121,7 @@ class DateInstant : Parcelable {
} }
} }
private fun isSameDate(d1: Date?, d2: Date?): Boolean { private fun isSameDate(d1: Date, d2: Date): Boolean {
val cal1 = Calendar.getInstance() val cal1 = Calendar.getInstance()
cal1.time = d1 cal1.time = d1
cal1.set(Calendar.MILLISECOND, 0) cal1.set(Calendar.MILLISECOND, 0)
@@ -142,7 +142,7 @@ class DateInstant : Parcelable {
fun getDateTimeString(resources: Resources, date: Date): String { fun getDateTimeString(resources: Resources, date: Date): String {
return java.text.DateFormat.getDateTimeInstance( return java.text.DateFormat.getDateTimeInstance(
java.text.DateFormat.MEDIUM, java.text.DateFormat.MEDIUM,
java.text.DateFormat.MEDIUM, java.text.DateFormat.SHORT,
ConfigurationCompat.getLocales(resources.configuration)[0]) ConfigurationCompat.getLocales(resources.configuration)[0])
.format(date) .format(date)
} }

View File

@@ -26,17 +26,25 @@ import java.util.UUID
class DeletedObject { class DeletedObject {
var uuid: UUID = DatabaseVersioned.UUID_ZERO var uuid: UUID = DatabaseVersioned.UUID_ZERO
var deletionTime: Date? = null private var mDeletionTime: Date? = null
get() = if (field == null) {
Date(System.currentTimeMillis()) fun getDeletionTime(): Date {
} else field if (mDeletionTime == null) {
mDeletionTime = Date(System.currentTimeMillis())
}
return mDeletionTime!!
}
fun setDeletionTime(deletionTime: Date) {
this.mDeletionTime = deletionTime
}
constructor() constructor()
@JvmOverloads @JvmOverloads
constructor(uuid: UUID, deletionTime: Date = Date()) { constructor(uuid: UUID, deletionTime: Date = Date()) {
this.uuid = uuid this.uuid = uuid
this.deletionTime = deletionTime this.mDeletionTime = deletionTime
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

View File

@@ -398,6 +398,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
database?.startManageEntry(this) database?.startManageEntry(this)
entryInfo.id = nodeId.toString() entryInfo.id = nodeId.toString()
entryInfo.title = title entryInfo.title = title
entryInfo.icon = icon
entryInfo.username = username entryInfo.username = username
entryInfo.password = password entryInfo.password = password
entryInfo.url = url entryInfo.url = url

View File

@@ -28,6 +28,7 @@ import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.* import com.kunzisoft.keepass.database.element.node.*
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@@ -251,6 +252,14 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
ArrayList() ArrayList()
} }
fun getChildEntriesInfo(database: Database): List<EntryInfo> {
val entriesInfo = ArrayList<EntryInfo>()
getChildEntries().forEach { entry ->
entriesInfo.add(entry.getEntryInfo(database))
}
return entriesInfo
}
fun getFilteredChildEntries(vararg filter: ChildFilter): List<Entry> { fun getFilteredChildEntries(vararg filter: ChildFilter): List<Entry> {
val withoutMetaStream = filter.contains(ChildFilter.META_STREAM) val withoutMetaStream = filter.contains(ChildFilter.META_STREAM)
val showExpiredEntries = !filter.contains(ChildFilter.EXPIRED) val showExpiredEntries = !filter.contains(ChildFilter.EXPIRED)

View File

@@ -26,7 +26,6 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.stream.NullOutputStream import com.kunzisoft.keepass.stream.NullOutputStream
import java.io.IOException import java.io.IOException
@@ -231,8 +230,9 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
* @param node Node to remove * @param node Node to remove
* @return true if node can be recycle, false elsewhere * @return true if node can be recycle, false elsewhere
*/ */
fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean { // TODO #394 Backup KDB
// TODO #394 Backup pw3 // fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
fun canRecycle(): Boolean {
return true return true
} }
@@ -261,6 +261,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
} }
companion object { companion object {
val TYPE = DatabaseKDB::class.java
const val BACKUP_FOLDER_TITLE = "Backup" const val BACKUP_FOLDER_TITLE = "Backup"
private const val BACKUP_FOLDER_UNDEFINED_ID = -1 private const val BACKUP_FOLDER_UNDEFINED_ID = -1

View File

@@ -29,7 +29,8 @@ import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX
@@ -551,6 +552,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} }
companion object { companion object {
val TYPE = DatabaseKDBX::class.java
private val TAG = DatabaseKDBX::class.java.name private val TAG = DatabaseKDBX::class.java.name
private const val DEFAULT_HISTORY_MAX_ITEMS = 10 // -1 unlimited private const val DEFAULT_HISTORY_MAX_ITEMS = 10 // -1 unlimited

View File

@@ -90,6 +90,7 @@ class FieldReferencesEngine {
if (result != null) { if (result != null) {
val found = result.entry val found = result.entry
found?.stopToManageFieldReferences()
val wanted = result.wanted val wanted = result.wanted
var data: String? = null var data: String? = null
@@ -145,22 +146,15 @@ class FieldReferencesEngine {
searchParametersV4.setupNone() searchParametersV4.setupNone()
searchParametersV4.searchString = ref.substring(4) searchParametersV4.searchString = ref.substring(4)
if (scan == 'T') { when (scan) {
searchParametersV4.searchInTitles = true 'T' -> searchParametersV4.searchInTitles = true
} else if (scan == 'U') { 'U' -> searchParametersV4.searchInUserNames = true
searchParametersV4.searchInUserNames = true 'A' -> searchParametersV4.searchInUrls = true
} else if (scan == 'A') { 'P' -> searchParametersV4.searchInPasswords = true
searchParametersV4.searchInUrls = true 'N' -> searchParametersV4.searchInNotes = true
} else if (scan == 'P') { 'I' -> searchParametersV4.searchInUUIDs = true
searchParametersV4.searchInPasswords = true 'O' -> searchParametersV4.searchInOther = true
} else if (scan == 'N') { else -> return null
searchParametersV4.searchInNotes = true
} else if (scan == 'I') {
searchParametersV4.searchInUUIDs = true
} else if (scan == 'O') {
searchParametersV4.searchInOther = true
} else {
return null
} }
val list = ArrayList<EntryKDBX>() val list = ArrayList<EntryKDBX>()

View File

@@ -40,6 +40,7 @@ interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>,
fun allowAddEntryIfIsRoot(): Boolean fun allowAddEntryIfIsRoot(): Boolean
@Suppress("UNCHECKED_CAST")
fun doForEachChildAndForIt(entryHandler: NodeHandler<Entry>, fun doForEachChildAndForIt(entryHandler: NodeHandler<Entry>,
groupHandler: NodeHandler<Group>) { groupHandler: NodeHandler<Group>) {
doForEachChild(entryHandler, groupHandler) doForEachChild(entryHandler, groupHandler)

View File

@@ -21,8 +21,7 @@ package com.kunzisoft.keepass.database.element.node
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import java.util.*
import java.util.UUID
class NodeIdUUID : NodeId<UUID> { class NodeIdUUID : NodeId<UUID> {

View File

@@ -61,7 +61,9 @@ class BinaryAttachment : Parcelable {
val compressedByte = parcel.readByte().toInt() val compressedByte = parcel.readByte().toInt()
isCompressed = if (compressedByte == 2) null else compressedByte != 0 isCompressed = if (compressedByte == 2) null else compressedByte != 0
isProtected = parcel.readByte().toInt() != 0 isProtected = parcel.readByte().toInt() != 0
dataFile = File(parcel.readString()) parcel.readString()?.let {
dataFile = File(it)
}
} }
@Throws(IOException::class) @Throws(IOException::class)
@@ -74,10 +76,10 @@ class BinaryAttachment : Parcelable {
@Throws(IOException::class) @Throws(IOException::class)
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) { fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
if (dataFile != null) { dataFile?.let { concreteDataFile ->
// To compress, create a new binary with file // To compress, create a new binary with file
if (isCompressed != true) { if (isCompressed != true) {
val fileBinaryCompress = File(dataFile!!.parent, dataFile!!.name + "_temp") val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
var outputStream: GZIPOutputStream? = null var outputStream: GZIPOutputStream? = null
var inputStream: InputStream? = null var inputStream: InputStream? = null
try { try {
@@ -91,8 +93,8 @@ class BinaryAttachment : Parcelable {
outputStream?.close() outputStream?.close()
// Remove unGzip file // Remove unGzip file
if (dataFile!!.delete()) { if (concreteDataFile.delete()) {
if (fileBinaryCompress.renameTo(dataFile)) { if (fileBinaryCompress.renameTo(concreteDataFile)) {
// Harmonize with database compression // Harmonize with database compression
isCompressed = true isCompressed = true
} }
@@ -104,9 +106,9 @@ class BinaryAttachment : Parcelable {
@Throws(IOException::class) @Throws(IOException::class)
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) { fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
if (dataFile != null) { dataFile?.let { concreteDataFile ->
if (isCompressed != false) { if (isCompressed != false) {
val fileBinaryDecompress = File(dataFile!!.parent, dataFile!!.name + "_temp") val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
var outputStream: FileOutputStream? = null var outputStream: FileOutputStream? = null
var inputStream: GZIPInputStream? = null var inputStream: GZIPInputStream? = null
try { try {
@@ -120,8 +122,8 @@ class BinaryAttachment : Parcelable {
outputStream?.close() outputStream?.close()
// Remove gzip file // Remove gzip file
if (dataFile!!.delete()) { if (concreteDataFile.delete()) {
if (fileBinaryDecompress.renameTo(dataFile)) { if (fileBinaryDecompress.renameTo(concreteDataFile)) {
// Harmonize with database compression // Harmonize with database compression
isCompressed = false isCompressed = false
} }

View File

@@ -127,11 +127,7 @@ object DatabaseKDBXXML {
const val ElemCustomData = "CustomData" const val ElemCustomData = "CustomData"
const val ElemStringDictExItem = "Item" const val ElemStringDictExItem = "Item"
val dateFormatter: ThreadLocal<SimpleDateFormat> = object : ThreadLocal<SimpleDateFormat>() { val DateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT).apply {
override fun initialValue(): SimpleDateFormat { timeZone = TimeZone.getTimeZone("UTC")
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
return dateFormat
}
} }
} }

View File

@@ -660,7 +660,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
KdbContext.DeletedObject -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) { KdbContext.DeletedObject -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
ctxDeletedObject?.uuid = readUuid(xpp) ctxDeletedObject?.uuid = readUuid(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) {
ctxDeletedObject?.deletionTime = readTime(xpp) ctxDeletedObject?.setDeletionTime(readTime(xpp))
} else { } else {
readUnknown(xpp) readUnknown(xpp)
} }
@@ -829,7 +829,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
} else { } else {
try { try {
utcDate = DatabaseKDBXXML.dateFormatter.get()?.parse(sDate) utcDate = DatabaseKDBXXML.DateFormatter.parse(sDate)
} catch (e: ParseException) { } catch (e: ParseException) {
// Catch with null test below // Catch with null test below
} }

View File

@@ -384,9 +384,9 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
} }
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeObject(name: String, value: Date?) { private fun writeObject(name: String, value: Date) {
if (header!!.version < DatabaseHeaderKDBX.FILE_VERSION_32_4) { if (header!!.version < DatabaseHeaderKDBX.FILE_VERSION_32_4) {
writeObject(name, DatabaseKDBXXML.dateFormatter.get().format(value)) writeObject(name, DatabaseKDBXXML.DateFormatter.format(value))
} else { } else {
val dt = DateTime(value) val dt = DateTime(value)
val seconds = DateKDBXUtil.convertDateToKDBX4Time(dt) val seconds = DateKDBXUtil.convertDateToKDBX4Time(dt)
@@ -553,7 +553,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject) xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject)
writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid) writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid)
writeObject(DatabaseKDBXXML.ElemDeletionTime, value.deletionTime) writeObject(DatabaseKDBXXML.ElemDeletionTime, value.getDeletionTime())
xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject) xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject)
} }

View File

@@ -26,7 +26,6 @@ import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIterator import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIterator
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDB import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDB
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDBX import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDBX
import java.util.*
class SearchHelper(private val isOmitBackup: Boolean) { class SearchHelper(private val isOmitBackup: Boolean) {
@@ -36,22 +35,19 @@ class SearchHelper(private val isOmitBackup: Boolean) {
private var incrementEntry = 0 private var incrementEntry = 0
fun search(database: Database, qStr: String, max: Int): Group? { fun createVirtualGroupWithSearchResult(database: Database, searchQuery: String, max: Int): Group? {
val searchGroup = database.createGroup() val searchGroup = database.createGroup()
searchGroup?.title = "\"" + qStr + "\"" searchGroup?.title = "\"" + searchQuery + "\""
// Search all entries // Search all entries
val loc = Locale.getDefault()
val finalQStr = qStr.toLowerCase(loc)
incrementEntry = 0 incrementEntry = 0
database.rootGroup?.doForEachChild( database.rootGroup?.doForEachChild(
object : NodeHandler<Entry>() { object : NodeHandler<Entry>() {
override fun operate(node: Entry): Boolean { override fun operate(node: Entry): Boolean {
if (incrementEntry >= max) if (incrementEntry >= max)
return false return false
if (entryContainsString(node, finalQStr, loc)) { if (entryContainsString(node, searchQuery)) {
searchGroup?.addChildEntry(node) searchGroup?.addChildEntry(node)
incrementEntry++ incrementEntry++
} }
@@ -73,7 +69,7 @@ class SearchHelper(private val isOmitBackup: Boolean) {
return searchGroup return searchGroup
} }
private fun entryContainsString(entry: Entry, searchString: String, locale: Locale): Boolean { private fun entryContainsString(entry: Entry, searchString: String): Boolean {
// Entry don't contains string if the search string is empty // Entry don't contains string if the search string is empty
if (searchString.isEmpty()) if (searchString.isEmpty())
@@ -90,11 +86,10 @@ class SearchHelper(private val isOmitBackup: Boolean) {
iterator?.let { iterator?.let {
while (it.hasNext()) { while (it.hasNext()) {
val str = it.next() val currentString = it.next()
if (str.isNotEmpty()) { if (currentString.isNotEmpty()
if (str.toLowerCase(locale).contains(searchString)) { && currentString.contains(searchString, true)) {
return true return true
}
} }
} }
} }

View File

@@ -39,7 +39,7 @@ constructor(private val mEntry: EntryKDB,
title -> mEntry.title title -> mEntry.title
url -> mEntry.url url -> mEntry.url
username -> mEntry.username username -> mEntry.username
comment -> mEntry.notes notes -> mEntry.notes
else -> "" else -> ""
} }
} }
@@ -73,7 +73,7 @@ constructor(private val mEntry: EntryKDB,
title -> mSearchParameters.searchInTitles title -> mSearchParameters.searchInTitles
url -> mSearchParameters.searchInUrls url -> mSearchParameters.searchInUrls
username -> mSearchParameters.searchInUserNames username -> mSearchParameters.searchInUserNames
comment -> mSearchParameters.searchInNotes notes -> mSearchParameters.searchInNotes
else -> true else -> true
} }
@@ -88,7 +88,7 @@ constructor(private val mEntry: EntryKDB,
private const val title = 0 private const val title = 0
private const val url = 1 private const val url = 1
private const val username = 2 private const val username = 2
private const val comment = 3 private const val notes = 3
private const val maxEntries = 4 private const val maxEntries = 4
} }

View File

@@ -22,8 +22,8 @@ package com.kunzisoft.keepass.education
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.preference.PreferenceManager
import android.util.Log import android.util.Log
import androidx.preference.PreferenceManager
import com.getkeepsafe.taptargetview.TapTarget import com.getkeepsafe.taptargetview.TapTarget
import com.getkeepsafe.taptargetview.TapTargetView import com.getkeepsafe.taptargetview.TapTargetView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
@@ -94,7 +94,8 @@ open class Education(val activity: Activity) {
R.string.education_copy_username_key, R.string.education_copy_username_key,
R.string.education_entry_edit_key, R.string.education_entry_edit_key,
R.string.education_password_generator_key, R.string.education_password_generator_key,
R.string.education_entry_new_field_key) R.string.education_entry_new_field_key,
R.string.education_setup_OTP_key)
/** /**
@@ -271,6 +272,18 @@ open class Education(val activity: Activity) {
context.resources.getBoolean(R.bool.education_entry_new_field_default)) context.resources.getBoolean(R.bool.education_entry_new_field_default))
} }
/**
* Determines whether the explanatory view to setup OTP has already been displayed.
*
* @param context The context to open the SharedPreferences
* @return boolean value of education_setup_OTP_key key
*/
fun isEducationSetupOTPPerformed(context: Context): Boolean {
val prefs = getEducationSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.education_setup_OTP_key),
context.resources.getBoolean(R.bool.education_setup_OTP_default))
}
/** /**
* Defines if the reset education preference has been reclicked * Defines if the reset education preference has been reclicked
* *

View File

@@ -37,7 +37,7 @@ class EntryEditActivityEducation(activity: Activity)
activity.getString(R.string.education_generate_password_title), activity.getString(R.string.education_generate_password_title),
activity.getString(R.string.education_generate_password_summary)) activity.getString(R.string.education_generate_password_summary))
.textColorInt(Color.WHITE) .textColorInt(Color.WHITE)
.tintTarget(false) .tintTarget(true)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {
override fun onTargetClick(view: TapTargetView) { override fun onTargetClick(view: TapTargetView) {
@@ -66,7 +66,7 @@ class EntryEditActivityEducation(activity: Activity)
activity.getString(R.string.education_entry_new_field_title), activity.getString(R.string.education_entry_new_field_title),
activity.getString(R.string.education_entry_new_field_summary)) activity.getString(R.string.education_entry_new_field_summary))
.textColorInt(Color.WHITE) .textColorInt(Color.WHITE)
.tintTarget(false) .tintTarget(true)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {
override fun onTargetClick(view: TapTargetView) { override fun onTargetClick(view: TapTargetView) {
@@ -82,4 +82,33 @@ class EntryEditActivityEducation(activity: Activity)
}, },
R.string.education_entry_new_field_key) R.string.education_entry_new_field_key)
} }
/**
* Check and display learning views
* Displays the explanation to setup OTP
*/
fun checkAndPerformedSetUpOTPEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationSetupOTPPerformed(activity),
TapTarget.forView(educationView,
activity.getString(R.string.education_setup_OTP_title),
activity.getString(R.string.education_setup_OTP_summary))
.textColorInt(Color.WHITE)
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {
override fun onTargetClick(view: TapTargetView) {
super.onTargetClick(view)
onEducationViewClick?.invoke(view)
}
override fun onOuterCircleClick(view: TapTargetView?) {
super.onOuterCircleClick(view)
view?.dismiss(false)
onOuterViewClick?.invoke(view)
}
},
R.string.education_setup_OTP_key)
}
} }

View File

@@ -22,16 +22,16 @@ package com.kunzisoft.keepass.icons
import android.content.Context import android.content.Context
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Bitmap import android.graphics.*
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import androidx.core.content.res.ResourcesCompat
import androidx.core.widget.ImageViewCompat
import android.util.Log import android.util.Log
import android.widget.ImageView import android.widget.ImageView
import android.widget.RemoteViews
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.core.widget.ImageViewCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
@@ -70,6 +70,23 @@ class IconDrawableFactory {
} }
} }
/**
* Utility method to assign a drawable to a RemoteView and tint it
*/
fun assignDrawableToRemoteViews(superDrawable: SuperDrawable,
remoteViews: RemoteViews,
imageId: Int,
tintColor: Int = Color.BLACK) {
val bitmap = superDrawable.drawable.toBitmap()
// Tint bitmap if it's not a custom icon
if (!superDrawable.custom) {
Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply {
colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)
})
}
remoteViews.setImageViewBitmap(imageId, bitmap)
}
/** /**
* Get the [SuperDrawable] [icon] (from cache, or build it and add it to the cache if not exists yet), then [tint] it with [tintColor] if needed * Get the [SuperDrawable] [icon] (from cache, or build it and add it to the cache if not exists yet), then [tint] it with [tintColor] if needed
*/ */
@@ -233,7 +250,6 @@ class IconDrawableFactory {
*/ */
fun ImageView.assignDefaultDatabaseIcon(iconFactory: IconDrawableFactory, tintColor: Int = Color.WHITE) { fun ImageView.assignDefaultDatabaseIcon(iconFactory: IconDrawableFactory, tintColor: Int = Color.WHITE) {
IconPackChooser.getSelectedIconPack(context)?.let { selectedIconPack -> IconPackChooser.getSelectedIconPack(context)?.let { selectedIconPack ->
iconFactory.assignDrawableToImageView( iconFactory.assignDrawableToImageView(
iconFactory.getIconSuperDrawable(context, iconFactory.getIconSuperDrawable(context,
selectedIconPack.defaultIconId, selectedIconPack.defaultIconId,
@@ -249,9 +265,10 @@ fun ImageView.assignDefaultDatabaseIcon(iconFactory: IconDrawableFactory, tintCo
/** /**
* Assign a database [icon] to an ImageView and tint it with [tintColor] if needed * Assign a database [icon] to an ImageView and tint it with [tintColor] if needed
*/ */
fun ImageView.assignDatabaseIcon(iconFactory: IconDrawableFactory, icon: IconImage, tintColor: Int = Color.WHITE) { fun ImageView.assignDatabaseIcon(iconFactory: IconDrawableFactory,
icon: IconImage,
tintColor: Int = Color.WHITE) {
IconPackChooser.getSelectedIconPack(context)?.let { selectedIconPack -> IconPackChooser.getSelectedIconPack(context)?.let { selectedIconPack ->
iconFactory.assignDrawableToImageView( iconFactory.assignDrawableToImageView(
iconFactory.getIconSuperDrawable(context, iconFactory.getIconSuperDrawable(context,
icon, icon,
@@ -263,3 +280,19 @@ fun ImageView.assignDatabaseIcon(iconFactory: IconDrawableFactory, icon: IconIma
tintColor) tintColor)
} }
} }
fun RemoteViews.assignDatabaseIcon(context: Context,
imageId: Int,
iconFactory: IconDrawableFactory,
icon: IconImage,
tintColor: Int = Color.BLACK) {
iconFactory.assignDrawableToRemoteViews(
iconFactory.getIconSuperDrawable(context,
icon,
24,
true,
tintColor),
this,
imageId,
tintColor)
}

View File

@@ -24,14 +24,13 @@ import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
class KeyboardLauncherActivity : AppCompatActivity() { class KeyboardLauncherActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
if (Database.getInstance().loaded && TimeoutHelper.checkTime(this)) if (Database.getInstance().loaded && TimeoutHelper.checkTime(this))
GroupActivity.launchForKeyboardSelection(this, PreferencesUtil.enableReadOnlyDatabase(this)) GroupActivity.launchForKeyboardSelection(this)
else { else {
// Pass extra to get entry // Pass extra to get entry
FileDatabaseSelectActivity.launchForKeyboardSelection(this) FileDatabaseSelectActivity.launchForKeyboardSelection(this)

View File

@@ -17,12 +17,12 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
@file:Suppress("DEPRECATION")
package com.kunzisoft.keepass.magikeyboard package com.kunzisoft.keepass.magikeyboard
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.inputmethodservice.InputMethodService import android.inputmethodservice.InputMethodService
import android.inputmethodservice.Keyboard import android.inputmethodservice.Keyboard
import android.inputmethodservice.KeyboardView import android.inputmethodservice.KeyboardView
@@ -42,8 +42,7 @@ import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Field import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.utils.REMOVE_ENTRY_MAGIKEYBOARD_ACTION
class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener { class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
@@ -55,29 +54,18 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
private var fieldsAdapter: FieldsAdapter? = null private var fieldsAdapter: FieldsAdapter? = null
private var playSoundDuringCLick: Boolean = false private var playSoundDuringCLick: Boolean = false
private var lockBroadcastReceiver: BroadcastReceiver? = null private var lockReceiver: LockReceiver? = null
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// Remove the entry and lock the keyboard when the lock signal is receive // Remove the entry and lock the keyboard when the lock signal is receive
lockBroadcastReceiver = object : BroadcastReceiver() { lockReceiver = LockReceiver {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
REMOVE_ENTRY_MAGIKEYBOARD_ACTION, LOCK_ACTION -> {
removeEntryInfo() removeEntryInfo()
assignKeyboardView() assignKeyboardView()
}
}
}
} }
registerReceiver(lockBroadcastReceiver, registerLockReceiver(lockReceiver, true)
IntentFilter().apply {
addAction(LOCK_ACTION)
addAction(REMOVE_ENTRY_MAGIKEYBOARD_ACTION)
}
)
} }
override fun onCreateInputView(): View { override fun onCreateInputView(): View {
@@ -187,12 +175,12 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
private fun switchToPreviousKeyboard() { private fun switchToPreviousKeyboard() {
var imeManager: InputMethodManager? = null var imeManager: InputMethodManager? = null
try { try {
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
switchToPreviousInputMethod() switchToPreviousInputMethod()
} else { } else {
window.window?.let { window -> window.window?.let { window ->
imeManager.switchToLastInputMethod(window.attributes.token) imeManager?.switchToLastInputMethod(window.attributes.token)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -214,8 +202,8 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
KEY_BACK_KEYBOARD -> switchToPreviousKeyboard() KEY_BACK_KEYBOARD -> switchToPreviousKeyboard()
KEY_CHANGE_KEYBOARD -> { KEY_CHANGE_KEYBOARD -> {
val imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager (getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?)
imeManager.showInputMethodPicker() ?.showInputMethodPicker()
} }
KEY_UNLOCK -> { KEY_UNLOCK -> {
} }
@@ -301,7 +289,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
override fun onDestroy() { override fun onDestroy() {
dismissCustomKeys() dismissCustomKeys()
unregisterReceiver(lockBroadcastReceiver) unregisterLockReceiver(lockReceiver)
super.onDestroy() super.onDestroy()
} }

View File

@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.model
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import java.util.* import java.util.*
@@ -29,6 +30,7 @@ class EntryInfo : Parcelable {
var id: String = "" var id: String = ""
var title: String = "" var title: String = ""
var icon: IconImage? = null
var username: String = "" var username: String = ""
var password: String = "" var password: String = ""
var url: String = "" var url: String = ""
@@ -41,11 +43,12 @@ class EntryInfo : Parcelable {
private constructor(parcel: Parcel) { private constructor(parcel: Parcel) {
id = parcel.readString() ?: id id = parcel.readString() ?: id
title = parcel.readString() ?: title title = parcel.readString() ?: title
icon = parcel.readParcelable(IconImage::class.java.classLoader)
username = parcel.readString() ?: username username = parcel.readString() ?: username
password = parcel.readString() ?: password password = parcel.readString() ?: password
url = parcel.readString() ?: url url = parcel.readString() ?: url
notes = parcel.readString() ?: notes notes = parcel.readString() ?: notes
parcel.readList(customFields, Field::class.java.classLoader) parcel.readList(customFields as List<Field>, Field::class.java.classLoader)
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
} }
@@ -56,6 +59,7 @@ class EntryInfo : Parcelable {
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(id) parcel.writeString(id)
parcel.writeString(title) parcel.writeString(title)
parcel.writeParcelable(icon, flags)
parcel.writeString(username) parcel.writeString(username)
parcel.writeString(password) parcel.writeString(password)
parcel.writeString(url) parcel.writeString(url)

View File

@@ -0,0 +1,42 @@
package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.Parcelable
class SearchInfo : Parcelable {
var applicationId: String? = null
var webDomain: String? = null
constructor()
private constructor(parcel: Parcel) {
val readAppId = parcel.readString()
applicationId = if (readAppId.isNullOrEmpty()) null else readAppId
val readDomain = parcel.readString()
webDomain = if (readDomain.isNullOrEmpty()) null else readDomain
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(applicationId ?: "")
parcel.writeString(webDomain ?: "")
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<SearchInfo> = object : Parcelable.Creator<SearchInfo> {
override fun createFromParcel(parcel: Parcel): SearchInfo {
return SearchInfo(parcel)
}
override fun newArray(size: Int): Array<SearchInfo?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -94,20 +94,21 @@ class AttachmentFileNotificationService: LockNotificationService() {
val nextNotificationId = (downloadFileUris.values.maxBy { it.notificationId } val nextNotificationId = (downloadFileUris.values.maxBy { it.notificationId }
?.notificationId ?: notificationId) + 1 ?.notificationId ?: notificationId) + 1
val entryAttachment: EntryAttachment = intent.getParcelableExtra(ATTACHMENT_KEY)
val attachmentNotification = AttachmentNotification(nextNotificationId, entryAttachment)
downloadFileUris[downloadFileUri] = attachmentNotification
try { try {
AttachmentFileAsyncTask(downloadFileUri, intent.getParcelableExtra<EntryAttachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
attachmentNotification, val attachmentNotification = AttachmentNotification(nextNotificationId, entryAttachment)
contentResolver).apply { downloadFileUris[downloadFileUri] = attachmentNotification
onUpdate = { uri, attachment, notificationIdAttach -> AttachmentFileAsyncTask(downloadFileUri,
newNotification(uri, attachment, notificationIdAttach) attachmentNotification,
mActionTaskListeners.forEach { actionListener -> contentResolver).apply {
actionListener.onAttachmentProgress(downloadFileUri, attachment) onUpdate = { uri, attachment, notificationIdAttach ->
newNotification(uri, attachment, notificationIdAttach)
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentProgress(downloadFileUri, attachment)
}
} }
} }.execute()
}.execute() }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to download $downloadFileUri", e) Log.e(TAG, "Unable to download $downloadFileUri", e)
} }

View File

@@ -22,14 +22,11 @@ package com.kunzisoft.keepass.notifications
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.preference.PreferenceManager
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.exception.ClipboardException
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper.NEVER import com.kunzisoft.keepass.timeout.TimeoutHelper.NEVER
import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.LOCK_ACTION
import java.util.* import java.util.*
@@ -62,9 +59,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
mEntryInfo = intent?.getParcelableExtra(EXTRA_ENTRY_INFO) mEntryInfo = intent?.getParcelableExtra(EXTRA_ENTRY_INFO)
//Get settings //Get settings
notificationTimeoutMilliSecs = PreferenceManager.getDefaultSharedPreferences(this) notificationTimeoutMilliSecs = PreferencesUtil.getClipboardTimeout(this)
.getString(getString(R.string.clipboard_timeout_key),
getString(R.string.clipboard_timeout_default))?.toLong() ?: TimeoutHelper.DEFAULT_TIMEOUT
when { when {
intent == null -> Log.w(TAG, "null intent") intent == null -> Log.w(TAG, "null intent")
@@ -78,12 +73,14 @@ class ClipboardEntryNotificationService : LockNotificationService() {
} }
else -> for (actionKey in ClipboardEntryNotificationField.allActionKeys) { else -> for (actionKey in ClipboardEntryNotificationField.allActionKeys) {
if (actionKey == intent.action) { if (actionKey == intent.action) {
val fieldToCopy = intent.getParcelableExtra<ClipboardEntryNotificationField>( intent.getParcelableExtra<ClipboardEntryNotificationField>(
ClipboardEntryNotificationField.getExtraKeyLinkToActionKey(actionKey)) ClipboardEntryNotificationField.getExtraKeyLinkToActionKey(actionKey))?.let {
val nextFields = constructListOfField(intent) fieldToCopy ->
// Remove the current field from the next fields val nextFields = constructListOfField(intent)
nextFields.remove(fieldToCopy) // Remove the current field from the next fields
copyField(fieldToCopy, nextFields) nextFields.remove(fieldToCopy)
copyField(fieldToCopy, nextFields)
}
} }
} }
} }
@@ -91,10 +88,12 @@ class ClipboardEntryNotificationService : LockNotificationService() {
} }
private fun constructListOfField(intent: Intent?): ArrayList<ClipboardEntryNotificationField> { private fun constructListOfField(intent: Intent?): ArrayList<ClipboardEntryNotificationField> {
var fieldList = ArrayList<ClipboardEntryNotificationField>() val fieldList = ArrayList<ClipboardEntryNotificationField>()
if (intent != null && intent.extras != null) { if (intent?.extras?.containsKey(EXTRA_CLIPBOARD_FIELDS) == true) {
if (intent.extras!!.containsKey(EXTRA_CLIPBOARD_FIELDS)) intent.getParcelableArrayListExtra<ClipboardEntryNotificationField>(EXTRA_CLIPBOARD_FIELDS)?.let { retrieveFields ->
fieldList = intent.getParcelableArrayListExtra(EXTRA_CLIPBOARD_FIELDS) fieldList.clear()
fieldList.addAll(retrieveFields)
}
} }
return fieldList return fieldList
} }

View File

@@ -26,8 +26,9 @@ import android.os.Build
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.closeDatabase
class DatabaseOpenNotificationService: LockNotificationService() { class DatabaseOpenNotificationService: LockNotificationService() {
@@ -36,8 +37,14 @@ class DatabaseOpenNotificationService: LockNotificationService() {
private fun stopNotificationAndSendLock() { private fun stopNotificationAndSendLock() {
// Send lock action // Send lock action
sendBroadcast(Intent(LOCK_ACTION)) sendBroadcast(Intent(LOCK_ACTION))
// Stop the service }
stopSelf()
override fun actionOnLock() {
closeDatabase()
// Remove the lock timer (no more needed if it exists)
TimeoutHelper.cancelLockTimer(this)
// Service is stopped after receive the broadcast
super.actionOnLock()
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -60,13 +67,16 @@ class DatabaseOpenNotificationService: LockNotificationService() {
val database = Database.getInstance() val database = Database.getInstance()
if (database.loaded) { if (database.loaded) {
notificationManager?.notify(notificationId, buildNewNotification().apply { startForeground(notificationId, buildNewNotification().apply {
setSmallIcon(R.drawable.notification_ic_database_open) setSmallIcon(R.drawable.notification_ic_database_open)
setContentTitle(getString(R.string.database_opened)) setContentTitle(getString(R.string.database_opened))
setContentText(database.name + " (" + database.version + ")") setContentText(database.name + " (" + database.version + ")")
setAutoCancel(false) setAutoCancel(false)
setContentIntent(pendingDatabaseIntent) setContentIntent(pendingDatabaseIntent)
// Unfortunately swipe is disabled in lollipop+
setDeleteIntent(pendingDeleteIntent) setDeleteIntent(pendingDeleteIntent)
addAction(R.drawable.ic_lock_white_24dp, getString(R.string.lock),
pendingDeleteIntent)
}.build()) }.build())
} else { } else {
stopSelf() stopSelf()
@@ -80,9 +90,11 @@ class DatabaseOpenNotificationService: LockNotificationService() {
companion object { companion object {
const val ACTION_CLOSE_DATABASE = "ACTION_CLOSE_DATABASE" const val ACTION_CLOSE_DATABASE = "ACTION_CLOSE_DATABASE"
fun startIfAllowed(context: Context) { fun start(context: Context) {
if (PreferencesUtil.isPersistentNotificationEnable(context)) { // Start the opening notification, keep it active to receive lock
// Start the opening notification if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(Intent(context, DatabaseOpenNotificationService::class.java))
} else {
context.startService(Intent(context, DatabaseOpenNotificationService::class.java)) context.startService(Intent(context, DatabaseOpenNotificationService::class.java))
} }
} }

View File

@@ -32,6 +32,7 @@ import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseR
import com.kunzisoft.keepass.database.action.history.RestoreEntryHistoryDatabaseRunnable import com.kunzisoft.keepass.database.action.history.RestoreEntryHistoryDatabaseRunnable
import com.kunzisoft.keepass.database.action.node.* import com.kunzisoft.keepass.database.action.node.*
import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
@@ -209,8 +210,12 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(KEY_FILE_CHECKED_KEY) && intent.hasExtra(KEY_FILE_CHECKED_KEY)
&& intent.hasExtra(KEY_FILE_KEY) && intent.hasExtra(KEY_FILE_KEY)
) { ) {
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY) val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY)
if (databaseUri == null)
return null
return CreateDatabaseRunnable(this, return CreateDatabaseRunnable(this,
Database.getInstance(), Database.getInstance(),
databaseUri, databaseUri,
@@ -236,12 +241,15 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(FIX_DUPLICATE_UUID_KEY) && intent.hasExtra(FIX_DUPLICATE_UUID_KEY)
) { ) {
val database = Database.getInstance() val database = Database.getInstance()
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
val masterPassword: String? = intent.getStringExtra(MASTER_PASSWORD_KEY) val masterPassword: String? = intent.getStringExtra(MASTER_PASSWORD_KEY)
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY) val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY)
val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true) val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true)
val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY) val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY)
if (databaseUri == null)
return null
return LoadDatabaseRunnable( return LoadDatabaseRunnable(
this, this,
database, database,
@@ -275,9 +283,10 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(KEY_FILE_CHECKED_KEY) && intent.hasExtra(KEY_FILE_CHECKED_KEY)
&& intent.hasExtra(KEY_FILE_KEY) && intent.hasExtra(KEY_FILE_KEY)
) { ) {
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null
AssignPasswordInDatabaseRunnable(this, AssignPasswordInDatabaseRunnable(this,
Database.getInstance(), Database.getInstance(),
intent.getParcelableExtra(DATABASE_URI_KEY), databaseUri,
intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false), intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false),
intent.getStringExtra(MASTER_PASSWORD_KEY), intent.getStringExtra(MASTER_PASSWORD_KEY),
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false), intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
@@ -304,10 +313,17 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(SAVE_DATABASE_KEY) && intent.hasExtra(SAVE_DATABASE_KEY)
) { ) {
val database = Database.getInstance() val database = Database.getInstance()
database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { parent -> val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
if (parentId == null
|| newGroup == null)
return null
database.getGroupById(parentId)?.let { parent ->
AddGroupRunnable(this, AddGroupRunnable(this,
database, database,
intent.getParcelableExtra(GROUP_KEY), newGroup,
parent, parent,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false), intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable()) AfterActionNodesRunnable())
@@ -323,8 +339,14 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(SAVE_DATABASE_KEY) && intent.hasExtra(SAVE_DATABASE_KEY)
) { ) {
val database = Database.getInstance() val database = Database.getInstance()
database.getGroupById(intent.getParcelableExtra(GROUP_ID_KEY))?.let { oldGroup -> val groupId: NodeId<*>? = intent.getParcelableExtra(GROUP_ID_KEY)
val newGroup: Group = intent.getParcelableExtra(GROUP_KEY) val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
if (groupId == null
|| newGroup == null)
return null
database.getGroupById(groupId)?.let { oldGroup ->
UpdateGroupRunnable(this, UpdateGroupRunnable(this,
database, database,
oldGroup, oldGroup,
@@ -343,10 +365,17 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(SAVE_DATABASE_KEY) && intent.hasExtra(SAVE_DATABASE_KEY)
) { ) {
val database = Database.getInstance() val database = Database.getInstance()
database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { parent -> val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
if (parentId == null
|| newEntry == null)
return null
database.getGroupById(parentId)?.let { parent ->
AddEntryRunnable(this, AddEntryRunnable(this,
database, database,
intent.getParcelableExtra(ENTRY_KEY), newEntry,
parent, parent,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false), intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable()) AfterActionNodesRunnable())
@@ -362,8 +391,14 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(SAVE_DATABASE_KEY) && intent.hasExtra(SAVE_DATABASE_KEY)
) { ) {
val database = Database.getInstance() val database = Database.getInstance()
database.getEntryById(intent.getParcelableExtra(ENTRY_ID_KEY))?.let { oldEntry -> val entryId: NodeId<UUID>? = intent.getParcelableExtra(ENTRY_ID_KEY)
val newEntry: Entry = intent.getParcelableExtra(ENTRY_KEY) val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
if (entryId == null
|| newEntry == null)
return null
database.getEntryById(entryId)?.let { oldEntry ->
UpdateEntryRunnable(this, UpdateEntryRunnable(this,
database, database,
oldEntry, oldEntry,
@@ -383,7 +418,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(SAVE_DATABASE_KEY) && intent.hasExtra(SAVE_DATABASE_KEY)
) { ) {
val database = Database.getInstance() val database = Database.getInstance()
database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { newParent -> val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
database.getGroupById(parentId)?.let { newParent ->
CopyNodesRunnable(this, CopyNodesRunnable(this,
database, database,
getListNodesFromBundle(database, intent.extras!!), getListNodesFromBundle(database, intent.extras!!),
@@ -403,7 +440,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(SAVE_DATABASE_KEY) && intent.hasExtra(SAVE_DATABASE_KEY)
) { ) {
val database = Database.getInstance() val database = Database.getInstance()
database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { newParent -> val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
database.getGroupById(parentId)?.let { newParent ->
MoveNodesRunnable(this, MoveNodesRunnable(this,
database, database,
getListNodesFromBundle(database, intent.extras!!), getListNodesFromBundle(database, intent.extras!!),
@@ -438,7 +477,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(SAVE_DATABASE_KEY) && intent.hasExtra(SAVE_DATABASE_KEY)
) { ) {
val database = Database.getInstance() val database = Database.getInstance()
database.getEntryById(intent.getParcelableExtra(ENTRY_ID_KEY))?.let { mainEntry -> val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
database.getEntryById(entryId)?.let { mainEntry ->
RestoreEntryHistoryDatabaseRunnable(this, RestoreEntryHistoryDatabaseRunnable(this,
database, database,
mainEntry, mainEntry,
@@ -456,7 +497,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(SAVE_DATABASE_KEY) && intent.hasExtra(SAVE_DATABASE_KEY)
) { ) {
val database = Database.getInstance() val database = Database.getInstance()
database.getEntryById(intent.getParcelableExtra(ENTRY_ID_KEY))?.let { mainEntry -> val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
database.getEntryById(entryId)?.let { mainEntry ->
DeleteEntryHistoryDatabaseRunnable(this, DeleteEntryHistoryDatabaseRunnable(this,
database, database,
mainEntry, mainEntry,
@@ -472,10 +515,18 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
return if (intent.hasExtra(OLD_ELEMENT_KEY) return if (intent.hasExtra(OLD_ELEMENT_KEY)
&& intent.hasExtra(NEW_ELEMENT_KEY) && intent.hasExtra(NEW_ELEMENT_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)) { && intent.hasExtra(SAVE_DATABASE_KEY)) {
val oldElement: CompressionAlgorithm? = intent.getParcelableExtra(OLD_ELEMENT_KEY)
val newElement: CompressionAlgorithm? = intent.getParcelableExtra(NEW_ELEMENT_KEY)
if (oldElement == null
|| newElement == null)
return null
return UpdateCompressionBinariesDatabaseRunnable(this, return UpdateCompressionBinariesDatabaseRunnable(this,
Database.getInstance(), Database.getInstance(),
intent.getParcelableExtra(OLD_ELEMENT_KEY), oldElement,
intent.getParcelableExtra(NEW_ELEMENT_KEY), newElement,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false) intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
).apply { ).apply {
mAfterSaveDatabase = { result -> mAfterSaveDatabase = { result ->

View File

@@ -62,7 +62,9 @@ class KeyboardEntryNotificationService : LockNotificationService() {
else -> { else -> {
notificationManager?.cancel(notificationId) notificationManager?.cancel(notificationId)
if (intent.hasExtra(ENTRY_INFO_KEY)) { if (intent.hasExtra(ENTRY_INFO_KEY)) {
newNotification(intent.getParcelableExtra(ENTRY_INFO_KEY)) intent.getParcelableExtra<EntryInfo>(ENTRY_INFO_KEY)?.let {
newNotification(it)
}
} }
} }
} }

View File

@@ -19,31 +19,28 @@
*/ */
package com.kunzisoft.keepass.notifications package com.kunzisoft.keepass.notifications
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import com.kunzisoft.keepass.utils.LockReceiver
import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.registerLockReceiver
import com.kunzisoft.keepass.utils.unregisterLockReceiver
abstract class LockNotificationService : NotificationService() { abstract class LockNotificationService : NotificationService() {
private var lockBroadcastReceiver: BroadcastReceiver? = null private var mLockReceiver: LockReceiver? = null
protected open fun actionOnLock() {
// Stop the service in all cases
stopSelf()
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// Register a lock receiver to stop notification service when lock on keyboard is performed // Register a lock receiver to stop notification service when lock on keyboard is performed
lockBroadcastReceiver = object : BroadcastReceiver() { mLockReceiver = LockReceiver {
override fun onReceive(context: Context?, intent: Intent?) { actionOnLock()
// Stop the service in all cases
stopSelf()
}
} }
registerReceiver(lockBroadcastReceiver, registerLockReceiver(mLockReceiver)
IntentFilter().apply {
addAction(LOCK_ACTION)
}
)
} }
protected fun stopTask(task: Thread?) { protected fun stopTask(task: Thread?) {
@@ -59,7 +56,7 @@ abstract class LockNotificationService : NotificationService() {
override fun onDestroy() { override fun onDestroy() {
unregisterReceiver(lockBroadcastReceiver) unregisterLockReceiver(mLockReceiver)
super.onDestroy() super.onDestroy()
} }

View File

@@ -74,7 +74,7 @@ object OtpEntryFields {
// [^&=\s]+=[^&=\s]+(&[^&=\s]+=[^&=\s]+)* // [^&=\s]+=[^&=\s]+(&[^&=\s]+=[^&=\s]+)*
private const val validKeyValue = "[^&=\\s]+" private const val validKeyValue = "[^&=\\s]+"
private const val validKeyValuePair = "$validKeyValue=$validKeyValue" private const val validKeyValuePair = "$validKeyValue=$validKeyValue"
private const val validKeyValueRegex = "$validKeyValuePair&($validKeyValuePair)*" private const val validKeyValueRegex = "$validKeyValuePair(&$validKeyValuePair)*"
/** /**
* Parse fields of an entry to retrieve an OtpElement * Parse fields of an entry to retrieve an OtpElement
@@ -243,21 +243,18 @@ object OtpEntryFields {
val plainText = getField(OTP_FIELD) val plainText = getField(OTP_FIELD)
if (plainText != null && plainText.isNotEmpty()) { if (plainText != null && plainText.isNotEmpty()) {
if (Pattern.matches(validKeyValueRegex, plainText)) { if (Pattern.matches(validKeyValueRegex, plainText)) {
try { return try {
// KeeOtp string format // KeeOtp string format
val query = breakDownKeyValuePairs(plainText) val query = breakDownKeyValuePairs(plainText)
var secretString = query[SEED_KEY] otpElement.setBase32Secret(query[SEED_KEY] ?: "")
if (secretString == null) otpElement.digits = query[DIGITS_KEY]?.toIntOrNull() ?: OTP_DEFAULT_DIGITS
secretString = "" otpElement.period = query[STEP_KEY]?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
otpElement.setBase32Secret(secretString)
otpElement.digits = query[DIGITS_KEY]?.toIntOrNull() ?: OTP_DEFAULT_DIGITS
otpElement.period = query[STEP_KEY]?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
otpElement.type = OtpType.TOTP otpElement.type = OtpType.TOTP
return true true
} catch (exception: Exception) { } catch (exception: Exception) {
return false false
} }
} else { } else {
// Malformed // Malformed
@@ -281,8 +278,10 @@ object OtpEntryFields {
// malformed // malformed
return false return false
} }
otpElement.period = matcher.group(1).toIntOrNull() ?: TOTP_DEFAULT_PERIOD otpElement.period = matcher.group(1)?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
otpElement.tokenType = OtpTokenType.getFromString(matcher.group(2)) otpElement.tokenType = matcher.group(2)?.let {
OtpTokenType.getFromString(it)
} ?: OtpTokenType.RFC6238
} }
} catch (exception: Exception) { } catch (exception: Exception) {
return false return false

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.settings
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.widget.Toolbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishActivity
class AutofillSettingsActivity : StylishActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_toolbar)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.setTitle(R.string.autofill_preference_title)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, AutofillSettingsFragment())
.commit()
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> onBackPressed()
}
return super.onOptionsItemSelected(item)
}
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2019 Jeremy Jamet / Kunzisoft. * Copyright 2020 Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePassDX. * This file is part of KeePassDX.
* *
@@ -20,16 +20,14 @@
package com.kunzisoft.keepass.settings package com.kunzisoft.keepass.settings
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.preference.PreferenceFragmentCompat
class SettingsAutofillActivity : SettingsActivity() { import com.kunzisoft.keepass.R
override fun onCreate(savedInstanceState: Bundle?) { class AutofillSettingsFragment : PreferenceFragmentCompat() {
super.onCreate(savedInstanceState)
mTimeoutEnable = false
}
override fun retrieveMainFragment(): Fragment { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
return NestedSettingsFragment.newInstance(NestedSettingsFragment.Screen.FORM_FILLING) // Load the preferences from an XML resource
setPreferencesFromResource(R.xml.preferences_autofill, rootKey)
} }
} }

View File

@@ -26,7 +26,7 @@ import android.view.MenuItem
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishActivity import com.kunzisoft.keepass.activities.stylish.StylishActivity
class MagikIMESettings : StylishActivity() { class MagikeyboardSettingsActivity : StylishActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -39,7 +39,7 @@ class MagikIMESettings : StylishActivity() {
if (savedInstanceState == null) { if (savedInstanceState == null) {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, MagikIMESettingsFragment()) .replace(R.id.fragment_container, MagikeyboardSettingsFragment())
.commit() .commit()
} }
} }

View File

@@ -24,7 +24,7 @@ import androidx.preference.PreferenceFragmentCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
class MagikIMESettingsFragment : PreferenceFragmentCompat() { class MagikeyboardSettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
// Load the preferences from an XML resource // Load the preferences from an XML resource

View File

@@ -24,7 +24,6 @@ import android.os.Bundle
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
class MainPreferenceFragment : PreferenceFragmentCompat() { class MainPreferenceFragment : PreferenceFragmentCompat() {

View File

@@ -160,12 +160,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
} }
findPreference<Preference>(getString(R.string.magic_keyboard_preference_key))?.setOnPreferenceClickListener { findPreference<Preference>(getString(R.string.magic_keyboard_preference_key))?.setOnPreferenceClickListener {
startActivity(Intent(context, MagikIMESettings::class.java)) startActivity(Intent(context, MagikeyboardSettingsActivity::class.java))
false
}
findPreference<Preference>(getString(R.string.clipboard_explanation_key))?.setOnPreferenceClickListener {
UriUtil.gotoUrl(context!!, R.string.clipboard_explanation_url)
false false
} }
@@ -174,6 +169,16 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
false false
} }
findPreference<Preference>(getString(R.string.settings_autofill_key))?.setOnPreferenceClickListener {
startActivity(Intent(context, AutofillSettingsActivity::class.java))
false
}
findPreference<Preference>(getString(R.string.clipboard_explanation_key))?.setOnPreferenceClickListener {
UriUtil.gotoUrl(context!!, R.string.clipboard_explanation_url)
false
}
// Present in two places // Present in two places
allowCopyPassword() allowCopyPassword()
} }
@@ -248,13 +253,19 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
BiometricUnlockDatabaseHelper.deleteEntryKeyInKeystoreForBiometric( BiometricUnlockDatabaseHelper.deleteEntryKeyInKeystoreForBiometric(
activity, activity,
object : BiometricUnlockDatabaseHelper.BiometricUnlockErrorCallback { object : BiometricUnlockDatabaseHelper.BiometricUnlockErrorCallback {
override fun onInvalidKeyException(e: Exception) {} fun showException(e: Exception) {
override fun onBiometricException(e: Exception) {
Toast.makeText(context, Toast.makeText(context,
getString(R.string.biometric_scanning_error, e.localizedMessage), getString(R.string.biometric_scanning_error, e.localizedMessage),
Toast.LENGTH_SHORT).show() Toast.LENGTH_SHORT).show()
} }
override fun onInvalidKeyException(e: Exception) {
showException(e)
}
override fun onBiometricException(e: Exception) {
showException(e)
}
}) })
} }
CipherDatabaseAction.getInstance(context.applicationContext).deleteAll() CipherDatabaseAction.getInstance(context.applicationContext).deleteAll()
@@ -276,6 +287,9 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
private fun onCreateAppearancePreferences(rootKey: String?) { private fun onCreateAppearancePreferences(rootKey: String?) {
setPreferencesFromResource(R.xml.preferences_appearance, rootKey) setPreferencesFromResource(R.xml.preferences_appearance, rootKey)
// To change list items appearance
PreferencesUtil.APPEARANCE_CHANGED = true
activity?.let { activity -> activity?.let { activity ->
findPreference<ListPreference>(getString(R.string.setting_style_key))?.setOnPreferenceChangeListener { _, newValue -> findPreference<ListPreference>(getString(R.string.setting_style_key))?.setOnPreferenceChangeListener { _, newValue ->
var styleEnabled = true var styleEnabled = true

View File

@@ -31,7 +31,6 @@ import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.lock
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
@@ -551,14 +550,10 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
val settingActivity = activity as SettingsActivity? val settingActivity = activity as SettingsActivity?
when (item.itemId) { return when (item.itemId) {
R.id.menu_lock -> {
settingActivity?.lock()
return true
}
R.id.menu_save_database -> { R.id.menu_save_database -> {
settingActivity?.mProgressDialogThread?.startDatabaseSave(!mDatabaseReadOnly) settingActivity?.mProgressDialogThread?.startDatabaseSave(!mDatabaseReadOnly)
return true true
} }
else -> { else -> {
@@ -566,7 +561,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
settingActivity?.let { settingActivity?.let {
MenuUtil.onDefaultMenuOptionsItemSelected(it, item, mDatabaseReadOnly, true) MenuUtil.onDefaultMenuOptionsItemSelected(it, item, mDatabaseReadOnly, true)
} }
return super.onOptionsItemSelected(item) super.onOptionsItemSelected(item)
} }
} }
} }

View File

@@ -20,7 +20,8 @@
package com.kunzisoft.keepass.settings package com.kunzisoft.keepass.settings
import android.content.Context import android.content.Context
import android.preference.PreferenceManager import android.net.Uri
import androidx.preference.PreferenceManager
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.SortNodeEnum import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
@@ -28,6 +29,38 @@ import java.util.*
object PreferencesUtil { object PreferencesUtil {
var APPEARANCE_CHANGED = false
fun saveDefaultDatabasePath(context: Context, defaultDatabaseUri: Uri?) {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs?.edit()?.apply {
defaultDatabaseUri?.let {
putString(context.getString(R.string.default_database_path_key), it.toString())
} ?: kotlin.run {
remove(context.getString(R.string.default_database_path_key))
}
apply()
}
}
fun getDefaultDatabasePath(context: Context): String? {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getString(context.getString(R.string.default_database_path_key), "")
}
fun saveNodeSort(context: Context,
sortNodeEnum: SortNodeEnum,
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs?.edit()?.apply {
putString(context.getString(R.string.sort_node_key), sortNodeEnum.name)
putBoolean(context.getString(R.string.sort_ascending_key), sortNodeParameters.ascending)
putBoolean(context.getString(R.string.sort_group_before_key), sortNodeParameters.groupsBefore)
putBoolean(context.getString(R.string.sort_recycle_bin_bottom_key), sortNodeParameters.recycleBinBottom)
apply()
}
}
fun rememberDatabaseLocations(context: Context): Boolean { fun rememberDatabaseLocations(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.remember_database_locations_key), return prefs.getBoolean(context.getString(R.string.remember_database_locations_key),
@@ -147,6 +180,13 @@ object PreferencesUtil {
} }
} }
fun getClipboardTimeout(context: Context): Long {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getString(context.getString(R.string.clipboard_timeout_key),
context.getString(R.string.clipboard_timeout_default))?.toLong()
?: TimeoutHelper.DEFAULT_TIMEOUT
}
fun isLockDatabaseWhenScreenShutOffEnable(context: Context): Boolean { fun isLockDatabaseWhenScreenShutOffEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.lock_database_screen_off_key), return prefs.getBoolean(context.getString(R.string.lock_database_screen_off_key),
@@ -159,18 +199,18 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.lock_database_back_root_default)) context.resources.getBoolean(R.bool.lock_database_back_root_default))
} }
fun showLockDatabaseButton(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.lock_database_show_button_key),
context.resources.getBoolean(R.bool.lock_database_show_button_default))
}
fun isAutoSaveDatabaseEnabled(context: Context): Boolean { fun isAutoSaveDatabaseEnabled(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.enable_auto_save_database_key), return prefs.getBoolean(context.getString(R.string.enable_auto_save_database_key),
context.resources.getBoolean(R.bool.enable_auto_save_database_default)) context.resources.getBoolean(R.bool.enable_auto_save_database_default))
} }
fun isPersistentNotificationEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.persistent_notification_key),
context.resources.getBoolean(R.bool.persistent_notification_default))
}
fun isBiometricUnlockEnable(context: Context): Boolean { fun isBiometricUnlockEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key), return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
@@ -314,4 +354,10 @@ object PreferencesUtil {
return prefs.getBoolean(context.getString(R.string.keyboard_key_sound_key), return prefs.getBoolean(context.getString(R.string.keyboard_key_sound_key),
context.resources.getBoolean(R.bool.keyboard_key_sound_default)) context.resources.getBoolean(R.bool.keyboard_key_sound_default))
} }
fun isAutofillAutoSearchEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.autofill_auto_search_key),
context.resources.getBoolean(R.bool.autofill_auto_search_default))
}
} }

View File

@@ -26,6 +26,8 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@@ -47,6 +49,7 @@ open class SettingsActivity
private var coordinatorLayout: CoordinatorLayout? = null private var coordinatorLayout: CoordinatorLayout? = null
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
private var lockView: View? = null
companion object { companion object {
@@ -84,6 +87,11 @@ open class SettingsActivity
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener {
lockAndExit()
}
if (savedInstanceState == null) { if (savedInstanceState == null) {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, retrieveMainFragment()) .add(R.id.fragment_container, retrieveMainFragment())
@@ -154,6 +162,23 @@ open class SettingsActivity
keyFile: Uri?) { keyFile: Uri?) {
} }
private fun hideOrShowLockButton(key: NestedSettingsFragment.Screen) {
if (PreferencesUtil.showLockDatabaseButton(this)) {
when (key) {
NestedSettingsFragment.Screen.DATABASE,
NestedSettingsFragment.Screen.DATABASE_MASTER_KEY,
NestedSettingsFragment.Screen.DATABASE_SECURITY -> {
lockView?.visibility = View.VISIBLE
}
else -> {
lockView?.visibility = View.GONE
}
}
} else {
lockView?.visibility = View.GONE
}
}
override fun onBackPressed() { override fun onBackPressed() {
// this if statement is necessary to navigate through nested and main fragments // this if statement is necessary to navigate through nested and main fragments
if (supportFragmentManager.backStackEntryCount == 0) { if (supportFragmentManager.backStackEntryCount == 0) {
@@ -162,6 +187,7 @@ open class SettingsActivity
supportFragmentManager.popBackStack() supportFragmentManager.popBackStack()
} }
toolbar?.setTitle(R.string.settings) toolbar?.setTitle(R.string.settings)
hideOrShowLockButton(NestedSettingsFragment.Screen.APPLICATION)
} }
private fun replaceFragment(key: NestedSettingsFragment.Screen) { private fun replaceFragment(key: NestedSettingsFragment.Screen) {
@@ -173,6 +199,7 @@ open class SettingsActivity
.commit() .commit()
toolbar?.title = NestedSettingsFragment.retrieveTitle(resources, key) toolbar?.title = NestedSettingsFragment.retrieveTitle(resources, key)
hideOrShowLockButton(key)
} }
override fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen) { override fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen) {

View File

@@ -120,7 +120,7 @@ class HashedBlockInputStream(inputStream: InputStream) : InputStream() {
} }
val computedHash = messageDigest.digest(buffer) val computedHash = messageDigest.digest(buffer)
if (computedHash == null || computedHash.size != HASH_SIZE) { if (computedHash.size != HASH_SIZE) {
throw IOException("Hash wrong size") throw IOException("Hash wrong size")
} }

View File

@@ -23,6 +23,7 @@ import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
import java.security.MessageDigest import java.security.MessageDigest
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import kotlin.math.min
class HashedBlockOutputStream : OutputStream { class HashedBlockOutputStream : OutputStream {
@@ -61,11 +62,11 @@ class HashedBlockOutputStream : OutputStream {
override fun close() { override fun close() {
if (bufferPos != 0) { if (bufferPos != 0) {
// Write remaining buffered amount // Write remaining buffered amount
WriteHashedBlock() writeHashedBlock()
} }
// Write terminating block // Write terminating block
WriteHashedBlock() writeHashedBlock()
flush() flush()
baseStream!!.close() baseStream!!.close()
@@ -82,12 +83,12 @@ class HashedBlockOutputStream : OutputStream {
var counter = count var counter = count
while (counter > 0) { while (counter > 0) {
if (bufferPos == buffer!!.size) { if (bufferPos == buffer!!.size) {
WriteHashedBlock() writeHashedBlock()
} }
val copyLen = Math.min(buffer!!.size - bufferPos, counter) val copyLen = min(buffer!!.size - bufferPos, counter)
System.arraycopy(b, currentOffset, buffer, bufferPos, copyLen) System.arraycopy(b, currentOffset, buffer!!, bufferPos, copyLen)
currentOffset += copyLen currentOffset += copyLen
bufferPos += copyLen bufferPos += copyLen
@@ -97,21 +98,21 @@ class HashedBlockOutputStream : OutputStream {
} }
@Throws(IOException::class) @Throws(IOException::class)
private fun WriteHashedBlock() { private fun writeHashedBlock() {
baseStream!!.writeUInt(bufferIndex) baseStream!!.writeUInt(bufferIndex)
bufferIndex++ bufferIndex++
if (bufferPos > 0) { if (bufferPos > 0) {
var md: MessageDigest? = null val messageDigest: MessageDigest
try { try {
md = MessageDigest.getInstance("SHA-256") messageDigest = MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) { } catch (e: NoSuchAlgorithmException) {
throw IOException("SHA-256 not implemented here.") throw IOException("SHA-256 not implemented here.")
} }
val hash: ByteArray val hash: ByteArray
md!!.update(buffer, 0, bufferPos) messageDigest.update(buffer!!, 0, bufferPos)
hash = md.digest() hash = messageDigest.digest()
/* /*
if ( bufferPos == buffer.length) { if ( bufferPos == buffer.length) {
hash = md.digest(buffer); hash = md.digest(buffer);

View File

@@ -25,7 +25,6 @@ import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.preference.PreferenceManager
import android.text.SpannableString import android.text.SpannableString
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.text.util.Linkify import android.text.util.Linkify
@@ -33,6 +32,7 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.exception.ClipboardException import com.kunzisoft.keepass.database.exception.ClipboardException
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.* import java.util.*
class ClipboardHelper(private val context: Context) { class ClipboardHelper(private val context: Context) {
@@ -58,13 +58,9 @@ class ClipboardHelper(private val context: Context) {
return return
} }
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val clipboardTimeout = PreferencesUtil.getClipboardTimeout(context)
val sClipClear = prefs.getString(context.getString(R.string.clipboard_timeout_key), if (clipboardTimeout > 0) {
context.getString(R.string.clipboard_timeout_default)) mTimer.schedule(ClearClipboardTask(context, text), clipboardTimeout)
val clipClearTime = (sClipClear ?: "300000").toLong()
if (clipClearTime > 0) {
mTimer.schedule(ClearClipboardTask(context, text), clipClearTime)
} }
} }
@@ -89,7 +85,7 @@ class ClipboardHelper(private val context: Context) {
@Throws(ClipboardException::class) @Throws(ClipboardException::class)
fun copyToClipboard(label: String, value: String) { fun copyToClipboard(label: String, value: String) {
try { try {
getClipboardManager()?.primaryClip = ClipData.newPlainText(label, value) getClipboardManager()?.setPrimaryClip(ClipData.newPlainText(label, value))
} catch (e: Exception) { } catch (e: Exception) {
throw ClipboardException(e) throw ClipboardException(e)
} }

View File

@@ -46,53 +46,63 @@ object TimeoutHelper {
private set private set
private fun getLockPendingIntent(context: Context): PendingIntent { private fun getLockPendingIntent(context: Context): PendingIntent {
return PendingIntent.getBroadcast(context, return PendingIntent.getBroadcast(context.applicationContext,
REQUEST_ID, REQUEST_ID,
Intent(LOCK_ACTION), Intent(LOCK_ACTION),
PendingIntent.FLAG_CANCEL_CURRENT) PendingIntent.FLAG_CANCEL_CURRENT)
} }
/** /**
* Record the current time to check it later with checkTime * Start the lock timer by creating an alarm,
* if the method is recalled with a previous lock timer pending, the previous one is deleted
*/ */
fun recordTime(context: Context) { private fun startLockTimer(context: Context) {
// Record timeout time in case timeout service is killed
PreferencesUtil.saveCurrentTime(context)
if (Database.getInstance().loaded) { if (Database.getInstance().loaded) {
val timeout = PreferencesUtil.getAppTimeout(context) val timeout = PreferencesUtil.getAppTimeout(context)
// No timeout don't start timeout service
if (timeout != NEVER) { if (timeout != NEVER) {
val triggerTime = System.currentTimeMillis() + timeout // No timeout don't start timeout service
val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager (context.applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
Log.d(TAG, "TimeoutHelper start") val triggerTime = System.currentTimeMillis() + timeout
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { Log.d(TAG, "TimeoutHelper start")
am.setExact(AlarmManager.RTC, triggerTime, getLockPendingIntent(context)) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
} else { alarmManager.setExact(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
am.set(AlarmManager.RTC, triggerTime, getLockPendingIntent(context)) } else {
alarmManager.set(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
}
} }
} }
} }
} }
/**
* Cancel the lock timer currently pending, useful if lock was triggered by another way
*/
fun cancelLockTimer(context: Context) {
(context.applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
Log.d(TAG, "TimeoutHelper cancel")
alarmManager.cancel(getLockPendingIntent(context))
}
}
/**
* Record the current time, to check it later with checkTime and start a new lock timer
*/
fun recordTime(context: Context) {
// Record timeout time in case timeout service is killed
PreferencesUtil.saveCurrentTime(context)
startLockTimer(context)
}
/** /**
* Check the time previously record with recordTime and do the [timeoutAction] if timeout * Check the time previously record with recordTime and do the [timeoutAction] if timeout
* if temporarilyDisableTimeout() is called, the function as no effect until releaseTemporarilyDisableTimeoutAndCheckTime() is called * if temporarilyDisableTimeout() is called, the function as no effect until releaseTemporarilyDisableTimeoutAndCheckTime() is called
* return 'false' if timeout, 'true' if in time * return 'false' and send broadcast lock action if timeout, 'true' if in time
*/ */
fun checkTime(context: Context, timeoutAction: (() -> Unit)? = null): Boolean { fun checkTime(context: Context, timeoutAction: (() -> Unit)? = null): Boolean {
// No effect if temporarily disable // No effect if temporarily disable
if (temporarilyDisableTimeout) if (temporarilyDisableTimeout)
return true return true
// Cancel the lock PendingIntent
if (Database.getInstance().loaded) {
val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
Log.d(TAG, "TimeoutHelper cancel")
am.cancel(getLockPendingIntent(context))
}
// Check whether the timeout has expired // Check whether the timeout has expired
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
@@ -115,6 +125,7 @@ object TimeoutHelper {
if (diff >= appTimeout) { if (diff >= appTimeout) {
// We have timed out // We have timed out
timeoutAction?.invoke() timeoutAction?.invoke()
context.sendBroadcast(Intent(LOCK_ACTION))
return false return false
} }
return true return true
@@ -142,27 +153,14 @@ object TimeoutHelper {
/** /**
* Temporarily disable timeout, checkTime() function always return true * Temporarily disable timeout, checkTime() function always return true
*/ */
fun temporarilyDisableTimeout(context: Context) { fun temporarilyDisableTimeout() {
temporarilyDisableTimeout = true temporarilyDisableTimeout = true
// Stop the opening notification
DatabaseOpenNotificationService.stop(context)
} }
/** /**
* Release the temporarily disable timeout and directly call checkTime() * Release the temporarily disable timeout
*/ */
fun releaseTemporarilyDisableTimeoutAndLockIfTimeout(context: Context): Boolean { fun releaseTemporarilyDisableTimeout() {
temporarilyDisableTimeout = false temporarilyDisableTimeout = false
val inTime = if (context is LockingActivity) {
checkTimeAndLockIfTimeout(context)
} else {
checkTime(context)
}
if (inTime) {
// Start the opening notification
DatabaseOpenNotificationService.startIfAllowed(context)
}
return inTime
} }
} }

View File

@@ -19,10 +19,110 @@
*/ */
package com.kunzisoft.keepass.utils package com.kunzisoft.keepass.utils
import android.app.AlarmManager
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Context.ALARM_SERVICE
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
const val DATABASE_START_TASK_ACTION = "com.kunzisoft.keepass.DATABASE_START_TASK_ACTION" const val DATABASE_START_TASK_ACTION = "com.kunzisoft.keepass.DATABASE_START_TASK_ACTION"
const val DATABASE_STOP_TASK_ACTION = "com.kunzisoft.keepass.DATABASE_STOP_TASK_ACTION" const val DATABASE_STOP_TASK_ACTION = "com.kunzisoft.keepass.DATABASE_STOP_TASK_ACTION"
const val LOCK_ACTION = "com.kunzisoft.keepass.LOCK" const val LOCK_ACTION = "com.kunzisoft.keepass.LOCK"
const val REMOVE_ENTRY_MAGIKEYBOARD_ACTION = "com.kunzisoft.keepass.REMOVE_ENTRY_MAGIKEYBOARD" const val REMOVE_ENTRY_MAGIKEYBOARD_ACTION = "com.kunzisoft.keepass.REMOVE_ENTRY_MAGIKEYBOARD"
class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
var mLockPendingIntent: PendingIntent? = null
override fun onReceive(context: Context, intent: Intent) {
// If allowed, lock and exit
if (!TimeoutHelper.temporarilyDisableTimeout) {
intent.action?.let {
when (it) {
Intent.ACTION_SCREEN_ON -> {
cancelLockPendingIntent(context)
}
Intent.ACTION_SCREEN_OFF -> {
if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(context)) {
mLockPendingIntent = PendingIntent.getBroadcast(context,
4575,
Intent(intent).apply {
action = LOCK_ACTION
},
0)
// Launch the effective action after a small time
val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong()
val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager?
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager?.setExact(AlarmManager.RTC_WAKEUP, first, mLockPendingIntent)
} else {
alarmManager?.set(AlarmManager.RTC_WAKEUP, first, mLockPendingIntent)
}
} else {
cancelLockPendingIntent(context)
}
}
LOCK_ACTION,
REMOVE_ENTRY_MAGIKEYBOARD_ACTION -> lockAction.invoke()
else -> {}
}
}
}
}
private fun cancelLockPendingIntent(context: Context) {
mLockPendingIntent?.let {
val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager?
alarmManager?.cancel(mLockPendingIntent)
mLockPendingIntent = null
}
}
}
fun Context.registerLockReceiver(lockReceiver: LockReceiver?,
registerRemoveEntryMagikeyboard: Boolean = false) {
lockReceiver?.let {
registerReceiver(it, IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_SCREEN_ON)
addAction(LOCK_ACTION)
if (registerRemoveEntryMagikeyboard)
addAction(REMOVE_ENTRY_MAGIKEYBOARD_ACTION)
})
}
}
fun Context.unregisterLockReceiver(lockReceiver: LockReceiver?) {
lockReceiver?.let {
unregisterReceiver(it)
}
}
fun Context.closeDatabase() {
// Stop the Magikeyboard service
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
MagikIME.removeEntry(this)
// Stop the notification service
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
Log.i(Context::class.java.name, "Shutdown after inactivity or manual lock")
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager?)?.apply {
cancelAll()
}
// Clear data
Database.getInstance().closeAndClear(applicationContext.filesDir)
}

View File

@@ -21,25 +21,77 @@ package com.kunzisoft.keepass.utils
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.text.format.Formatter
import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import java.io.Serializable
import java.text.DateFormat
import java.util.*
class FileDatabaseInfo : FileInfo { class FileDatabaseInfo : Serializable {
constructor(context: Context, fileUri: Uri): super(context, fileUri) private var context: Context
private var documentFile: DocumentFile? = null
var fileUri: Uri?
private set
constructor(context: Context, filePath: String): super(context, filePath) constructor(context: Context, fileUri: Uri) {
this.context = context
this.fileUri = fileUri
init()
}
constructor(context: Context, filePath: String) {
this.context = context
this.fileUri = UriUtil.parse(filePath)
init()
}
fun init() {
documentFile = UriUtil.getFileData(context, fileUri)
}
var exists: Boolean = false
get() {
return documentFile?.exists() ?: field
}
private set
var canRead: Boolean = false
get() {
return documentFile?.canRead() ?: field
}
private set
var canWrite: Boolean = false
get() {
return documentFile?.canWrite() ?: field
}
private set
fun getModificationString(): String? {
return documentFile?.lastModified()?.let {
DateFormat.getDateTimeInstance()
.format(Date(it))
}
}
fun getSizeString(): String? {
return documentFile?.let {
Formatter.formatFileSize(context, it.length())
}
}
fun retrieveDatabaseAlias(alias: String): String { fun retrieveDatabaseAlias(alias: String): String {
return when { return when {
alias.isNotEmpty() -> alias alias.isNotEmpty() -> alias
PreferencesUtil.isFullFilePathEnable(context) -> filePath ?: "" PreferencesUtil.isFullFilePathEnable(context) -> fileUri?.path ?: ""
else -> fileName ?: "" else -> if (exists) documentFile?.name ?: "" else fileUri?.path ?: ""
} }
} }
fun retrieveDatabaseTitle(titleCallback: (String)->Unit) { fun retrieveDatabaseTitle(titleCallback: (String)->Unit) {
fileUri?.let { fileUri -> fileUri?.let { fileUri ->
FileDatabaseHistoryAction.getInstance(context.applicationContext) FileDatabaseHistoryAction.getInstance(context.applicationContext)
.getFileDatabaseHistory(fileUri) { fileDatabaseHistoryEntity -> .getFileDatabaseHistory(fileUri) { fileDatabaseHistoryEntity ->
@@ -48,5 +100,4 @@ class FileDatabaseInfo : FileInfo {
} }
} }
} }
} }

View File

@@ -1,84 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.utils
import android.content.Context
import android.net.Uri
import android.text.format.Formatter
import java.io.Serializable
import java.text.DateFormat
import java.util.*
open class FileInfo : Serializable {
var context: Context
var fileUri: Uri?
var filePath: String? = null
var fileName: String? = ""
var lastModification = Date(0L)
var size: Long = 0L
constructor(context: Context, fileUri: Uri) {
this.context = context
this.fileUri = fileUri
init()
}
constructor(context: Context, filePath: String) {
this.context = context
this.fileUri = UriUtil.parse(filePath)
init()
}
fun init() {
this.filePath = fileUri?.path
UriUtil.getFileData(context, fileUri)?.let { file ->
size = file.length()
fileName = file.name
lastModification = Date(file.lastModified())
}
if (fileName == null || fileName!!.isEmpty()) {
fileName = filePath
}
}
fun lastModificationAccessible(): Boolean {
return lastModification.after(Date(0L))
}
fun sizeAccessible(): Boolean {
return size != 0L
}
fun dataAccessible(): Boolean {
return UriUtil.isUriAccessible(context.contentResolver, fileUri)
}
fun getModificationString(): String {
return DateFormat.getDateTimeInstance()
.format(lastModification)
}
fun getSizeString(): String {
return Formatter.formatFileSize(context, size)
}
}

View File

@@ -100,5 +100,5 @@ object ParcelableUtil {
inline fun <reified T : Enum<T>> Parcel.readEnum() = inline fun <reified T : Enum<T>> Parcel.readEnum() =
readString()?.let { enumValueOf<T>(it) } readString()?.let { enumValueOf<T>(it) }
inline fun <T : Enum<T>> Parcel.writeEnum(value: T?) = fun <T : Enum<T>> Parcel.writeEnum(value: T?) =
writeString(value?.name) writeString(value?.name)

View File

@@ -19,8 +19,7 @@
*/ */
package com.kunzisoft.keepass.utils package com.kunzisoft.keepass.utils
import java.util.ArrayList import java.util.*
import java.util.Locale
object StringUtil { object StringUtil {
@@ -85,5 +84,17 @@ object StringUtil {
return currentText return currentText
} }
}
fun UUID.toKeePassRefString(): String {
val tempString = toString().replace("-", "").toUpperCase(Locale.ENGLISH)
return StringBuffer(reverseString2(tempString.substring(12, 16)))
.append(reverseString2(tempString.substring(8, 12)))
.append(reverseString2(tempString.substring(0, 8)))
.append(reverseString2(tempString.substring(20, 32)))
.append(reverseString2(tempString.substring(16, 20))).toString()
}
private fun reverseString2(string: String): String {
return string.chunked(2).reversed().joinToString("")
} }

View File

@@ -24,7 +24,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
@@ -37,31 +36,6 @@ import java.util.*
object UriUtil { object UriUtil {
fun isUriAccessible(contentResolver: ContentResolver, fileUri: Uri?): Boolean {
if (fileUri == null)
return false
return try {
//https://developer.android.com/reference/android/content/res/AssetFileDescriptor
contentResolver.openAssetFileDescriptor(fileUri, "r")?.close()
true
} catch (e: Exception) {
Log.e(UriUtil.javaClass.name, "Unable to access uri $fileUri : ${e.message}")
false
}
}
fun isUriNotWritable(contentResolver: ContentResolver, fileUri: Uri?): Boolean {
if (fileUri == null)
return true
return try {
contentResolver.openAssetFileDescriptor(fileUri, "wa")?.close()
false
} catch (e: Exception) {
Log.e(UriUtil.javaClass.name, "Unable to access uri $fileUri : ${e.message}")
true
}
}
fun getFileData(context: Context, fileUri: Uri?): DocumentFile? { fun getFileData(context: Context, fileUri: Uri?): DocumentFile? {
if (fileUri == null) if (fileUri == null)
return null return null

View File

@@ -69,8 +69,8 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
} }
private fun inflate(context: Context) { private fun inflate(context: Context) {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater.inflate(R.layout.view_button_add_node, this) inflater?.inflate(R.layout.view_button_add_node, this)
addEntryEnable = true addEntryEnable = true
addGroupEnable = true addGroupEnable = true
@@ -132,7 +132,7 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
} }
fun showButton() { fun showButton() {
if (addButtonView?.visibility != VISIBLE) if (isEnable && addButtonView?.visibility != VISIBLE)
addButtonView?.show(onAddButtonVisibilityChangedListener) addButtonView?.show(onAddButtonVisibilityChangedListener)
} }

View File

@@ -44,8 +44,8 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
init { init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater.inflate(R.layout.view_advanced_unlock, this) inflater?.inflate(R.layout.view_advanced_unlock, this)
unlockContainerView = findViewById(R.id.fingerprint_container) unlockContainerView = findViewById(R.id.fingerprint_container)

View File

@@ -20,6 +20,7 @@ package com.kunzisoft.keepass.view
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.text.util.Linkify
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@@ -31,6 +32,7 @@ import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.EntryAttachmentsAdapter import com.kunzisoft.keepass.adapters.EntryAttachmentsAdapter
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
@@ -40,8 +42,8 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.EntryAttachment import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpType import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.utils.toKeePassRefString
import java.util.* import java.util.*
import androidx.recyclerview.widget.SimpleItemAnimator
class EntryContentsView @JvmOverloads constructor(context: Context, class EntryContentsView @JvmOverloads constructor(context: Context,
@@ -70,8 +72,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val urlContainerView: View private val urlContainerView: View
private val urlView: TextView private val urlView: TextView
private val commentContainerView: View private val notesContainerView: View
private val commentView: TextView private val notesView: TextView
private val extrasContainerView: View private val extrasContainerView: View
private val extrasView: ViewGroup private val extrasView: ViewGroup
@@ -91,6 +93,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val historyAdapter = EntryHistoryAdapter(context) private val historyAdapter = EntryHistoryAdapter(context)
private val uuidView: TextView private val uuidView: TextView
private val uuidReferenceView: TextView
val isUserNamePresent: Boolean val isUserNamePresent: Boolean
get() = userNameContainerView.visibility == View.VISIBLE get() = userNameContainerView.visibility == View.VISIBLE
@@ -99,8 +102,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
get() = passwordContainerView.visibility == View.VISIBLE get() = passwordContainerView.visibility == View.VISIBLE
init { init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater.inflate(R.layout.view_entry_contents, this) inflater?.inflate(R.layout.view_entry_contents, this)
userNameContainerView = findViewById(R.id.entry_user_name_container) userNameContainerView = findViewById(R.id.entry_user_name_container)
userNameView = findViewById(R.id.entry_user_name) userNameView = findViewById(R.id.entry_user_name)
@@ -118,8 +121,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
urlContainerView = findViewById(R.id.entry_url_container) urlContainerView = findViewById(R.id.entry_url_container)
urlView = findViewById(R.id.entry_url) urlView = findViewById(R.id.entry_url)
commentContainerView = findViewById(R.id.entry_notes_container) notesContainerView = findViewById(R.id.entry_notes_container)
commentView = findViewById(R.id.entry_notes) notesView = findViewById(R.id.entry_notes)
extrasContainerView = findViewById(R.id.extra_strings_container) extrasContainerView = findViewById(R.id.extra_strings_container)
extrasView = findViewById(R.id.extra_strings) extrasView = findViewById(R.id.extra_strings)
@@ -146,6 +149,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
} }
uuidView = findViewById(R.id.entry_UUID) uuidView = findViewById(R.id.entry_UUID)
uuidReferenceView = findViewById(R.id.entry_UUID_reference)
val attrColorAccent = intArrayOf(R.attr.colorAccent) val attrColorAccent = intArrayOf(R.attr.colorAccent)
val taColorAccent = context.theme.obtainStyledAttributes(attrColorAccent) val taColorAccent = context.theme.obtainStyledAttributes(attrColorAccent)
@@ -286,15 +290,17 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
fun assignComment(comment: String?) { fun assignComment(comment: String?) {
if (comment != null && comment.isNotEmpty()) { if (comment != null && comment.isNotEmpty()) {
commentContainerView.visibility = View.VISIBLE notesContainerView.visibility = View.VISIBLE
commentView.apply { notesView.apply {
text = comment text = comment
if (fontInVisibility) if (fontInVisibility)
applyFontVisibility() applyFontVisibility()
} }
try {
Linkify.addLinks(notesView, Linkify.ALL)
} catch (e: Exception) {}
} else { } else {
commentContainerView.visibility = View.GONE notesContainerView.visibility = View.GONE
} }
} }
@@ -346,6 +352,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
fun assignUUID(uuid: UUID) { fun assignUUID(uuid: UUID) {
uuidView.text = uuid.toString() uuidView.text = uuid.toString()
uuidReferenceView.text = uuid.toKeePassRefString()
} }
/* ------------- /* -------------

View File

@@ -43,8 +43,8 @@ open class EntryCustomField @JvmOverloads constructor(context: Context,
init { init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater.inflate(R.layout.item_entry_new_field, this) inflater?.inflate(R.layout.item_entry_new_field, this)
labelView = findViewById(R.id.title) labelView = findViewById(R.id.title)
valueView = findViewById(R.id.value) valueView = findViewById(R.id.value)

View File

@@ -21,21 +21,21 @@ package com.kunzisoft.keepass.view
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import com.google.android.material.textfield.TextInputLayout
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.EditText import android.widget.*
import android.widget.ImageView import com.google.android.material.textfield.TextInputLayout
import android.widget.LinearLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.icons.IconDrawableFactory import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.icons.assignDefaultDatabaseIcon import com.kunzisoft.keepass.icons.assignDefaultDatabaseIcon
import com.kunzisoft.keepass.model.Field import com.kunzisoft.keepass.model.Field
import org.joda.time.Duration
import org.joda.time.Instant
class EntryEditContentsView @JvmOverloads constructor(context: Context, class EntryEditContentsView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
@@ -52,16 +52,26 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
private val entryPasswordLayoutView: TextInputLayout private val entryPasswordLayoutView: TextInputLayout
private val entryPasswordView: EditText private val entryPasswordView: EditText
private val entryConfirmationPasswordView: EditText private val entryConfirmationPasswordView: EditText
val generatePasswordView: View private val entryExpiresCheckBox: CompoundButton
private val entryCommentView: EditText private val entryExpiresTextView: TextView
private val entryNotesView: EditText
private val entryExtraFieldsContainer: ViewGroup private val entryExtraFieldsContainer: ViewGroup
val addNewFieldButton: View
private var iconColor: Int = 0 private var iconColor: Int = 0
private var expiresInstant: DateInstant = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
var onDateClickListener: OnClickListener? = null
set(value) {
field = value
if (entryExpiresCheckBox.isChecked)
entryExpiresTextView.setOnClickListener(value)
else
entryExpiresTextView.setOnClickListener(null)
}
init { init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater.inflate(R.layout.view_entry_edit_contents, this) inflater?.inflate(R.layout.view_entry_edit_contents, this)
entryTitleLayoutView = findViewById(R.id.entry_edit_container_title) entryTitleLayoutView = findViewById(R.id.entry_edit_container_title)
entryTitleView = findViewById(R.id.entry_edit_title) entryTitleView = findViewById(R.id.entry_edit_title)
@@ -71,10 +81,14 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
entryPasswordLayoutView = findViewById(R.id.entry_edit_container_password) entryPasswordLayoutView = findViewById(R.id.entry_edit_container_password)
entryPasswordView = findViewById(R.id.entry_edit_password) entryPasswordView = findViewById(R.id.entry_edit_password)
entryConfirmationPasswordView = findViewById(R.id.entry_edit_confirmation_password) entryConfirmationPasswordView = findViewById(R.id.entry_edit_confirmation_password)
generatePasswordView = findViewById(R.id.entry_edit_generate_button) entryExpiresCheckBox = findViewById(R.id.entry_edit_expires_checkbox)
entryCommentView = findViewById(R.id.entry_edit_notes) entryExpiresTextView = findViewById(R.id.entry_edit_expires_text)
entryNotesView = findViewById(R.id.entry_edit_notes)
entryExtraFieldsContainer = findViewById(R.id.entry_edit_advanced_container) entryExtraFieldsContainer = findViewById(R.id.entry_edit_advanced_container)
addNewFieldButton = findViewById(R.id.entry_edit_add_new_field)
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
assignExpiresDateText()
}
// Retrieve the textColor to tint the icon // Retrieve the textColor to tint the icon
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
@@ -141,43 +155,61 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
} }
} }
fun setOnPasswordGeneratorClickListener(clickListener: () -> Unit) { private fun assignExpiresDateText() {
generatePasswordView.setOnClickListener { clickListener.invoke() } entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
entryExpiresTextView.setOnClickListener(onDateClickListener)
expiresInstant.getDateTimeString(resources)
} else {
entryExpiresTextView.setOnClickListener(null)
resources.getString(R.string.never)
}
if (fontInVisibility)
entryExpiresTextView.applyFontVisibility()
} }
var expires: Boolean
get() {
return entryExpiresCheckBox.isChecked
}
set(value) {
entryExpiresCheckBox.isChecked = value
assignExpiresDateText()
}
var expiresDate: DateInstant
get() {
return expiresInstant
}
set(value) {
expiresInstant = value
assignExpiresDateText()
}
var notes: String var notes: String
get() { get() {
return entryCommentView.text.toString() return entryNotesView.text.toString()
} }
set(value) { set(value) {
entryCommentView.setText(value) entryNotesView.setText(value)
if (fontInVisibility) if (fontInVisibility)
entryCommentView.applyFontVisibility() entryNotesView.applyFontVisibility()
} }
fun allowCustomField(allow: Boolean, action: () -> Unit) {
addNewFieldButton.apply {
if (allow) {
visibility = View.VISIBLE
setOnClickListener { action.invoke() }
} else {
visibility = View.GONE
setOnClickListener(null)
}
}
}
val customFields: MutableList<Field> val customFields: MutableList<Field>
get() { get() {
val customFieldsArray = ArrayList<Field>() val customFieldsArray = ArrayList<Field>()
// Add extra fields from views // Add extra fields from views
entryExtraFieldsContainer.let { entryExtraFieldsContainer.let {
for (i in 0 until it.childCount) { try {
val view = it.getChildAt(i) as EntryEditCustomField for (i in 0 until it.childCount) {
val key = view.label val view = it.getChildAt(i) as EntryEditCustomField
val value = view.value val key = view.label
val protect = view.isProtected val value = view.value
customFieldsArray.add(Field(key, ProtectedString(protect, value))) val protect = view.isProtected
customFieldsArray.add(Field(key, ProtectedString(protect, value)))
}
} catch (exception: Exception) {
// Extra field container contains another type of view
} }
} }
return customFieldsArray return customFieldsArray
@@ -187,11 +219,14 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
* Add a new view to fill in the information of the customized field and focus it * Add a new view to fill in the information of the customized field and focus it
*/ */
fun addEmptyCustomField() { fun addEmptyCustomField() {
val entryEditCustomField = EntryEditCustomField(context).apply { // Fix current custom field before adding a new one
setFontVisibility(fontInVisibility) if (isValid()) {
requestFocus() val entryEditCustomField = EntryEditCustomField(context).apply {
setFontVisibility(fontInVisibility)
requestFocus()
}
entryExtraFieldsContainer.addView(entryEditCustomField)
} }
entryExtraFieldsContainer.addView(entryEditCustomField)
} }
/** /**
@@ -226,34 +261,34 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
* @return ErrorValidation An error with a message or a validation without message * @return ErrorValidation An error with a message or a validation without message
*/ */
fun isValid(): Boolean { fun isValid(): Boolean {
var isValid = true
// Require title
if (entryTitleView.text.toString().isEmpty()) {
entryTitleLayoutView.error = context.getString(R.string.error_title_required)
isValid = false
} else {
entryTitleLayoutView.error = null
}
// Validate password // Validate password
if (entryPasswordView.text.toString() != entryConfirmationPasswordView.text.toString()) { if (entryPasswordView.text.toString() != entryConfirmationPasswordView.text.toString()) {
entryPasswordLayoutView.error = context.getString(R.string.error_pass_match) entryPasswordLayoutView.error = context.getString(R.string.error_pass_match)
isValid = false return false
} else { } else {
entryPasswordLayoutView.error = null entryPasswordLayoutView.error = null
} }
// Validate extra fields // Validate extra fields
entryExtraFieldsContainer.let { entryExtraFieldsContainer.let {
for (i in 0 until it.childCount) { try {
val entryEditCustomField = it.getChildAt(i) as EntryEditCustomField val customFieldLabelSet = HashSet<String>()
if (!entryEditCustomField.isValid()) { for (i in 0 until it.childCount) {
isValid = false val entryEditCustomField = it.getChildAt(i) as EntryEditCustomField
if (customFieldLabelSet.contains(entryEditCustomField.label)) {
entryEditCustomField.setError(R.string.error_label_exists)
return false
}
customFieldLabelSet.add(entryEditCustomField.label)
if (!entryEditCustomField.isValid()) {
return false
}
} }
} catch (exception: Exception) {
return false
} }
} }
return isValid return true
} }
} }

View File

@@ -30,6 +30,7 @@ import android.widget.CompoundButton
import android.widget.EditText import android.widget.EditText
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.annotation.StringRes
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
@@ -54,8 +55,8 @@ class EntryEditCustomField @JvmOverloads constructor(context: Context,
init { init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater.inflate(R.layout.view_entry_new_field, this) inflater?.inflate(R.layout.view_entry_new_field, this)
val deleteView = findViewById<View>(R.id.entry_new_field_delete) val deleteView = findViewById<View>(R.id.entry_new_field_delete)
deleteView.setOnClickListener { deleteViewFromParent() } deleteView.setOnClickListener { deleteViewFromParent() }
@@ -84,14 +85,20 @@ class EntryEditCustomField @JvmOverloads constructor(context: Context,
fun isValid(): Boolean { fun isValid(): Boolean {
// Validate extra field // Validate extra field
if (label.isEmpty()) { if (label.isEmpty()) {
labelLayoutView.error = context.getString(R.string.error_string_key) setError(R.string.error_string_key)
return false return false
} else { } else {
labelLayoutView.error = null setError(null)
} }
return true return true
} }
fun setError(@StringRes errorId: Int?) {
labelLayoutView.error = if (errorId == null) null else {
context.getString(errorId)
}
}
fun setFontVisibility(applyFontVisibility: Boolean) { fun setFontVisibility(applyFontVisibility: Boolean) {
if (applyFontVisibility) if (applyFontVisibility)
valueView.applyFontVisibility() valueView.applyFontVisibility()
@@ -103,7 +110,7 @@ class EntryEditCustomField @JvmOverloads constructor(context: Context,
parent.removeView(this) parent.removeView(this)
parent.invalidate() parent.invalidate()
} catch (e: ClassCastException) { } catch (e: ClassCastException) {
Log.e(javaClass.name, e.message) Log.e(javaClass.name, "Unable to delete view", e)
} }
} }
} }

View File

@@ -0,0 +1,60 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.net.Uri
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.documentfile.provider.DocumentFile
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
class KeyFileSelectionView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: ConstraintLayout(context, attrs, defStyle) {
private var mUri: Uri? = null
private val keyFileNameInputLayout: TextInputLayout
private val keyFileNameView: TextView
private val keyFileOpenView: ImageView
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_keyfile_selection, this)
keyFileNameInputLayout = findViewById(R.id.input_entry_keyfile)
keyFileNameView = findViewById(R.id.keyfile_name)
keyFileOpenView = findViewById(R.id.keyfile_open_button)
}
override fun setOnClickListener(l: OnClickListener?) {
super.setOnClickListener(l)
keyFileNameView.setOnClickListener(l)
}
override fun setOnLongClickListener(l: OnLongClickListener?) {
super.setOnLongClickListener(l)
keyFileNameView.setOnLongClickListener(l)
}
var error: CharSequence?
get() = keyFileNameInputLayout.error
set(value) {
keyFileNameInputLayout.error = value
}
var uri: Uri?
get() {
return mUri
}
set(value) {
mUri = value
keyFileNameView.text = value?.let {
DocumentFile.fromSingleUri(context, value)?.name ?: ""
} ?: ""
}
}

View File

@@ -17,6 +17,8 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
@file:Suppress("DEPRECATION")
package com.kunzisoft.keepass.view package com.kunzisoft.keepass.view
import android.content.Context import android.content.Context

View File

@@ -33,6 +33,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import java.util.*
/** /**
* Replace font by monospace, must be called after seText() * Replace font by monospace, must be called after seText()

View File

@@ -384,7 +384,7 @@ typedef struct _master_key {
} master_key; } master_key;
void *generate_key_material(void *arg) { uint32_t generate_key_material(void *arg) {
#if defined(KPD_PROFILE) #if defined(KPD_PROFILE)
struct timespec start, end; struct timespec start, end;
#endif #endif
@@ -435,7 +435,7 @@ void *generate_key_material(void *arg) {
pthread_mutex_unlock(&mk->lock2); pthread_mutex_unlock(&mk->lock2);
} }
return (void *)flip; return flip;
} }
JNIEXPORT jbyteArray JNICALL Java_com_kunzisoft_keepass_crypto_finalkey_NativeFinalKey_nTransformMasterKey(JNIEnv *env, jobject this, jbyteArray seed, jbyteArray key, jlong rounds) { JNIEXPORT jbyteArray JNICALL Java_com_kunzisoft_keepass_crypto_finalkey_NativeFinalKey_nTransformMasterKey(JNIEnv *env, jobject this, jbyteArray seed, jbyteArray key, jlong rounds) {
@@ -474,12 +474,12 @@ JNIEXPORT jbyteArray JNICALL Java_com_kunzisoft_keepass_crypto_finalkey_NativeFi
(*env)->GetByteArrayRegion(env, key, 0, MASTER_KEY_SIZE, (jbyte *)mk.key1); (*env)->GetByteArrayRegion(env, key, 0, MASTER_KEY_SIZE, (jbyte *)mk.key1);
// step 2: encrypt the hash "rounds" (default: 6000) times // step 2: encrypt the hash "rounds" (default: 6000) times
iret = pthread_create( &t1, NULL, generate_key_material, (void*)&mk ); iret = pthread_create( &t1, NULL, (void*)generate_key_material, (void*)&mk );
if( iret != 0 ) { if( iret != 0 ) {
(*env)->ThrowNew(env, bad_arg, "TransformMasterKey: failed to launch thread 1"); // FIXME: get a better exception class for this... (*env)->ThrowNew(env, bad_arg, "TransformMasterKey: failed to launch thread 1"); // FIXME: get a better exception class for this...
return NULL; return NULL;
} }
iret = pthread_create( &t2, NULL, generate_key_material, (void*)&mk ); iret = pthread_create( &t2, NULL, (void*)generate_key_material, (void*)&mk );
if( iret != 0 ) { if( iret != 0 ) {
(*env)->ThrowNew(env, bad_arg, "TransformMasterKey: failed to launch thread 2"); // FIXME: get a better exception class for this... (*env)->ThrowNew(env, bad_arg, "TransformMasterKey: failed to launch thread 2"); // FIXME: get a better exception class for this...
return NULL; return NULL;

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorPrimaryDark" android:state_pressed="true" />
<item android:color="@color/grey_dark" android:state_enabled="false" />
<item android:color="?android:windowBackground" android:state_enabled="true" />
</selector>

View File

@@ -5,8 +5,12 @@
tools:targetApi="lollipop"> tools:targetApi="lollipop">
<item> <item>
<shape> <shape>
<stroke android:color="?attr/colorAccent" android:width="1dp"/> <corners
<solid android:color="@color/transparent"/> android:topLeftRadius="0dp"
android:topRightRadius="40dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"/>
<solid android:color="?android:attr/windowBackground"/>
</shape> </shape>
</item> </item>
</ripple> </ripple>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:color="@color/white"
tools:targetApi="lollipop">
<item>
<shape>
<stroke
android:width="1dp"
android:color="?android:attr/textColorPrimary" />
<corners
android:topLeftRadius="0dp"
android:topRightRadius="40dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"/>
<solid
android:color="@color/transparent"/>
</shape>
</item>
</ripple>

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<stroke android:color="@color/grey" android:width="1dp"/>
<solid android:color="@color/transparent"/>
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_pressed="true">
<shape>
<corners
android:topLeftRadius="0dp"
android:topRightRadius="40dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"/>
<padding
android:left="4dp"
android:right="12dp"
android:top="18dp"
android:bottom="8dp"/>
<solid android:color="@color/grey_dark"/>
</shape>
</item>
<item>
<shape>
<corners
android:topLeftRadius="0dp"
android:topRightRadius="40dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"/>
<padding
android:left="4dp"
android:right="12dp"
android:top="18dp"
android:bottom="8dp"/>
<solid android:color="@color/grey"/>
</shape>
</item>
</selector>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/white">
<item>
<shape>
<stroke
android:width="1dp"
android:color="@color/green" />
<corners
android:topLeftRadius="0dp"
android:topRightRadius="40dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"/>
<solid
android:color="@color/transparent"/>
</shape>
</item>
</selector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M4,8h4L8,4L4,4v4zM10,20h4v-4h-4v4zM4,20h4v-4L4,16v4zM4,14h4v-4L4,10v4zM10,14h4v-4h-4v4zM16,4v4h4L20,4h-4zM10,8h4L14,4h-4v4zM16,14h4v-4h-4v4zM16,20h4v-4h-4v4z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M16.5,6v11.5c0,2.21 -1.79,4 -4,4s-4,-1.79 -4,-4V5c0,-1.38 1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5v10.5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1V6H10v9.5c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5V5c0,-2.21 -1.79,-4 -4,-4S7,2.79 7,5v12.5c0,3.04 2.46,5.5 5.5,5.5s5.5,-2.46 5.5,-5.5V6h-1.5z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M11,17c0,0.55 0.45,1 1,1s1,-0.45 1,-1 -0.45,-1 -1,-1 -1,0.45 -1,1zM11,3v4h2L13,5.08c3.39,0.49 6,3.39 6,6.92 0,3.87 -3.13,7 -7,7s-7,-3.13 -7,-7c0,-1.68 0.59,-3.22 1.58,-4.42L12,13l1.41,-1.41 -6.8,-6.8v0.02C4.42,6.45 3,9.05 3,12c0,4.97 4.02,9 9,9 4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9h-1zM18,12c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1 0.45,1 1,1 1,-0.45 1,-1zM6,12c0,0.55 0.45,1 1,1s1,-0.45 1,-1 -0.45,-1 -1,-1 -1,0.45 -1,1z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

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