Compare commits

...

334 Commits

Author SHA1 Message Date
J-Jamet
8413d2b31a Merge branch 'release/3.1' 2022-01-18 12:44:44 +01:00
J-Jamet
04a03da382 Merge branch 'master' into release/3.1 2022-01-18 12:44:30 +01:00
J-Jamet
e3274657ea /bin/bash: q: command not found 2022-01-18 12:13:03 +01:00
J-Jamet
f3b25cb792 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2022-01-18 12:08:48 +01:00
J-Jamet
d181f886fe Update CHANGELOG 2022-01-18 12:02:33 +01:00
J-Jamet
616d073395 Capture duplicate error exception 2022-01-18 11:44:45 +01:00
J-Jamet
d36fc19585 Add try catch exception when populate view 2022-01-18 11:32:11 +01:00
J-Jamet
95d9e07e2f TODO fix meta stream 2022-01-17 23:51:37 +01:00
solokot
91ebf2ba6f Translated using Weblate (Russian)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-01-15 11:13:08 +01:00
J-Jamet
9e97042dd1 Larger database color view 2022-01-14 18:42:32 +01:00
J-Jamet
b7c4a99e71 Upgrade README Year 2022-01-14 18:37:29 +01:00
J-Jamet
48e315453e Smooth counter 2022-01-14 18:11:36 +01:00
J-Jamet
a8a6d14ca3 Merge branch 'feature/Entry_Color' into develop 2022-01-14 18:05:21 +01:00
J-Jamet
e895dd3430 Upgrade CHANGELOG 2022-01-14 18:05:03 +01:00
J-Jamet
f59859137a Upgrade libs 2022-01-14 18:03:00 +01:00
J-Jamet
dee92e9e40 Upgrade libs 2022-01-14 18:01:35 +01:00
J-Jamet
6701f4f95e Fix color in KitKat 2022-01-14 17:55:54 +01:00
J-Jamet
e20f769854 Fix theme refresh 2022-01-14 16:00:36 +01:00
J-Jamet
4f762a9432 Change background color icon 2022-01-14 15:54:41 +01:00
J-Jamet
3c49eb1635 Fix color picker 2022-01-14 15:45:54 +01:00
J-Jamet
bdc6a282e2 Colorize entry view 2022-01-14 14:25:06 +01:00
J-Jamet
8392ab2cc4 Fix icon color 2022-01-14 01:13:37 +01:00
Allan Nordhøy
93c7c09f8c Translated using Weblate (Norwegian Bokmål)
Currently translated at 90.0% (514 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2022-01-14 00:53:27 +01:00
Allan Nordhøy
120116414f Translated using Weblate (Danish)
Currently translated at 91.5% (523 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2022-01-14 00:53:24 +01:00
J-Jamet
11794e5819 Add color change listener 2022-01-14 00:39:20 +01:00
J-Jamet
edce3d7bec Add color in entry 2022-01-13 20:22:10 +01:00
J-Jamet
e133e32e7c Add color in app bar 2022-01-13 19:15:18 +01:00
J-Jamet
471859e448 Add color in entry edit 2022-01-13 18:08:20 +01:00
Óscar Fernández Díaz
d8de66eb14 Translated using Weblate (Spanish)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2022-01-12 22:22:29 +01:00
J-Jamet
cfdc0237d7 Add foreground and background colors in list 2022-01-12 19:50:38 +01:00
J-Jamet
05fad24eda Fix color picker fragment 2022-01-12 17:01:35 +01:00
J-Jamet
d4818c5567 Select entry colors 2022-01-12 14:19:04 +01:00
J-Jamet
8e8e6a7b93 Invert info container visibility 2022-01-11 18:02:43 +01:00
J-Jamet
6547f0ffad First entry color test 2022-01-11 17:46:50 +01:00
Ngô Ngọc Đức Huy
6f172fffa8 Translated using Weblate (Vietnamese)
Currently translated at 29.5% (169 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2022-01-11 13:49:38 +01:00
Matthaiks
ed16e06676 Translated using Weblate (Polish)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-01-11 13:49:38 +01:00
Ngô Ngọc Đức Huy
1874f06f42 Translated using Weblate (Vietnamese)
Currently translated at 29.4% (168 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2022-01-09 01:54:52 +01:00
zeritti
e9db24429a Translated using Weblate (Czech)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2022-01-09 01:54:51 +01:00
J-Jamet
a59f4d45ca Better color implementation 2022-01-08 22:29:43 +01:00
J-Jamet
c7b3e0926c Fix save database color 2022-01-08 21:45:11 +01:00
J-Jamet
f0f5258bc9 Fix refresh UI 2022-01-08 20:36:59 +01:00
J-Jamet
12c07cf793 Fix refresh database metadata 2022-01-08 20:22:39 +01:00
J-Jamet
d2b8c85015 Add database color #913 2022-01-08 20:03:56 +01:00
J-Jamet
b9652291bd Manage default user name and color in KDB 2022-01-08 19:35:42 +01:00
J-Jamet
b0d1f93bfc Change header sig 2022-01-08 16:06:10 +01:00
J-Jamet
553416c927 Fix number of entries and refactor GroupFragment 2022-01-07 18:59:23 +01:00
J-Jamet
b83696bc60 Fix create entry in the right group 2022-01-07 18:28:44 +01:00
J-Jamet
23ce320d75 Fix output KDB and private indexes 2022-01-07 18:12:45 +01:00
J-Jamet
27d5733dbc Strike out expires group 2022-01-07 14:38:18 +01:00
ssantos
8d7d01bf88 Translated using Weblate (Portuguese)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2022-01-03 21:55:51 +01:00
solokot
0bc37d2fc2 Translated using Weblate (Russian)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-01-03 21:55:50 +01:00
André Marcelo Alvarenga
aaa1655af1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-01-03 21:55:50 +01:00
Mr-Update
f1bd4e1bba Translated using Weblate (German)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-01-03 21:55:50 +01:00
J-Jamet
15a28e7c83 Fix action in breadcrumb 2022-01-03 19:51:59 +01:00
Serdar Sağlam
ab26e561fd Translated using Weblate (Turkish)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-01-01 13:56:33 +01:00
Eric
66968a28a3 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-01-01 13:56:32 +01:00
Ihor Hordiichuk
37d1f91224 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-01-01 13:56:32 +01:00
Gabriel Cardoso
9e69068d42 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-01-01 13:56:32 +01:00
Matthaiks
8e2a9fcd01 Translated using Weblate (Polish)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-01-01 13:56:31 +01:00
Retrial
1753887916 Translated using Weblate (Greek)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-01-01 13:56:31 +01:00
Hosted Weblate
21bcffcc87 Merge branch 'origin/develop' into Weblate. 2021-12-31 17:27:28 +01:00
I. Musthafa
1caed49c75 Translated using Weblate (Indonesian)
Currently translated at 84.7% (482 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-12-31 09:20:33 +01:00
Vitor Henrique
619ea35168 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-12-31 09:20:32 +01:00
Giai Ngo
877f913e8f Translated using Weblate (Vietnamese)
Currently translated at 25.8% (147 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2021-12-23 03:50:28 +01:00
I. Musthafa
25c47390c0 Translated using Weblate (Indonesian)
Currently translated at 81.1% (462 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-12-23 03:50:26 +01:00
J-Jamet
b3c46348a1 Allow to change root group 2021-12-22 19:01:51 +01:00
J-Jamet
6f154194f1 Upgrade bouncy castle #833 2021-12-22 17:40:18 +01:00
J-Jamet
c3ab08ce17 Upgrade to SDK 31 2021-12-22 17:28:29 +01:00
J-Jamet
004fffa992 Add exact alarm message 2021-12-22 17:23:57 +01:00
J-Jamet
d6bd80c9c0 Fix UI in Android 8 #509 2021-12-22 14:12:30 +01:00
J-Jamet
318bcdd011 Remove WRITE_EXTERNAL_STORAGE permission 2021-12-22 13:57:05 +01:00
J-Jamet
3076f2af68 Add backup rules 2021-12-21 17:14:30 +01:00
Jérémy JAMET
3dd9ef5564 Update bug_report.md 2021-12-21 10:01:34 +01:00
I. Musthafa
367e5fa84e Translated using Weblate (Indonesian)
Currently translated at 81.0% (461 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-12-20 15:50:26 +01:00
J-Jamet
97cd61fd13 First pass to update API 31 2021-12-17 17:57:09 +01:00
Y. Sakamoto
869bf7a345 Translated using Weblate (Japanese)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-12-17 12:53:25 +01:00
J-Jamet
9e15ac242d Fix small element 2021-12-16 21:36:55 +01:00
J-Jamet
84943e58f1 Fix small elements 2021-12-16 21:30:40 +01:00
J-Jamet
2289bf0a27 Fix show UUID in V1 format 2021-12-16 21:15:23 +01:00
J-Jamet
7fda40c983 Show UUID in group 2021-12-16 20:52:49 +01:00
J-Jamet
7eeed8f670 Edit group on long click 2021-12-16 20:20:46 +01:00
J-Jamet
4d3f4ed5c2 Add group info dialog #1177 2021-12-16 19:12:31 +01:00
J-Jamet
145a4f5c20 Merge branch 'feature/Breadcrumb' into develop 2021-12-16 17:44:20 +01:00
J-Jamet
9afe3d26e9 Arrow as breadcrumb delimiter 2021-12-16 17:37:17 +01:00
J-Jamet
b73a7f1ed8 Upgrade version to 3.1.0 and changelog 2021-12-16 17:29:17 +01:00
J-Jamet
91a2bc3862 Fix virtual content 2021-12-16 17:25:41 +01:00
J-Jamet
78a8a840b0 Add path in search result 2021-12-16 17:20:33 +01:00
J-Jamet
f4d54b6ca3 Fix margin 2021-12-16 16:54:53 +01:00
J-Jamet
bc7a1c332c Breadcrunb toolbar animation and remove back button 2021-12-16 16:49:56 +01:00
J-Jamet
0e75cb9095 Change parallax 2021-12-16 16:23:09 +01:00
J-Jamet
41b6fb6dcd Add selectable background 2021-12-16 13:23:18 +01:00
J-Jamet
2ca3cbc88f Fix database title 2021-12-16 13:12:08 +01:00
J-Jamet
d05641a3d6 Change toolbar parallax 2021-12-16 13:02:04 +01:00
J-Jamet
28bf84e05c Fix search 2021-12-16 12:56:14 +01:00
J-Jamet
ff51b53660 Back group in breadcrumb 2021-12-16 12:25:25 +01:00
J-Jamet
8b8e034b18 Refresh breadcrumb 2021-12-16 12:06:42 +01:00
J-Jamet
39927b06e3 Change breadcrumb UI 2021-12-16 12:01:26 +01:00
J-Jamet
66db2e7d16 Better breadcrumb implementation 2021-12-16 11:26:17 +01:00
Kunzisoft
a927c33ef1 Translated using Weblate (French)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2021-12-15 12:50:48 +01:00
J-Jamet
17bc18b881 Add breadcrumb for group 2021-12-13 14:24:12 +01:00
Oymate
aa643c4a82 Translated using Weblate (Bengali (Bangladesh))
Currently translated at 7.7% (44 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bn_BD/
2021-12-12 07:52:01 +01:00
J-Jamet
836fbea676 Merge tag '3.0.4' into develop
3.0.4
2021-12-09 20:11:08 +01:00
J-Jamet
045049243c Merge branch 'release/3.0.4' 2021-12-09 20:10:57 +01:00
J-Jamet
b9813a3494 Upgrade version code 2021-12-09 19:54:12 +01:00
J-Jamet
9b42a93ce1 Change lock button 2021-12-09 19:49:38 +01:00
J-Jamet
8502bceef1 Fix search 2021-12-09 18:38:38 +01:00
J-Jamet
663387476f Change select entry min height 2021-12-09 14:33:07 +01:00
J-Jamet
daafd83df9 Better extra key implementation 2021-12-09 14:29:41 +01:00
J-Jamet
f780f2725b Fix compat inline suggestions request 2021-12-09 14:15:40 +01:00
J-Jamet
483aca871a Upgrade to version 3.0.4 and fix inline autofill suggestion #1165 2021-12-09 13:38:42 +01:00
J-Jamet
352e709c3b Merge branch 'master' into develop 2021-12-09 11:53:40 +01:00
J-Jamet
629057b2c1 Fix autofill exception #1173 2021-12-09 11:46:39 +01:00
J-Jamet
0e5f53596d Merge tag '3.0.3' into develop
3.0.3
2021-12-08 17:58:49 +01:00
J-Jamet
0d91f07646 Merge branch 'release/3.0.3' 2021-12-08 17:58:37 +01:00
J-Jamet
db882a26ab Upgrade constraint layout lib 2021-12-08 11:37:02 +01:00
Ihor Hordiichuk
35c8ea22b1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-12-08 03:51:55 +01:00
J-Jamet
7f01619358 Fix notification in older android versions 2021-12-07 18:14:36 +01:00
J-Jamet
ee109b4ceb Merge branch 'feature/StartActivityResult' into develop 2021-12-07 16:36:52 +01:00
J-Jamet
7a398e5453 Fix activity result for advanced unlocking 2021-12-07 16:20:57 +01:00
Óscar Fernández Díaz
23a548f9b4 Translated using Weblate (Spanish)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-12-02 19:54:32 +01:00
J-Jamet
d4655d7034 Fix search in activity 2021-12-02 13:31:41 +01:00
J-Jamet
9feb96b541 Fix start autofill service 2021-12-02 13:30:02 +01:00
J-Jamet
e939278193 Suppress deprecation with setTargetFragment 2021-12-02 13:21:25 +01:00
J-Jamet
d4ef1a2617 Fix small warnings 2021-12-02 11:39:42 +01:00
J-Jamet
5f8746ced3 Fix result with entry edit 2021-12-01 17:16:19 +01:00
J-Jamet
40a063e94f Fix result exit lock 2021-11-30 11:50:07 +01:00
J-Jamet
8f5439b958 Icon selection with activity result launcher 2021-11-30 11:20:09 +01:00
J-Jamet
e347f57d8b Refactor FileHelper and fix key file selection 2021-11-30 10:47:31 +01:00
Milo Ivir
7169b15fd8 Translated using Weblate (Croatian)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-11-29 16:54:00 +01:00
abidin toumi
f9def8c96f Translated using Weblate (Arabic)
Currently translated at 78.7% (448 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-11-29 16:53:59 +01:00
Mr-Update
ef43837af1 Translated using Weblate (German)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-11-25 10:50:58 +01:00
J-Jamet
2efb8e8b8c Merge branch 'develop' into feature/StartActivityResult 2021-11-23 18:28:08 +01:00
J-Jamet
e5bb69ea5f Fix startActivityResult for Autofill 2021-11-23 18:28:01 +01:00
random r
0979ca607d Translated using Weblate (Italian)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-11-23 15:40:37 +01:00
J-Jamet
6ae186b2af Fix exported and pending intent 2021-11-23 12:10:57 +01:00
Beytullah AKYÜZ
98fb27f77d Translated using Weblate (Turkish)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-11-22 14:52:03 +01:00
JY3
d68510bbaa Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-11-22 14:52:03 +01:00
J-Jamet
71fdd2d92d Fix lowercase and uppercase 2021-11-22 13:22:18 +01:00
J-Jamet
3656689ff3 Fix kotlin code warning 2021-11-22 13:10:42 +01:00
J-Jamet
7d78406db6 Fix pending intent 2021-11-22 12:24:22 +01:00
J-Jamet
ac47748e41 Fix pending intent 2021-11-22 11:51:30 +01:00
J-Jamet
80f9b46479 New lock icon in notification 2021-11-20 13:30:26 +01:00
J-Jamet
999f1bf47a New lock icon in notification 2021-11-20 13:10:23 +01:00
J-Jamet
9e114eb2b8 Add lock button in database notification 2021-11-20 12:30:58 +01:00
SC
4177d34b00 Translated using Weblate (Portuguese)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2021-11-19 19:52:59 +01:00
Serdar Sağlam
3ec5c04bf6 Translated using Weblate (Turkish)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-11-19 19:52:59 +01:00
Eric
a877c068b6 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-11-19 19:52:58 +01:00
Ihor Hordiichuk
6a3db90c1e Translated using Weblate (Ukrainian)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-11-19 19:52:58 +01:00
solokot
a079e0d864 Translated using Weblate (Russian)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-11-19 19:52:58 +01:00
Matthaiks
719776d66e Translated using Weblate (Polish)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-11-19 19:52:58 +01:00
Stephan Paternotte
c5af1241e9 Translated using Weblate (Dutch)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2021-11-19 19:52:57 +01:00
Retrial
27e4d7b563 Translated using Weblate (Greek)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-11-19 19:52:57 +01:00
VfBFan
450ab34721 Translated using Weblate (German)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-11-19 19:52:57 +01:00
Hosted Weblate
3e2d4eae2c Merge branch 'origin/develop' into Weblate. 2021-11-17 20:33:06 +01:00
J-Jamet
d89b6529ef Upgrade kotlin and fragment versions 2021-11-16 16:51:06 +01:00
J-Jamet
5caf11556a Remove unused translation 2021-11-16 16:15:08 +01:00
J-Jamet
78cc6f0f40 Merge branch 'translations' into develop 2021-11-16 16:10:58 +01:00
J-Jamet
0007cd4668 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2021-11-16 16:09:26 +01:00
J-Jamet
05195e41de Upgrade appcompat and material libs 2021-11-16 14:53:54 +01:00
J-Jamet
66f44ef87d Encapsulate lib version through modules 2021-11-16 12:48:38 +01:00
J-Jamet
a0585d9b11 Upgrade repo and libs 2021-11-16 12:07:48 +01:00
J-Jamet
5067946b13 Change backup configuration #1144 2021-11-16 11:12:07 +01:00
J-Jamet
f52241d5a8 Change backup configuration #1144 2021-11-16 11:09:14 +01:00
J-Jamet
04ccb25fa3 Catch key file out of memory exception 2021-11-15 12:25:27 +01:00
J-Jamet
5a3f4b60b8 Catch style exception 2021-11-15 12:19:36 +01:00
J-Jamet
4408b2e488 Catch exception in run action 2021-11-15 12:04:46 +01:00
J-Jamet
9a26acee35 Change version to 3.0.3 and add issue tag 2021-11-15 11:35:08 +01:00
Rsec
6ac377348b Translated using Weblate (Romanian)
Currently translated at 63.2% (359 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2021-11-10 18:49:56 +01:00
I. Musthafa
daeb88d4f4 Translated using Weblate (Indonesian)
Currently translated at 79.0% (449 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-11-08 07:50:09 +01:00
J-Jamet
47bf199f52 Merge branch 'feature/Biometric_Refactor' into develop 2021-11-03 14:46:55 +01:00
J-Jamet
91540b022d Use strong box to store in security chip #1145 2021-11-02 17:55:30 +01:00
J-Jamet
505a51b6b5 Update CHANGELOG 2021-11-02 17:31:09 +01:00
J-Jamet
28400488aa Fix advanced unlock menu button 2021-11-02 17:22:23 +01:00
J-Jamet
45ae600289 Change biometric views 2021-11-02 17:10:06 +01:00
J-Jamet
8be6874651 Auto remove all biometric keys when invalidated 2021-11-02 16:27:29 +01:00
Milo Ivir
f694a500e0 Translated using Weblate (Croatian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-10-30 00:37:53 +02:00
J-Jamet
c415fa01fc Update Autofill compatibility list #725 2021-10-29 17:40:16 +02:00
J-Jamet
5225a9459c Fix template chars limit 2021-10-27 20:01:47 +02:00
J-Jamet
2974b150af Fix template spinner 2021-10-27 19:55:14 +02:00
J-Jamet
cf353c8067 Fix template icons 2021-10-27 19:27:21 +02:00
Serdar Sağlam
3aacb5d8b3 Translated using Weblate (Turkish)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-10-27 01:38:42 +02:00
abidin toumi
114fbdbe01 Translated using Weblate (Arabic)
Currently translated at 77.4% (440 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-10-27 01:38:42 +02:00
abidin toumi
f1f83cbec4 Translated using Weblate (Arabic)
Currently translated at 76.5% (435 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-10-25 20:38:00 +02:00
J-Jamet
335c28c2c9 Fix save default username 2021-10-25 15:57:01 +02:00
J-Jamet
daf12cbcce Change Wifi to Wi-Fi 2021-10-25 15:44:18 +02:00
J-Jamet
bf56eca003 Fix templates #1128 2021-10-25 15:41:20 +02:00
J-Jamet
a12b7fd58a Merge branch 'SUPERYAO541-feature/Complete_zh-rTW' into develop 2021-10-25 11:30:33 +02:00
J-Jamet
98d004edbf Merge branch 'feature/Complete_zh-rTW' of git://github.com/SUPERYAO541/KeePassDX into SUPERYAO541-feature/Complete_zh-rTW 2021-10-25 11:30:18 +02:00
random r
67586a98b3 Translated using Weblate (Italian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-10-23 11:41:24 +02:00
Milo Ivir
c6d8911883 Translated using Weblate (Croatian)
Currently translated at 99.8% (567 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-10-21 21:36:24 +02:00
Neko Nekowazarashi
12e398ce9b Translated using Weblate (Indonesian)
Currently translated at 78.1% (444 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-10-16 13:40:45 +02:00
abidin toumi
5c4a202616 Translated using Weblate (Arabic)
Currently translated at 76.4% (434 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-10-14 23:34:38 +02:00
solokot
adc1ec8444 Translated using Weblate (Russian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-10-14 23:34:36 +02:00
Darin Avdeyeva
0227d2fcb4 Translated using Weblate (Russian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-10-13 19:02:33 +02:00
abidin toumi
95abe3b5ac Translated using Weblate (Arabic)
Currently translated at 67.6% (384 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-10-10 18:02:20 +02:00
abidin toumi
133a902c54 Translated using Weblate (Arabic)
Currently translated at 61.4% (349 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-10-09 17:05:15 +02:00
Stephan Paternotte
aacb03d9ef Translated using Weblate (Dutch)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2021-10-07 20:03:28 +02:00
J-Jamet
fbeaa1781f Add no internet connection required in description 2021-10-06 18:45:14 +02:00
J-Jamet
ca73aad538 Add dynamic templates in description 2021-10-06 18:09:54 +02:00
André Marcelo Alvarenga
4a4bfefd17 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-10-05 10:08:58 +02:00
J-Jamet
6796b0cd2a Upgrade gradle 2021-10-04 16:49:15 +02:00
abidin toumi
29e1f824b0 Translated using Weblate (Arabic)
Currently translated at 61.2% (348 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-10-03 17:19:42 +02:00
hokonch
51263a2911 Translated using Weblate (Japanese)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-09-30 06:36:59 +02:00
Kunzisoft
dd7f857475 Translated using Weblate (French)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2021-09-30 06:36:59 +02:00
J-Jamet
f0fdd4a537 Add & edit custom icon name #976 2021-09-28 18:12:49 +02:00
J-Jamet
9b847a0561 Change default Argon2 parameters #1098
and upgrade to 3.1.0
2021-09-28 12:57:14 +02:00
Hosted Weblate
469e76b80a Merge branch 'origin/develop' into Weblate. 2021-09-28 09:56:49 +02:00
SC
9c6994b476 Translated using Weblate (Portuguese)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2021-09-27 19:36:47 +02:00
Oğuz Ersen
52ba487617 Translated using Weblate (Turkish)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-09-27 19:36:47 +02:00
Eric
21c57c9484 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-09-27 19:36:47 +02:00
Ihor Hordiichuk
9b1ea6a07a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-09-27 19:36:47 +02:00
solokot
f7d7bb0ea3 Translated using Weblate (Russian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-09-27 19:36:46 +02:00
SC
dd96b9ef53 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2021-09-27 19:36:46 +02:00
Matthaiks
b6b01893ba Translated using Weblate (Polish)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-09-27 19:36:46 +02:00
Óscar Fernández Díaz
ad531d793d Translated using Weblate (Spanish)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-09-27 19:36:45 +02:00
Retrial
a9dd11e24a Translated using Weblate (Greek)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-09-27 19:36:45 +02:00
VfBFan
115983830b Translated using Weblate (German)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-09-27 19:36:45 +02:00
zeritti
442cece081 Translated using Weblate (Czech)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-09-27 19:36:45 +02:00
SUPERYAO
14e08457b9 Complete and improve zh-rTW 2021-09-25 21:59:59 +08:00
J-Jamet
fabcc08cd5 Merge tag '3.0.2' into develop
3.0.2
2021-09-24 19:35:07 +02:00
J-Jamet
7750843b04 Merge branch 'release/3.0.2' 2021-09-24 19:34:58 +02:00
Hosted Weblate
c03188e976 Merge branch 'origin/develop' into Weblate. 2021-09-24 13:54:09 +02:00
J-Jamet
d7da1ce333 Upgrade to 3.0.2 and update CHANGELOG 2021-09-24 12:55:08 +02:00
J-Jamet
dd9ee8c3f8 Merge branch 'chenxiaolong-samsung_dex' into develop 2021-09-24 12:50:40 +02:00
Braja Yudhistira
34bbd8f439 Translated using Weblate (Indonesian)
Currently translated at 77.8% (442 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-09-23 21:34:44 +02:00
Milo Ivir
053f57cff5 Translated using Weblate (Croatian)
Currently translated at 99.8% (567 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-09-23 21:34:44 +02:00
Balázs Meskó
365d2e2844 Translated using Weblate (Hungarian)
Currently translated at 82.5% (469 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2021-09-23 21:34:43 +02:00
Andrew Gunnerson
c0ac01a34a Add workaround to support Samsung DeX
This commit changes the Magikeyboard service behavior so that KeePassDX
is able to run in Samsung DeX mode. Currently, the app cannot run in
DeX mode because apps which have services using `BIND_INPUT_METHOD` are
blocked.

A new broadcast receiver has been added to listen for DeX's enter/leave
events [1] and disable/enable the `Magikeyboard` service appropriately.
The enabled state of a service lives in the Android framework's
`PackageManager` and survives app crashes and device reboots (though it
does get reset when app data is cleared).

Additionally, an extra check is added to `FileDatabaseSelectActivity` to
ensure the service's enabled state is correct. This is necessary if the
app crashes or is force quit within DeX mode and then the user exits DeX
mode. Otherwise, the service would stay disabled until the user entered
and exited DeX again.

With the new behavior, KeePassDX will generally just work with DeX,
though there's one caveat: after the initial installation, the user must
open the app once outside of DeX. Otherwise, Android will not trigger
the broadcast receiver. This could be fixed by making the service
intially disabled in the manifest with `android:enabled="false"`, but
Android's Settings app in SDK 15 through 25 does not correctly refresh
the keyboard list when changing the service from disabled to enabled.
I opted *not* to introduce different behavior based on the API version.

[1] https://developer.samsung.com/sdp/blog/en-us/2017/07/27/samsung-dex-how-to-detect-the-samsung-dex-mode

Fixes: #245
Signed-off-by: Andrew Gunnerson <chillermillerlong@hotmail.com>
2021-09-18 23:30:37 -04:00
Stephan Paternotte
22c0bc0adb Translated using Weblate (Dutch)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2021-09-17 18:36:29 +02:00
J-Jamet
1b88f2ddf0 Merge tag '3.0.1' into develop
3.0.1
2021-09-15 13:25:03 +02:00
J-Jamet
b4f2a1eb89 Merge branch 'release/3.0.1' 2021-09-15 13:24:53 +02:00
J-Jamet
e9fc9cbc2a Capture cast exception 2021-09-15 12:21:44 +02:00
J-Jamet
b809180a1b Small changes 2021-09-15 11:25:15 +02:00
J-Jamet
ecc75df3a1 Fix search actions #1091 #1092 2021-09-15 11:16:32 +02:00
J-Jamet
d1b6863143 Update CHANGELOG 2021-09-15 10:36:41 +02:00
J-Jamet
faf27143aa Fix timeout reset #1107 2021-09-15 10:33:37 +02:00
J-Jamet
aee58a4475 Fix exception after group name change and save #1112 2021-09-15 09:48:48 +02:00
VfBFan
c4cbf07d78 Translated using Weblate (German)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-09-15 07:34:50 +02:00
J-Jamet
c5e07f643f Fix Magikeyboard URL auto action #1100 2021-09-14 20:40:22 +02:00
J-Jamet
818f5820d5 Update CHANGELOG 2021-09-14 20:13:35 +02:00
J-Jamet
77c6e28876 Fix max lines #1073 2021-09-14 20:06:49 +02:00
J-Jamet
fb7f66012d Upgrade to 3.0.1 2021-09-14 19:35:30 +02:00
J-Jamet
0f7f7bbe6c Min height to 48dp 2021-09-14 19:30:50 +02:00
J-Jamet
8dedb8deb4 Fix text dimension 2021-09-14 19:10:48 +02:00
Long Nguyễn Khánh
dbb2c10bba Translated using Weblate (Vietnamese)
Currently translated at 16.1% (92 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2021-09-13 12:46:09 +02:00
Ihor Hordiichuk
67a5eef7d6 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-09-12 00:23:20 +02:00
HARADA Hiroyuki
979f651251 Translated using Weblate (Japanese)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-09-12 00:23:19 +02:00
SC
3b93cbb009 Translated using Weblate (Portuguese)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2021-09-11 04:18:42 +02:00
SC
30c63bfc4b Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2021-09-11 04:18:42 +02:00
SC
bed40324a1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-11 04:18:37 +02:00
Wilker Santana da Silva
bc035de377 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-11 04:18:37 +02:00
SC
4db3cb6936 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.8% (567 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-11 01:07:20 +02:00
Wilker Santana da Silva
ed4b91f4bd Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.8% (567 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-11 01:07:20 +02:00
SC
24c7151276 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.6% (566 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-11 01:00:00 +02:00
Wilker Santana da Silva
804a9c07b8 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.6% (566 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-11 00:59:58 +02:00
SC
528ea56821 Translated using Weblate (Portuguese)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2021-09-11 00:01:53 +02:00
SC
7ba9c69ff8 Translated using Weblate (Portuguese (Brazil))
Currently translated at 88.7% (504 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-10 20:50:55 +02:00
Wilker Santana da Silva
0fc34da08a Translated using Weblate (Portuguese (Brazil))
Currently translated at 88.7% (504 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-10 20:50:54 +02:00
SC
3fbf8cdbc8 Translated using Weblate (Portuguese (Brazil))
Currently translated at 83.0% (472 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-10 20:36:00 +02:00
Wilker Santana da Silva
e21f20d818 Translated using Weblate (Portuguese (Brazil))
Currently translated at 83.0% (472 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-10 20:35:59 +02:00
Sina bagheri
9fd9a60ca3 Translated using Weblate (Persian)
Currently translated at 56.6% (322 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fa/
2021-09-09 17:32:51 +02:00
Hosted Weblate
78b683d724 Merge branch 'origin/develop' into Weblate. 2021-09-08 07:26:45 +02:00
vachan-maker
ad2f5036e1 Translated using Weblate (Malayalam)
Currently translated at 70.2% (399 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2021-09-08 07:26:45 +02:00
random r
4afbad8faa Translated using Weblate (Italian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-09-08 07:26:44 +02:00
J-Jamet
d284db4d3c Merge tag '3.0.0' into develop
3.0.0
2021-09-07 18:43:40 +02:00
J-Jamet
82450c0ae8 Merge branch 'release/3.0.0' 2021-09-07 18:43:31 +02:00
J-Jamet
8dd6c33901 Fix add entry education hint with default templates 2021-09-07 14:00:02 +02:00
J-Jamet
f920d40db5 Fix autofill popup window application id #1046 2021-09-07 13:45:35 +02:00
J-Jamet
19be6c1acc Upgrade to 3.0.0 2021-09-07 13:17:34 +02:00
J-Jamet
7d9d8ad0e4 Manage magikeyboard in landscape 2021-09-07 13:09:24 +02:00
J-Jamet
85f8237d5f Merge branch 'fullscreen' of git://github.com/chenxiaolong/KeePassDX into chenxiaolong-fullscreen 2021-09-07 12:41:24 +02:00
J-Jamet
c542894734 Small change in progress dialog 2021-09-07 12:18:21 +02:00
J-Jamet
d348987077 Remove unused code 2021-09-07 11:21:28 +02:00
J-Jamet
3718610595 Copy OTP Token from list to provide suitable alternative to #553 2021-09-07 10:44:34 +02:00
J-Jamet
9c36ec0623 Fix datetime font size 2021-09-07 10:29:08 +02:00
J-Jamet
c6917b5d74 Update CHANGELOG 2021-09-07 10:16:38 +02:00
Oliver Cervera
ae8b1c0c29 Translated using Weblate (Italian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-09-06 21:34:18 +02:00
Óscar Fernández Díaz
27978c459c Translated using Weblate (Spanish)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-09-06 21:34:17 +02:00
Martin
1dc7f5c666 Translated using Weblate (Czech)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-09-06 21:34:17 +02:00
zeritti
12ac870d3a Translated using Weblate (Czech)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-09-06 21:34:17 +02:00
Aman Kirely
dd1baa0224 Translated using Weblate (Czech)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-09-06 21:34:17 +02:00
Andrew Gunnerson
4eaa179789 magikeyboard: Don't force full screen EditTexts on large screen devices
Per [1], Android defaults to showing EditTexts in full screen mode when
the device is in landscape orientation. This makes sense for phones,
but not so much for larger screen devices, like tablets.

This commit updates MagiKeyboard to not use full screen mode on large
screen devices. The condition for disabling full screen mode is the same
as in the AOSP keyboard and should match what other OEM keyboards do as
well.

[1] https://developer.android.com/reference/android/inputmethodservice/InputMethodService#fullscreen-mode
2021-09-06 13:01:56 -04:00
J-Jamet
9008cd4549 Remove max lines in TextFieldView #1073 #1076 2021-09-06 12:18:17 +02:00
Oğuz Ersen
bb27ef41cc Translated using Weblate (Turkish)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-09-04 17:34:43 +02:00
Allan Nordhøy
2d35ac1df8 Translated using Weblate (Norwegian Bokmål)
Currently translated at 74.8% (425 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2021-09-04 17:34:43 +02:00
Eric
589ffc0c06 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-09-04 17:34:42 +02:00
Ihor Hordiichuk
1f7f38c7d3 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-09-04 17:34:41 +02:00
solokot
83817a2dc0 Translated using Weblate (Russian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-09-04 17:34:41 +02:00
Matthaiks
11af9da66f Translated using Weblate (Polish)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-09-04 17:34:40 +02:00
Retrial
af3926acf3 Translated using Weblate (Greek)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-09-04 17:34:40 +02:00
Hosted Weblate
ab40c2b3fd Merge branch 'origin/develop' into Weblate. 2021-09-03 16:07:31 +02:00
J-Jamet
cc3204453e Upgrade to 3.0.0_beta03 2021-09-03 15:31:17 +02:00
J-Jamet
5ef8d3b7b9 Update CHANGELOG 2021-09-03 15:25:30 +02:00
J-Jamet
2a9de97a19 Default manual selection to true 2021-09-03 15:14:39 +02:00
J-Jamet
9cecfed417 Add dots 2021-09-03 15:10:18 +02:00
J-Jamet
319715918a Small change to merge views 2021-09-03 15:02:40 +02:00
J-Jamet
a3bf6e8b6d Small change for consistency 2021-09-03 14:41:45 +02:00
J-Jamet
c4062658ce Fix search info parcelable 2021-09-03 14:39:19 +02:00
J-Jamet
01a5de413e Merge branch 'develop' of git://github.com/uduerholz/KeePassDX into uduerholz-develop 2021-09-03 14:18:51 +02:00
J-Jamet
e4c22b1f29 Change remote views when the database is open 2021-09-03 12:44:01 +02:00
Oliver Cervera
fd05670dbc Translated using Weblate (Italian)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-09-03 00:32:28 +02:00
Óscar Fernández Díaz
1ac094bfae Translated using Weblate (Spanish)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-09-03 00:32:27 +02:00
J-Jamet
b10e60126f Fix UUID view 2021-09-02 17:39:08 +02:00
J-Jamet
ef1f27f421 Check null view model callback 2021-09-02 17:31:51 +02:00
J-Jamet
0ed208675c Fix reloading from history 2021-09-02 17:18:01 +02:00
J-Jamet
00f7a0a194 Better entry activity view model to fix reloading 2021-09-02 17:05:07 +02:00
J-Jamet
935d4f4a64 Unused throw 2021-09-02 16:13:25 +02:00
J-Jamet
dc4d88260d Fix database reload 2021-09-02 16:13:08 +02:00
J-Jamet
18934601da Fix education 2021-09-02 14:26:08 +02:00
J-Jamet
4ea811aeda Fix menu in template creation 2021-09-02 13:48:48 +02:00
J-Jamet
f8fdecdc8f Fix multiple loading by move variables in entry edit view model 2021-09-02 11:12:19 +02:00
J-Jamet
5467c61137 Add equals in node info 2021-09-02 11:10:51 +02:00
J-Jamet
9c72b4cc56 Fix timeout switch 2021-09-01 18:51:18 +02:00
J-Jamet
9102217bc3 Fix template lost after orientation change #1069 2021-09-01 17:38:29 +02:00
Uli
0e8fd7b2c4 Merge branch 'Kunzisoft:develop' into develop 2021-09-01 14:41:07 +02:00
Oğuz Ersen
fdf052cddb Translated using Weblate (Turkish)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-09-01 10:34:02 +02:00
Eric
9a8d50ba6f Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-09-01 10:34:02 +02:00
Ihor Hordiichuk
136c97c312 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-09-01 10:34:02 +02:00
solokot
bf00b88ef3 Translated using Weblate (Russian)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-09-01 10:34:02 +02:00
Matthaiks
bafd1ea549 Translated using Weblate (Polish)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-09-01 10:34:01 +02:00
Hisikawa Mizuki
982618511b Translated using Weblate (Japanese)
Currently translated at 99.2% (561 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-09-01 10:34:01 +02:00
Oliver Cervera
a4ad7ca3b1 Translated using Weblate (Italian)
Currently translated at 99.4% (562 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-09-01 10:34:01 +02:00
Éfrit
99d71b57a4 Translated using Weblate (French)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2021-09-01 10:34:00 +02:00
Retrial
1b2d8502e0 Translated using Weblate (Greek)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-09-01 10:34:00 +02:00
VfBFan
53e4ea9334 Translated using Weblate (German)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-09-01 10:34:00 +02:00
zeritti
3ce704155c Translated using Weblate (Czech)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-09-01 10:33:59 +02:00
Uli
c6e2342ab4 Merge branch 'Kunzisoft:develop' into develop 2021-08-29 17:04:31 +02:00
Ulrich Dürholz
b977792168 Autofill manual selection for all form fields 2021-08-29 11:23:22 +02:00
Uli
2595cf87d8 Merge branch 'Kunzisoft:develop' into develop 2021-08-29 10:50:08 +02:00
Ulrich Dürholz
f4342f1448 Manual selection for inline suggestions 2021-08-29 10:16:15 +02:00
Ulrich Dürholz
c71ef24052 Add icon for manual autofill selection 2021-08-28 12:48:03 +02:00
Ulrich Dürholz
39b817bc69 Let user select entry for autofill 2021-08-27 18:23:32 +02:00
Uli
e3adaba3b3 Merge branch 'Kunzisoft:develop' into develop 2021-08-24 16:39:45 +02:00
Ulrich Dürholz
c62064002f Fix small issue with credit card autofill 2021-07-27 17:36:56 +02:00
237 changed files with 7350 additions and 3187 deletions

View File

@@ -24,6 +24,7 @@ A clear and concise description of what you expected to happen.
- Created with: [e.g Windows KeePass 2.42] - Created with: [e.g Windows KeePass 2.42]
- Version: [e.g. 2] - Version: [e.g. 2]
- Location: [e.g. Remote file retrieved with GDrive app] - Location: [e.g. Remote file retrieved with GDrive app]
- File provider (`content://` URI): [e.g. `content://com.google.android.apps.docs.storage/5`]
- Size: [e.g. 150Mo] - Size: [e.g. 150Mo]
- Contains attachment: [e.g. Yes] - Contains attachment: [e.g. Yes]

View File

@@ -1,10 +1,46 @@
KeePassDX(3.1.0)
* Add breadcrumb
* Add path in search results #1148
* Add group info dialog #1177
* Manage colors #64 #913
* Fix UI in Android 8 #509
* Upgrade libs and SDK to 31 #833
* Fix parser of database v1 #1201
* Stop asking WRITE_EXTERNAL_STORAGE permission
KeePassDX(3.0.4)
* Fix autofill inline bugs #1173 #1165
* Small UI change
KeePassDX(3.0.3)
* Change default Argon2 parameters #1098
* Add & edit custom icon name #976
* Fix templates #1128 #1133 #1138
* Update Autofill compatibility list #725 #1154
* Improve fingerprint usage #1137 #1145
* Change backup configuration #1144
* Add lock button in database notification
KeePassDX(3.0.2)
* Samsung DeX mode #1114 #245 (Thx @chenxiaolong)
KeePassDX(3.0.1)
* Fix text size and smallest margin #1085
* Fix number of lines during an edition #1073
* Fix Magikeyboard URL auto action #1100
* Fix exception after group name change and save #1112
* Fix timeout reset #1107
* Fix search actions #1091 #1092
* Small changes #1106 #1085
KeePassDX(3.0.0) KeePassDX(3.0.0)
* Add / Manage dynamic templates #191 * Add / Manage dynamic templates #191
* Manually select RecycleBin group and Templates group #191 * Manually select RecycleBin group and Templates group #191
* Setting to display OTP Token in list #655 * Setting to display OTP Token in list #655
* Fix timeout in dialogs #716 * Fix timeout in dialogs #716
* Check URI permissions #626 * Check URI permissions #626
* Improvements #1035 #1043 #942 #1021 #1027 * Better autofill implementation #943 #946 #984 #1070 (Thx @uduerholz)
* Improvements #680 #1035 #1043 #942 #1021 #1027 #1046 #1082 #1083 (Thx @chenxiaolong)
KeePassDX(2.10.5) KeePassDX(2.10.5)
* Increase the saving speed of database #1028 * Increase the saving speed of database #1028

View File

@@ -15,6 +15,7 @@
- Material design with **themes**. - Material design with **themes**.
- **Auto-Fill** and Integration. - **Auto-Fill** and Integration.
- Field filling **keyboard**. - Field filling **keyboard**.
- Dynamic **templates**
- **History** of each entry. - **History** of each entry.
- Precise management of **settings**. - Precise management of **settings**.
- Code written in **native languages** *(Kotlin / Java / JNI / C)*. - Code written in **native languages** *(Kotlin / Java / JNI / C)*.
@@ -71,7 +72,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
## License ## License
Copyright © 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com). Copyright © 2022 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
This file is part of KeePassDX. This file is part of KeePassDX.

View File

@@ -3,16 +3,16 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
android { android {
compileSdkVersion 30 compileSdkVersion 31
buildToolsVersion "30.0.3" buildToolsVersion "31.0.0"
ndkVersion "21.4.7075529" ndkVersion "21.4.7075529"
defaultConfig { defaultConfig {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 30 targetSdkVersion 31
versionCode = 85 versionCode = 92
versionName = "3.0.0_beta02" versionName = "3.1.0"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"
@@ -99,33 +99,33 @@ android {
} }
} }
def room_version = "2.2.6" def room_version = "2.4.1"
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0' implementation "androidx.appcompat:appcompat:$android_appcompat_version"
implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.1.0' implementation 'androidx.biometric:biometric:1.1.0'
implementation 'androidx.media:media:1.4.3'
// Lifecycle - LiveData - ViewModel - Coroutines // Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:1.3.2" implementation "androidx.core:core-ktx:$android_core_version"
implementation 'androidx.fragment:fragment-ktx:1.2.5' implementation 'androidx.fragment:fragment-ktx:1.4.0'
// WARNING: Don't upgrade because slowdown https://github.com/Kunzisoft/KeePassDX/issues/923 implementation "com.google.android.material:material:$android_material_version"
implementation 'com.google.android.material:material:1.1.0'
// Database // Database
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
// Autofill // Autofill
implementation "androidx.autofill:autofill:1.1.0" implementation "androidx.autofill:autofill:1.1.0"
// Time // Time
implementation 'joda-time:joda-time:2.10.6' implementation 'joda-time:joda-time:2.10.13'
// Color // Color
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4' implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6'
// Education // Education
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0' implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3'
// Apache Commons // Apache Commons
implementation 'commons-io:commons-io:2.8.0' implementation 'commons-io:commons-io:2.8.0'
implementation 'commons-codec:commons-codec:1.15' implementation 'commons-codec:commons-codec:1.15'
@@ -136,6 +136,6 @@ dependencies {
implementation project(path: ':icon-pack-material') implementation project(path: ':icon-pack-material')
// Tests // Tests
androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation "androidx.test:runner:$android_test_version"
androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation "androidx.test:rules:$android_test_version"
} }

View File

@@ -10,15 +10,12 @@
android:anyDensity="true" /> android:anyDensity="true" />
<uses-permission <uses-permission
android:name="android.permission.FOREGROUND_SERVICE" /> android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission <uses-permission
android:name="android.permission.USE_BIOMETRIC" /> android:name="android.permission.USE_BIOMETRIC" />
<uses-permission <uses-permission
android:name="android.permission.VIBRATE"/> android:name="android.permission.VIBRATE"/>
<!-- Write permission until Android 10 -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"
tools:ignore="ScopedStorage" />
<!-- Open apps from links --> <!-- Open apps from links -->
<uses-permission <uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES" android:name="android.permission.QUERY_ALL_PACKAGES"
@@ -30,12 +27,13 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:name="com.kunzisoft.keepass.app.App" android:name="com.kunzisoft.keepass.app.App"
android:allowBackup="true" android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/old_backup_rules"
android:dataExtractionRules="@xml/backup_rules"
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent" android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
android:largeHeap="true" android:largeHeap="true"
android:resizeableActivity="true" android:resizeableActivity="true"
android:theme="@style/KeepassDXStyle.Night" android:theme="@style/KeepassDXStyle.Night"
tools:targetApi="n"> tools:targetApi="s">
<meta-data <meta-data
android:name="com.google.android.backup.api_key" android:name="com.google.android.backup.api_key"
android:value="${googleAndroidBackupAPIKey}" /> android:value="${googleAndroidBackupAPIKey}" />
@@ -44,6 +42,7 @@
android:theme="@style/KeepassDXStyle.SplashScreen" android:theme="@style/KeepassDXStyle.SplashScreen"
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTop" android:launchMode="singleTop"
android:exported="true"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:windowSoftInputMode="stateHidden|stateAlwaysHidden" > android:windowSoftInputMode="stateHidden|stateAlwaysHidden" >
<intent-filter> <intent-filter>
@@ -53,6 +52,7 @@
</activity> </activity>
<activity <activity
android:name="com.kunzisoft.keepass.activities.PasswordActivity" android:name="com.kunzisoft.keepass.activities.PasswordActivity"
android:exported="true"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustResize|stateUnchanged"> android:windowSoftInputMode="adjustResize|stateUnchanged">
<intent-filter> <intent-filter>
@@ -111,6 +111,7 @@
<!-- Main Activity --> <!-- Main Activity -->
<activity <activity
android:name="com.kunzisoft.keepass.activities.GroupActivity" android:name="com.kunzisoft.keepass.activities.GroupActivity"
android:exported="false"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustPan"> android:windowSoftInputMode="adjustPan">
<meta-data <meta-data
@@ -154,7 +155,8 @@
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" /> android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
<activity <activity
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity" android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
android:theme="@style/Theme.Transparent"> android:theme="@style/Theme.Transparent"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@@ -173,7 +175,8 @@
android:theme="@style/Theme.Transparent" /> android:theme="@style/Theme.Transparent" />
<activity <activity
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity" android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
android:label="@string/keyboard_setting_label"> android:label="@string/keyboard_setting_label"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
</intent-filter> </intent-filter>
@@ -199,6 +202,7 @@
<service <service
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService" android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
android:label="@string/autofill_service_name" android:label="@string/autofill_service_name"
android:exported="true"
android:permission="android.permission.BIND_AUTOFILL_SERVICE"> android:permission="android.permission.BIND_AUTOFILL_SERVICE">
<meta-data <meta-data
android:name="android.autofill" android:name="android.autofill"
@@ -210,6 +214,7 @@
<service <service
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService" android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
android:label="@string/keyboard_label" android:label="@string/keyboard_label"
android:exported="true"
android:permission="android.permission.BIND_INPUT_METHOD" > android:permission="android.permission.BIND_INPUT_METHOD" >
<meta-data android:name="android.view.im" <meta-data android:name="android.view.im"
android:resource="@xml/keyboard_method"/> android:resource="@xml/keyboard_method"/>
@@ -221,6 +226,14 @@
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService" android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
<receiver
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.app.action.ENTER_KNOX_DESKTOP_MODE" />
<action android:name="android.app.action.EXIT_KNOX_DESKTOP_MODE" />
</intent-filter>
</receiver>
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" /> <meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
</application> </application>

View File

@@ -23,28 +23,33 @@ import android.app.Activity
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.content.IntentSender
import android.os.Build import android.os.Build
import android.view.inputmethod.InlineSuggestionsRequest import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
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.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
import com.kunzisoft.keepass.autofill.KeeAutofillService import com.kunzisoft.keepass.autofill.KeeAutofillService
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : DatabaseModeActivity() { class AutofillLauncherActivity : DatabaseModeActivity() {
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this, true)
else null
override fun applyCustomStyle(): Boolean { override fun applyCustomStyle(): Boolean {
return false return false
} }
@@ -60,16 +65,37 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode -> EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
when (specialMode) { when (specialMode) {
SpecialMode.SELECTION -> { SpecialMode.SELECTION -> {
// Build search param intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle ->
val searchInfo = SearchInfo().apply { // To pass extra inline request
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID) var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME) compatInlineSuggestionsRequest = bundle.getParcelable(KEY_INLINE_SUGGESTION)
} }
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain -> // Build search param
searchInfo.webDomain = concreteWebDomain bundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
launchSelection(database, searchInfo) SearchInfo.getConcreteWebDomain(
this,
searchInfo.webDomain
) { concreteWebDomain ->
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val assistStructure = AutofillHelper
.retrieveAutofillComponent(intent)
?.assistStructure
val newAutofillComponent = if (assistStructure != null) {
AutofillComponent(
assistStructure,
compatInlineSuggestionsRequest
)
} else {
null
}
searchInfo.webDomain = concreteWebDomain
launchSelection(database, newAutofillComponent, searchInfo)
}
}
} }
// Remove bundle
intent.removeExtra(KEY_SELECTION_BUNDLE)
} }
SpecialMode.REGISTRATION -> { SpecialMode.REGISTRATION -> {
// To register info // To register info
@@ -90,10 +116,8 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
} }
private fun launchSelection(database: Database?, private fun launchSelection(database: Database?,
autofillComponent: AutofillComponent?,
searchInfo: SearchInfo) { searchInfo: SearchInfo) {
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
if (autofillComponent == null) { if (autofillComponent == null) {
setResult(Activity.RESULT_CANCELED) setResult(Activity.RESULT_CANCELED)
finish() finish()
@@ -118,6 +142,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
// Show the database UI to select the entry // Show the database UI to select the entry
GroupActivity.launchForAutofillResult(this, GroupActivity.launchForAutofillResult(this,
openedDatabase, openedDatabase,
mAutofillActivityResultLauncher,
autofillComponent, autofillComponent,
searchInfo, searchInfo,
false) false)
@@ -125,6 +150,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
{ {
// If database not open // If database not open
FileDatabaseSelectActivity.launchForAutofillResult(this, FileDatabaseSelectActivity.launchForAutofillResult(this,
mAutofillActivityResultLauncher,
autofillComponent, autofillComponent,
searchInfo) searchInfo)
} }
@@ -185,53 +211,47 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show() Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
if (PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
// Close the database
sendBroadcast(Intent(LOCK_ACTION))
}
super.onActivityResult(requestCode, resultCode, data)
}
companion object { companion object {
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID" private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN" private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME" private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO" private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
fun getAuthIntentSenderForSelection(context: Context, fun getPendingIntentForSelection(context: Context,
searchInfo: SearchInfo? = null, searchInfo: SearchInfo? = null,
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): IntentSender { compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent {
return PendingIntent.getActivity(context, 0, return PendingIntent.getActivity(context, 0,
// Doesn't work with Parcelable (don't know why?) // Doesn't work with direct extra Parcelable (don't know why?)
Intent(context, AutofillLauncherActivity::class.java).apply { // Wrap into a bundle to bypass the problem
searchInfo?.let { Intent(context, AutofillLauncherActivity::class.java).apply {
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId) putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
putExtra(KEY_SEARCH_DOMAIN, it.webDomain) putParcelable(KEY_SEARCH_INFO, searchInfo)
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.let { putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
}
} }
}, })
PendingIntent.FLAG_CANCEL_CURRENT).intentSender },
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
})
} }
fun getAuthIntentSenderForRegistration(context: Context, fun getPendingIntentForRegistration(context: Context,
registerInfo: RegisterInfo): IntentSender { registerInfo: RegisterInfo): PendingIntent {
return PendingIntent.getActivity(context, 0, return PendingIntent.getActivity(context, 0,
Intent(context, AutofillLauncherActivity::class.java).apply { Intent(context, AutofillLauncherActivity::class.java).apply {
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION) EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
putExtra(KEY_REGISTER_INFO, registerInfo) putExtra(KEY_REGISTER_INFO, registerInfo)
}, },
PendingIntent.FLAG_CANCEL_CURRENT).intentSender if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
})
} }
fun launchForRegistration(context: Context, fun launchForRegistration(context: Context,

View File

@@ -32,10 +32,16 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar import android.widget.ProgressBar
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.graphics.ColorUtils
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.appbar.CollapsingToolbarLayout
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.fragments.EntryFragment import com.kunzisoft.keepass.activities.fragments.EntryFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
@@ -57,20 +63,24 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.view.changeControlColor
import com.kunzisoft.keepass.view.changeTitleColor
import com.kunzisoft.keepass.view.hideByFading import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.showActionErrorIfNeeded import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.EntryViewModel import com.kunzisoft.keepass.viewmodels.EntryViewModel
import java.util.* import java.util.*
import kotlin.collections.HashMap
class EntryActivity : DatabaseLockActivity() { class EntryActivity : DatabaseLockActivity() {
private var coordinatorLayout: CoordinatorLayout? = null private var coordinatorLayout: CoordinatorLayout? = null
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var appBarLayout: AppBarLayout? = null
private var titleIconView: ImageView? = null private var titleIconView: ImageView? = null
private var historyView: View? = null private var historyView: View? = null
private var entryProgress: ProgressBar? = null private var entryProgress: LinearProgressIndicator? = null
private var lockView: View? = null private var lockView: View? = null
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
private var loadingView: ProgressBar? = null private var loadingView: ProgressBar? = null
@@ -81,13 +91,24 @@ class EntryActivity : DatabaseLockActivity() {
private var mHistoryPosition: Int = -1 private var mHistoryPosition: Int = -1
private var mEntryIsHistory: Boolean = false private var mEntryIsHistory: Boolean = false
private var mUrl: String? = null private var mUrl: String? = null
private var mEntryLoaded = false
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var mAttachmentSelected: Attachment? = null
private var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) {
// Reload the current id from database
mEntryViewModel.loadDatabase(mDatabase)
}
private var mIcon: IconImage? = null private var mIcon: IconImage? = null
private var mIconColor: Int = 0 private var mColorAccent: Int = 0
private var mControlColor: Int = 0
private var mColorPrimary: Int = 0
private var mColorBackground: Int = 0
private var mBackgroundColor: Int? = null
private var mForegroundColor: Int? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -102,6 +123,7 @@ class EntryActivity : DatabaseLockActivity() {
// Get views // Get views
coordinatorLayout = findViewById(R.id.toolbar_coordinator) coordinatorLayout = findViewById(R.id.toolbar_coordinator)
collapsingToolbarLayout = findViewById(R.id.toolbar_layout) collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
appBarLayout = findViewById(R.id.app_bar)
titleIconView = findViewById(R.id.entry_icon) titleIconView = findViewById(R.id.entry_icon)
historyView = findViewById(R.id.history_container) historyView = findViewById(R.id.history_container)
entryProgress = findViewById(R.id.entry_progress) entryProgress = findViewById(R.id.entry_progress)
@@ -112,18 +134,28 @@ class EntryActivity : DatabaseLockActivity() {
collapsingToolbarLayout?.title = " " collapsingToolbarLayout?.title = " "
toolbar?.title = " " toolbar?.title = " "
// Retrieve the textColor to tint the icon // Retrieve the textColor to tint the toolbar
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent)) val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
mIconColor = taIconColor.getColor(0, Color.BLACK) val taControlColor = theme.obtainStyledAttributes(intArrayOf(R.attr.toolbarColorControl))
taIconColor.recycle() val taColorPrimary = theme.obtainStyledAttributes(intArrayOf(R.attr.colorPrimary))
val taColorBackground = theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
mColorAccent = taColorAccent.getColor(0, Color.BLACK)
mControlColor = taControlColor.getColor(0, Color.BLACK)
mColorPrimary = taColorPrimary.getColor(0, Color.BLACK)
mColorBackground = taColorBackground.getColor(0, Color.BLACK)
taColorAccent.recycle()
taControlColor.recycle()
taColorPrimary.recycle()
taColorBackground.recycle()
// Get Entry from UUID // Get Entry from UUID
try { try {
intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { entryId -> intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { mainEntryId ->
mMainEntryId = entryId
intent.removeExtra(KEY_ENTRY) intent.removeExtra(KEY_ENTRY)
mHistoryPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1) val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
intent.removeExtra(KEY_ENTRY_HISTORY_POSITION) intent.removeExtra(KEY_ENTRY_HISTORY_POSITION)
mEntryViewModel.loadEntry(mDatabase, mainEntryId, historyPosition)
} }
} catch (e: ClassCastException) { } catch (e: ClassCastException) {
Log.e(TAG, "Unable to retrieve the entry key") Log.e(TAG, "Unable to retrieve the entry key")
@@ -131,6 +163,15 @@ class EntryActivity : DatabaseLockActivity() {
// Init SAF manager // Init SAF manager
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
mAttachmentSelected?.let { attachment ->
if (createdFileUri != null) {
mAttachmentFileBinderManager
?.startDownloadAttachment(createdFileUri, attachment)
}
mAttachmentSelected = null
}
}
// Init attachment service binder manager // Init attachment service binder manager
mAttachmentFileBinderManager = AttachmentFileBinderManager(this) mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
@@ -138,59 +179,57 @@ class EntryActivity : DatabaseLockActivity() {
lockAndExit() lockAndExit()
} }
mEntryViewModel.mainEntryId.observe(this) { mainEntryId -> mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
this.mMainEntryId = mainEntryId if (entryInfoHistory != null) {
invalidateOptionsMenu() this.mMainEntryId = entryInfoHistory.mainEntryId
}
mEntryViewModel.historyPosition.observe(this) { historyPosition -> // Manage history position
this.mHistoryPosition = historyPosition val historyPosition = entryInfoHistory.historyPosition
val entryIsHistory = historyPosition > -1 this.mHistoryPosition = historyPosition
this.mEntryIsHistory = entryIsHistory val entryIsHistory = historyPosition > -1
// Assign history dedicated view this.mEntryIsHistory = entryIsHistory
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE // Assign history dedicated view
if (entryIsHistory) { historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent)) if (entryIsHistory) {
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK)) collapsingToolbarLayout?.contentScrim =
taColorAccent.recycle() ColorDrawable(mColorAccent)
}
invalidateOptionsMenu()
}
mEntryViewModel.entryInfo.observe(this) { entryInfo ->
// Manage entry copy to start notification if allowed (at the first start)
if (savedInstanceState == null) {
// Manage entry to launch copying notification if allowed
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
} }
val entryInfo = entryInfoHistory.entryInfo
// Manage entry copy to start notification if allowed (at the first start)
if (savedInstanceState == null) {
// Manage entry to launch copying notification if allowed
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
}
}
// Assign title icon
mIcon = entryInfo.icon
// Assign title text
val entryTitle =
if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
collapsingToolbarLayout?.title = entryTitle
toolbar?.title = entryTitle
mUrl = entryInfo.url
// Assign colors
mBackgroundColor = entryInfo.backgroundColor
mForegroundColor = entryInfo.foregroundColor
loadingView?.hideByFading()
mEntryLoaded = true
} else {
finish()
} }
// Assign title icon
mIcon = entryInfo.icon
titleIconView?.let { iconView ->
mIconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, mIconColor)
}
// Assign title text
val entryTitle = if (entryInfo.title.isNotEmpty()) entryInfo.title else entryInfo.id.toString()
collapsingToolbarLayout?.title = entryTitle
toolbar?.title = entryTitle
mUrl = entryInfo.url
// Refresh Menu // Refresh Menu
invalidateOptionsMenu() invalidateOptionsMenu()
loadingView?.hideByFading()
} }
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement -> mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
if (otpElement == null) if (otpElement == null) {
entryProgress?.visibility = View.GONE entryProgress?.visibility = View.GONE
when (otpElement?.type) { } else when (otpElement.type) {
// Only add token if HOTP // Only add token if HOTP
OtpType.HOTP -> { OtpType.HOTP -> {
entryProgress?.visibility = View.GONE entryProgress?.visibility = View.GONE
@@ -199,7 +238,7 @@ class EntryActivity : DatabaseLockActivity() {
OtpType.TOTP -> { OtpType.TOTP -> {
entryProgress?.apply { entryProgress?.apply {
max = otpElement.period max = otpElement.period
progress = otpElement.secondsRemaining setProgressCompat(otpElement.secondsRemaining, true)
visibility = View.VISIBLE visibility = View.VISIBLE
} }
} }
@@ -207,9 +246,8 @@ class EntryActivity : DatabaseLockActivity() {
} }
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected -> mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
mExternalFileHelper?.createDocument(attachmentSelected.name)?.let { requestCode -> mAttachmentSelected = attachmentSelected
mAttachmentsToDownload[requestCode] = attachmentSelected mExternalFileHelper?.createDocument(attachmentSelected.name)
}
} }
mEntryViewModel.historySelected.observe(this) { historySelected -> mEntryViewModel.historySelected.observe(this) { historySelected ->
@@ -218,7 +256,8 @@ class EntryActivity : DatabaseLockActivity() {
this, this,
database, database,
historySelected.nodeId, historySelected.nodeId,
historySelected.historyPosition historySelected.historyPosition,
mEntryActivityResultLauncher
) )
} }
} }
@@ -235,14 +274,7 @@ class EntryActivity : DatabaseLockActivity() {
override fun onDatabaseRetrieved(database: Database?) { override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
mEntryViewModel.loadEntry(mDatabase, mMainEntryId, mHistoryPosition) mEntryViewModel.loadDatabase(database)
// Assign title icon
mIcon?.let { icon ->
titleIconView?.let { iconView ->
mIconDrawableFactory?.assignDatabaseIcon(iconView, icon, mIconColor)
}
}
} }
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
@@ -288,54 +320,67 @@ class EntryActivity : DatabaseLockActivity() {
super.onPause() super.onPause()
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { private fun applyToolbarColors() {
super.onActivityResult(requestCode, resultCode, data) appBarLayout?.setBackgroundColor(mBackgroundColor ?: mColorPrimary)
collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorPrimary)
when (requestCode) { val backgroundDarker = if (mBackgroundColor != null) {
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> { ColorUtils.blendARGB(mBackgroundColor!!, Color.WHITE, 0.1f)
// Reload the current id from database } else {
mEntryViewModel.updateEntry(mDatabase) mColorBackground
} }
} titleIconView?.background?.colorFilter = BlendModeColorFilterCompat
.createBlendModeColorFilterCompat(backgroundDarker, BlendModeCompat.SRC_IN)
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri -> mIcon?.let { icon ->
if (createdFileUri != null) { titleIconView?.let { iconView ->
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload -> mIconDrawableFactory?.assignDatabaseIcon(
mAttachmentFileBinderManager iconView,
?.startDownloadAttachment(createdFileUri, attachmentToDownload) icon,
} mForegroundColor ?: mColorAccent
)
} }
} }
toolbar?.changeControlColor(mForegroundColor ?: mControlColor)
collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mControlColor)
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
if (mEntryLoaded) {
val inflater = menuInflater
MenuUtil.contributionMenuInflater(inflater, menu)
val inflater = menuInflater inflater.inflate(R.menu.entry, menu)
MenuUtil.contributionMenuInflater(inflater, menu) inflater.inflate(R.menu.database, menu)
inflater.inflate(R.menu.entry, menu) if (mEntryIsHistory && !mDatabaseReadOnly) {
inflater.inflate(R.menu.database, menu) inflater.inflate(R.menu.entry_history, menu)
}
if (mUrl?.isEmpty() != false) { // Show education views
menu.findItem(R.id.menu_goto_url)?.isVisible = false Handler(Looper.getMainLooper()).post {
performedNextEducation(
EntryActivityEducation(
this
), menu
)
}
} }
return true
}
if (mEntryIsHistory && !mDatabaseReadOnly) { override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
inflater.inflate(R.menu.entry_history, menu) if (mUrl?.isEmpty() != false) {
menu?.findItem(R.id.menu_goto_url)?.isVisible = false
} }
if (mEntryIsHistory || mDatabaseReadOnly) { if (mEntryIsHistory || mDatabaseReadOnly) {
menu.findItem(R.id.menu_save_database)?.isVisible = false menu?.findItem(R.id.menu_save_database)?.isVisible = false
menu.findItem(R.id.menu_edit)?.isVisible = false menu?.findItem(R.id.menu_edit)?.isVisible = false
} }
if (mSpecialMode != SpecialMode.DEFAULT) { if (mSpecialMode != SpecialMode.DEFAULT) {
menu.findItem(R.id.menu_reload_database)?.isVisible = false menu?.findItem(R.id.menu_reload_database)?.isVisible = false
} }
applyToolbarColors()
// Show education views return super.onPrepareOptionsMenu(menu)
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
return true
} }
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation, private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
@@ -380,7 +425,8 @@ class EntryActivity : DatabaseLockActivity() {
EntryEditActivity.launchToUpdate( EntryEditActivity.launchToUpdate(
this, this,
database, database,
entryId entryId,
mEntryActivityResultLauncher
) )
} }
} }
@@ -421,7 +467,7 @@ class EntryActivity : DatabaseLockActivity() {
// Transit data in previous Activity after an update // Transit data in previous Activity after an update
Intent().apply { Intent().apply {
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId) putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
setResult(EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE, this) setResult(Activity.RESULT_OK, this)
} }
super.finish() super.finish()
} }
@@ -439,15 +485,13 @@ class EntryActivity : DatabaseLockActivity() {
*/ */
fun launch(activity: Activity, fun launch(activity: Activity,
database: Database, database: Database,
entryId: NodeId<UUID>) { entryId: NodeId<UUID>,
activityResultLauncher: ActivityResultLauncher<Intent>) {
if (database.loaded) { if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java) val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId) intent.putExtra(KEY_ENTRY, entryId)
activity.startActivityForResult( activityResultLauncher.launch(intent)
intent,
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
)
} }
} }
} }
@@ -458,16 +502,14 @@ class EntryActivity : DatabaseLockActivity() {
fun launch(activity: Activity, fun launch(activity: Activity,
database: Database, database: Database,
entryId: NodeId<UUID>, entryId: NodeId<UUID>,
historyPosition: Int) { historyPosition: Int,
activityResultLauncher: ActivityResultLauncher<Intent>) {
if (database.loaded) { if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java) val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId) intent.putExtra(KEY_ENTRY, entryId)
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition) intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
activity.startActivityForResult( activityResultLauncher.launch(intent)
intent,
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
)
} }
} }
} }

View File

@@ -33,12 +33,17 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
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.* import com.kunzisoft.keepass.activities.dialogs.*
@@ -48,12 +53,9 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
import com.kunzisoft.keepass.app.database.IOActionTask
import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.template.* import com.kunzisoft.keepass.database.element.template.*
@@ -72,6 +74,7 @@ import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.* import com.kunzisoft.keepass.view.*
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
import org.joda.time.DateTime import org.joda.time.DateTime
import java.util.* import java.util.*
@@ -95,15 +98,13 @@ class EntryEditActivity : DatabaseLockActivity(),
private var lockView: View? = null private var lockView: View? = null
private var loadingView: ProgressBar? = null private var loadingView: ProgressBar? = null
private var mEntryId: NodeId<UUID>? = null
private var mParentId: NodeId<*>? = null
private var mRegisterInfo: RegisterInfo? = null
private var mSearchInfo: SearchInfo? = null
private val mEntryEditViewModel: EntryEditViewModel by viewModels() private val mEntryEditViewModel: EntryEditViewModel by viewModels()
private var mParent: Group? = null private var mTemplate: Template? = null
private var mEntry: Entry? = null
private var mIsTemplate: Boolean = false private var mIsTemplate: Boolean = false
private var mEntryLoaded: Boolean = false
private var mTemplatesSelectorAdapter: TemplatesSelectorAdapter? = null
private val mColorPickerViewModel: ColorPickerViewModel by viewModels()
private var mAllowCustomFields = false private var mAllowCustomFields = false
private var mAllowOTP = false private var mAllowOTP = false
@@ -114,6 +115,10 @@ class EntryEditActivity : DatabaseLockActivity(),
// Education // Education
private var entryEditActivityEducation: EntryEditActivityEducation? = null private var entryEditActivityEducation: EntryEditActivityEducation? = null
private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon ->
mEntryEditViewModel.selectIcon(icon)
}
// To ask data lost only one time // To ask data lost only one time
private var backPressedAlreadyApproved = false private var backPressedAlreadyApproved = false
@@ -138,25 +143,45 @@ class EntryEditActivity : DatabaseLockActivity(),
stopService(Intent(this, ClipboardEntryNotificationService::class.java)) stopService(Intent(this, ClipboardEntryNotificationService::class.java))
stopService(Intent(this, KeyboardEntryNotificationService::class.java)) stopService(Intent(this, KeyboardEntryNotificationService::class.java))
mRegisterInfo = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)
mSearchInfo = EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
// Entry is retrieve, it's an entry to update // Entry is retrieve, it's an entry to update
var entryId: NodeId<UUID>? = null
intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let { entryToUpdate -> intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let { entryToUpdate ->
//intent.removeExtra(KEY_ENTRY) intent.removeExtra(KEY_ENTRY)
mEntryId = entryToUpdate entryId = entryToUpdate
} }
// Parent is retrieve, it's a new entry to create // Parent is retrieve, it's a new entry to create
var parentId: NodeId<*>? = null
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let { parent -> intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let { parent ->
//intent.removeExtra(KEY_PARENT) intent.removeExtra(KEY_PARENT)
mParentId = parent parentId = parent
} }
retrieveEntry(mDatabase) mEntryEditViewModel.loadTemplateEntry(
mDatabase,
entryId,
parentId,
EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent),
EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
)
// To retrieve attachment // To retrieve attachment
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let { attachmentToUploadUri ->
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
documentFile.name?.let { fileName ->
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
.show(supportFragmentManager, "fileTooBigFragment")
} else {
mEntryEditViewModel.buildNewAttachment(attachmentToUploadUri, fileName)
}
}
}
}
}
mAttachmentFileBinderManager = AttachmentFileBinderManager(this) mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
// Verify the education views // Verify the education views
entryEditActivityEducation = EntryEditActivityEducation(this) entryEditActivityEducation = EntryEditActivityEducation(this)
@@ -166,43 +191,68 @@ class EntryEditActivity : DatabaseLockActivity(),
// Save button // Save button
validateButton?.setOnClickListener { saveEntry() } validateButton?.setOnClickListener { saveEntry() }
mEntryEditViewModel.templatesEntry.observe(this) { templatesEntry -> mEntryEditViewModel.onTemplateChanged.observe(this) { template ->
// Change template dynamically this.mTemplate = template
templatesEntry?.templates?.let { templates -> }
val defaultTemplate = templatesEntry.defaultTemplate
templateSelectorSpinner?.apply {
// Build template selector
if (templates.isNotEmpty()) {
adapter = TemplatesSelectorAdapter(
this@EntryEditActivity,
mIconDrawableFactory,
templates
)
setSelection(templates.indexOf(defaultTemplate))
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
mEntryEditViewModel.changeTemplate(templates[position])
}
override fun onNothingSelected(parent: AdapterView<*>?) {} mEntryEditViewModel.templatesEntry.observe(this) { templatesEntry ->
if (templatesEntry != null) {
// Change template dynamically
this.mIsTemplate = templatesEntry.isTemplate
templatesEntry.templates.let { templates ->
templateSelectorSpinner?.apply {
// Build template selector
if (templates.isNotEmpty()) {
mTemplatesSelectorAdapter = TemplatesSelectorAdapter(
this@EntryEditActivity,
templates
).apply {
iconDrawableFactory = mIconDrawableFactory
}
adapter = mTemplatesSelectorAdapter
val selectedTemplate = if (mTemplate != null)
mTemplate
else
templatesEntry.defaultTemplate
setSelection(templates.indexOf(selectedTemplate))
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
mEntryEditViewModel.changeTemplate(templates[position])
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
} else {
visibility = View.GONE
} }
} else {
visibility = View.GONE
} }
} }
}
loadingView?.hideByFading() loadingView?.hideByFading()
mEntryLoaded = true
} else {
finish()
}
invalidateOptionsMenu()
} }
// View model listeners // View model listeners
mEntryEditViewModel.requestIconSelection.observe(this) { iconImage -> mEntryEditViewModel.requestIconSelection.observe(this) { iconImage ->
IconPickerActivity.launch(this@EntryEditActivity, iconImage) IconPickerActivity.launch(this@EntryEditActivity, iconImage, mIconSelectionActivityResultLauncher)
}
mEntryEditViewModel.requestColorSelection.observe(this) { color ->
ColorPickerDialogFragment.newInstance(color)
.show(supportFragmentManager, "ColorPickerFragment")
}
mColorPickerViewModel.colorPicked.observe(this) { color ->
mEntryEditViewModel.selectColor(color)
} }
mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant -> mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
@@ -309,77 +359,10 @@ class EntryEditActivity : DatabaseLockActivity(),
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
mAllowCustomFields = database?.allowEntryCustomFields() == true mAllowCustomFields = database?.allowEntryCustomFields() == true
mAllowOTP = database?.allowOTP == true mAllowOTP = database?.allowOTP == true
retrieveEntry(database) mEntryEditViewModel.loadDatabase(database)
} mTemplatesSelectorAdapter?.apply {
iconDrawableFactory = mIconDrawableFactory
private fun retrieveEntry(database: Database?) { notifyDataSetChanged()
database?.let {
mEntryId?.let {
IOActionTask(
{
// Create an Entry copy to modify from the database entry
mEntry = database.getEntryById(it)
// Retrieve the parent
mEntry?.let { entry ->
// If no parent, add root group as parent
if (entry.parent == null) {
entry.parent = database.rootGroup
}
}
// Define if current entry is a template (in direct template group)
mIsTemplate = database.entryIsTemplate(mEntry)
},
{
mEntryEditViewModel.loadTemplateEntry(
database,
mEntry,
mIsTemplate,
mRegisterInfo,
mSearchInfo
)
}
).execute()
mEntryId = null
}
mParentId?.let {
IOActionTask(
{
mParent = database.getGroupById(it)
mParent?.let { parentGroup ->
mEntry = database.createEntry()?.apply {
// Add the default icon from parent if not a folder
val parentIcon = parentGroup.icon
// Set default icon
if (parentIcon.custom.isUnknown
&& parentIcon.standard.id != IconImageStandard.FOLDER_ID
) {
icon = IconImage(parentIcon.standard)
}
if (!parentIcon.custom.isUnknown) {
icon = IconImage(parentIcon.custom)
}
// Set default username
username = database.defaultUsername
// Warning only the entry recognize is parent, parent don't yet recognize the new entry
// Useful to recognize child state (ie: entry is a template)
parent = parentGroup
}
}
mIsTemplate = database.entryIsTemplate(mEntry)
},
{
mEntryEditViewModel.loadTemplateEntry(
database,
mEntry,
mIsTemplate,
mRegisterInfo,
mSearchInfo
)
}
).execute()
mParentId = null
}
} }
} }
@@ -532,29 +515,6 @@ class EntryEditActivity : DatabaseLockActivity(),
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon ->
mEntryEditViewModel.selectIcon(icon)
}
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
uri?.let { attachmentToUploadUri ->
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
documentFile.name?.let { fileName ->
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
.show(supportFragmentManager, "fileTooBigFragment")
} else {
mEntryEditViewModel.buildNewAttachment(attachmentToUploadUri, fileName)
}
}
}
}
}
}
/** /**
* Set up OTP (HOTP or TOTP) and add it as extra field * Set up OTP (HOTP or TOTP) and add it as extra field
*/ */
@@ -571,29 +531,33 @@ class EntryEditActivity : DatabaseLockActivity(),
*/ */
private fun saveEntry() { private fun saveEntry() {
mAttachmentFileBinderManager?.stopUploadAllAttachments() mAttachmentFileBinderManager?.stopUploadAllAttachments()
mEntryEditViewModel.requestEntryInfoUpdate(mDatabase, mEntry, mParent) mEntryEditViewModel.requestEntryInfoUpdate(mDatabase)
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.entry_edit, menu) if (mEntryLoaded) {
menuInflater.inflate(R.menu.entry_edit, menu)
entryEditActivityEducation?.let {
Handler(Looper.getMainLooper()).post {
performedNextEducation(it)
}
}
}
return true return true
} }
override fun onPrepareOptionsMenu(menu: Menu?): Boolean { override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
menu?.findItem(R.id.menu_add_field)?.apply { menu?.findItem(R.id.menu_add_field)?.apply {
isEnabled = mAllowCustomFields isEnabled = mAllowCustomFields
isVisible = isEnabled isVisible = isEnabled
} }
menu?.findItem(R.id.menu_add_attachment)?.apply { menu?.findItem(R.id.menu_add_attachment)?.apply {
// Attachment not compatible below KitKat // Attachment not compatible below KitKat
isEnabled = !mIsTemplate isEnabled = !mIsTemplate
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
isVisible = isEnabled isVisible = isEnabled
} }
menu?.findItem(R.id.menu_add_otp)?.apply { menu?.findItem(R.id.menu_add_otp)?.apply {
// OTP not compatible below KitKat // OTP not compatible below KitKat
isEnabled = mAllowOTP isEnabled = mAllowOTP
@@ -601,14 +565,10 @@ class EntryEditActivity : DatabaseLockActivity(),
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
isVisible = isEnabled isVisible = isEnabled
} }
entryEditActivityEducation?.let {
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
}
return super.onPrepareOptionsMenu(menu) return super.onPrepareOptionsMenu(menu)
} }
fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) { private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
val entryEditFragment = supportFragmentManager.findFragmentById(R.id.entry_edit_content) val entryEditFragment = supportFragmentManager.findFragmentById(R.id.entry_edit_content)
as? EntryEditFragment? as? EntryEditFragment?
@@ -645,7 +605,7 @@ class EntryEditActivity : DatabaseLockActivity(),
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation( && entryEditActivityEducation.checkAndPerformedAttachmentEducation(
attachmentView, attachmentView,
{ {
mExternalFileHelper?.openDocument() addNewAttachment()
}, },
{ {
performedNextEducation(entryEditActivityEducation) performedNextEducation(entryEditActivityEducation)
@@ -746,7 +706,7 @@ class EntryEditActivity : DatabaseLockActivity(),
val intentEntry = Intent() val intentEntry = Intent()
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId) bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
intentEntry.putExtras(bundle) intentEntry.putExtras(bundle)
setResult(ADD_OR_UPDATE_ENTRY_RESULT_CODE, intentEntry) setResult(Activity.RESULT_OK, intentEntry)
super.finish() super.finish()
} catch (e: Exception) { } catch (e: Exception) {
// Exception when parcelable can't be done // Exception when parcelable can't be done
@@ -761,23 +721,46 @@ class EntryEditActivity : DatabaseLockActivity(),
// Keys for current Activity // Keys for current Activity
const val KEY_ENTRY = "entry" const val KEY_ENTRY = "entry"
const val KEY_PARENT = "parent" const val KEY_PARENT = "parent"
// Keys for callback
const val ADD_OR_UPDATE_ENTRY_RESULT_CODE = 31
const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY" const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
fun registerForEntryResult(fragment: Fragment,
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
entryAddedOrUpdatedListener.invoke(
result.data?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY)
)
} else {
entryAddedOrUpdatedListener.invoke(null)
}
}
}
fun registerForEntryResult(activity: FragmentActivity,
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
entryAddedOrUpdatedListener.invoke(
result.data?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY)
)
} else {
entryAddedOrUpdatedListener.invoke(null)
}
}
}
/** /**
* Launch EntryEditActivity to update an existing entry by his [entryId] * Launch EntryEditActivity to update an existing entry by his [entryId]
*/ */
fun launchToUpdate(activity: Activity, fun launchToUpdate(activity: Activity,
database: Database, database: Database,
entryId: NodeId<UUID>) { entryId: NodeId<UUID>,
activityResultLauncher: ActivityResultLauncher<Intent>) {
if (database.loaded && !database.isReadOnly) { if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java) val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId) intent.putExtra(KEY_ENTRY, entryId)
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE) activityResultLauncher.launch(intent)
} }
} }
} }
@@ -787,12 +770,13 @@ class EntryEditActivity : DatabaseLockActivity(),
*/ */
fun launchToCreate(activity: Activity, fun launchToCreate(activity: Activity,
database: Database, database: Database,
groupId: NodeId<*>) { groupId: NodeId<*>,
activityResultLauncher: ActivityResultLauncher<Intent>) {
if (database.loaded && !database.isReadOnly) { if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java) val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId) intent.putExtra(KEY_PARENT, groupId)
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE) activityResultLauncher.launch(intent)
} }
} }
} }
@@ -855,8 +839,9 @@ class EntryEditActivity : DatabaseLockActivity(),
* Launch EntryEditActivity to add a new entry in autofill selection * Launch EntryEditActivity to add a new entry in autofill selection
*/ */
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity, fun launchForAutofillResult(activity: AppCompatActivity,
database: Database, database: Database,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent, autofillComponent: AutofillComponent,
groupId: NodeId<*>, groupId: NodeId<*>,
searchInfo: SearchInfo? = null) { searchInfo: SearchInfo? = null) {
@@ -867,6 +852,7 @@ class EntryEditActivity : DatabaseLockActivity(),
AutofillHelper.startActivityForAutofillResult( AutofillHelper.startActivityForAutofillResult(
activity, activity,
intent, intent,
activityResultLauncher,
autofillComponent, autofillComponent,
searchInfo searchInfo
) )

View File

@@ -31,8 +31,10 @@ 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 androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -85,9 +87,20 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this)
else null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Enabling/disabling MagikeyboardService is normally done by DexModeReceiver, but this
// additional check will allow the keyboard to be reenabled more easily if the app crashes
// or is force quit within DeX mode and then the user leaves DeX mode. Without this, the
// user would need to enter and exit DeX mode once to reenable the service.
MagikeyboardUtil.setEnabled(this, !DexUtil.isDexMode(resources.configuration))
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext) mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
setContentView(R.layout.activity_file_selection) setContentView(R.layout.activity_file_selection)
@@ -103,6 +116,22 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
// Open database button // Open database button
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let {
launchPasswordActivityWithPath(uri)
}
}
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
mDatabaseFileUri = databaseFileCreatedUri
if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment.getInstance(true)
.show(supportFragmentManager, "passwordDialog")
} else {
val error = getString(R.string.error_create_database)
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
Log.e(TAG, error)
}
}
openDatabaseButtonView = findViewById(R.id.open_keyfile_button) openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper) openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
@@ -250,8 +279,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
* Create a new file by calling the content provider * Create a new file by calling the content provider
*/ */
private fun createNewFile() { private fun createNewFile() {
mExternalFileHelper?.createDocument( getString(R.string.database_file_name_default) + mExternalFileHelper?.createDocument(
getString(R.string.database_file_extension_default), "application/x-keepass") getString(R.string.database_file_name_default) +
getString(R.string.database_file_extension_default))
} }
private fun fileNoFoundAction(e: FileNotFoundException) { private fun fileNoFoundAction(e: FileNotFoundException) {
@@ -268,7 +298,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
fileNoFoundAction(exception) fileNoFoundAction(exception)
}, },
{ onCancelSpecialMode() }, { onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() }) { onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher)
} }
private fun launchGroupActivityIfLoaded(database: Database) { private fun launchGroupActivityIfLoaded(database: Database) {
@@ -277,7 +308,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
database, database,
{ onValidateSpecialMode() }, { onValidateSpecialMode() },
{ onCancelSpecialMode() }, { onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() }) { onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher)
} }
} }
@@ -353,33 +385,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {} override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
if (uri != null) {
launchPasswordActivityWithPath(uri)
}
}
// Retrieve the created URI from the file manager
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
mDatabaseFileUri = databaseFileCreatedUri
if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment.getInstance(true)
.show(supportFragmentManager, "passwordDialog")
} else {
val error = getString(R.string.error_create_database)
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
Log.e(TAG, error)
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
@@ -493,11 +498,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
*/ */
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity, fun launchForAutofillResult(activity: AppCompatActivity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent, autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null) { searchInfo: SearchInfo? = null) {
AutofillHelper.startActivityForAutofillResult(activity, AutofillHelper.startActivityForAutofillResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java), Intent(activity, FileDatabaseSelectActivity::class.java),
activityResultLauncher,
autofillComponent, autofillComponent,
searchInfo) searchInfo)
} }

View File

@@ -25,7 +25,7 @@ import android.app.TimePickerDialog
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.PorterDuff
import android.os.* import android.os.*
import android.util.Log import android.util.Log
import android.view.Menu import android.view.Menu
@@ -33,18 +33,23 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.*
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.* import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.fragments.GroupFragment import com.kunzisoft.keepass.activities.fragments.GroupFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.BreadcrumbAdapter
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
@@ -58,6 +63,7 @@ import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
@@ -75,6 +81,7 @@ class GroupActivity : DatabaseLockActivity(),
GroupFragment.NodeClickListener, GroupFragment.NodeClickListener,
GroupFragment.NodesActionMenuListener, GroupFragment.NodesActionMenuListener,
GroupFragment.OnScrollListener, GroupFragment.OnScrollListener,
GroupFragment.GroupRefreshedListener,
SortDialogFragment.SortSelectionListener { SortDialogFragment.SortSelectionListener {
// Views // Views
@@ -82,18 +89,25 @@ class GroupActivity : DatabaseLockActivity(),
private var coordinatorLayout: CoordinatorLayout? = null private var coordinatorLayout: CoordinatorLayout? = null
private var lockView: View? = null private var lockView: View? = null
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
private var databaseNameContainer: ViewGroup? = null
private var databaseColorView: ImageView? = null
private var databaseNameView: TextView? = null
private var searchContainer: ViewGroup? = null
private var searchNumbers: TextView? = null
private var searchString: TextView? = null
private var toolbarBreadcrumb: Toolbar? = null
private var searchTitleView: View? = null private var searchTitleView: View? = null
private var toolbarAction: ToolbarAction? = null private var toolbarAction: ToolbarAction? = null
private var iconView: ImageView? = null
private var numberChildrenView: TextView? = null private var numberChildrenView: TextView? = null
private var addNodeButtonView: AddNodeButtonView? = null private var addNodeButtonView: AddNodeButtonView? = null
private var groupNameView: TextView? = null private var breadcrumbListView: RecyclerView? = null
private var groupMetaView: TextView? = null
private var loadingView: ProgressBar? = null private var loadingView: ProgressBar? = null
private val mGroupViewModel: GroupViewModel by viewModels() private val mGroupViewModel: GroupViewModel by viewModels()
private val mGroupEditViewModel: GroupEditViewModel by viewModels() private val mGroupEditViewModel: GroupEditViewModel by viewModels()
private var mBreadcrumbAdapter: BreadcrumbAdapter? = null
private var mGroupFragment: GroupFragment? = null private var mGroupFragment: GroupFragment? = null
private var mRecyclingBinEnabled = false private var mRecyclingBinEnabled = false
private var mRecyclingBinIsCurrentGroup = false private var mRecyclingBinIsCurrentGroup = false
@@ -111,7 +125,15 @@ class GroupActivity : DatabaseLockActivity(),
private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null
private var mOnSuggestionListener: SearchView.OnSuggestionListener? = null private var mOnSuggestionListener: SearchView.OnSuggestionListener? = null
private var mIconColor: Int = 0 private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon ->
// To create tree dialog for icon
mGroupEditViewModel.selectIcon(icon)
}
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this)
else null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -122,13 +144,18 @@ class GroupActivity : DatabaseLockActivity(),
// Initialize views // Initialize views
rootContainerView = findViewById(R.id.activity_group_container_view) rootContainerView = findViewById(R.id.activity_group_container_view)
coordinatorLayout = findViewById(R.id.group_coordinator) coordinatorLayout = findViewById(R.id.group_coordinator)
iconView = findViewById(R.id.group_icon)
numberChildrenView = findViewById(R.id.group_numbers) numberChildrenView = findViewById(R.id.group_numbers)
addNodeButtonView = findViewById(R.id.add_node_button) addNodeButtonView = findViewById(R.id.add_node_button)
toolbar = findViewById(R.id.toolbar) toolbar = findViewById(R.id.toolbar)
databaseNameContainer = findViewById(R.id.database_name_container)
databaseColorView = findViewById(R.id.database_color)
databaseNameView = findViewById(R.id.database_name)
searchContainer = findViewById(R.id.search_container)
searchNumbers = findViewById(R.id.search_numbers)
searchString = findViewById(R.id.search_string)
toolbarBreadcrumb = findViewById(R.id.toolbar_breadcrumb)
searchTitleView = findViewById(R.id.search_title) searchTitleView = findViewById(R.id.search_title)
groupNameView = findViewById(R.id.group_name) breadcrumbListView = findViewById(R.id.breadcrumb_list)
groupMetaView = findViewById(R.id.group_meta)
toolbarAction = findViewById(R.id.toolbar_action) toolbarAction = findViewById(R.id.toolbar_action)
lockView = findViewById(R.id.lock_button) lockView = findViewById(R.id.lock_button)
loadingView = findViewById(R.id.loading) loadingView = findViewById(R.id.loading)
@@ -140,10 +167,42 @@ class GroupActivity : DatabaseLockActivity(),
toolbar?.title = "" toolbar?.title = ""
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
// Retrieve the textColor to tint the icon mBreadcrumbAdapter = BreadcrumbAdapter(this).apply {
val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse)) // Open group on breadcrumb click
mIconColor = taTextColor.getColor(0, Color.WHITE) onItemClickListener = { node, _ ->
taTextColor.recycle() // If last item & not a virtual root group
val currentGroup = mCurrentGroup
if (currentGroup != null && node == currentGroup
&& (currentGroup != mDatabase?.rootGroup
|| mDatabase?.rootGroupIsVirtual == false)
) {
finishNodeAction()
launchDialogToShowGroupInfo(currentGroup)
} else {
if (mGroupFragment?.nodeActionSelectionMode == true) {
finishNodeAction()
}
mDatabase?.let { database ->
onNodeClick(database, node)
}
}
}
onLongItemClickListener = { node, position ->
val currentGroup = mCurrentGroup
if (currentGroup != null && node == currentGroup
&& (currentGroup != mDatabase?.rootGroup
|| mDatabase?.rootGroupIsVirtual == false)
) {
finishNodeAction()
launchDialogForGroupUpdate(currentGroup)
} else {
onItemClickListener?.invoke(node, position)
}
}
}
breadcrumbListView?.apply {
adapter = mBreadcrumbAdapter
}
// Retrieve group if defined at launch // Retrieve group if defined at launch
manageIntent(intent) manageIntent(intent)
@@ -201,21 +260,22 @@ class GroupActivity : DatabaseLockActivity(),
// Add listeners to the add buttons // Add listeners to the add buttons
addNodeButtonView?.setAddGroupClickListener { addNodeButtonView?.setAddGroupClickListener {
GroupEditDialogFragment.create(GroupInfo().apply { launchDialogForGroupCreation(currentGroup)
if (currentGroup.allowAddNoteInGroup) {
notes = ""
}
}).show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP)
} }
addNodeButtonView?.setAddEntryClickListener { addNodeButtonView?.setAddEntryClickListener {
mDatabase?.let { database -> mDatabase?.let { database ->
EntrySelectionHelper.doSpecialAction(intent, EntrySelectionHelper.doSpecialAction(intent,
{ {
EntryEditActivity.launchToCreate( mCurrentGroup?.nodeId?.let { currentParentGroupId ->
this@GroupActivity, mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
database, EntryEditActivity.launchToCreate(
currentGroup.nodeId this@GroupActivity,
) database,
currentParentGroupId,
resultLauncher
)
}
}
}, },
{ {
// Search not used // Search not used
@@ -243,6 +303,7 @@ class GroupActivity : DatabaseLockActivity(),
EntryEditActivity.launchForAutofillResult( EntryEditActivity.launchForAutofillResult(
this@GroupActivity, this@GroupActivity,
database, database,
mAutofillActivityResultLauncher,
autofillComponent, autofillComponent,
currentGroup.nodeId, currentGroup.nodeId,
searchInfo searchInfo
@@ -266,9 +327,6 @@ class GroupActivity : DatabaseLockActivity(),
} }
} }
assignGroupViewElements(currentGroup)
invalidateOptionsMenu()
loadingView?.hideByFading() loadingView?.hideByFading()
} }
@@ -277,7 +335,7 @@ class GroupActivity : DatabaseLockActivity(),
} }
mGroupEditViewModel.requestIconSelection.observe(this) { iconImage -> mGroupEditViewModel.requestIconSelection.observe(this) { iconImage ->
IconPickerActivity.launch(this@GroupActivity, iconImage) IconPickerActivity.launch(this@GroupActivity, iconImage, mIconSelectionActivityResultLauncher)
} }
mGroupEditViewModel.requestDateTimeSelection.observe(this) { dateInstant -> mGroupEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
@@ -319,6 +377,29 @@ class GroupActivity : DatabaseLockActivity(),
return rootContainerView return rootContainerView
} }
private fun loadGroup(database: Database?) {
when {
Intent.ACTION_SEARCH == intent.action -> {
finishNodeAction()
val searchString =
intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
mGroupViewModel.loadGroupFromSearch(
database,
searchString,
PreferencesUtil.omitBackup(this)
)
}
mCurrentGroupState == null -> {
mRootGroup?.let { rootGroup ->
mGroupViewModel.loadGroup(database, rootGroup, 0)
}
}
else -> {
mGroupViewModel.loadGroup(database, mCurrentGroupState)
}
}
}
override fun onDatabaseRetrieved(database: Database?) { override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
@@ -328,17 +409,23 @@ class GroupActivity : DatabaseLockActivity(),
&& database?.isRecycleBinEnabled == true && database?.isRecycleBinEnabled == true
mRootGroup = database?.rootGroup mRootGroup = database?.rootGroup
if (mCurrentGroupState == null) { loadGroup(database)
mRootGroup?.let { rootGroup ->
mGroupViewModel.loadGroup(database, rootGroup, 0)
}
} else {
mGroupViewModel.loadGroup(database, mCurrentGroupState)
}
// Search suggestion // Search suggestion
database?.let { database?.let {
databaseNameView?.text = if (it.name.isNotEmpty()) it.name else getString(R.string.database)
val customColor = it.customColor
if (customColor != null) {
databaseColorView?.visibility = View.VISIBLE
databaseColorView?.setColorFilter(
customColor,
PorterDuff.Mode.SRC_IN
)
} else {
databaseColorView?.visibility = View.GONE
}
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, it) mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, it)
mBreadcrumbAdapter?.iconDrawableFactory = it.iconDrawableFactory
mOnSuggestionListener = object : SearchView.OnSuggestionListener { mOnSuggestionListener = object : SearchView.OnSuggestionListener {
override fun onSuggestionClick(position: Int): Boolean { override fun onSuggestionClick(position: Int): Boolean {
mSearchSuggestionAdapter?.let { searchAdapter -> mSearchSuggestionAdapter?.let { searchAdapter ->
@@ -413,16 +500,27 @@ class GroupActivity : DatabaseLockActivity(),
) )
} }
} }
ACTION_DATABASE_UPDATE_GROUP_TASK -> {
if (result.isSuccess) {
try {
if (mCurrentGroup == newNodes[0] as Group)
reloadCurrentGroup()
} catch (e: Exception) {
Log.e(
TAG,
"Unable to perform action after group update",
e
)
}
}
}
} }
coordinatorLayout?.showActionErrorIfNeeded(result) coordinatorLayout?.showActionErrorIfNeeded(result)
if (!result.isSuccess) { if (!result.isSuccess) {
reloadCurrentGroup() reloadCurrentGroup()
} }
finishNodeAction() finishNodeAction()
refreshNumberOfChildren(mCurrentGroup)
} }
/** /**
@@ -447,16 +545,7 @@ class GroupActivity : DatabaseLockActivity(),
} }
// To transform KEY_SEARCH_INFO in ACTION_SEARCH // To transform KEY_SEARCH_INFO in ACTION_SEARCH
transformSearchInfoIntent(intent) transformSearchInfoIntent(intent)
if (Intent.ACTION_SEARCH == intent.action) { loadGroup(mDatabase)
finishNodeAction()
val searchString =
intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
mGroupViewModel.loadGroupFromSearch(
mDatabase,
searchString,
PreferencesUtil.omitBackup(this)
)
}
} }
} }
@@ -476,60 +565,44 @@ class GroupActivity : DatabaseLockActivity(),
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
override fun onGroupRefreshed() {
mCurrentGroup?.let { currentGroup ->
assignGroupViewElements(currentGroup)
}
}
private fun assignGroupViewElements(group: Group?) { private fun assignGroupViewElements(group: Group?) {
// Assign title // Assign title
if (group != null) {
if (groupNameView != null) {
val title = group.title
groupNameView?.text = if (title.isNotEmpty()) title else getText(R.string.root)
groupNameView?.invalidate()
}
if (groupMetaView != null) {
val meta = group.nodeId.toString()
groupMetaView?.text = meta
if (meta.isNotEmpty() && PreferencesUtil.showUUID(this)) {
groupMetaView?.visibility = View.VISIBLE
} else {
groupMetaView?.visibility = View.GONE
}
groupMetaView?.invalidate()
}
}
if (group?.isVirtual == true) { if (group?.isVirtual == true) {
searchTitleView?.visibility = View.VISIBLE searchContainer?.visibility = View.VISIBLE
if (toolbar != null) { val title = group.title
toolbar?.navigationIcon = null searchString?.text = if (title.isNotEmpty()) title else ""
} searchNumbers?.text = group.numberOfChildEntries.toString()
iconView?.visibility = View.GONE databaseNameContainer?.visibility = View.GONE
toolbarBreadcrumb?.navigationIcon = null
toolbarBreadcrumb?.collapse()
} else { } else {
searchTitleView?.visibility = View.GONE searchContainer?.visibility = View.GONE
// Assign the group icon depending of IconPack or custom icon databaseNameContainer?.visibility = View.VISIBLE
iconView?.visibility = View.VISIBLE // Refresh breadcrumb
group?.let { currentGroup -> if (toolbarBreadcrumb?.isVisible != true) {
iconView?.let { imageView -> toolbarBreadcrumb?.expand {
mIconDrawableFactory?.assignDatabaseIcon( setBreadcrumbNode(group)
imageView,
currentGroup.icon,
mIconColor
)
}
if (toolbar != null) {
if (group.containsParent())
toolbar?.setNavigationIcon(R.drawable.ic_arrow_up_white_24dp)
else {
toolbar?.navigationIcon = null
}
} }
} else {
// Add breadcrumb
setBreadcrumbNode(group)
} }
} }
// Assign number of children
refreshNumberOfChildren(group)
// Hide button
initAddButton(group) initAddButton(group)
invalidateOptionsMenu()
}
private fun setBreadcrumbNode(group: Group?) {
mBreadcrumbAdapter?.apply {
setNode(group)
breadcrumbListView?.scrollToPosition(itemCount -1)
}
} }
private fun initAddButton(group: Group?) { private fun initAddButton(group: Group?) {
@@ -551,18 +624,6 @@ class GroupActivity : DatabaseLockActivity(),
} }
} }
private fun refreshNumberOfChildren(group: Group?) {
numberChildrenView?.apply {
if (PreferencesUtil.showNumberEntries(context)) {
group?.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context))
text = group?.numberOfChildEntries?.toString() ?: ""
visibility = View.VISIBLE
} else {
visibility = View.GONE
}
}
}
override fun onScrolled(dy: Int) { override fun onScrolled(dy: Int) {
if (actionNodeMode == null) if (actionNodeMode == null)
addNodeButtonView?.hideOrShowButtonOnScrollListener(dy) addNodeButtonView?.hideOrShowButtonOnScrollListener(dy)
@@ -592,11 +653,14 @@ class GroupActivity : DatabaseLockActivity(),
val entryVersioned = node as Entry val entryVersioned = node as Entry
EntrySelectionHelper.doSpecialAction(intent, EntrySelectionHelper.doSpecialAction(intent,
{ {
EntryActivity.launch( mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
this@GroupActivity, EntryActivity.launch(
database, this@GroupActivity,
entryVersioned.nodeId database,
) entryVersioned.nodeId,
resultLauncher
)
}
}, },
{ {
// Nothing here, a search is simply performed // Nothing here, a search is simply performed
@@ -651,6 +715,8 @@ class GroupActivity : DatabaseLockActivity(),
Log.e(TAG, "Node can't be cast in Entry") Log.e(TAG, "Node can't be cast in Entry")
} }
} }
reloadGroupIfSearch()
} }
private fun entrySelectedForSave(database: Database, entry: Entry, searchInfo: SearchInfo) { private fun entrySelectedForSave(database: Database, entry: Entry, searchInfo: SearchInfo) {
@@ -736,6 +802,12 @@ class GroupActivity : DatabaseLockActivity(),
actionNodeMode?.finish() actionNodeMode?.finish()
} }
private fun reloadGroupIfSearch() {
if (Intent.ACTION_SEARCH == intent.action) {
reloadCurrentGroup()
}
}
override fun onNodeSelected( override fun onNodeSelected(
database: Database, database: Database,
nodes: List<Node> nodes: List<Node>
@@ -778,22 +850,42 @@ class GroupActivity : DatabaseLockActivity(),
finishNodeAction() finishNodeAction()
when (node.type) { when (node.type) {
Type.GROUP -> { Type.GROUP -> {
mOldGroupToUpdate = node as Group launchDialogForGroupUpdate(node as Group)
GroupEditDialogFragment.update(mOldGroupToUpdate!!.getGroupInfo()) }
.show( Type.ENTRY -> {
supportFragmentManager, mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
GroupEditDialogFragment.TAG_CREATE_GROUP EntryEditActivity.launchToUpdate(
) this@GroupActivity,
database,
(node as Entry).nodeId,
resultLauncher
)
}
} }
Type.ENTRY -> EntryEditActivity.launchToUpdate(
this@GroupActivity,
database,
(node as Entry).nodeId
)
} }
reloadGroupIfSearch()
return true return true
} }
private fun launchDialogToShowGroupInfo(group: Group) {
GroupDialogFragment.launch(group.getGroupInfo())
.show(supportFragmentManager, GroupDialogFragment.TAG_SHOW_GROUP)
}
private fun launchDialogForGroupCreation(group: Group) {
GroupEditDialogFragment.create(GroupInfo().apply {
if (group.allowAddNoteInGroup) {
notes = ""
}
}).show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP)
}
private fun launchDialogForGroupUpdate(group: Group) {
mOldGroupToUpdate = group
GroupEditDialogFragment.update(group.getGroupInfo())
.show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP)
}
override fun onCopyMenuClick( override fun onCopyMenuClick(
database: Database, database: Database,
nodes: List<Node> nodes: List<Node>
@@ -845,6 +937,7 @@ class GroupActivity : DatabaseLockActivity(),
): Boolean { ): Boolean {
deleteNodes(nodes) deleteNodes(nodes)
finishNodeAction() finishNodeAction()
reloadGroupIfSearch()
return true return true
} }
@@ -932,9 +1025,7 @@ class GroupActivity : DatabaseLockActivity(),
) { ) {
// If no node, show education to add new one // If no node, show education to add new one
val addNodeButtonEducationPerformed = mGroupFragment != null val addNodeButtonEducationPerformed = actionNodeMode == null
&& mGroupFragment!!.isEmpty
&& actionNodeMode == null
&& addNodeButtonView?.addButtonView != null && addNodeButtonView?.addButtonView != null
&& addNodeButtonView!!.isEnable && addNodeButtonView!!.isEnable
&& groupActivityEducation.checkAndPerformedAddNodeButtonEducation( && groupActivityEducation.checkAndPerformedAddNodeButtonEducation(
@@ -974,7 +1065,7 @@ class GroupActivity : DatabaseLockActivity(),
if (!sortMenuEducationPerformed) { if (!sortMenuEducationPerformed) {
// lockMenuEducationPerformed // lockMenuEducationPerformed
val lockButtonView = findViewById<View>(R.id.lock_button_icon) val lockButtonView = findViewById<View>(R.id.lock_button)
lockButtonView != null lockButtonView != null
&& groupActivityEducation.checkAndPerformedLockMenuEducation( && groupActivityEducation.checkAndPerformedLockMenuEducation(
lockButtonView, lockButtonView,
@@ -992,7 +1083,7 @@ class GroupActivity : DatabaseLockActivity(),
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
onBackPressed() // TODO change database
return true return true
} }
R.id.menu_search -> R.id.menu_search ->
@@ -1047,37 +1138,6 @@ class GroupActivity : DatabaseLockActivity(),
} }
} }
override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) {
/*
* ACTION_SEARCH automatically forces a new task. This occurs when you open a kdb file in
* another app such as Files or GoogleDrive and then Search for an entry. Here we remove the
* FLAG_ACTIVITY_NEW_TASK flag bit allowing search to open it's activity in the current task.
*/
if (Intent.ACTION_SEARCH == intent.action) {
var flags = intent.flags
flags = flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv()
intent.flags = flags
}
super.startActivityForResult(intent, requestCode, options)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// To create tree dialog for icon
IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon ->
mGroupEditViewModel.selectIcon(icon)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
// Directly used the onActivityResult in fragment
mGroupFragment?.onActivityResult(requestCode, resultCode, data)
}
private fun removeSearch() { private fun removeSearch() {
intent.removeExtra(AUTO_SEARCH_KEY) intent.removeExtra(AUTO_SEARCH_KEY)
if (Intent.ACTION_SEARCH == intent.action) { if (Intent.ACTION_SEARCH == intent.action) {
@@ -1093,7 +1153,7 @@ class GroupActivity : DatabaseLockActivity(),
try { try {
mGroupViewModel.loadGroup(mDatabase, mCurrentGroupState) mGroupViewModel.loadGroup(mDatabase, mCurrentGroupState)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to rebuild the list after deletion", e) Log.e(TAG, "Unable to rebuild the group", e)
} }
} }
@@ -1282,8 +1342,9 @@ class GroupActivity : DatabaseLockActivity(),
* ------------------------- * -------------------------
*/ */
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity, fun launchForAutofillResult(activity: AppCompatActivity,
database: Database, database: Database,
activityResultLaunch: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent, autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null, searchInfo: SearchInfo? = null,
autoSearch: Boolean = false) { autoSearch: Boolean = false) {
@@ -1293,6 +1354,7 @@ class GroupActivity : DatabaseLockActivity(),
AutofillHelper.startActivityForAutofillResult( AutofillHelper.startActivityForAutofillResult(
activity, activity,
intent, intent,
activityResultLaunch,
autofillComponent, autofillComponent,
searchInfo searchInfo
) )
@@ -1325,11 +1387,12 @@ class GroupActivity : DatabaseLockActivity(),
* Global Launch * Global Launch
* ------------------------- * -------------------------
*/ */
fun launch(activity: Activity, fun launch(activity: AppCompatActivity,
database: Database, database: Database,
onValidateSpecialMode: () -> Unit, onValidateSpecialMode: () -> Unit,
onCancelSpecialMode: () -> Unit, onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit) { onLaunchActivitySpecialMode: () -> Unit,
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
EntrySelectionHelper.doSpecialAction(activity.intent, EntrySelectionHelper.doSpecialAction(activity.intent,
{ {
GroupActivity.launch( GroupActivity.launch(
@@ -1441,6 +1504,7 @@ class GroupActivity : DatabaseLockActivity(),
// Here no search info found, disable auto search // Here no search info found, disable auto search
GroupActivity.launchForAutofillResult(activity, GroupActivity.launchForAutofillResult(activity,
database, database,
autofillActivityResultLauncher,
autofillComponent, autofillComponent,
searchInfo, searchInfo,
false) false)

View File

@@ -27,12 +27,16 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.commit import androidx.fragment.app.commit
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.IconEditDialogFragment
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
@@ -81,6 +85,9 @@ class IconPickerActivity : DatabaseLockActivity() {
coordinatorLayout = findViewById(R.id.icon_picker_coordinator) coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
addCustomIcon(uri)
}
uploadButton = findViewById(R.id.icon_picker_upload) uploadButton = findViewById(R.id.icon_picker_upload)
@@ -139,6 +146,16 @@ class IconPickerActivity : DatabaseLockActivity() {
} }
uploadButton.isEnabled = true uploadButton.isEnabled = true
} }
iconPickerViewModel.customIconUpdated.observe(this) { iconCustomUpdated ->
if (iconCustomUpdated.error && !iconCustomUpdated.errorConsumed) {
Snackbar.make(coordinatorLayout, iconCustomUpdated.errorStringId, Snackbar.LENGTH_LONG).asError().show()
iconCustomUpdated.errorConsumed = true
}
iconCustomUpdated.iconCustom?.let {
mDatabase?.updateCustomIcon(it)
}
iconPickerViewModel.deselectAllCustomIcons()
}
} }
override fun viewToInvalidateTimeout(): View? { override fun viewToInvalidateTimeout(): View? {
@@ -197,6 +214,10 @@ class IconPickerActivity : DatabaseLockActivity() {
} }
override fun onPrepareOptionsMenu(menu: Menu?): Boolean { override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
menu?.findItem(R.id.menu_edit)?.apply {
isEnabled = mIconsSelected.size == 1
isVisible = isEnabled
}
menu?.findItem(R.id.menu_delete)?.apply { menu?.findItem(R.id.menu_delete)?.apply {
isEnabled = mCustomIconsSelectionMode isEnabled = mCustomIconsSelectionMode
isVisible = isEnabled isVisible = isEnabled
@@ -213,6 +234,9 @@ class IconPickerActivity : DatabaseLockActivity() {
onBackPressed() onBackPressed()
} }
} }
R.id.menu_edit -> {
updateCustomIcon(mIconsSelected[0])
}
R.id.menu_delete -> { R.id.menu_delete -> {
mIconsSelected.forEach { iconToRemove -> mIconsSelected.forEach { iconToRemove ->
removeCustomIcon(iconToRemove) removeCustomIcon(iconToRemove)
@@ -277,6 +301,11 @@ class IconPickerActivity : DatabaseLockActivity() {
} }
} }
private fun updateCustomIcon(iconImageCustom: IconImageCustom) {
IconEditDialogFragment.update(iconImageCustom)
.show(supportFragmentManager, IconEditDialogFragment.TAG_UPDATE_ICON)
}
private fun removeCustomIcon(iconImageCustom: IconImageCustom) { private fun removeCustomIcon(iconImageCustom: IconImageCustom) {
uploadButton.isEnabled = false uploadButton.isEnabled = false
iconPickerViewModel.deselectAllCustomIcons() iconPickerViewModel.deselectAllCustomIcons()
@@ -286,14 +315,6 @@ class IconPickerActivity : DatabaseLockActivity() {
) )
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
addCustomIcon(uri)
}
}
private fun setResult() { private fun setResult() {
setResult(Activity.RESULT_OK, Intent().apply { setResult(Activity.RESULT_OK, Intent().apply {
putExtra(EXTRA_ICON, mIconImage) putExtra(EXTRA_ICON, mIconImage)
@@ -308,30 +329,28 @@ class IconPickerActivity : DatabaseLockActivity() {
companion object { companion object {
private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG" private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG"
private const val ICON_SELECTED_REQUEST = 15861
private const val EXTRA_ICON = "EXTRA_ICON" private const val EXTRA_ICON = "EXTRA_ICON"
private const val MAX_ICON_SIZE = 5242880 private const val MAX_ICON_SIZE = 5242880
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, listener: (icon: IconImage) -> Unit) { fun registerIconSelectionForResult(context: FragmentActivity,
if (requestCode == ICON_SELECTED_REQUEST) { listener: (icon: IconImage) -> Unit): ActivityResultLauncher<Intent> {
if (resultCode == Activity.RESULT_OK) { return context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
listener.invoke(data?.getParcelableExtra(EXTRA_ICON) ?: IconImage()) if (result.resultCode == Activity.RESULT_OK) {
listener.invoke(result.data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
} }
} }
} }
fun launch(context: Activity, fun launch(context: FragmentActivity,
previousIcon: IconImage?) { previousIcon: IconImage?,
resultLauncher: ActivityResultLauncher<Intent>) {
// Create an instance to return the picker icon // Create an instance to return the picker icon
context.startActivityForResult( resultLauncher.launch(
Intent(context, Intent(context, IconPickerActivity::class.java).apply {
IconPickerActivity::class.java).apply {
if (previousIcon != null) if (previousIcon != null)
putExtra(EXTRA_ICON, previousIcon) putExtra(EXTRA_ICON, previousIcon)
}, }
ICON_SELECTED_REQUEST) )
} }
} }
} }

View File

@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.activities
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
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
@@ -35,12 +34,12 @@ import android.view.KeyEvent.KEYCODE_ENTER
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.* import android.widget.*
import android.widget.TextView.OnEditorActionListener import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ActivityCompat
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
@@ -71,11 +70,12 @@ import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import java.io.FileNotFoundException import java.io.FileNotFoundException
open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener { class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
// Views // Views
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
@@ -89,7 +89,8 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
private lateinit var coordinatorLayout: CoordinatorLayout private lateinit var coordinatorLayout: CoordinatorLayout
private var advancedUnlockFragment: AdvancedUnlockFragment? = null private var advancedUnlockFragment: AdvancedUnlockFragment? = null
private val databaseFileViewModel: DatabaseFileViewModel by viewModels() private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels()
private var mDefaultDatabase: Boolean = false private var mDefaultDatabase: Boolean = false
private var mDatabaseFileUri: Uri? = null private var mDatabaseFileUri: Uri? = null
@@ -98,20 +99,13 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
private var mRememberKeyFile: Boolean = false private var mRememberKeyFile: Boolean = false
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var mPermissionAsked = false
private var mReadOnly: Boolean = false private var mReadOnly: Boolean = false
private var mForceReadOnly: Boolean = false private var mForceReadOnly: Boolean = false
set(value) {
infoContainerView?.visibility = if (value) {
mReadOnly = true
View.VISIBLE
} else {
View.GONE
}
field = value
}
private var mAllowAutoOpenBiometricPrompt: Boolean = true private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this)
else null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -133,7 +127,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
infoContainerView = findViewById(R.id.activity_password_info_container) infoContainerView = findViewById(R.id.activity_password_info_container)
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout) coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) { mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
savedInstanceState.getBoolean(KEY_READ_ONLY) savedInstanceState.getBoolean(KEY_READ_ONLY)
} else { } else {
@@ -142,6 +135,12 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this) mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity) mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) {
mDatabaseKeyFileUri = uri
populateKeyFileTextView(uri)
}
}
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper) keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
passwordView?.setOnEditorActionListener(onEditorActionListener) passwordView?.setOnEditorActionListener(onEditorActionListener)
@@ -170,9 +169,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) { if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE)) mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
} }
if (savedInstanceState?.containsKey(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) == true) {
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
}
// Init Biometric elements // Init Biometric elements
advancedUnlockFragment = supportFragmentManager advancedUnlockFragment = supportFragmentManager
@@ -188,21 +184,30 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
// Listen password checkbox to init advanced unlock and confirmation button // Listen password checkbox to init advanced unlock and confirmation button
checkboxPasswordView?.setOnCheckedChangeListener { _, _ -> checkboxPasswordView?.setOnCheckedChangeListener { _, _ ->
advancedUnlockFragment?.checkUnlockAvailability() mAdvancedUnlockViewModel.checkUnlockAvailability()
enableOrNotTheConfirmationButton() enableOrNotTheConfirmationButton()
} }
// Observe if default database // Observe if default database
databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase -> mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
mDefaultDatabase = isDefaultDatabase mDefaultDatabase = isDefaultDatabase
} }
// Observe database file change // Observe database file change
databaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile -> mDatabaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
// Force read only if the file does not exists // Force read only if the file does not exists
mForceReadOnly = databaseFile?.let { val databaseFileNotExists = databaseFile?.let {
!it.databaseFileExists !it.databaseFileExists
} ?: true } ?: true
infoContainerView?.visibility = if (databaseFileNotExists) {
mReadOnly = true
View.VISIBLE
} else {
View.GONE
}
mForceReadOnly = databaseFileNotExists
invalidateOptionsMenu() invalidateOptionsMenu()
// Post init uri with KeyFile only if needed // Post init uri with KeyFile only if needed
@@ -232,15 +237,13 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
} }
// Don't allow auto open prompt if lock become when UI visible // Don't allow auto open prompt if lock become when UI visible
mAllowAutoOpenBiometricPrompt = if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) {
false mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
else
mAllowAutoOpenBiometricPrompt
mDatabaseFileUri?.let { databaseFileUri ->
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
} }
checkPermission() mDatabaseFileUri?.let { databaseFileUri ->
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
}
mDatabase?.let { database -> mDatabase?.let { database ->
launchGroupActivityIfLoaded(database) launchGroupActivityIfLoaded(database)
@@ -263,7 +266,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
when (actionTask) { when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> { ACTION_DATABASE_LOAD_TASK -> {
// Recheck advanced unlock if error // Recheck advanced unlock if error
advancedUnlockFragment?.initAdvancedUnlockMode() mAdvancedUnlockViewModel.initAdvancedUnlockMode()
if (result.isSuccess) { if (result.isSuccess) {
launchGroupActivityIfLoaded(database) launchGroupActivityIfLoaded(database)
@@ -311,7 +314,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
is FileNotFoundDatabaseException -> { is FileNotFoundDatabaseException -> {
// Remove this default database inaccessible // Remove this default database inaccessible
if (mDefaultDatabase) { if (mDefaultDatabase) {
databaseFileViewModel.removeDefaultDatabase() mDatabaseFileViewModel.removeDefaultDatabase()
} }
} }
} }
@@ -344,7 +347,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE) mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
} }
mDatabaseFileUri?.let { mDatabaseFileUri?.let {
databaseFileViewModel.checkIfIsDefaultDatabase(it) mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
} }
} }
@@ -361,7 +364,8 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
database, database,
{ onValidateSpecialMode() }, { onValidateSpecialMode() },
{ onCancelSpecialMode() }, { onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() } { onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher
) )
} }
} }
@@ -435,8 +439,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
verifyCheckboxesAndLoadDatabase(password, keyFileUri) verifyCheckboxesAndLoadDatabase(password, keyFileUri)
} else { } else {
// Init Biometric elements // Init Biometric elements
advancedUnlockFragment?.loadDatabase(databaseFileUri, mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
mAllowAutoOpenBiometricPrompt)
} }
enableOrNotTheConfirmationButton() enableOrNotTheConfirmationButton()
@@ -496,18 +499,15 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
override fun onPause() { override fun onPause() {
// Reinit locking activity UI variable // Reinit locking activity UI variable
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
mAllowAutoOpenBiometricPrompt = true
super.onPause() super.onPause()
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
mDatabaseKeyFileUri?.let { mDatabaseKeyFileUri?.let {
outState.putString(KEY_KEYFILE, it.toString()) outState.putString(KEY_KEYFILE, it.toString())
} }
outState.putBoolean(KEY_READ_ONLY, mReadOnly) outState.putBoolean(KEY_READ_ONLY, mReadOnly)
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
@@ -606,35 +606,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
return true return true
} }
// Check permission
private fun checkPermission() {
if (Build.VERSION.SDK_INT in 23..28
&& !mReadOnly
&& !mPermissionAsked) {
mPermissionAsked = true
// Check self permission to show or not the dialog
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
val permissions = arrayOf(writePermission)
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) { private fun launchEducation(menu: Menu) {
@@ -709,45 +680,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mAllowAutoOpenBiometricPrompt = false
// To get device credential unlock result
advancedUnlockFragment?.onActivityResult(requestCode, resultCode, data)
// To get entry in result
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
var keyFileResult = false
mExternalFileHelper?.let {
keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
if (uri != null) {
mDatabaseKeyFileUri = uri
populateKeyFileTextView(uri)
}
}
}
if (!keyFileResult) {
// this block if not a key file response
when (resultCode) {
DatabaseLockActivity.RESULT_EXIT_LOCK -> {
clearCredentialsViews()
closeDatabase()
}
Activity.RESULT_CANCELED -> {
clearCredentialsViews()
}
}
}
}
companion object { companion object {
private val TAG = PasswordActivity::class.java.name private val TAG = PasswordActivity::class.java.name
@@ -761,10 +693,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
private const val KEY_READ_ONLY = "KEY_READ_ONLY" private const val KEY_READ_ONLY = "KEY_READ_ONLY"
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 const val ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT = "ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT"
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?, private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
intentBuildLauncher: (Intent) -> Unit) { intentBuildLauncher: (Intent) -> Unit) {
@@ -855,15 +783,17 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
@Throws(FileNotFoundException::class) @Throws(FileNotFoundException::class)
fun launchForAutofillResult(activity: Activity, fun launchForAutofillResult(activity: AppCompatActivity,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?, keyFile: Uri?,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent, autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) { searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
AutofillHelper.startActivityForAutofillResult( AutofillHelper.startActivityForAutofillResult(
activity, activity,
intent, intent,
activityResultLauncher,
autofillComponent, autofillComponent,
searchInfo) searchInfo)
} }
@@ -891,12 +821,13 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
* Global Launch * Global Launch
* ------------------------- * -------------------------
*/ */
fun launch(activity: Activity, fun launch(activity: AppCompatActivity,
databaseUri: Uri, databaseUri: Uri,
keyFile: Uri?, keyFile: Uri?,
fileNoFoundAction: (exception: FileNotFoundException) -> Unit, fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
onCancelSpecialMode: () -> Unit, onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit) { onLaunchActivitySpecialMode: () -> Unit,
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
try { try {
EntrySelectionHelper.doSpecialAction(activity.intent, EntrySelectionHelper.doSpecialAction(activity.intent,
@@ -926,6 +857,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PasswordActivity.launchForAutofillResult(activity, PasswordActivity.launchForAutofillResult(activity,
databaseUri, keyFile, databaseUri, keyFile,
autofillActivityResultLauncher,
autofillComponent, autofillComponent,
searchInfo) searchInfo)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()

View File

@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
@@ -133,6 +132,18 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection) keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let { pathUri ->
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
keyFileSelectionView?.error = null
keyFileCheckBox?.isChecked = true
keyFileSelectionView?.uri = pathUri
if (lengthFile <= 0L) {
showEmptyKeyFileConfirmationDialog()
}
}
}
}
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper) keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
val dialog = builder.create() val dialog = builder.create()
@@ -208,7 +219,11 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match) passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
} }
if (mMasterPassword == null || mMasterPassword!!.isEmpty()) { if ((mMasterPassword == null
|| mMasterPassword!!.isEmpty())
&& (keyFileCheckBox == null
|| !keyFileCheckBox!!.isChecked
|| keyFileSelectionView?.uri == null)) {
error = true error = true
showEmptyPasswordConfirmationDialog() showEmptyPasswordConfirmationDialog()
} }
@@ -282,23 +297,6 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
uri?.let { pathUri ->
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
keyFileSelectionView?.error = null
keyFileCheckBox?.isChecked = true
keyFileSelectionView?.uri = pathUri
if (lengthFile <= 0L) {
showEmptyKeyFileConfirmationDialog()
}
}
}
}
}
companion object { companion object {
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG" private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"

View File

@@ -0,0 +1,95 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.graphics.Color
import android.os.Bundle
import android.widget.CompoundButton
import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import com.kunzisoft.androidclearchroma.view.ChromaColorView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
class ColorPickerDialogFragment : DatabaseDialogFragment() {
private val mColorPickerViewModel: ColorPickerViewModel by activityViewModels()
private lateinit var enableSwitchView: CompoundButton
private lateinit var chromaColorView: ChromaColorView
private var mDefaultColor = Color.WHITE
private var mActivated = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_color_picker, null)
enableSwitchView = root.findViewById(R.id.switch_element)
chromaColorView = root.findViewById(R.id.chroma_color_view)
if (savedInstanceState != null) {
if (savedInstanceState.containsKey(ARG_INITIAL_COLOR)) {
mDefaultColor = savedInstanceState.getInt(ARG_INITIAL_COLOR)
}
if (savedInstanceState.containsKey(ARG_ACTIVATED)) {
mActivated = savedInstanceState.getBoolean(ARG_ACTIVATED)
}
} else {
arguments?.apply {
if (containsKey(ARG_INITIAL_COLOR)) {
mDefaultColor = getInt(ARG_INITIAL_COLOR)
}
if (containsKey(ARG_ACTIVATED)) {
mActivated = getBoolean(ARG_ACTIVATED)
}
}
}
enableSwitchView.isChecked = mActivated
chromaColorView.currentColor = mDefaultColor
chromaColorView.setOnColorChangedListener {
if (!enableSwitchView.isChecked)
enableSwitchView.isChecked = true
}
val builder = AlertDialog.Builder(activity)
builder.setView(root)
.setPositiveButton(android.R.string.ok) { _, _ ->
val color: Int? = if (enableSwitchView.isChecked)
chromaColorView.currentColor
else
null
mColorPickerViewModel.pickColor(color)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
// Do nothing
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(ARG_INITIAL_COLOR, chromaColorView.currentColor)
outState.putBoolean(ARG_ACTIVATED, mActivated)
}
companion object {
private const val ARG_INITIAL_COLOR = "ARG_INITIAL_COLOR"
private const val ARG_ACTIVATED = "ARG_ACTIVATED"
fun newInstance(
@ColorInt initialColor: Int?,
): ColorPickerDialogFragment {
return ColorPickerDialogFragment().apply {
arguments = Bundle().apply {
putInt(ARG_INITIAL_COLOR, initialColor ?: Color.WHITE)
putBoolean(ARG_ACTIVATED, initialColor != null)
}
}
}
}
}

View File

@@ -29,6 +29,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
} }
} }
@Suppress("DEPRECATION")
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.view.DateTimeFieldView
class GroupDialogFragment : DatabaseDialogFragment() {
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
private var mGroupInfo = GroupInfo()
private lateinit var iconView: ImageView
private var mIconColor: Int = 0
private lateinit var nameTextView: TextView
private lateinit var notesTextLabelView: TextView
private lateinit var notesTextView: TextView
private lateinit var expirationView: DateTimeFieldView
private lateinit var creationView: TextView
private lateinit var modificationView: TextView
private lateinit var uuidContainerView: ViewGroup
private lateinit var uuidReferenceView: TextView
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
}
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_group, null)
iconView = root.findViewById(R.id.group_icon)
nameTextView = root.findViewById(R.id.group_name)
notesTextLabelView = root.findViewById(R.id.group_note_label)
notesTextView = root.findViewById(R.id.group_note)
expirationView = root.findViewById(R.id.group_expiration)
creationView = root.findViewById(R.id.group_created)
modificationView = root.findViewById(R.id.group_modified)
uuidContainerView = root.findViewById(R.id.group_UUID_container)
uuidReferenceView = root.findViewById(R.id.group_UUID_reference)
// Retrieve the textColor to tint the icon
val ta = activity.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
mIconColor = ta.getColor(0, Color.WHITE)
ta.recycle()
if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
} else {
arguments?.apply {
if (containsKey(KEY_GROUP_INFO)) {
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
}
}
}
// populate info in views
val title = mGroupInfo.title
if (title.isEmpty()) {
nameTextView.visibility = View.GONE
} else {
nameTextView.text = title
nameTextView.visibility = View.VISIBLE
}
val notes = mGroupInfo.notes
if (notes == null || notes.isEmpty()) {
notesTextLabelView.visibility = View.GONE
notesTextView.visibility = View.GONE
} else {
notesTextView.text = notes
notesTextLabelView.visibility = View.VISIBLE
notesTextView.visibility = View.VISIBLE
}
expirationView.activation = mGroupInfo.expires
expirationView.dateTime = mGroupInfo.expiryTime
creationView.text = mGroupInfo.creationTime.getDateTimeString(resources)
modificationView.text = mGroupInfo.lastModificationTime.getDateTimeString(resources)
val uuid = UuidUtil.toHexString(mGroupInfo.id)
if (uuid == null || uuid.isEmpty()) {
uuidContainerView.visibility = View.GONE
} else {
uuidReferenceView.text = uuid
uuidContainerView.apply {
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
}
}
val builder = AlertDialog.Builder(activity)
builder.setView(root)
.setPositiveButton(android.R.string.ok){ _, _ ->
// Do nothing
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
super.onSaveInstanceState(outState)
}
data class Error(val isError: Boolean, val messageId: Int?)
companion object {
const val TAG_SHOW_GROUP = "TAG_SHOW_GROUP"
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
fun launch(groupInfo: GroupInfo): GroupDialogFragment {
val bundle = Bundle()
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
val fragment = GroupDialogFragment()
fragment.arguments = bundle
return fragment
}
}
}

View File

@@ -246,8 +246,8 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
companion object { companion object {
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP" const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
const val KEY_ACTION_ID = "KEY_ACTION_ID" private const val KEY_ACTION_ID = "KEY_ACTION_ID"
const val KEY_GROUP_INFO = "KEY_GROUP_INFO" private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
fun create(groupInfo: GroupInfo): GroupEditDialogFragment { fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
val bundle = Bundle() val bundle = Bundle()

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.os.Bundle
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
class IconEditDialogFragment : DatabaseDialogFragment() {
private val mIconPickerViewModel: IconPickerViewModel by activityViewModels()
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
private lateinit var iconView: ImageView
private lateinit var nameTextLayoutView: TextInputLayout
private lateinit var nameTextView: TextView
private var mCustomIcon: IconImageCustom? = null
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
}
mCustomIcon?.let { customIcon ->
populateViewsWithCustomIcon(customIcon)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_edit, null)
iconView = root.findViewById(R.id.icon_edit_image)
nameTextLayoutView = root.findViewById(R.id.icon_edit_name_container)
nameTextView = root.findViewById(R.id.icon_edit_name)
if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_CUSTOM_ICON_ID)) {
mCustomIcon = savedInstanceState.getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
} else {
arguments?.apply {
if (containsKey(KEY_CUSTOM_ICON_ID)) {
mCustomIcon = getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
}
}
}
val builder = AlertDialog.Builder(activity)
builder.setView(root)
.setPositiveButton(android.R.string.ok) { _, _ ->
retrieveIconInfoFromViews()
mCustomIcon?.let { customIcon ->
mIconPickerViewModel.updateCustomIcon(
IconPickerViewModel.IconCustomState(customIcon, false)
)
}
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
// Do nothing
mIconPickerViewModel.updateCustomIcon(
IconPickerViewModel.IconCustomState(null, false)
)
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun populateViewsWithCustomIcon(customIcon: IconImageCustom) {
mPopulateIconMethod?.invoke(iconView, customIcon.getIconImageToDraw())
nameTextView.text = customIcon.name
}
private fun retrieveIconInfoFromViews() {
mCustomIcon?.name = nameTextView.text.toString()
mCustomIcon?.lastModificationTime = DateInstant()
}
override fun onSaveInstanceState(outState: Bundle) {
retrieveIconInfoFromViews()
outState.putParcelable(KEY_CUSTOM_ICON_ID, mCustomIcon)
super.onSaveInstanceState(outState)
}
companion object {
const val TAG_UPDATE_ICON = "TAG_UPDATE_ICON"
const val KEY_CUSTOM_ICON_ID = "KEY_CUSTOM_ICON_ID"
fun update(customIcon: IconImageCustom): IconEditDialogFragment {
val bundle = Bundle()
bundle.putParcelable(KEY_CUSTOM_ICON_ID, IconImageCustom(customIcon))
val fragment = IconEditDialogFragment()
fragment.arguments = bundle
return fragment
}
}
}

View File

@@ -309,7 +309,7 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
s?.toString()?.let { userString -> s?.toString()?.let { userString ->
try { try {
mOtpElement.setBase32Secret(userString.toUpperCase(Locale.ENGLISH)) mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
otpSecretContainer?.error = null otpSecretContainer?.error = null
} catch (exception: Exception) { } catch (exception: Exception) {
otpSecretContainer?.error = getString(R.string.error_otp_secret_key) otpSecretContainer?.error = getString(R.string.error_otp_secret_key)

View File

@@ -35,6 +35,7 @@ import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.model.AttachmentState import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
@@ -55,6 +56,7 @@ class EntryEditFragment: DatabaseFragment() {
private lateinit var attachmentsListView: RecyclerView private lateinit var attachmentsListView: RecyclerView
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
private var mTemplate: Template? = null
private var mAllowMultipleAttachments: Boolean = false private var mAllowMultipleAttachments: Boolean = false
private var mIconColor: Int = 0 private var mIconColor: Int = 0
@@ -97,6 +99,12 @@ class EntryEditFragment: DatabaseFragment() {
setOnIconClickListener { setOnIconClickListener {
mEntryEditViewModel.requestIconSelection(templateView.getIcon()) mEntryEditViewModel.requestIconSelection(templateView.getIcon())
} }
setOnBackgroundColorClickListener {
mEntryEditViewModel.requestBackgroundColorSelection(templateView.getBackgroundColor())
}
setOnForegroundColorClickListener {
mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor())
}
setOnCustomEditionActionClickListener { field -> setOnCustomEditionActionClickListener { field ->
mEntryEditViewModel.requestCustomFieldEdition(field) mEntryEditViewModel.requestCustomFieldEdition(field)
} }
@@ -115,19 +123,26 @@ class EntryEditFragment: DatabaseFragment() {
} }
mEntryEditViewModel.onTemplateChanged.observe(viewLifecycleOwner) { template -> mEntryEditViewModel.onTemplateChanged.observe(viewLifecycleOwner) { template ->
this.mTemplate = template
templateView.setTemplate(template) templateView.setTemplate(template)
} }
mEntryEditViewModel.templatesEntry.observe(viewLifecycleOwner) { templateEntry -> mEntryEditViewModel.templatesEntry.observe(viewLifecycleOwner) { templateEntry ->
templateView.setTemplate(templateEntry.defaultTemplate) if (templateEntry != null) {
// Load entry info only the first time to keep change locally val selectedTemplate = if (mTemplate != null)
if (savedInstanceState == null) { mTemplate
assignEntryInfo(templateEntry.entryInfo) else
templateEntry.defaultTemplate
templateView.setTemplate(selectedTemplate)
// Load entry info only the first time to keep change locally
if (savedInstanceState == null) {
assignEntryInfo(templateEntry.entryInfo)
}
// To prevent flickering
rootView.showByFading()
// Apply timeout reset
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
} }
// To prevent flickering
rootView.showByFading()
// Apply timeout reset
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
} }
mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) { mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) {
@@ -138,6 +153,14 @@ class EntryEditFragment: DatabaseFragment() {
templateView.setIcon(iconImage) templateView.setIcon(iconImage)
} }
mEntryEditViewModel.onBackgroundColorSelected.observe(this) { color ->
templateView.setBackgroundColor(color)
}
mEntryEditViewModel.onForegroundColorSelected.observe(this) { color ->
templateView.setForegroundColor(color)
}
mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField -> mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField ->
templateView.setPasswordField(passwordField) templateView.setPasswordField(passwordField)
} }

View File

@@ -42,7 +42,6 @@ class EntryFragment: DatabaseFragment() {
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
private lateinit var uuidContainerView: View private lateinit var uuidContainerView: View
private lateinit var uuidView: TextView
private lateinit var uuidReferenceView: TextView private lateinit var uuidReferenceView: TextView
private var mClipboardHelper: ClipboardHelper? = null private var mClipboardHelper: ClipboardHelper? = null
@@ -88,18 +87,16 @@ class EntryFragment: DatabaseFragment() {
uuidContainerView.apply { uuidContainerView.apply {
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
} }
uuidView = view.findViewById(R.id.entry_UUID)
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference) uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
mEntryViewModel.template.observe(viewLifecycleOwner) { template -> mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory ->
templateView.setTemplate(template) if (entryInfoHistory != null) {
} templateView.setTemplate(entryInfoHistory.template)
assignEntryInfo(entryInfoHistory.entryInfo)
mEntryViewModel.entryInfo.observe(viewLifecycleOwner) { entryInfo -> // Smooth appearing
assignEntryInfo(entryInfo) rootView.showByFading()
// Smooth appearing resetAppTimeoutWhenViewFocusedOrChanged(rootView)
rootView.showByFading() }
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
} }
mEntryViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState -> mEntryViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->
@@ -201,7 +198,6 @@ class EntryFragment: DatabaseFragment() {
} }
private fun assignUUID(uuid: UUID?) { private fun assignUUID(uuid: UUID?) {
uuidView.text = uuid?.toString()
uuidReferenceView.text = UuidUtil.toHexString(uuid) uuidReferenceView.text = UuidUtil.toHexString(uuid)
} }

View File

@@ -55,9 +55,11 @@ class EntryHistoryFragment: StylishFragment() {
* History * History
* ------------- * -------------
*/ */
private fun assignHistory(history: List<EntryInfo>) { private fun assignHistory(history: List<EntryInfo>?) {
historyAdapter?.clear() historyAdapter?.clear()
historyAdapter?.entryHistoryList?.addAll(history) history?.let {
historyAdapter?.entryHistoryList?.addAll(history)
}
historyAdapter?.onItemClickListener = { item, position -> historyAdapter?.onItemClickListener = { item, position ->
mEntryViewModel.onHistorySelected(item, position) mEntryViewModel.onHistorySelected(item, position)
} }

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.activities.fragments package com.kunzisoft.keepass.activities.fragments
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.* import android.view.*
@@ -34,12 +33,11 @@ import com.kunzisoft.keepass.activities.EntryEditActivity
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.adapters.NodeAdapter import com.kunzisoft.keepass.adapters.NodesAdapter
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum import com.kunzisoft.keepass.database.element.SortNodeEnum
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.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -50,10 +48,11 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
private var nodeClickListener: NodeClickListener? = null private var nodeClickListener: NodeClickListener? = null
private var onScrollListener: OnScrollListener? = null private var onScrollListener: OnScrollListener? = null
private var groupRefreshed: GroupRefreshedListener? = null
private var mNodesRecyclerView: RecyclerView? = null private var mNodesRecyclerView: RecyclerView? = null
private var mLayoutManager: LinearLayoutManager? = null private var mLayoutManager: LinearLayoutManager? = null
private var mAdapter: NodeAdapter? = null private var mAdapter: NodesAdapter? = null
private val mGroupViewModel: GroupViewModel by activityViewModels() private val mGroupViewModel: GroupViewModel by activityViewModels()
@@ -74,8 +73,18 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
private var mRecycleBinEnable: Boolean = false private var mRecycleBinEnable: Boolean = false
private var mRecycleBin: Group? = null private var mRecycleBin: Group? = null
val isEmpty: Boolean var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) { entryId ->
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0 entryId?.let {
// Simply refresh the list
rebuildList()
// Scroll to the new entry
mDatabase?.getEntryById(it)?.let { entry ->
mAdapter?.indexOf(entry)?.let { position ->
mNodesRecyclerView?.scrollToPosition(position)
}
}
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
}
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() { private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
@@ -92,12 +101,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
// TODO Change to ViewModel
try { try {
nodeClickListener = context as NodeClickListener nodeClickListener = context as NodeClickListener
} catch (e: ClassCastException) { } catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception // The activity doesn't implement the interface, throw exception
throw ClassCastException(context.toString() throw ClassCastException(context.toString()
+ " must implement " + NodeAdapter.NodeClickCallback::class.java.name) + " must implement " + NodesAdapter.NodeClickCallback::class.java.name)
} }
try { try {
@@ -105,14 +116,24 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
} catch (e: ClassCastException) { } catch (e: ClassCastException) {
onScrollListener = null onScrollListener = null
// Context menu can be omit // Context menu can be omit
Log.w(TAG, context.toString() Log.w(
TAG, context.toString()
+ " must implement " + RecyclerView.OnScrollListener::class.java.name) + " must implement " + RecyclerView.OnScrollListener::class.java.name)
} }
try {
groupRefreshed = context as GroupRefreshedListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context.toString()
+ " must implement " + GroupRefreshedListener::class.java.name)
}
} }
override fun onDetach() { override fun onDetach() {
nodeClickListener = null nodeClickListener = null
onScrollListener = null onScrollListener = null
groupRefreshed = null
super.onDetach() super.onDetach()
} }
@@ -128,8 +149,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
contextThemed?.let { context -> contextThemed?.let { context ->
database?.let { database -> database?.let { database ->
mAdapter = NodeAdapter(context, database).apply { mAdapter = NodesAdapter(context, database).apply {
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback { setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
override fun onNodeClick(database: Database, node: Node) { override fun onNodeClick(database: Database, node: Node) {
if (nodeActionSelectionMode) { if (nodeActionSelectionMode) {
if (listActionNodes.contains(node)) { if (listActionNodes.contains(node)) {
@@ -185,7 +206,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
// To apply theme // To apply theme
return inflater.cloneInContext(contextThemed) return inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_group, container, false) .inflate(R.layout.fragment_nodes, container, false)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -233,7 +254,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
return mLayoutManager?.findFirstVisibleItemPosition() ?: 0 return mLayoutManager?.findFirstVisibleItemPosition() ?: 0
} }
@Throws(IllegalArgumentException::class)
private fun rebuildList() { private fun rebuildList() {
try { try {
// Add elements to the list // Add elements to the list
@@ -251,6 +271,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
} else { } else {
notFoundView?.visibility = View.GONE notFoundView?.visibility = View.GONE
} }
groupRefreshed?.onGroupRefreshed()
} }
override fun onSortSelected(sortNodeEnum: SortNodeEnum, override fun onSortSelected(sortNodeEnum: SortNodeEnum,
@@ -283,15 +305,17 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
val sortDialogFragment: SortDialogFragment = val sortDialogFragment: SortDialogFragment =
if (mRecycleBinEnable) { if (mRecycleBinEnable) {
SortDialogFragment.getInstance( SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context), PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context), PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context), PreferencesUtil.getGroupsBeforeSort(context),
PreferencesUtil.getRecycleBinBottomSort(context)) PreferencesUtil.getRecycleBinBottomSort(context)
)
} else { } else {
SortDialogFragment.getInstance( SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context), PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context), PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context)) PreferencesUtil.getGroupsBeforeSort(context)
)
} }
sortDialogFragment.show(childFragmentManager, "sortDialog") sortDialogFragment.show(childFragmentManager, "sortDialog")
@@ -403,27 +427,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
if (resultCode == EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE) {
data?.getParcelableExtra<NodeId<UUID>>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let {
// Simply refresh the list
rebuildList()
// Scroll to the new entry
mDatabase?.getEntryById(it)?.let { entry ->
mAdapter?.indexOf(entry)?.let { position ->
mNodesRecyclerView?.scrollToPosition(position)
}
}
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
}
}
}
}
/** /**
* Callback listener to redefine to do an action when a node is click * Callback listener to redefine to do an action when a node is click
*/ */
@@ -459,6 +462,10 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
fun onScrolled(dy: Int) fun onScrolled(dy: Int)
} }
interface GroupRefreshedListener {
fun onGroupRefreshed()
}
companion object { companion object {
private val TAG = GroupFragment::class.java.name private val TAG = GroupFragment::class.java.name
} }

View File

@@ -55,8 +55,10 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
iconCustomAdded?.iconCustom?.let { icon -> iconCustomAdded?.iconCustom?.let { icon ->
iconPickerAdapter.addIcon(icon) iconPickerAdapter.addIcon(icon)
iconCustomAdded.iconCustom = null iconCustomAdded.iconCustom = null
try {
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
} catch (ignore: Exception) {}
} }
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
} }
} }
iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved -> iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved ->
@@ -67,6 +69,14 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
} }
} }
} }
iconPickerViewModel.customIconUpdated.observe(viewLifecycleOwner) { iconCustomUpdated ->
if (!iconCustomUpdated.error) {
iconCustomUpdated?.iconCustom?.let { icon ->
iconPickerAdapter.updateIcon(icon)
iconCustomUpdated.iconCustom = null
}
}
}
} }
override fun onIconClickListener(icon: IconImageCustom) { override fun onIconClickListener(icon: IconImageCustom) {

View File

@@ -20,14 +20,16 @@
package com.kunzisoft.keepass.activities.helpers package com.kunzisoft.keepass.activities.helpers
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import androidx.annotation.RequiresApi import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
@@ -38,6 +40,10 @@ class ExternalFileHelper {
private var activity: FragmentActivity? = null private var activity: FragmentActivity? = null
private var fragment: Fragment? = null private var fragment: Fragment? = null
private var getContentResultLauncher: ActivityResultLauncher<String>? = null
private var openDocumentResultLauncher: ActivityResultLauncher<Array<String>>? = null
private var createDocumentResultLauncher: ActivityResultLauncher<String>? = null
constructor(context: FragmentActivity) { constructor(context: FragmentActivity) {
this.activity = context this.activity = context
this.fragment = null this.fragment = null
@@ -48,94 +54,81 @@ class ExternalFileHelper {
this.fragment = context this.fragment = context
} }
fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) {
val resultCallback = ActivityResultCallback<Uri> { result ->
result?.let { uri ->
UriUtil.takeUriPermission(activity?.contentResolver, uri)
onFileSelected?.invoke(uri)
}
}
getContentResultLauncher = if (fragment != null) {
fragment?.registerForActivityResult(
GetContent(),
resultCallback
)
} else {
activity?.registerForActivityResult(
GetContent(),
resultCallback
)
}
openDocumentResultLauncher = if (fragment != null) {
fragment?.registerForActivityResult(
OpenDocument(),
resultCallback
)
} else {
activity?.registerForActivityResult(
OpenDocument(),
resultCallback
)
}
}
fun buildCreateDocument(typeString: String = "application/octet-stream",
onFileCreated: (fileCreated: Uri?)->Unit) {
val resultCallback = ActivityResultCallback<Uri> { result ->
onFileCreated.invoke(result)
}
createDocumentResultLauncher = if (fragment != null) {
fragment?.registerForActivityResult(
CreateDocument(typeString),
resultCallback
)
} else {
activity?.registerForActivityResult(
CreateDocument(typeString),
resultCallback
)
}
}
fun openDocument(getContent: Boolean = false, fun openDocument(getContent: Boolean = false,
typeString: String = "*/*") { typeString: String = "*/*") {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { try {
try { if (getContent) {
if (getContent) { getContentResultLauncher?.launch(typeString)
openActivityWithActionGetContent(typeString) } else {
} else { openDocumentResultLauncher?.launch(arrayOf(typeString))
openActivityWithActionOpenDocument(typeString)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to open document", e)
showFileManagerDialogFragment()
} }
} else { } catch (e: Exception) {
Log.e(TAG, "Unable to open document", e)
showFileManagerDialogFragment() showFileManagerDialogFragment()
} }
} }
@RequiresApi(Build.VERSION_CODES.KITKAT) fun createDocument(titleString: String) {
private fun openActivityWithActionOpenDocument(typeString: String) { try {
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { createDocumentResultLauncher?.launch(titleString)
addCategory(Intent.CATEGORY_OPENABLE) } catch (e: Exception) {
type = typeString Log.e(TAG, "Unable to create document", e)
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) showFileManagerDialogFragment()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
}
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
} }
if (fragment != null)
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
else
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
}
@RequiresApi(Build.VERSION_CODES.KITKAT)
private fun openActivityWithActionGetContent(typeString: String) {
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
}
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
if (fragment != null)
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
else
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
}
/**
* To use in onActivityResultCallback in Fragment or Activity
* @param onFileSelected Callback retrieve from data
* @return true if requestCode was captured, false elsewhere
*/
fun onOpenDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
onFileSelected: ((uri: Uri?) -> Unit)?): Boolean {
when (requestCode) {
FILE_BROWSE -> {
if (resultCode == RESULT_OK) {
val filename = data?.dataString
var keyUri: Uri? = null
if (filename != null) {
keyUri = UriUtil.parse(filename)
}
onFileSelected?.invoke(keyUri)
}
return true
}
GET_CONTENT, OPEN_DOC -> {
if (resultCode == RESULT_OK) {
if (data != null) {
val uri = data.data
if (uri != null) {
UriUtil.takeUriPermission(activity?.contentResolver, uri)
onFileSelected?.invoke(uri)
}
}
}
return true
}
}
return false
} }
/** /**
@@ -155,62 +148,50 @@ class ExternalFileHelper {
} }
} }
fun createDocument(titleString: String, class OpenDocument : ActivityResultContracts.OpenDocument() {
typeString: String = "application/octet-stream"): Int? { @SuppressLint("InlinedApi")
val idCode = getUnusedCreateFileRequestCode() override fun createIntent(context: Context, input: Array<out String>): Intent {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { return super.createIntent(context, input).apply {
try { addCategory(Intent.CATEGORY_OPENABLE)
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
addCategory(Intent.CATEGORY_OPENABLE) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
type = typeString addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
putExtra(Intent.EXTRA_TITLE, titleString)
} }
if (fragment != null) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
fragment?.startActivityForResult(intent, idCode) addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
else
activity?.startActivityForResult(intent, idCode)
return idCode
} catch (e: Exception) {
Log.e(TAG, "Unable to create document", e)
showFileManagerDialogFragment()
} }
} else {
showFileManagerDialogFragment()
} }
return null
} }
/** class GetContent : ActivityResultContracts.GetContent() {
* To use in onActivityResultCallback in Fragment or Activity @SuppressLint("InlinedApi")
* @param onFileCreated Callback retrieve from data override fun createIntent(context: Context, input: String): Intent {
* @return true if requestCode was captured, false elsewhere return super.createIntent(context, input).apply {
*/ addCategory(Intent.CATEGORY_OPENABLE)
fun onCreateDocumentResult(requestCode: Int, resultCode: Int, data: Intent?, addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
onFileCreated: (fileCreated: Uri?)->Unit) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Retrieve the created URI from the file manager addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
if (fileRequestCodes.contains(requestCode) && resultCode == RESULT_OK) { }
onFileCreated.invoke(data?.data) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
fileRequestCodes.remove(requestCode) addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
} }
} }
class CreateDocument(private val typeString: String) : ActivityResultContracts.CreateDocument() {
override fun createIntent(context: Context, input: String): Intent {
return super.createIntent(context, input).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
}
}
}
companion object { companion object {
private const val TAG = "OpenFileHelper" private const val TAG = "OpenFileHelper"
private const val GET_CONTENT = 25745
private const val OPEN_DOC = 25845
private const val FILE_BROWSE = 25645
private var CREATE_FILE_REQUEST_CODE_DEFAULT = 3853
private var fileRequestCodes = ArrayList<Int>()
private fun getUnusedCreateFileRequestCode(): Int {
val newCreateFileRequestCode = CREATE_FILE_REQUEST_CODE_DEFAULT++
fileRequestCodes.add(newCreateFileRequestCode)
return newCreateFileRequestCode
}
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager, fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager,
typeString: String = "application/octet-stream"): Boolean { typeString: String = "application/octet-stream"): Boolean {
@@ -231,7 +212,7 @@ class ExternalFileHelper {
fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) { fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) {
externalFileHelper?.let { fileHelper -> externalFileHelper?.let { fileHelper ->
setOnClickListener { setOnClickListener {
fileHelper.openDocument() fileHelper.openDocument(false)
} }
setOnLongClickListener { setOnLongClickListener {
fileHelper.openDocument(true) fileHelper.openDocument(true)

View File

@@ -7,14 +7,9 @@ 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.database.action.DatabaseTaskProvider import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
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.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import java.util.*
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval { abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
@@ -28,7 +23,11 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
mDatabaseTaskProvider = DatabaseTaskProvider(this) mDatabaseTaskProvider = DatabaseTaskProvider(this)
mDatabaseTaskProvider?.onDatabaseRetrieved = { database -> mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
if (mDatabase == null || mDatabase != database) { val databaseWasReloaded = database?.wasReloaded == true
if (databaseWasReloaded && finishActivityIfReloadRequested()) {
finish()
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) {
database?.wasReloaded = false
onDatabaseRetrieved(database) onDatabaseRetrieved(database)
} }
} }
@@ -69,17 +68,8 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
mDatabase?.clearAndClose(this) mDatabase?.clearAndClose(this)
} }
override fun reloadActivity() {
super.reloadActivity()
mDatabase?.wasReloaded = false
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (mDatabase?.wasReloaded == true) {
reloadActivity()
}
mDatabaseTaskProvider?.registerProgressTask() mDatabaseTaskProvider?.registerProgressTask()
} }

View File

@@ -100,7 +100,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
} }
mDatabaseViewModel.saveDefaultUsername.observe(this) { mDatabaseViewModel.saveDefaultUsername.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save) mDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(it.oldValue, it.newValue, it.save)
} }
mDatabaseViewModel.saveColor.observe(this) { mDatabaseViewModel.saveColor.observe(this) {
@@ -180,8 +180,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
closeDatabase(database) closeDatabase(database)
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null) if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
// Add onActivityForResult response mExitLock = true
setResult(RESULT_EXIT_LOCK)
closeOptionsMenu() closeOptionsMenu()
finish() finish()
} }
@@ -353,14 +352,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable) mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_EXIT_LOCK) {
mExitLock = true
lockAndExit()
}
}
private fun checkRegister() { private fun checkRegister() {
// If in ave or registration mode, don't allow read only // If in ave or registration mode, don't allow read only
if ((mSpecialMode == SpecialMode.SAVE if ((mSpecialMode == SpecialMode.SAVE
@@ -440,8 +431,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
const val TAG = "LockingActivity" const val TAG = "LockingActivity"
const val RESULT_EXIT_LOCK = 1450
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY" const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
const val TIMEOUT_ENABLE_KEY_DEFAULT = true const val TIMEOUT_ENABLE_KEY_DEFAULT = true

View File

@@ -39,7 +39,11 @@ object Stylish {
*/ */
fun load(context: Context) { fun load(context: Context) {
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName) Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
themeString = PreferencesUtil.getStyle(context) try {
themeString = PreferencesUtil.getStyle(context)
} catch (e: Exception) {
Log.e("Stylish", "Unable to get preference style", e)
}
} }
fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String { fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {

View File

@@ -28,7 +28,7 @@ import android.util.Log
import android.view.WindowManager import android.view.WindowManager
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_APPEARANCE_PREFERENCE_CHANGED import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_PREFERENCE_CHANGED
/** /**
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from * Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
@@ -89,8 +89,8 @@ abstract class StylishActivity : AppCompatActivity() {
super.onResume() super.onResume()
if ((customStyle && Stylish.getThemeId(this) != this.themeId) if ((customStyle && Stylish.getThemeId(this) != this.themeId)
|| DATABASE_APPEARANCE_PREFERENCE_CHANGED) { || DATABASE_PREFERENCE_CHANGED) {
DATABASE_APPEARANCE_PREFERENCE_CHANGED = false DATABASE_PREFERENCE_CHANGED = false
Log.d(this.javaClass.name, "Theme change detected, restarting activity") Log.d(this.javaClass.name, "Theme change detected, restarting activity")
recreateActivity() recreateActivity()
} }

View File

@@ -23,12 +23,12 @@ import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.annotation.StyleRes
import androidx.fragment.app.Fragment
import androidx.appcompat.view.ContextThemeWrapper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.StyleRes
import androidx.appcompat.view.ContextThemeWrapper
import androidx.fragment.app.Fragment
abstract class StylishFragment : Fragment() { abstract class StylishFragment : Fragment() {
@@ -42,7 +42,6 @@ abstract class StylishFragment : Fragment() {
contextThemed = ContextThemeWrapper(context, themeId) contextThemed = ContextThemeWrapper(context, themeId)
} }
@Suppress("DEPRECATION")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// To fix status bar color // To fix status bar color
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@@ -58,6 +57,7 @@ abstract class StylishFragment : Fragment() {
try { try {
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar)) val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
if (taWindowStatusLight?.getBoolean(0, false) == true) { if (taWindowStatusLight?.getBoolean(0, false) == true) {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} }
taWindowStatusLight?.recycle() taWindowStatusLight?.recycle()

View File

@@ -0,0 +1,150 @@
package com.kunzisoft.keepass.adapters
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.strikeOut
class BreadcrumbAdapter(val context: Context)
: RecyclerView.Adapter<BreadcrumbAdapter.BreadcrumbGroupViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
var iconDrawableFactory: IconDrawableFactory? = null
@SuppressLint("NotifyDataSetChanged")
set(value) {
field = value
notifyDataSetChanged()
}
private var mNodeBreadcrumb: MutableList<Node?> = mutableListOf()
var onItemClickListener: ((item: Node, position: Int)->Unit)? = null
var onLongItemClickListener: ((item: Node, position: Int)->Unit)? = null
private var mShowNumberEntries = false
private var mShowUUID = false
private var mIconColor: Int = 0
init {
mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
mShowUUID = PreferencesUtil.showUUID(context)
// Retrieve the textColor to tint the icon
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
mIconColor = taTextColor.getColor(0, Color.WHITE)
taTextColor.recycle()
}
@SuppressLint("NotifyDataSetChanged")
fun setNode(node: Node?) {
mNodeBreadcrumb.clear()
node?.let {
var currentNode = it
mNodeBreadcrumb.add(0, currentNode)
while (currentNode.containsParent()) {
currentNode.parent?.let { parent ->
currentNode = parent
mNodeBreadcrumb.add(0, currentNode)
}
}
}
notifyDataSetChanged()
}
override fun getItemViewType(position: Int): Int {
return when (position) {
mNodeBreadcrumb.size - 1 -> 0
else -> 1
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BreadcrumbGroupViewHolder {
return BreadcrumbGroupViewHolder(inflater.inflate(
when (viewType) {
0 -> R.layout.item_group
else -> R.layout.item_breadcrumb
}, parent, false)
)
}
override fun onBindViewHolder(holder: BreadcrumbGroupViewHolder, position: Int) {
val node = mNodeBreadcrumb[position]
holder.groupNameView.apply {
text = node?.title ?: ""
strikeOut(node?.isCurrentlyExpires ?: false)
}
holder.itemView.apply {
setOnClickListener {
node?.let {
onItemClickListener?.invoke(it, position)
}
}
setOnLongClickListener {
node?.let {
onLongItemClickListener?.invoke(it, position)
}
true
}
}
if (node?.type == Type.GROUP) {
(node as Group).let { group ->
holder.groupIconView?.let { imageView ->
iconDrawableFactory?.assignDatabaseIcon(
imageView,
group.icon,
mIconColor
)
}
holder.groupNumbersView?.apply {
if (mShowNumberEntries) {
group.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context))
text = group.numberOfChildEntries.toString()
visibility = View.VISIBLE
} else {
visibility = View.GONE
}
}
holder.groupMetaView?.apply {
val meta = group.nodeId.toVisualString()
visibility = if (meta != null
&& !group.isVirtual
&& mShowUUID
) {
text = meta
View.VISIBLE
} else {
View.GONE
}
}
}
}
}
override fun getItemCount(): Int {
return mNodeBreadcrumb.size
}
inner class BreadcrumbGroupViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var groupIconView: ImageView? = itemView.findViewById(R.id.group_icon)
var groupNumbersView: TextView? = itemView.findViewById(R.id.group_numbers)
var groupNameView: TextView = itemView.findViewById(R.id.group_name)
var groupMetaView: TextView? = itemView.findViewById(R.id.group_meta)
}
}

View File

@@ -28,11 +28,13 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback import androidx.recyclerview.widget.SortedListAdapterCallback
import com.google.android.material.progressindicator.CircularProgressIndicator
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
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
@@ -41,9 +43,11 @@ import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.template.TemplateField
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.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.setTextSize import com.kunzisoft.keepass.view.setTextSize
import com.kunzisoft.keepass.view.strikeOut import com.kunzisoft.keepass.view.strikeOut
import java.util.* import java.util.*
@@ -52,9 +56,9 @@ import java.util.*
* Create node list adapter with contextMenu or not * Create node list adapter with contextMenu or not
* @param context Context to use * @param context Context to use
*/ */
class NodeAdapter (private val context: Context, class NodesAdapter (private val context: Context,
private val database: Database) private val database: Database)
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() { : RecyclerView.Adapter<NodesAdapter.NodeViewHolder>() {
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
private val mNodeSortedListCallback: NodeSortedListCallback private val mNodeSortedListCallback: NodeSortedListCallback
@@ -64,8 +68,10 @@ class NodeAdapter (private val context: Context,
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
private var mPrefSizeMultiplier: Float = 0F private var mPrefSizeMultiplier: Float = 0F
private var mSubtextDefaultDimension: Float = 0F private var mTextDefaultDimension: Float = 0F
private var mInfoTextDefaultDimension: Float = 0F private var mSubTextDefaultDimension: Float = 0F
private var mMetaTextDefaultDimension: Float = 0F
private var mOtpTokenTextDefaultDimension: Float = 0F
private var mNumberChildrenTextDefaultDimension: Float = 0F private var mNumberChildrenTextDefaultDimension: Float = 0F
private var mIconDefaultDimension: Float = 0F private var mIconDefaultDimension: Float = 0F
@@ -74,16 +80,25 @@ class NodeAdapter (private val context: Context,
private var mShowOTP: Boolean = false private var mShowOTP: Boolean = false
private var mShowUUID: Boolean = false private var mShowUUID: Boolean = false
private var mEntryFilters = arrayOf<Group.ChildFilter>() private var mEntryFilters = arrayOf<Group.ChildFilter>()
private var mOldVirtualGroup = false
private var mVirtualGroup = false
private var mActionNodesList = LinkedList<Node>() private var mActionNodesList = LinkedList<Node>()
private var mNodeClickCallback: NodeClickCallback? = null private var mNodeClickCallback: NodeClickCallback? = null
private var mClipboardHelper = ClipboardHelper(context)
@ColorInt @ColorInt
private val mContentSelectionColor: Int private val mContentSelectionColor: Int
@ColorInt @ColorInt
private val mIconGroupColor: Int private val mTextColorPrimary: Int
@ColorInt @ColorInt
private val mIconEntryColor: Int private val mTextColor: Int
@ColorInt
private val mTextColorSecondary: Int
@ColorInt
private val mColorAccentLight: Int
@ColorInt
private val mTextColorInverse: Int
/** /**
* Determine if the adapter contains or not any element * Determine if the adapter contains or not any element
@@ -104,12 +119,24 @@ class NodeAdapter (private val context: Context,
this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white) this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
// Retrieve the color to tint the icon // Retrieve the color to tint the icon
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary)) val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK) this.mTextColorPrimary = taTextColorPrimary.getColor(0, Color.BLACK)
taTextColorPrimary.recycle() taTextColorPrimary.recycle()
// In two times to fix bug compilation // To get text color
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
this.mIconEntryColor = taTextColor.getColor(0, Color.BLACK) this.mTextColor = taTextColor.getColor(0, Color.BLACK)
taTextColor.recycle() taTextColor.recycle()
// To get text color secondary
val taTextColorSecondary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorSecondary))
this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK)
taTextColorSecondary.recycle()
// To get background color for selection
val taSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccentLight))
this.mColorAccentLight = taSelectionColor.getColor(0, Color.GRAY)
taSelectionColor.recycle()
// To get text color for selection
val taSelectionTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
this.mTextColorInverse = taSelectionTextColor.getColor(0, Color.WHITE)
taSelectionTextColor.recycle()
} }
private fun assignPreferences() { private fun assignPreferences() {
@@ -139,6 +166,8 @@ class NodeAdapter (private val context: Context,
* Rebuild the list by clear and build children from the group * Rebuild the list by clear and build children from the group
*/ */
fun rebuildList(group: Group) { fun rebuildList(group: Group) {
mOldVirtualGroup = mVirtualGroup
mVirtualGroup = group.isVirtual
assignPreferences() assignPreferences()
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters)) mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
} }
@@ -149,14 +178,19 @@ class NodeAdapter (private val context: Context,
} }
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean { override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
if (mOldVirtualGroup != mVirtualGroup)
return false
var typeContentTheSame = true var typeContentTheSame = true
if (oldItem is Entry && newItem is Entry) { if (oldItem is Entry && newItem is Entry) {
typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle() typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle()
&& oldItem.username == newItem.username && oldItem.username == newItem.username
&& oldItem.backgroundColor == newItem.backgroundColor
&& oldItem.foregroundColor == newItem.foregroundColor
&& oldItem.getOtpElement() == newItem.getOtpElement() && oldItem.getOtpElement() == newItem.getOtpElement()
&& oldItem.containsAttachment() == newItem.containsAttachment() && oldItem.containsAttachment() == newItem.containsAttachment()
} else if (oldItem is Group && newItem is Group) { } else if (oldItem is Group && newItem is Group) {
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
&& oldItem.notes == newItem.notes
} }
return typeContentTheSame return typeContentTheSame
&& oldItem.nodeId == newItem.nodeId && oldItem.nodeId == newItem.nodeId
@@ -299,8 +333,10 @@ class NodeAdapter (private val context: Context,
mInflater.inflate(R.layout.item_list_nodes_entry, parent, false) mInflater.inflate(R.layout.item_list_nodes_entry, parent, false)
} }
val nodeViewHolder = NodeViewHolder(view) val nodeViewHolder = NodeViewHolder(view)
mInfoTextDefaultDimension = nodeViewHolder.text.textSize mTextDefaultDimension = nodeViewHolder.text.textSize
mSubtextDefaultDimension = nodeViewHolder.subText.textSize mSubTextDefaultDimension = nodeViewHolder.subText?.textSize ?: mSubTextDefaultDimension
mMetaTextDefaultDimension = nodeViewHolder.meta.textSize
mOtpTokenTextDefaultDimension = nodeViewHolder.otpToken?.textSize ?: mOtpTokenTextDefaultDimension
nodeViewHolder.numberChildren?.let { nodeViewHolder.numberChildren?.let {
mNumberChildrenTextDefaultDimension = it.textSize mNumberChildrenTextDefaultDimension = it.textSize
} }
@@ -311,14 +347,16 @@ class NodeAdapter (private val context: Context,
val subNode = mNodeSortedList.get(position) val subNode = mNodeSortedList.get(position)
// Node selection // Node selection
holder.container.isSelected = mActionNodesList.contains(subNode) holder.container.apply {
isSelected = mActionNodesList.contains(subNode)
}
// Assign image // Assign image
val iconColor = if (holder.container.isSelected) val iconColor = if (holder.container.isSelected)
mContentSelectionColor mContentSelectionColor
else when (subNode.type) { else when (subNode.type) {
Type.GROUP -> mIconGroupColor Type.GROUP -> mTextColorPrimary
Type.ENTRY -> mIconEntryColor Type.ENTRY -> mTextColor
} }
holder.imageIdentifier?.setColorFilter(iconColor) holder.imageIdentifier?.setColorFilter(iconColor)
holder.icon.apply { holder.icon.apply {
@@ -333,19 +371,28 @@ class NodeAdapter (private val context: Context,
// Assign text // Assign text
holder.text.apply { holder.text.apply {
text = subNode.title text = subNode.title
setTextSize(mTextSizeUnit, mInfoTextDefaultDimension, mPrefSizeMultiplier) setTextSize(mTextSizeUnit, mTextDefaultDimension, mPrefSizeMultiplier)
strikeOut(subNode.isCurrentlyExpires) strikeOut(subNode.isCurrentlyExpires)
} }
// Add subText with username
holder.subText.apply {
text = ""
strikeOut(subNode.isCurrentlyExpires)
visibility = View.GONE
}
// Add meta text to show UUID // Add meta text to show UUID
holder.meta.apply { holder.meta.apply {
text = subNode.nodeId.toString() val nodeId = subNode.nodeId?.toVisualString()
visibility = if (mShowUUID) View.VISIBLE else View.GONE if (mShowUUID && nodeId != null) {
text = nodeId
setTextSize(mTextSizeUnit, mMetaTextDefaultDimension, mPrefSizeMultiplier)
visibility = View.VISIBLE
} else {
visibility = View.GONE
}
}
// Add path to virtual group
if (mVirtualGroup) {
holder.path?.apply {
text = subNode.getPathString()
visibility = View.VISIBLE
}
} else {
holder.path?.visibility = View.GONE
} }
// Specific elements for entry // Specific elements for entry
@@ -354,12 +401,16 @@ class NodeAdapter (private val context: Context,
database.startManageEntry(entry) database.startManageEntry(entry)
holder.text.text = entry.getVisualTitle() holder.text.text = entry.getVisualTitle()
holder.subText.apply { // Add subText with username
holder.subText?.apply {
val username = entry.username val username = entry.username
if (mShowUserNames && username.isNotEmpty()) { if (mShowUserNames && username.isNotEmpty()) {
visibility = View.VISIBLE visibility = View.VISIBLE
text = username text = username
setTextSize(mTextSizeUnit, mSubtextDefaultDimension, mPrefSizeMultiplier) setTextSize(mTextSizeUnit, mSubTextDefaultDimension, mPrefSizeMultiplier)
strikeOut(subNode.isCurrentlyExpires)
} else {
visibility = View.GONE
} }
} }
@@ -385,6 +436,50 @@ class NodeAdapter (private val context: Context,
holder.attachmentIcon?.visibility = holder.attachmentIcon?.visibility =
if (entry.containsAttachment()) View.VISIBLE else View.GONE if (entry.containsAttachment()) View.VISIBLE else View.GONE
// Assign colors
val backgroundColor = entry.backgroundColor
if (!holder.container.isSelected) {
if (backgroundColor != null) {
holder.container.setBackgroundColor(backgroundColor)
} else {
holder.container.setBackgroundColor(Color.TRANSPARENT)
}
} else {
holder.container.setBackgroundColor(mColorAccentLight)
}
val foregroundColor = entry.foregroundColor
if (!holder.container.isSelected) {
if (foregroundColor != null) {
holder.text.setTextColor(foregroundColor)
holder.subText?.setTextColor(foregroundColor)
holder.otpToken?.setTextColor(foregroundColor)
holder.otpProgress?.setIndicatorColor(foregroundColor)
holder.attachmentIcon?.setColorFilter(foregroundColor)
holder.meta.setTextColor(foregroundColor)
holder.icon.apply {
database.iconDrawableFactory.assignDatabaseIcon(
this,
subNode.icon,
foregroundColor
)
}
} else {
holder.text.setTextColor(mTextColor)
holder.subText?.setTextColor(mTextColorSecondary)
holder.otpToken?.setTextColor(mTextColorSecondary)
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
holder.meta.setTextColor(mTextColor)
}
} else {
holder.text.setTextColor(mTextColorInverse)
holder.subText?.setTextColor(mTextColorInverse)
holder.otpToken?.setTextColor(mTextColorInverse)
holder.otpProgress?.setIndicatorColor(mTextColorInverse)
holder.attachmentIcon?.setColorFilter(mTextColorInverse)
holder.meta.setTextColor(mTextColorInverse)
}
database.stopManageEntry(entry) database.stopManageEntry(entry)
} }
@@ -417,17 +512,32 @@ class NodeAdapter (private val context: Context,
OtpType.HOTP -> { OtpType.HOTP -> {
holder?.otpProgress?.apply { holder?.otpProgress?.apply {
max = 100 max = 100
progress = 100 setProgressCompat(100, true)
} }
} }
OtpType.TOTP -> { OtpType.TOTP -> {
holder?.otpProgress?.apply { holder?.otpProgress?.apply {
max = otpElement.period max = otpElement.period
progress = otpElement.secondsRemaining setProgressCompat(otpElement.secondsRemaining, true)
} }
} }
null -> {}
}
holder?.otpToken?.apply {
text = otpElement?.token
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
}
holder?.otpContainer?.setOnClickListener {
otpElement?.token?.let { token ->
Toast.makeText(
context,
context.getString(R.string.copy_field,
TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN)),
Toast.LENGTH_LONG
).show()
mClipboardHelper.copyToClipboard(token)
}
} }
holder?.otpToken?.text = otpElement?.token
} }
class OtpRunnable(val view: View?): Runnable { class OtpRunnable(val view: View?): Runnable {
@@ -468,10 +578,11 @@ class NodeAdapter (private val context: Context,
var imageIdentifier: ImageView? = itemView.findViewById(R.id.node_image_identifier) var imageIdentifier: ImageView? = itemView.findViewById(R.id.node_image_identifier)
var icon: ImageView = itemView.findViewById(R.id.node_icon) var icon: ImageView = itemView.findViewById(R.id.node_icon)
var text: TextView = itemView.findViewById(R.id.node_text) var text: TextView = itemView.findViewById(R.id.node_text)
var subText: TextView = itemView.findViewById(R.id.node_subtext) var subText: TextView? = itemView.findViewById(R.id.node_subtext)
var meta: TextView = itemView.findViewById(R.id.node_meta) var meta: TextView = itemView.findViewById(R.id.node_meta)
var path: TextView? = itemView.findViewById(R.id.node_path)
var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container) var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container)
var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress) var otpProgress: CircularProgressIndicator? = itemView.findViewById(R.id.node_otp_progress)
var otpToken: TextView? = itemView.findViewById(R.id.node_otp_token) var otpToken: TextView? = itemView.findViewById(R.id.node_otp_token)
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer) var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers) var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
@@ -479,6 +590,6 @@ class NodeAdapter (private val context: Context,
} }
companion object { companion object {
private val TAG = NodeAdapter::class.java.name private val TAG = NodesAdapter::class.java.name
} }
} }

View File

@@ -16,9 +16,9 @@ import com.kunzisoft.keepass.icons.IconDrawableFactory
class TemplatesSelectorAdapter(private val context: Context, class TemplatesSelectorAdapter(private val context: Context,
private val iconDrawableFactory: IconDrawableFactory?,
private var templates: List<Template>): BaseAdapter() { private var templates: List<Template>): BaseAdapter() {
var iconDrawableFactory: IconDrawableFactory? = null
private val inflater: LayoutInflater = LayoutInflater.from(context) private val inflater: LayoutInflater = LayoutInflater.from(context)
private var mIconColor = Color.BLACK private var mIconColor = Color.BLACK

View File

@@ -4,4 +4,4 @@ import android.app.assist.AssistStructure
import android.view.inputmethod.InlineSuggestionsRequest import android.view.inputmethod.InlineSuggestionsRequest
data class AutofillComponent(val assistStructure: AssistStructure, data class AutofillComponent(val assistStructure: AssistStructure,
val inlineSuggestionsRequest: InlineSuggestionsRequest?) val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)

View File

@@ -34,34 +34,35 @@ import android.service.autofill.InlinePresentation
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.view.inputmethod.InlineSuggestionsRequest
import android.widget.RemoteViews import android.widget.RemoteViews
import android.widget.Toast import android.widget.Toast
import android.widget.inline.InlinePresentationSpec
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.autofill.inline.UiVersions import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi import androidx.autofill.inline.v1.InlineSuggestionUi
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.settings.AutofillSettingsActivity import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.* import com.kunzisoft.keepass.utils.LOCK_ACTION
import kotlin.collections.ArrayList
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
object AutofillHelper { object AutofillHelper {
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST" private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? { fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure -> intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
@@ -110,7 +111,7 @@ object AutofillHelper {
database: Database, database: Database,
entryInfo: EntryInfo, entryInfo: EntryInfo,
struct: StructureParser.Result, struct: StructureParser.Result,
inlinePresentation: InlinePresentation?): Dataset? { additionalBuild: ((build: Dataset.Builder) -> Unit)? = null): Dataset? {
val title = makeEntryTitle(entryInfo) val title = makeEntryTitle(entryInfo)
val views = newRemoteViews(context, database, title, entryInfo.icon) val views = newRemoteViews(context, database, title, entryInfo.icon)
val builder = Dataset.Builder(views) val builder = Dataset.Builder(views)
@@ -126,12 +127,14 @@ object AutofillHelper {
if (entryInfo.expires) { if (entryInfo.expires) {
val year = entryInfo.expiryTime.getYearInt() val year = entryInfo.expiryTime.getYearInt()
val month = entryInfo.expiryTime.getMonthInt() val month = entryInfo.expiryTime.getMonthInt()
val monthString = month.toString().padStart(2, '0')
val day = entryInfo.expiryTime.getDay() val day = entryInfo.expiryTime.getDay()
val dayString = day.toString().padStart(2, '0')
struct.creditCardExpirationDateId?.let { struct.creditCardExpirationDateId?.let {
if (struct.isWebView) { if (struct.isWebView) {
// set date string as defined in https://html.spec.whatwg.org // set date string as defined in https://html.spec.whatwg.org
builder.setValue(it, AutofillValue.forText("$year\u002D$month")) builder.setValue(it, AutofillValue.forText("$year\u002D$monthString"))
} else { } else {
builder.setValue(it, AutofillValue.forDate(entryInfo.expiryTime.date.time)) builder.setValue(it, AutofillValue.forDate(entryInfo.expiryTime.date.time))
} }
@@ -157,24 +160,24 @@ object AutofillHelper {
} }
struct.creditCardExpirationMonthId?.let { struct.creditCardExpirationMonthId?.let {
if (struct.isWebView) { if (struct.isWebView) {
builder.setValue(it, AutofillValue.forText(month.toString())) builder.setValue(it, AutofillValue.forText(monthString))
} else { } else {
if (struct.creditCardExpirationMonthOptions != null) { if (struct.creditCardExpirationMonthOptions != null) {
// index starts at 0 // index starts at 0
builder.setValue(it, AutofillValue.forList(month - 1)) builder.setValue(it, AutofillValue.forList(month - 1))
} else { } else {
builder.setValue(it, AutofillValue.forText(month.toString())) builder.setValue(it, AutofillValue.forText(monthString))
} }
} }
} }
struct.creditCardExpirationDayId?.let { struct.creditCardExpirationDayId?.let {
if (struct.isWebView) { if (struct.isWebView) {
builder.setValue(it, AutofillValue.forText(day.toString())) builder.setValue(it, AutofillValue.forText(dayString))
} else { } else {
if (struct.creditCardExpirationDayOptions != null) { if (struct.creditCardExpirationDayOptions != null) {
builder.setValue(it, AutofillValue.forList(day - 1)) builder.setValue(it, AutofillValue.forList(day - 1))
} else { } else {
builder.setValue(it, AutofillValue.forText(day.toString())) builder.setValue(it, AutofillValue.forText(dayString))
} }
} }
} }
@@ -197,11 +200,7 @@ object AutofillHelper {
} }
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { additionalBuild?.invoke(builder)
inlinePresentation?.let {
builder.setInlinePresentation(it)
}
}
return try { return try {
builder.build() builder.build()
@@ -232,49 +231,81 @@ object AutofillHelper {
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private fun buildInlinePresentationForEntry(context: Context, private fun buildInlinePresentationForEntry(context: Context,
database: Database, database: Database,
inlineSuggestionsRequest: InlineSuggestionsRequest, compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
positionItem: Int, positionItem: Int,
entryInfo: EntryInfo): InlinePresentation? { entryInfo: EntryInfo): InlinePresentation? {
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
if (positionItem <= maxSuggestion - 1 if (positionItem <= maxSuggestion - 1
&& inlinePresentationSpecs.size > positionItem) { && inlinePresentationSpecs.size > positionItem
val inlinePresentationSpec = inlinePresentationSpecs[positionItem] ) {
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
// Make sure that the IME spec claims support for v1 UI template. // Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style val imeStyle = inlinePresentationSpec.style
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1)) if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
return null return null
// Build the content for IME UI // Build the content for IME UI
val pendingIntent = PendingIntent.getActivity(context, val pendingIntent = PendingIntent.getActivity(
context,
0, 0,
Intent(context, AutofillSettingsActivity::class.java), Intent(context, AutofillSettingsActivity::class.java),
0) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return InlinePresentation( PendingIntent.FLAG_IMMUTABLE
} else {
0
}
)
return InlinePresentation(
InlineSuggestionUi.newContentBuilder(pendingIntent).apply { InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
setContentDescription(context.getString(R.string.autofill_sign_in_prompt)) setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
setTitle(entryInfo.title) setTitle(entryInfo.title)
setSubtitle(entryInfo.username) setSubtitle(entryInfo.username)
setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply { setStartIcon(
setTintBlendMode(BlendMode.DST) Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
}) setTintBlendMode(BlendMode.DST)
})
buildIconFromEntry(context, database, entryInfo)?.let { icon -> buildIconFromEntry(context, database, entryInfo)?.let { icon ->
setEndIcon(icon.apply { setEndIcon(icon.apply {
setTintBlendMode(BlendMode.DST) setTintBlendMode(BlendMode.DST)
}) })
} }
}.build().slice, inlinePresentationSpec, false) }.build().slice, inlinePresentationSpec, false
)
}
} }
return null return null
} }
@RequiresApi(Build.VERSION_CODES.R)
@SuppressLint("RestrictedApi")
private fun buildInlinePresentationForManualSelection(context: Context,
inlinePresentationSpec: InlinePresentationSpec,
pendingIntent: PendingIntent): InlinePresentation? {
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
return null
// Build the content for IME UI
return InlinePresentation(
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
setTitle(context.getString(R.string.autofill_select_entry))
setStartIcon(Icon.createWithResource(context, R.drawable.ic_arrow_right_green_24dp).apply {
setTintBlendMode(BlendMode.DST)
})
}.build().slice, inlinePresentationSpec, false)
}
fun buildResponse(context: Context, fun buildResponse(context: Context,
database: Database, database: Database,
entriesInfo: List<EntryInfo>, entriesInfo: List<EntryInfo>,
parseResult: StructureParser.Result, parseResult: StructureParser.Result,
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse? { compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
val responseBuilder = FillResponse.Builder() val responseBuilder = FillResponse.Builder()
// Add Header // Add Header
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -293,17 +324,63 @@ object AutofillHelper {
} }
// Add inline suggestion for new IME and dataset // Add inline suggestion for new IME and dataset
entriesInfo.forEachIndexed { index, entryInfo -> var numberInlineSuggestions = 0
val inlinePresentation = inlineSuggestionsRequest?.let { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
buildInlinePresentationForEntry(context, database, inlineSuggestionsRequest, index, entryInfo) numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
} else { if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
null if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
--numberInlineSuggestions
}
} }
} }
val dataSet = buildDataset(context, database, entryInfo, parseResult, inlinePresentation)
dataSet?.let { }
responseBuilder.addDataset(it)
entriesInfo.forEachIndexed { _, entry ->
if (numberInlineSuggestions > 0
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& compatInlineSuggestionsRequest != null) {
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult) { builder ->
buildInlinePresentationForEntry(context, database,
compatInlineSuggestionsRequest, numberInlineSuggestions--, entry
)?.let { inlinePresentation ->
builder.setInlinePresentation(inlinePresentation)
}
})
} else {
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult))
}
}
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
manualSelection = true
}
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
searchInfo, compatInlineSuggestionsRequest)
parseResult.allAutofillIds().let { autofillIds ->
autofillIds.forEach { id ->
val builder = Dataset.Builder(manualSelectionView)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
inlinePresentation?.let {
builder.setInlinePresentation(it)
}
}
}
builder.setValue(id, null)
builder.setAuthentication(pendingIntent.intentSender)
responseBuilder.addDataset(builder.build())
}
} }
} }
@@ -337,11 +414,11 @@ object AutofillHelper {
StructureParser(structure).parse()?.let { result -> StructureParser(structure).parse()?.let { result ->
// New Response // New Response
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val inlineSuggestionsRequest = activity.intent?.getParcelableExtra<InlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST) val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtra<CompatInlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
if (inlineSuggestionsRequest != null) { if (compatInlineSuggestionsRequest != null) {
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show() Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
} }
buildResponse(activity, database, entriesInfo, result, inlineSuggestionsRequest) buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest)
} else { } else {
buildResponse(activity, database, entriesInfo, result, null) buildResponse(activity, database, entriesInfo, result, null)
} }
@@ -361,37 +438,44 @@ object AutofillHelper {
} }
} }
fun buildActivityResultLauncher(activity: AppCompatActivity,
lockDatabase: Boolean = false): ActivityResultLauncher<Intent> {
return activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
// Utility method to loop and close each activity with return data
if (it.resultCode == Activity.RESULT_OK) {
activity.setResult(it.resultCode, it.data)
}
if (it.resultCode == Activity.RESULT_CANCELED) {
activity.setResult(Activity.RESULT_CANCELED)
}
activity.finish()
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(activity)) {
// Close the database
activity.sendBroadcast(Intent(LOCK_ACTION))
}
}
}
/** /**
* 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, fun startActivityForAutofillResult(activity: AppCompatActivity,
intent: Intent, intent: Intent,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent, autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) { searchInfo: SearchInfo?) {
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION) EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure) intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) { && PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
autofillComponent.inlineSuggestionsRequest?.let { autofillComponent.compatInlineSuggestionsRequest?.let {
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it) intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
} }
} }
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo) EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE) activityResultLauncher?.launch(intent)
}
/**
* Utility method to loop and close each activity with return data
*/
fun onActivityResultSetResultAndFinish(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == AUTOFILL_RESPONSE_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
activity.setResult(resultCode, data)
}
if (resultCode == Activity.RESULT_CANCELED) {
activity.setResult(Activity.RESULT_CANCELED)
}
activity.finish()
}
} }
} }

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2021 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.autofill
import android.annotation.TargetApi
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.service.autofill.FillRequest
import android.view.inputmethod.InlineSuggestionsRequest
import androidx.annotation.RequiresApi
/**
* Utility class only to prevent java.lang.NoClassDefFoundError for old Android version and new lib compilation
*/
@RequiresApi(Build.VERSION_CODES.O)
class CompatInlineSuggestionsRequest : Parcelable {
@TargetApi(Build.VERSION_CODES.R)
var inlineSuggestionsRequest: InlineSuggestionsRequest? = null
private set
constructor(fillRequest: FillRequest) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
this.inlineSuggestionsRequest = fillRequest.inlineSuggestionsRequest
} else {
this.inlineSuggestionsRequest = null
}
}
@RequiresApi(Build.VERSION_CODES.R)
constructor(inlineSuggestionsRequest: InlineSuggestionsRequest?) {
this.inlineSuggestionsRequest = inlineSuggestionsRequest
}
constructor(parcel: Parcel) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
this.inlineSuggestionsRequest =
parcel.readParcelable(FillRequest::class.java.classLoader)
}
else {
this.inlineSuggestionsRequest = null
}
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
parcel.writeParcelable(inlineSuggestionsRequest, flags)
}
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<CompatInlineSuggestionsRequest> {
override fun createFromParcel(parcel: Parcel): CompatInlineSuggestionsRequest {
return CompatInlineSuggestionsRequest(parcel)
}
override fun newArray(size: Int): Array<CompatInlineSuggestionsRequest?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -109,7 +109,7 @@ class KeeAutofillService : AutofillService() {
searchInfo.webDomain = webDomainWithoutSubDomain searchInfo.webDomain = webDomainWithoutSubDomain
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) { && autofillInlineSuggestionsEnabled) {
request.inlineSuggestionsRequest CompatInlineSuggestionsRequest(request)
} else { } else {
null null
} }
@@ -127,7 +127,7 @@ class KeeAutofillService : AutofillService() {
private fun launchSelection(database: Database?, private fun launchSelection(database: Database?,
searchInfo: SearchInfo, searchInfo: SearchInfo,
parseResult: StructureParser.Result, parseResult: StructureParser.Result,
inlineSuggestionsRequest: InlineSuggestionsRequest?, inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
callback: FillCallback) { callback: FillCallback) {
SearchHelper.checkAutoSearchInfo(this, SearchHelper.checkAutoSearchInfo(this,
database, database,
@@ -138,14 +138,14 @@ class KeeAutofillService : AutofillService() {
items, parseResult, inlineSuggestionsRequest) items, parseResult, inlineSuggestionsRequest)
) )
}, },
{ { openedDatabase ->
// Show UI if no search result // Show UI if no search result
showUIForEntrySelection(parseResult, showUIForEntrySelection(parseResult, openedDatabase,
searchInfo, inlineSuggestionsRequest, callback) searchInfo, inlineSuggestionsRequest, callback)
}, },
{ {
// Show UI if database not open // Show UI if database not open
showUIForEntrySelection(parseResult, showUIForEntrySelection(parseResult, null,
searchInfo, inlineSuggestionsRequest, callback) searchInfo, inlineSuggestionsRequest, callback)
} }
) )
@@ -153,26 +153,59 @@ class KeeAutofillService : AutofillService() {
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private fun showUIForEntrySelection(parseResult: StructureParser.Result, private fun showUIForEntrySelection(parseResult: StructureParser.Result,
database: Database?,
searchInfo: SearchInfo, searchInfo: SearchInfo,
inlineSuggestionsRequest: InlineSuggestionsRequest?, inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
callback: FillCallback) { callback: FillCallback) {
parseResult.allAutofillIds().let { autofillIds -> parseResult.allAutofillIds().let { autofillIds ->
if (autofillIds.isNotEmpty()) { 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 intentSender = AutofillLauncherActivity.getAuthIntentSenderForSelection(this, val intentSender = AutofillLauncherActivity.getPendingIntentForSelection(this,
searchInfo, inlineSuggestionsRequest) searchInfo, inlineSuggestionsRequest).intentSender
val responseBuilder = FillResponse.Builder() val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (!parseResult.webDomain.isNullOrEmpty()) { val remoteViewsUnlock: RemoteViews = if (database == null) {
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply { if (!parseResult.webDomain.isNullOrEmpty()) {
setTextViewText(R.id.autofill_web_domain_text, parseResult.webDomain) RemoteViews(
} packageName,
} else if (!parseResult.applicationId.isNullOrEmpty()) { R.layout.item_autofill_unlock_web_domain
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply { ).apply {
setTextViewText(R.id.autofill_app_id_text, parseResult.applicationId) setTextViewText(
R.id.autofill_web_domain_text,
parseResult.webDomain
)
}
} 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)
} }
} else { } else {
RemoteViews(packageName, R.layout.item_autofill_unlock) if (!parseResult.webDomain.isNullOrEmpty()) {
RemoteViews(
packageName,
R.layout.item_autofill_select_entry_web_domain
).apply {
setTextViewText(
R.id.autofill_web_domain_text,
parseResult.webDomain
)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_select_entry_app_id).apply {
setTextViewText(
R.id.autofill_app_id_text,
parseResult.applicationId
)
}
} else {
RemoteViews(packageName, R.layout.item_autofill_select_entry)
}
} }
// Tell the autofill framework the interest to save credentials // Tell the autofill framework the interest to save credentials
@@ -216,7 +249,7 @@ class KeeAutofillService : AutofillService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) { && autofillInlineSuggestionsEnabled) {
var inlinePresentation: InlinePresentation? = null var inlinePresentation: InlinePresentation? = null
inlineSuggestionsRequest?.let { inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
if (inlineSuggestionsRequest.maxSuggestionCount > 0 if (inlineSuggestionsRequest.maxSuggestionCount > 0
&& inlinePresentationSpecs.size > 0) { && inlinePresentationSpecs.size > 0) {
@@ -229,9 +262,13 @@ class KeeAutofillService : AutofillService() {
inlinePresentation = InlinePresentation( inlinePresentation = InlinePresentation(
InlineSuggestionUi.newContentBuilder( InlineSuggestionUi.newContentBuilder(
PendingIntent.getActivity(this, PendingIntent.getActivity(this,
0, 0,
Intent(this, AutofillSettingsActivity::class.java), Intent(this, AutofillSettingsActivity::class.java),
0) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
})
).apply { ).apply {
setContentDescription(getString(R.string.autofill_sign_in_prompt)) setContentDescription(getString(R.string.autofill_sign_in_prompt))
setTitle(getString(R.string.autofill_sign_in_prompt)) setTitle(getString(R.string.autofill_sign_in_prompt))
@@ -244,8 +281,9 @@ class KeeAutofillService : AutofillService() {
} }
// Build response // Build response
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation) responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
} else {
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
} }
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
callback.onSuccess(responseBuilder.build()) callback.onSuccess(responseBuilder.build())
} }
} }

View File

@@ -53,8 +53,10 @@ class StructureParser(private val structure: AssistStructure) {
applicationId = windowNode.title.toString().split("/")[0] applicationId = windowNode.title.toString().split("/")[0]
Log.d(TAG, "Autofill applicationId: $applicationId") Log.d(TAG, "Autofill applicationId: $applicationId")
if (parseViewNode(windowNode.rootViewNode)) if (applicationId?.contains("PopupWindow:") == false) {
break@mainLoop if (parseViewNode(windowNode.rootViewNode))
break@mainLoop
}
} }
// If not explicit username field found, add the field just before password field. // If not explicit username field found, add the field just before password field.
if (usernameId == null && passwordId != null && usernameIdCandidate != null) { if (usernameId == null && passwordId != null && usernameIdCandidate != null) {
@@ -143,19 +145,19 @@ class StructureParser(private val structure: AssistStructure) {
Log.d(TAG, "Autofill password hint") Log.d(TAG, "Autofill password hint")
return true return true
} }
it.contains("cc-name", true) -> { it.equals("cc-name", true) -> {
Log.d(TAG, "Autofill credit card name hint") Log.d(TAG, "Autofill credit card name hint")
result?.creditCardHolderId = autofillId result?.creditCardHolderId = autofillId
result?.creditCardHolder = node.autofillValue?.textValue?.toString() result?.creditCardHolder = node.autofillValue?.textValue?.toString()
} }
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_NUMBER, true) it.contains(View.AUTOFILL_HINT_CREDIT_CARD_NUMBER, true)
|| it.contains("cc-number", true) -> { || it.equals("cc-number", true) -> {
Log.d(TAG, "Autofill credit card number hint") Log.d(TAG, "Autofill credit card number hint")
result?.creditCardNumberId = autofillId result?.creditCardNumberId = autofillId
result?.creditCardNumber = node.autofillValue?.textValue?.toString() result?.creditCardNumber = node.autofillValue?.textValue?.toString()
} }
// expect date string as defined in https://html.spec.whatwg.org, e.g. 2014-12 // expect date string as defined in https://html.spec.whatwg.org, e.g. 2014-12
it.contains("cc-exp", true) -> { it.equals("cc-exp", true) -> {
Log.d(TAG, "Autofill credit card expiration date hint") Log.d(TAG, "Autofill credit card expiration date hint")
result?.creditCardExpirationDateId = autofillId result?.creditCardExpirationDateId = autofillId
node.autofillValue?.let { value -> node.autofillValue?.let { value ->
@@ -182,7 +184,7 @@ class StructureParser(private val structure: AssistStructure) {
} }
} }
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR, true) it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR, true)
|| it.contains("cc-exp-year", true) -> { || it.equals("cc-exp-year", true) -> {
Log.d(TAG, "Autofill credit card expiration year hint") Log.d(TAG, "Autofill credit card expiration year hint")
result?.creditCardExpirationYearId = autofillId result?.creditCardExpirationYearId = autofillId
if (node.autofillOptions != null) { if (node.autofillOptions != null) {
@@ -204,7 +206,7 @@ class StructureParser(private val structure: AssistStructure) {
} }
} }
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH, true) it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH, true)
|| it.contains("cc-exp-month", true) -> { || it.equals("cc-exp-month", true) -> {
Log.d(TAG, "Autofill credit card expiration month hint") Log.d(TAG, "Autofill credit card expiration month hint")
result?.creditCardExpirationMonthId = autofillId result?.creditCardExpirationMonthId = autofillId
if (node.autofillOptions != null) { if (node.autofillOptions != null) {
@@ -227,7 +229,7 @@ class StructureParser(private val structure: AssistStructure) {
} }
} }
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY, true) it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY, true)
|| it.contains("cc-exp-day", true) -> { || it.equals("cc-exp-day", true) -> {
Log.d(TAG, "Autofill credit card expiration day hint") Log.d(TAG, "Autofill credit card expiration day hint")
result?.creditCardExpirationDayId = autofillId result?.creditCardExpirationDayId = autofillId
if (node.autofillOptions != null) { if (node.autofillOptions != null) {
@@ -270,12 +272,12 @@ class StructureParser(private val structure: AssistStructure) {
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean { private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
val autofillId = node.autofillId val autofillId = node.autofillId
val nodHtml = node.htmlInfo val nodHtml = node.htmlInfo
when (nodHtml?.tag?.toLowerCase(Locale.ENGLISH)) { when (nodHtml?.tag?.lowercase(Locale.ENGLISH)) {
"input" -> { "input" -> {
nodHtml.attributes?.forEach { pairAttribute -> nodHtml.attributes?.forEach { pairAttribute ->
when (pairAttribute.first.toLowerCase(Locale.ENGLISH)) { when (pairAttribute.first.lowercase(Locale.ENGLISH)) {
"type" -> { "type" -> {
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) { when (pairAttribute.second.lowercase(Locale.ENGLISH)) {
"tel", "email" -> { "tel", "email" -> {
result?.usernameId = autofillId result?.usernameId = autofillId
result?.usernameValue = node.autofillValue result?.usernameValue = node.autofillValue
@@ -399,7 +401,6 @@ class StructureParser(private val structure: AssistStructure) {
class Result { class Result {
var isWebView: Boolean = false var isWebView: Boolean = false
var applicationId: String? = null var applicationId: String? = null
var webDomain: String? = null var webDomain: String? = null
set(value) { set(value) {
if (field == null) if (field == null)

View File

@@ -19,6 +19,7 @@
*/ */
package com.kunzisoft.keepass.biometric package com.kunzisoft.keepass.biometric
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@@ -27,9 +28,11 @@ import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.view.* import android.view.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.getkeepsafe.taptargetview.TapTargetView import com.getkeepsafe.taptargetview.TapTargetView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
@@ -39,6 +42,7 @@ import com.kunzisoft.keepass.database.exception.IODatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -59,9 +63,12 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
/** /**
* Manage setting to auto open biometric prompt * Manage setting to auto open biometric prompt
*/ */
private var mAutoOpenPrompt: Boolean = false private var mAutoOpenPrompt: Boolean
get() { get() {
return field && mAutoOpenPromptEnabled return mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt && mAutoOpenPromptEnabled
}
set(value) {
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = value
} }
// Variable to check if the prompt can be open (if the right activity is currently shown) // Variable to check if the prompt can be open (if the right activity is currently shown)
@@ -72,6 +79,8 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by activityViewModels()
// Only to fix multiple fingerprint menu #332 // Only to fix multiple fingerprint menu #332
private var mAllowAdvancedUnlockMenu = false private var mAllowAdvancedUnlockMenu = false
private var mAddBiometricMenuInProgress = false private var mAddBiometricMenuInProgress = false
@@ -79,6 +88,15 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
// Only keep connection when we request a device credential activity // Only keep connection when we request a device credential activity
private var keepConnection = false private var keepConnection = false
private var mDeviceCredentialResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
// To wait resume
if (keepConnection) {
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = result.resultCode == Activity.RESULT_OK
}
keepConnection = false
}
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
@@ -97,10 +115,21 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
retainInstance = true
setHasOptionsMenu(true) setHasOptionsMenu(true)
cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext) cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
mAdvancedUnlockViewModel.onInitAdvancedUnlockModeRequested.observe(this) {
initAdvancedUnlockMode()
}
mAdvancedUnlockViewModel.onUnlockAvailabilityCheckRequested.observe(this) {
checkUnlockAvailability()
}
mAdvancedUnlockViewModel.onDatabaseFileLoaded.observe(this) {
onDatabaseLoaded(it)
}
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@@ -114,17 +143,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
return rootView return rootView
} }
private data class ActivityResult(var requestCode: Int, var resultCode: Int, var data: Intent?)
private var activityResult: ActivityResult? = null
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// To wait resume
if (keepConnection) {
activityResult = ActivityResult(requestCode, resultCode, data)
}
keepConnection = false
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
context?.let { context?.let {
@@ -154,32 +172,38 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
fun loadDatabase(databaseUri: Uri?, autoOpenPrompt: Boolean) { private fun onDatabaseLoaded(databaseUri: Uri?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// To get device credential unlock result, only if same database uri // To get device credential unlock result, only if same database uri
if (databaseUri != null if (databaseUri != null
&& mAdvancedUnlockEnabled) { && mAdvancedUnlockEnabled) {
activityResult?.let { val deviceCredentialAuthSucceeded = mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded
deviceCredentialAuthSucceeded?.let {
if (databaseUri == databaseFileUri) { if (databaseUri == databaseFileUri) {
advancedUnlockManager?.onActivityResult(it.requestCode, it.resultCode) if (deviceCredentialAuthSucceeded == true) {
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationSucceeded()
} else {
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationFailed()
}
} else { } else {
disconnect() disconnect()
} }
} ?: run { } ?: run {
this.mAutoOpenPrompt = autoOpenPrompt if (databaseUri != databaseFileUri) {
connect(databaseUri) connect(databaseUri)
}
} }
} else { } else {
disconnect() disconnect()
} }
activityResult = null mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = null
} }
} }
/** /**
* Check unlock availability and change the current mode depending of device's state * Check unlock availability and change the current mode depending of device's state
*/ */
fun checkUnlockAvailability() { private fun checkUnlockAvailability() {
context?.let { context -> context?.let { context ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
allowOpenBiometricPrompt = true allowOpenBiometricPrompt = true
@@ -317,7 +341,8 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
if (cryptoPrompt.isDeviceCredentialOperation) if (cryptoPrompt.isDeviceCredentialOperation)
keepConnection = true keepConnection = true
try { try {
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt) advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt,
mDeviceCredentialResultLauncher)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to open advanced unlock prompt", e) Log.e(TAG, "Unable to open advanced unlock prompt", e)
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized) setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
@@ -369,8 +394,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} ?: throw Exception("AdvancedUnlockManager not initialized") } ?: throw Exception("AdvancedUnlockManager not initialized")
} }
@Synchronized private fun initAdvancedUnlockMode() {
fun initAdvancedUnlockMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mAllowAdvancedUnlockMenu = false mAllowAdvancedUnlockMenu = false
try { try {
@@ -444,6 +468,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
fun deleteEncryptedDatabaseKey() { fun deleteEncryptedDatabaseKey() {
mAllowAdvancedUnlockMenu = false
advancedUnlockManager?.closeBiometricPrompt() advancedUnlockManager?.closeBiometricPrompt()
databaseFileUri?.let { databaseUri -> databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) { cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
@@ -516,6 +541,11 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} }
} }
@RequiresApi(Build.VERSION_CODES.M)
override fun onUnrecoverableKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
}
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
override fun onInvalidKeyException(e: Exception) { override fun onInvalidKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key) setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)

View File

@@ -19,15 +19,18 @@
*/ */
package com.kunzisoft.keepass.biometric package com.kunzisoft.keepass.biometric
import android.app.Activity
import android.app.KeyguardManager import android.app.KeyguardManager
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.* import androidx.biometric.BiometricManager.Authenticators.*
@@ -35,6 +38,7 @@ import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import java.security.KeyStore import java.security.KeyStore
import java.security.UnrecoverableKeyException import java.security.UnrecoverableKeyException
@@ -136,18 +140,24 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
// and the constrains (purposes) in the constructor of the Builder // and the constrains (purposes) in the constructor of the Builder
keyGenerator?.init( keyGenerator?.init(
KeyGenParameterSpec.Builder( KeyGenParameterSpec.Builder(
ADVANCED_UNLOCK_KEYSTORE_KEY, ADVANCED_UNLOCK_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.apply {
// Require the user to authenticate with a fingerprint to authorize every use // Require the user to authenticate with a fingerprint to authorize every use
// of the key, don't use it for device credential because it's the user authentication // of the key, don't use it for device credential because it's the user authentication
.apply { if (biometricUnlockEnable) {
if (biometricUnlockEnable) { setUserAuthenticationRequired(true)
setUserAuthenticationRequired(true)
}
} }
.build()) // To store in the security chip
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
&& retrieveContext().packageManager.hasSystemFeature(
PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
setIsStrongBoxBacked(true)
}
}
.build())
keyGenerator?.generateKey() keyGenerator?.generateKey()
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -164,8 +174,12 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
return null return null
} }
fun initEncryptData(actionIfCypherInit fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) {
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) { initEncryptData(actionIfCypherInit, true)
}
private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
firstLaunch: Boolean) {
if (!isKeyManagerInitialized) { if (!isKeyManagerInitialized) {
return return
} }
@@ -185,10 +199,15 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} catch (unrecoverableKeyException: UnrecoverableKeyException) { } catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException) Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
advancedUnlockCallback?.onInvalidKeyException(unrecoverableKeyException) advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) { } catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException) Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException) if (firstLaunch) {
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
initEncryptData(actionIfCypherInit, false)
} else {
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to initialize encrypt data", e) Log.e(TAG, "Unable to initialize encrypt data", e)
advancedUnlockCallback?.onGenericException(e) advancedUnlockCallback?.onGenericException(e)
@@ -214,8 +233,14 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} }
fun initDecryptData(ivSpecValue: String, actionIfCypherInit fun initDecryptData(ivSpecValue: String,
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) { actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
initDecryptData(ivSpecValue, actionIfCypherInit, true)
}
private fun initDecryptData(ivSpecValue: String,
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
firstLaunch: Boolean = true) {
if (!isKeyManagerInitialized) { if (!isKeyManagerInitialized) {
return return
} }
@@ -239,10 +264,20 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} catch (unrecoverableKeyException: UnrecoverableKeyException) { } catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException) Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
deleteKeystoreKey() if (firstLaunch) {
deleteKeystoreKey()
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
} else {
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
}
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) { } catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException) Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException) if (firstLaunch) {
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
} else {
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to initialize decrypt data", e) Log.e(TAG, "Unable to initialize decrypt data", e)
advancedUnlockCallback?.onGenericException(e) advancedUnlockCallback?.onGenericException(e)
@@ -278,9 +313,9 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} }
@Suppress("DEPRECATION") fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt,
@Synchronized deviceCredentialResultLauncher: ActivityResultLauncher<Intent>
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) { ) {
// Init advanced unlock prompt // Init advanced unlock prompt
if (biometricPrompt == null) { if (biometricPrompt == null) {
biometricPrompt = BiometricPrompt(retrieveContext(), biometricPrompt = BiometricPrompt(retrieveContext(),
@@ -311,20 +346,10 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
else if (cryptoPrompt.isDeviceCredentialOperation) { else if (cryptoPrompt.isDeviceCredentialOperation) {
val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java) val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java)
retrieveContext().startActivityForResult( @Suppress("DEPRECATION")
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription), deviceCredentialResultLauncher.launch(
REQUEST_DEVICE_CREDENTIAL) keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription)
} )
}
@Synchronized
fun onActivityResult(requestCode: Int, resultCode: Int) {
if (requestCode == REQUEST_DEVICE_CREDENTIAL) {
if (resultCode == Activity.RESULT_OK) {
advancedUnlockCallback?.onAuthenticationSucceeded()
} else {
advancedUnlockCallback?.onAuthenticationFailed()
}
} }
} }
@@ -333,6 +358,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
interface AdvancedUnlockErrorCallback { interface AdvancedUnlockErrorCallback {
fun onUnrecoverableKeyException(e: Exception)
fun onInvalidKeyException(e: Exception) fun onInvalidKeyException(e: Exception)
fun onGenericException(e: Exception) fun onGenericException(e: Exception)
} }
@@ -355,8 +381,6 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7 private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
private const val REQUEST_DEVICE_CREDENTIAL = 556
@RequiresApi(api = Build.VERSION_CODES.M) @RequiresApi(api = Build.VERSION_CODES.M)
fun canAuthenticate(context: Context): Int { fun canAuthenticate(context: Context): Int {
return try { return try {
@@ -449,6 +473,10 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
override fun handleDecryptedResult(decryptedValue: String) {} override fun handleDecryptedResult(decryptedValue: String) {}
override fun onUnrecoverableKeyException(e: Exception) {
advancedCallback.onUnrecoverableKeyException(e)
}
override fun onInvalidKeyException(e: Exception) { override fun onInvalidKeyException(e: Exception) {
advancedCallback.onInvalidKeyException(e) advancedCallback.onInvalidKeyException(e)
} }
@@ -460,6 +488,33 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
deleteKeystoreKey() deleteKeystoreKey()
} }
} }
fun deleteAllEntryKeysInKeystoreForBiometric(activity: FragmentActivity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
deleteEntryKeyInKeystoreForBiometric(
activity,
object : AdvancedUnlockErrorCallback {
fun showException(e: Exception) {
Toast.makeText(activity,
activity.getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
Toast.LENGTH_SHORT).show()
}
override fun onUnrecoverableKeyException(e: Exception) {
showException(e)
}
override fun onInvalidKeyException(e: Exception) {
showException(e)
}
override fun onGenericException(e: Exception) {
showException(e)
}
})
}
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
}
} }
} }

View File

@@ -36,7 +36,11 @@ abstract class ActionNodeDatabaseRunnable(
abstract fun nodeAction() abstract fun nodeAction()
override fun onStartRun() { override fun onStartRun() {
nodeAction() try {
nodeAction()
} catch (e: Exception) {
setError(e)
}
super.onStartRun() super.onStartRun()
} }

View File

@@ -42,6 +42,9 @@ class UpdateGroupRunnable constructor(
// Update group with new values // Update group with new values
mNewGroup.touch(modified = true, touchParents = true) mNewGroup.touch(modified = true, touchParents = true)
if (database.rootGroup == mOldGroup) {
database.rootGroup = mNewGroup
}
// Only change data in index // Only change data in index
database.updateGroup(mNewGroup) database.updateGroup(mNewGroup)
} }
@@ -50,6 +53,9 @@ class UpdateGroupRunnable constructor(
override fun nodeFinish(): ActionNodesValues { override fun nodeFinish(): ActionNodesValues {
if (!result.isSuccess) { if (!result.isSuccess) {
// If we fail to save, back out changes to global structure // If we fail to save, back out changes to global structure
if (database.rootGroup == mNewGroup) {
database.rootGroup = mOldGroup
}
database.updateGroup(mOldGroup) database.updateGroup(mOldGroup)
} }

View File

@@ -192,17 +192,17 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
private val MIN_VERSION = UnsignedInt(0x10) private val MIN_VERSION = UnsignedInt(0x10)
private val MAX_VERSION = UnsignedInt(0x13) private val MAX_VERSION = UnsignedInt(0x13)
private val DEFAULT_ITERATIONS = UnsignedLong(2L) private val DEFAULT_ITERATIONS = UnsignedLong(3L)
private val MIN_ITERATIONS = UnsignedLong(1L) private val MIN_ITERATIONS = UnsignedLong(1L)
private val MAX_ITERATIONS = UnsignedLong(4294967295L) private val MAX_ITERATIONS = UnsignedLong(4294967295L)
private val DEFAULT_MEMORY = UnsignedLong((1024L * 1024L)) private val DEFAULT_MEMORY = UnsignedLong((1024L * 1024L * 16L))
private val MIN_MEMORY = UnsignedLong(1024L * 8L) private val MIN_MEMORY = UnsignedLong(1024L * 8L)
private val MAX_MEMORY = UnsignedInt.MAX_VALUE.toKotlinLong() private val MAX_MEMORY = UnsignedInt.MAX_VALUE.toKotlinLong()
private const val MEMORY_BLOCK_SIZE: Long = 1024L private const val MEMORY_BLOCK_SIZE: Long = 1024L
private val DEFAULT_PARALLELISM = UnsignedInt(2) private val DEFAULT_PARALLELISM = UnsignedInt(4)
private val MIN_PARALLELISM = UnsignedInt.fromKotlinLong(1L) private val MIN_PARALLELISM = UnsignedInt.fromKotlinLong(1L)
private val MAX_PARALLELISM = UnsignedInt.fromKotlinLong(((1 shl 24) - 1)) private val MAX_PARALLELISM = UnsignedInt.fromKotlinLong(((1 shl 24) - 1).toLong())
} }
} }

View File

@@ -68,7 +68,7 @@ abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>>
pwEntry.notes = getString(getColumnIndex(COLUMN_INDEX_NOTES)) pwEntry.notes = getString(getColumnIndex(COLUMN_INDEX_NOTES))
pwEntry.expiryTime = DateInstant(getString(getColumnIndex(COLUMN_INDEX_EXPIRY_TIME))) pwEntry.expiryTime = DateInstant(getString(getColumnIndex(COLUMN_INDEX_EXPIRY_TIME)))
pwEntry.expires = getString(getColumnIndex(COLUMN_INDEX_EXPIRES)) pwEntry.expires = getString(getColumnIndex(COLUMN_INDEX_EXPIRES))
.toLowerCase(Locale.ENGLISH) != "false" .lowercase(Locale.ENGLISH) != "false"
} }
companion object { companion object {

View File

@@ -22,10 +22,10 @@ package com.kunzisoft.keepass.database.element
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Build
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.database.action.node.NodeHandler import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
@@ -147,6 +147,10 @@ class Database {
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid) iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
} }
fun updateCustomIcon(customIcon: IconImageCustom) {
iconsManager.getIcon(customIcon.uuid).updateWith(customIcon)
}
fun getTemplates(templateCreation: Boolean): List<Template> { fun getTemplates(templateCreation: Boolean): List<Template> {
return mDatabaseKDBX?.getTemplates(templateCreation) ?: listOf() return mDatabaseKDBX?.getTemplates(templateCreation) ?: listOf()
} }
@@ -222,31 +226,33 @@ class Database {
mDatabaseKDBX?.descriptionChanged = DateInstant() mDatabaseKDBX?.descriptionChanged = DateInstant()
} }
val allowDefaultUsername: Boolean
get() = mDatabaseKDBX != null
// TODO get() = mDatabaseKDB != null || mDatabaseKDBX != null
var defaultUsername: String var defaultUsername: String
get() { get() {
return mDatabaseKDBX?.defaultUserName ?: "" // TODO mDatabaseKDB default username return mDatabaseKDB?.defaultUserName ?: mDatabaseKDBX?.defaultUserName ?: ""
} }
set(username) { set(username) {
mDatabaseKDB?.defaultUserName = username
mDatabaseKDBX?.defaultUserName = username mDatabaseKDBX?.defaultUserName = username
mDatabaseKDBX?.defaultUserNameChanged = DateInstant() mDatabaseKDBX?.defaultUserNameChanged = DateInstant()
} }
val allowCustomColor: Boolean var customColor: Int?
get() = mDatabaseKDBX != null
// TODO get() = mDatabaseKDB != null || mDatabaseKDBX != null
// with format "#000000"
var customColor: String
get() { get() {
return mDatabaseKDBX?.color ?: "" // TODO mDatabaseKDB color var colorInt: Int? = null
mDatabaseKDBX?.color?.let {
try {
colorInt = Color.parseColor(it)
} catch (e: Exception) {}
}
return mDatabaseKDB?.color ?: colorInt
} }
set(value) { set(value) {
// TODO Check color string mDatabaseKDB?.color = value
mDatabaseKDBX?.color = value mDatabaseKDBX?.color = if (value == null) {
""
} else {
ChromaUtil.getFormattedColorString(value, false)
}
} }
val allowOTP: Boolean val allowOTP: Boolean
@@ -360,7 +366,7 @@ class Database {
mDatabaseKDBX?.masterKey = masterKey mDatabaseKDBX?.masterKey = masterKey
} }
val rootGroup: Group? var rootGroup: Group?
get() { get() {
mDatabaseKDB?.rootGroup?.let { mDatabaseKDB?.rootGroup?.let {
return Group(it) return Group(it)
@@ -370,6 +376,25 @@ class Database {
} }
return null return null
} }
set(value) {
value?.groupKDB?.let { rootKDB ->
mDatabaseKDB?.rootGroup = rootKDB
}
value?.groupKDBX?.let { rootKDBX ->
mDatabaseKDBX?.rootGroup = rootKDBX
}
}
val rootGroupIsVirtual: Boolean
get() {
mDatabaseKDB?.let {
return true
}
mDatabaseKDBX?.let {
return false
}
return true
}
/** /**
* Do not modify groups here, used for read only * Do not modify groups here, used for read only

View File

@@ -19,8 +19,10 @@
*/ */
package com.kunzisoft.keepass.database.element package com.kunzisoft.keepass.database.element
import android.graphics.Color
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.database.element.binary.AttachmentPool import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
@@ -238,6 +240,42 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryKDBX?.notes = value entryKDBX?.notes = value
} }
var backgroundColor: Int?
get() {
var colorInt: Int? = null
entryKDBX?.backgroundColor?.let {
try {
colorInt = Color.parseColor(it)
} catch (e: Exception) {}
}
return colorInt
}
set(value) {
entryKDBX?.backgroundColor = if (value == null) {
""
} else {
ChromaUtil.getFormattedColorString(value, false)
}
}
var foregroundColor: Int?
get() {
var colorInt: Int? = null
entryKDBX?.foregroundColor?.let {
try {
colorInt = Color.parseColor(it)
} catch (e: Exception) {}
}
return colorInt
}
set(value) {
entryKDBX?.foregroundColor = if (value == null) {
""
} else {
ChromaUtil.getFormattedColorString(value, false)
}
}
private fun isTan(): Boolean { private fun isTan(): Boolean {
return title == PMS_TAN_ENTRY && username.isNotEmpty() return title == PMS_TAN_ENTRY && username.isNotEmpty()
} }
@@ -419,6 +457,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryInfo.expiryTime = expiryTime entryInfo.expiryTime = expiryTime
entryInfo.url = url entryInfo.url = url
entryInfo.notes = notes entryInfo.notes = notes
entryInfo.backgroundColor = backgroundColor
entryInfo.foregroundColor = foregroundColor
entryInfo.customFields = getExtraFields().toMutableList() entryInfo.customFields = getExtraFields().toMutableList()
// Add otpElement to generate token // Add otpElement to generate token
entryInfo.otpModel = getOtpElement()?.otpModel entryInfo.otpModel = getOtpElement()?.otpModel
@@ -453,6 +493,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
expiryTime = newEntryInfo.expiryTime expiryTime = newEntryInfo.expiryTime
url = newEntryInfo.url url = newEntryInfo.url
notes = newEntryInfo.notes notes = newEntryInfo.notes
backgroundColor = newEntryInfo.backgroundColor
foregroundColor = newEntryInfo.foregroundColor
addExtraFields(newEntryInfo.customFields) addExtraFields(newEntryInfo.customFields)
database?.attachmentPool?.let { binaryPool -> database?.attachmentPool?.let { binaryPool ->
newEntryInfo.attachments.forEach { attachment -> newEntryInfo.attachments.forEach { attachment ->

View File

@@ -31,6 +31,7 @@ import com.kunzisoft.keepass.database.element.node.*
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.GroupInfo import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UuidUtil
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@@ -177,16 +178,20 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
fun addChildrenFrom(group: Group) { fun addChildrenFrom(group: Group) {
group.groupKDB?.getChildEntries()?.forEach { entryToAdd -> group.groupKDB?.getChildEntries()?.forEach { entryToAdd ->
groupKDB?.addChildEntry(entryToAdd) groupKDB?.addChildEntry(entryToAdd)
entryToAdd.parent = groupKDB
} }
group.groupKDB?.getChildGroups()?.forEach { groupToAdd -> group.groupKDB?.getChildGroups()?.forEach { groupToAdd ->
groupKDB?.addChildGroup(groupToAdd) groupKDB?.addChildGroup(groupToAdd)
groupToAdd.parent = groupKDB
} }
group.groupKDBX?.getChildEntries()?.forEach { entryToAdd -> group.groupKDBX?.getChildEntries()?.forEach { entryToAdd ->
groupKDBX?.addChildEntry(entryToAdd) groupKDBX?.addChildEntry(entryToAdd)
entryToAdd.parent = groupKDBX
} }
group.groupKDBX?.getChildGroups()?.forEach { groupToAdd -> group.groupKDBX?.getChildGroups()?.forEach { groupToAdd ->
groupKDBX?.addChildGroup(groupToAdd) groupKDBX?.addChildGroup(groupToAdd)
groupToAdd.parent = groupKDBX
} }
} }
@@ -304,8 +309,9 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
val withoutMetaStream = filters.contains(ChildFilter.META_STREAM) val withoutMetaStream = filters.contains(ChildFilter.META_STREAM)
val showExpiredEntries = !filters.contains(ChildFilter.EXPIRED) val showExpiredEntries = !filters.contains(ChildFilter.EXPIRED)
// TODO Change KDB parser to remove meta entries
return groupKDB?.getChildEntries()?.filter { return groupKDB?.getChildEntries()?.filter {
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream)) (!withoutMetaStream || (withoutMetaStream && !it.isMetaStream()))
&& (!it.isCurrentlyExpires or showExpiredEntries) && (!it.isCurrentlyExpires or showExpiredEntries)
}?.map { }?.map {
Entry(it) Entry(it)
@@ -449,6 +455,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
fun getGroupInfo(): GroupInfo { fun getGroupInfo(): GroupInfo {
val groupInfo = GroupInfo() val groupInfo = GroupInfo()
groupInfo.id = groupKDBX?.nodeId?.id
groupInfo.title = title groupInfo.title = title
groupInfo.icon = icon groupInfo.icon = icon
groupInfo.creationTime = creationTime groupInfo.creationTime = creationTime

View File

@@ -31,6 +31,10 @@ class CustomIconPool(private val binaryCache: BinaryCache) : BinaryPool<UUID>(bi
return newUUID return newUUID
} }
fun getCustomIcon(key: UUID): IconImageCustom? {
return customIcons[key]
}
fun any(predicate: (IconImageCustom)-> Boolean): Boolean { fun any(predicate: (IconImageCustom)-> Boolean): Boolean {
return customIcons.any { predicate(it.value) } return customIcons.any { predicate(it.value) }
} }

View File

@@ -41,18 +41,16 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
private var kdfListV3: MutableList<KdfEngine> = ArrayList() private var kdfListV3: MutableList<KdfEngine> = ArrayList()
override val version: String override val version: String
get() = "KeePass 1" get() = "V1"
init { init {
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
rootGroup = createGroup().apply {
icon.standard = getStandardIcon(IconImageStandard.DATABASE_ID)
}
kdfListV3.add(KdfFactory.aesKdf) kdfListV3.add(KdfFactory.aesKdf)
} }
private fun getGroupById(groupId: Int): GroupKDB? {
if (groupId == -1)
return null
return getGroupById(NodeIdInt(groupId))
}
val backupGroup: GroupKDB? val backupGroup: GroupKDB?
get() { get() {
return retrieveBackup() return retrieveBackup()
@@ -63,6 +61,10 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return listOf(BACKUP_FOLDER_TITLE) return listOf(BACKUP_FOLDER_TITLE)
} }
var defaultUserName: String = ""
var color: Int? = null
override val kdfEngine: KdfEngine override val kdfEngine: KdfEngine
get() = kdfListV3[0] get() = kdfListV3[0]
@@ -77,11 +79,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return list return list
} }
val rootGroups: List<GroupKDB>
get() {
return rootGroup?.getChildGroups() ?: ArrayList()
}
override val passwordEncoding: String override val passwordEncoding: String
get() = "ISO-8859-1" get() = "ISO-8859-1"

View File

@@ -156,7 +156,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
FILE_VERSION_41 -> "4.1" FILE_VERSION_41 -> "4.1"
else -> "UNKNOWN" else -> "UNKNOWN"
} }
return "KeePass 2 - KDBX$kdbxStringVersion" return "V2 - KDBX$kdbxStringVersion"
} }
override val kdfEngine: KdfEngine? override val kdfEngine: KdfEngine?
@@ -414,37 +414,37 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} }
fun getEntryByTitle(title: String, recursionLevel: Int): EntryKDBX? { fun getEntryByTitle(title: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry -> return findEntry { entry ->
entry.decodeTitleKey(recursionLevel).equals(title, true) entry.decodeTitleKey(recursionLevel).equals(title, true)
} }
} }
fun getEntryByUsername(username: String, recursionLevel: Int): EntryKDBX? { fun getEntryByUsername(username: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry -> return findEntry { entry ->
entry.decodeUsernameKey(recursionLevel).equals(username, true) entry.decodeUsernameKey(recursionLevel).equals(username, true)
} }
} }
fun getEntryByURL(url: String, recursionLevel: Int): EntryKDBX? { fun getEntryByURL(url: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry -> return findEntry { entry ->
entry.decodeUrlKey(recursionLevel).equals(url, true) entry.decodeUrlKey(recursionLevel).equals(url, true)
} }
} }
fun getEntryByPassword(password: String, recursionLevel: Int): EntryKDBX? { fun getEntryByPassword(password: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry -> return findEntry { entry ->
entry.decodePasswordKey(recursionLevel).equals(password, true) entry.decodePasswordKey(recursionLevel).equals(password, true)
} }
} }
fun getEntryByNotes(notes: String, recursionLevel: Int): EntryKDBX? { fun getEntryByNotes(notes: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry -> return findEntry { entry ->
entry.decodeNotesKey(recursionLevel).equals(notes, true) entry.decodeNotesKey(recursionLevel).equals(notes, true)
} }
} }
fun getEntryByCustomData(customDataValue: String): EntryKDBX? { fun getEntryByCustomData(customDataValue: String): EntryKDBX? {
return entryIndexes.values.find { entry -> return findEntry { entry ->
entry.customData.containsItemWithValue(customDataValue) entry.customData.containsItemWithValue(customDataValue)
} }
} }

View File

@@ -67,7 +67,7 @@ abstract class DatabaseVersioned<
var changeDuplicateId = false var changeDuplicateId = false
private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>() private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>()
protected var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>() private var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
abstract val version: String abstract val version: String
@@ -89,6 +89,7 @@ abstract class DatabaseVersioned<
set(value) { set(value) {
field = value field = value
value?.let { value?.let {
removeGroupIndex(it)
addGroupIndex(it) addGroupIndex(it)
} }
} }
@@ -124,25 +125,29 @@ abstract class DatabaseVersioned<
@Throws(IOException::class) @Throws(IOException::class)
protected fun getFileKey(keyInputStream: InputStream): ByteArray { protected fun getFileKey(keyInputStream: InputStream): ByteArray {
val keyData = keyInputStream.readBytes() try {
val keyData = keyInputStream.readBytes()
// Check XML key file // Check XML key file
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData)) val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
if (xmlKeyByteArray != null) { if (xmlKeyByteArray != null) {
return xmlKeyByteArray return xmlKeyByteArray
}
// Check 32 bytes key file
when (keyData.size) {
32 -> return keyData
64 -> try {
return Hex.decodeHex(String(keyData).toCharArray())
} catch (ignoredException: Exception) {
// Key is not base 64, treat it as binary data
} }
// Check 32 bytes key file
when (keyData.size) {
32 -> return keyData
64 -> try {
return Hex.decodeHex(String(keyData).toCharArray())
} catch (ignoredException: Exception) {
// Key is not base 64, treat it as binary data
}
}
// Hash file as binary data
return HashManager.hashSha256(keyData)
} catch (outOfMemoryError: OutOfMemoryError) {
throw IOException("Keyfile data is too large", outOfMemoryError)
} }
// Hash file as binary data
return HashManager.hashSha256(keyData)
} }
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? { protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
@@ -194,12 +199,6 @@ abstract class DatabaseVersioned<
* ------------------------------------- * -------------------------------------
*/ */
fun doForEachGroupInIndex(action: (Group) -> Unit) {
for (group in groupIndexes) {
action.invoke(group.value)
}
}
/** /**
* Determine if an id number is already in use * Determine if an id number is already in use
* *
@@ -215,14 +214,7 @@ abstract class DatabaseVersioned<
return groupIndexes.values return groupIndexes.values
} }
fun setGroupIndexes(groupList: List<Group>) { open fun getGroupById(id: NodeId<GroupId>): Group? {
this.groupIndexes.clear()
for (currentGroup in groupList) {
this.groupIndexes[currentGroup.nodeId] = currentGroup
}
}
fun getGroupById(id: NodeId<GroupId>): Group? {
return this.groupIndexes[id] return this.groupIndexes[id]
} }
@@ -246,16 +238,6 @@ abstract class DatabaseVersioned<
this.groupIndexes.remove(group.nodeId) this.groupIndexes.remove(group.nodeId)
} }
fun numberOfGroups(): Int {
return groupIndexes.size
}
fun doForEachEntryInIndex(action: (Entry) -> Unit) {
for (entry in entryIndexes) {
action.invoke(entry.value)
}
}
fun isEntryIdUsed(id: NodeId<EntryId>): Boolean { fun isEntryIdUsed(id: NodeId<EntryId>): Boolean {
return entryIndexes.containsKey(id) return entryIndexes.containsKey(id)
} }
@@ -268,6 +250,10 @@ abstract class DatabaseVersioned<
return this.entryIndexes[id] return this.entryIndexes[id]
} }
fun findEntry(predicate: (Entry) -> Boolean): Entry? {
return this.entryIndexes.values.find(predicate)
}
fun addEntryIndex(entry: Entry) { fun addEntryIndex(entry: Entry) {
val entryId = entry.nodeId val entryId = entry.nodeId
if (entryIndexes.containsKey(entryId)) { if (entryIndexes.containsKey(entryId)) {
@@ -288,10 +274,6 @@ abstract class DatabaseVersioned<
this.entryIndexes.remove(entry.nodeId) this.entryIndexes.remove(entry.nodeId)
} }
fun numberOfEntries(): Int {
return entryIndexes.size
}
open fun clearCache() { open fun clearCache() {
this.groupIndexes.clear() this.groupIndexes.clear()
this.entryIndexes.clear() this.entryIndexes.clear()

View File

@@ -25,6 +25,7 @@ import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.binary.AttachmentPool import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
@@ -60,18 +61,43 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
private var binaryDataId: Int? = null private var binaryDataId: Int? = null
// Determine if this is a MetaStream entry // Determine if this is a MetaStream entry
val isMetaStream: Boolean fun isMetaStream(): Boolean {
get() { if (notes.isEmpty()) return false
if (notes.isEmpty()) return false if (binaryDescription != PMS_ID_BINDESC) return false
if (binaryDescription != PMS_ID_BINDESC) return false if (title.isEmpty()) return false
if (title.isEmpty()) return false if (title != PMS_ID_TITLE) return false
if (title != PMS_ID_TITLE) return false if (username.isEmpty()) return false
if (username.isEmpty()) return false if (username != PMS_ID_USER) return false
if (username != PMS_ID_USER) return false if (url.isEmpty()) return false
if (url.isEmpty()) return false if (url != PMS_ID_URL) return false
if (url != PMS_ID_URL) return false return icon.standard.id == KEY_ID
return icon.standard.id == KEY_ID }
}
fun isMetaStreamDefaultUsername(): Boolean {
return isMetaStream() && notes == PMS_STREAM_DEFAULTUSER
}
private fun setMetaStream() {
binaryDescription = PMS_ID_BINDESC
title = PMS_ID_TITLE
username = PMS_ID_USER
url = PMS_ID_URL
icon.standard = IconImageStandard(KEY_ID)
}
fun setMetaStreamDefaultUsername() {
notes = PMS_STREAM_DEFAULTUSER
setMetaStream()
}
fun isMetaStreamDatabaseColor(): Boolean {
return isMetaStream() && notes == PMS_STREAM_DBCOLOR
}
fun setMetaStreamDatabaseColor() {
notes = PMS_STREAM_DBCOLOR
setMetaStream()
}
override fun initNodeId(): NodeId<UUID> { override fun initNodeId(): NodeId<UUID> {
return NodeIdUUID() return NodeIdUUID()
@@ -184,6 +210,13 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
private const val PMS_ID_USER = "SYSTEM" private const val PMS_ID_USER = "SYSTEM"
private const val PMS_ID_URL = "$" private const val PMS_ID_URL = "$"
const val PMS_STREAM_SIMPLESTATE = "Simple UI State"
const val PMS_STREAM_DEFAULTUSER = "Default User Name"
const val PMS_STREAM_SEARCHHISTORYITEM = "Search History Item"
const val PMS_STREAM_CUSTOMKVP = "Custom KVP"
const val PMS_STREAM_DBCOLOR = "Database Color"
const val PMS_STREAM_KPXICON2 = "KPX_CUSTOM_ICONS_2"
@JvmField @JvmField
val CREATOR: Parcelable.Creator<EntryKDB> = object : Parcelable.Creator<EntryKDB> { val CREATOR: Parcelable.Creator<EntryKDB> = object : Parcelable.Creator<EntryKDB> {
override fun createFromParcel(parcel: Parcel): EntryKDB { override fun createFromParcel(parcel: Parcel): EntryKDB {

View File

@@ -32,6 +32,16 @@ class IconImageCustom : IconImageDraw {
var name: String = "" var name: String = ""
var lastModificationTime: DateInstant? = null var lastModificationTime: DateInstant? = null
fun updateWith(icon: IconImageCustom) {
this.name = icon.name
this.lastModificationTime = icon.lastModificationTime
}
constructor(copy: IconImageCustom) {
this.uuid = copy.uuid
updateWith(copy)
}
constructor(name: String = "", lastModificationTime: DateInstant? = null) { constructor(name: String = "", lastModificationTime: DateInstant? = null) {
this.uuid = DatabaseVersioned.UUID_ZERO this.uuid = DatabaseVersioned.UUID_ZERO
this.name = name this.name = name

View File

@@ -81,6 +81,7 @@ class IconImageStandard : IconImageDraw {
const val CREDIT_CARD_ID = 37 const val CREDIT_CARD_ID = 37
const val TRASH_ID = 43 const val TRASH_ID = 43
const val FOLDER_ID = 48 const val FOLDER_ID = 48
const val DATABASE_ID = 50
const val LIST_ID = 57 const val LIST_ID = 57
const val BUILD_ID = 59 const val BUILD_ID = 59
const val STAR_ID = 61 const val STAR_ID = 61

View File

@@ -65,7 +65,7 @@ class IconsManager(binaryCache: BinaryCache) {
} }
fun getIcon(iconUuid: UUID): IconImageCustom { fun getIcon(iconUuid: UUID): IconImageCustom {
return IconImageCustom(iconUuid) return customCache.getCustomIcon(iconUuid) ?: IconImageCustom(iconUuid)
} }
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean { fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {

View File

@@ -32,6 +32,19 @@ interface Node: NodeVersionedInterface<Group> {
fun removeParent() { fun removeParent() {
parent = null parent = null
} }
fun getPathString(): String {
val pathNodes = mutableListOf<Node>()
var currentNode = this
pathNodes.add(0, currentNode)
while (currentNode.containsParent()) {
currentNode.parent?.let { parent ->
currentNode = parent
pathNodes.add(0, currentNode)
}
}
return pathNodes.joinToString("/") { it.title }
}
} }
/** /**

View File

@@ -44,4 +44,6 @@ abstract class NodeId<Id> : Parcelable {
override fun hashCode(): Int { override fun hashCode(): Int {
return id?.hashCode() ?: 0 return id?.hashCode() ?: 0
} }
abstract fun toVisualString(): String?
} }

View File

@@ -63,6 +63,10 @@ class NodeIdInt : NodeId<Int> {
return id.toString() return id.toString()
} }
override fun toVisualString(): String? {
return null
}
companion object { companion object {
@JvmField @JvmField
val CREATOR: Parcelable.Creator<NodeIdInt> = object : Parcelable.Creator<NodeIdInt> { val CREATOR: Parcelable.Creator<NodeIdInt> = object : Parcelable.Creator<NodeIdInt> {

View File

@@ -64,6 +64,10 @@ class NodeIdUUID : NodeId<UUID> {
return UuidUtil.toHexString(id) ?: id.toString() return UuidUtil.toHexString(id) ?: id.toString()
} }
override fun toVisualString(): String {
return toString()
}
companion object { companion object {
@JvmField @JvmField
val CREATOR: Parcelable.Creator<NodeIdUUID> = object : Parcelable.Creator<NodeIdUUID> { val CREATOR: Parcelable.Creator<NodeIdUUID> = object : Parcelable.Creator<NodeIdUUID> {

View File

@@ -208,16 +208,8 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
when (attribute.type) { when (attribute.type) {
TemplateAttributeType.TEXT -> { TemplateAttributeType.TEXT -> {
try { try {
when (attribute.options.getNumberLines()) { // It's always a number of lines...
1 -> { attribute.options.setNumberLines(defaultOption.toInt())
// If one line, default attribute option is number of chars
attribute.options.setNumberChars(defaultOption.toInt())
}
else -> {
// else it's number of lines
attribute.options.setNumberLines(defaultOption.toInt())
}
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to transform default text option", e) Log.e(TAG, "Unable to transform default text option", e)
} }

View File

@@ -27,7 +27,7 @@ object TemplateField {
const val LABEL_DATE_OF_ISSUE = "Date of issue" const val LABEL_DATE_OF_ISSUE = "Date of issue"
const val LABEL_EMAIL = "Email" const val LABEL_EMAIL = "Email"
const val LABEL_EMAIL_ADDRESS = "Email address" const val LABEL_EMAIL_ADDRESS = "Email address"
const val LABEL_WIRELESS = "Wifi" const val LABEL_WIRELESS = "Wi-Fi"
const val LABEL_SSID = "SSID" const val LABEL_SSID = "SSID"
const val LABEL_TYPE = "Type" const val LABEL_TYPE = "Type"
const val LABEL_CRYPTOCURRENCY = "Cryptocurrency wallet" const val LABEL_CRYPTOCURRENCY = "Cryptocurrency wallet"

View File

@@ -19,8 +19,6 @@
*/ */
package com.kunzisoft.keepass.database.file package com.kunzisoft.keepass.database.file
import com.kunzisoft.keepass.utils.UnsignedInt
abstract class DatabaseHeader { abstract class DatabaseHeader {
/** /**
@@ -33,8 +31,4 @@ abstract class DatabaseHeader {
*/ */
var encryptionIV = ByteArray(16) var encryptionIV = ByteArray(16)
companion object {
val PWM_DBSIG_1 = UnsignedInt(-0x655d26fd)
}
} }

View File

@@ -34,7 +34,7 @@ class DatabaseHeaderKDB : DatabaseHeader() {
*/ */
var transformSeed = ByteArray(32) var transformSeed = ByteArray(32)
var signature1 = UnsignedInt(0) // = PWM_DBSIG_1 var signature1 = UnsignedInt(0) // = DBSIG_1
var signature2 = UnsignedInt(0) // = DBSIG_2 var signature2 = UnsignedInt(0) // = DBSIG_2
var flags= UnsignedInt(0) var flags= UnsignedInt(0)
var version= UnsignedInt(0) var version= UnsignedInt(0)
@@ -84,9 +84,9 @@ class DatabaseHeaderKDB : DatabaseHeader() {
companion object { companion object {
// DB sig from KeePass 1.03 // DB sig from KeePass 1.03
val DBSIG_2 = UnsignedInt(-0x4ab4049b) val DBSIG_1 = UnsignedInt(-0x655d26fd) // 0x9AA2D903
// DB sig from KeePass 1.03 val DBSIG_2 = UnsignedInt(-0x4ab4049b) // 0xB54BFB65
val DBVER_DW = UnsignedInt(0x00030003) val DBVER_DW = UnsignedInt(0x00030004)
val FLAG_SHA2 = UnsignedInt(1) val FLAG_SHA2 = UnsignedInt(1)
val FLAG_RIJNDAEL = UnsignedInt(2) val FLAG_RIJNDAEL = UnsignedInt(2)
@@ -97,7 +97,7 @@ class DatabaseHeaderKDB : DatabaseHeader() {
const val BUF_SIZE = 124 const val BUF_SIZE = 124
fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean { fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean {
return sig1.toKotlinInt() == PWM_DBSIG_1.toKotlinInt() && sig2.toKotlinInt() == DBSIG_2.toKotlinInt() return sig1.toKotlinInt() == DBSIG_1.toKotlinInt() && sig2.toKotlinInt() == DBSIG_2.toKotlinInt()
} }
fun compatibleHeaders(one: UnsignedInt, two: UnsignedInt): Boolean { fun compatibleHeaders(one: UnsignedInt, two: UnsignedInt): Boolean {

View File

@@ -311,8 +311,9 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
companion object { companion object {
val DBSIG_PRE2 = UnsignedInt(-0x4ab4049a) val DBSIG_1 = UnsignedInt(-0x655d26fd) // 0x9AA2D903
val DBSIG_2 = UnsignedInt(-0x4ab40499) val DBSIG_PRE2 = UnsignedInt(-0x4ab4049a) // 0xB54BFB66
val DBSIG_2 = UnsignedInt(-0x4ab40499) // 0xB54BFB67
private val FILE_VERSION_CRITICAL_MASK = UnsignedInt(-0x10000) private val FILE_VERSION_CRITICAL_MASK = UnsignedInt(-0x10000)
val FILE_VERSION_31 = UnsignedInt(0x00030001) val FILE_VERSION_31 = UnsignedInt(0x00030001)
@@ -335,7 +336,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
} }
fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean { fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean {
return sig1 == PWM_DBSIG_1 && (sig2 == DBSIG_PRE2 || sig2 == DBSIG_2) return sig1 == DBSIG_1 && (sig2 == DBSIG_PRE2 || sig2 == DBSIG_2)
} }
} }
} }

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.database.file.input package com.kunzisoft.keepass.database.file.input
import android.graphics.Color
import com.kunzisoft.encrypt.HashManager import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
@@ -30,7 +31,6 @@ 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.exception.* import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeader
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.utils.*
@@ -98,7 +98,7 @@ class DatabaseInputKDB(cacheDirectory: File,
if (fileSize != (contentSize + DatabaseHeaderKDB.BUF_SIZE)) if (fileSize != (contentSize + DatabaseHeaderKDB.BUF_SIZE))
throw IOException("Header corrupted") throw IOException("Header corrupted")
if (header.signature1 != DatabaseHeader.PWM_DBSIG_1 if (header.signature1 != DatabaseHeaderKDB.DBSIG_1
|| header.signature2 != DatabaseHeaderKDB.DBSIG_2) { || header.signature2 != DatabaseHeaderKDB.DBSIG_2) {
throw SignatureDatabaseException() throw SignatureDatabaseException()
} }
@@ -153,10 +153,6 @@ class DatabaseInputKDB(cacheDirectory: File,
) )
) )
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
val newRoot = mDatabase.createGroup()
mDatabase.rootGroup = newRoot
// Import all nodes // Import all nodes
val groupLevelList = HashMap<GroupKDB, Int>() val groupLevelList = HashMap<GroupKDB, Int>()
var newGroup: GroupKDB? = null var newGroup: GroupKDB? = null
@@ -303,7 +299,34 @@ class DatabaseInputKDB(cacheDirectory: File,
newGroup = null newGroup = null
} }
newEntry?.let { entry -> newEntry?.let { entry ->
mDatabase.addEntryIndex(entry) // Parse meta info
when {
entry.isMetaStreamDefaultUsername() -> {
var defaultUser = ""
entry.getBinary(mDatabase.attachmentPool)
?.getInputDataStream(mDatabase.binaryCache)?.use {
defaultUser = String(it.readBytes())
}
mDatabase.defaultUserName = defaultUser
}
entry.isMetaStreamDatabaseColor() -> {
var color: Int? = null
entry.getBinary(mDatabase.attachmentPool)
?.getInputDataStream(mDatabase.binaryCache)?.use {
val reverseColor = UnsignedInt(it.readBytes4ToUInt()).toKotlinInt()
color = Color.rgb(
Color.blue(reverseColor),
Color.green(reverseColor),
Color.red(reverseColor)
)
}
mDatabase.color = color
}
// TODO manager other meta stream
else -> {
mDatabase.addEntryIndex(entry)
}
}
currentEntryNumber++ currentEntryNumber++
newEntry = null newEntry = null
} }

View File

@@ -68,7 +68,7 @@ constructor(private val databaseKDBX: DatabaseKDBX,
@Throws(IOException::class) @Throws(IOException::class)
fun output() { fun output() {
mos.write4BytesUInt(DatabaseHeader.PWM_DBSIG_1) mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_1)
mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_2) mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_2)
mos.write4BytesUInt(header.version) mos.write4BytesUInt(header.version)
@@ -130,6 +130,6 @@ constructor(private val databaseKDBX: DatabaseKDBX,
} }
companion object { companion object {
private val EndHeaderValue = byteArrayOf('\r'.toByte(), '\n'.toByte(), '\r'.toByte(), '\n'.toByte()) private val EndHeaderValue = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte(), '\r'.code.toByte(), '\n'.code.toByte())
} }
} }

View File

@@ -19,9 +19,11 @@
*/ */
package com.kunzisoft.keepass.database.file.output package com.kunzisoft.keepass.database.file.output
import android.graphics.Color
import com.kunzisoft.encrypt.HashManager import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDB import com.kunzisoft.keepass.database.element.database.DatabaseKDB
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.exception.DatabaseOutputException import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.file.DatabaseHeader import com.kunzisoft.keepass.database.file.DatabaseHeader
@@ -34,7 +36,6 @@ import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
import java.security.* import java.security.*
import java.util.*
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.CipherOutputStream import javax.crypto.CipherOutputStream
@@ -44,6 +45,9 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
private var headerHashBlock: ByteArray? = null private var headerHashBlock: ByteArray? = null
private var mGroupList = mutableListOf<GroupKDB>()
private var mEntryList = mutableListOf<EntryKDB>()
@Throws(DatabaseOutputException::class) @Throws(DatabaseOutputException::class)
fun getFinalKey(header: DatabaseHeader): ByteArray? { fun getFinalKey(header: DatabaseHeader): ByteArray? {
try { try {
@@ -61,7 +65,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
// and remove any orphaned nodes that are no longer part of the tree hierarchy // and remove any orphaned nodes that are no longer part of the tree hierarchy
// also remove the virtual root not present in kdb // also remove the virtual root not present in kdb
val rootGroup = mDatabaseKDB.rootGroup val rootGroup = mDatabaseKDB.rootGroup
sortGroupsForOutput() sortNodesForOutput()
val header = outputHeader(mOutputStream) val header = outputHeader(mOutputStream)
@@ -91,6 +95,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
} finally { } finally {
// Add again the virtual root group for better management // Add again the virtual root group for better management
mDatabaseKDB.rootGroup = rootGroup mDatabaseKDB.rootGroup = rootGroup
clearParser()
} }
} }
@@ -105,7 +110,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDB { override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDB {
// Build header // Build header
val header = DatabaseHeaderKDB() val header = DatabaseHeaderKDB()
header.signature1 = DatabaseHeader.PWM_DBSIG_1 header.signature1 = DatabaseHeaderKDB.DBSIG_1
header.signature2 = DatabaseHeaderKDB.DBSIG_2 header.signature2 = DatabaseHeaderKDB.DBSIG_2
header.flags = DatabaseHeaderKDB.FLAG_SHA2 header.flags = DatabaseHeaderKDB.FLAG_SHA2
@@ -120,8 +125,9 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
} }
header.version = DatabaseHeaderKDB.DBVER_DW header.version = DatabaseHeaderKDB.DBVER_DW
header.numGroups = UnsignedInt(mDatabaseKDB.numberOfGroups()) // To remove root
header.numEntries = UnsignedInt(mDatabaseKDB.numberOfEntries()) header.numGroups = UnsignedInt(mGroupList.size)
header.numEntries = UnsignedInt(mEntryList.size)
header.numKeyEncRounds = UnsignedInt.fromKotlinLong(mDatabaseKDB.numberKeyEncryptionRounds) header.numKeyEncRounds = UnsignedInt.fromKotlinLong(mDatabaseKDB.numberKeyEncryptionRounds)
setIVs(header) setIVs(header)
@@ -194,31 +200,89 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
} }
// Groups // Groups
mDatabaseKDB.doForEachGroupInIndex { group -> mGroupList.forEach { group ->
GroupOutputKDB(group, outputStream).output() if (group != mDatabaseKDB.rootGroup) {
GroupOutputKDB(group, outputStream).output()
}
} }
// Entries // Entries
mDatabaseKDB.doForEachEntryInIndex { entry -> mEntryList.forEach { entry ->
EntryOutputKDB(mDatabaseKDB, entry, outputStream).output() EntryOutputKDB(mDatabaseKDB, entry, outputStream).output()
} }
} }
private fun sortGroupsForOutput() { private fun clearParser() {
val groupList = ArrayList<GroupKDB>() mGroupList.clear()
// Rebuild list according to sorting order removing any orphaned groups mEntryList.clear()
for (rootGroup in mDatabaseKDB.rootGroups) {
sortGroup(rootGroup, groupList)
}
mDatabaseKDB.setGroupIndexes(groupList)
} }
private fun sortGroup(group: GroupKDB, groupList: MutableList<GroupKDB>) { private fun sortNodesForOutput() {
clearParser()
// Rebuild list according to sorting order removing any orphaned groups
// Do not keep root
mDatabaseKDB.rootGroup?.getChildGroups()?.let { rootSubGroups ->
for (rootGroup in rootSubGroups) {
sortGroup(rootGroup)
}
}
}
private fun sortGroup(group: GroupKDB) {
// Add current tree // Add current tree
groupList.add(group) mGroupList.add(group)
for (childEntry in group.getChildEntries()) {
if (!childEntry.isMetaStreamDefaultUsername()
&& !childEntry.isMetaStreamDatabaseColor()) {
mEntryList.add(childEntry)
}
}
// Add MetaStream
if (mDatabaseKDB.defaultUserName.isNotEmpty()) {
val metaEntry = EntryKDB().apply {
setMetaStreamDefaultUsername()
setDefaultUsername(this)
}
mDatabaseKDB.addEntryTo(metaEntry, group)
mEntryList.add(metaEntry)
}
if (mDatabaseKDB.color != null) {
val metaEntry = EntryKDB().apply {
setMetaStreamDatabaseColor()
setDatabaseColor(this)
}
mDatabaseKDB.addEntryTo(metaEntry, group)
mEntryList.add(metaEntry)
}
// Recurse over children // Recurse over children
for (childGroup in group.getChildGroups()) { for (childGroup in group.getChildGroups()) {
sortGroup(childGroup, groupList) sortGroup(childGroup)
}
}
private fun setDefaultUsername(entryKDB: EntryKDB) {
val binaryData = mDatabaseKDB.buildNewAttachment()
entryKDB.putBinary(binaryData, mDatabaseKDB.attachmentPool)
BufferedOutputStream(binaryData.getOutputDataStream(mDatabaseKDB.binaryCache)).use { outputStream ->
outputStream.write(mDatabaseKDB.defaultUserName.toByteArray())
}
}
private fun setDatabaseColor(entryKDB: EntryKDB) {
val binaryData = mDatabaseKDB.buildNewAttachment()
entryKDB.putBinary(binaryData, mDatabaseKDB.attachmentPool)
BufferedOutputStream(binaryData.getOutputDataStream(mDatabaseKDB.binaryCache)).use { outputStream ->
var reversColor = Color.BLACK
mDatabaseKDB.color?.let {
reversColor = Color.rgb(
Color.blue(it),
Color.green(it),
Color.red(it)
)
}
outputStream.write4BytesUInt(UnsignedInt(reversColor))
} }
} }

View File

@@ -48,7 +48,6 @@ import com.kunzisoft.keepass.database.file.DateKDBXUtil
import com.kunzisoft.keepass.stream.HashedBlockOutputStream import com.kunzisoft.keepass.stream.HashedBlockOutputStream
import com.kunzisoft.keepass.stream.HmacBlockOutputStream import com.kunzisoft.keepass.stream.HmacBlockOutputStream
import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.utils.*
import org.joda.time.DateTime
import org.xmlpull.v1.XmlSerializer import org.xmlpull.v1.XmlSerializer
import java.io.IOException import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
@@ -765,7 +764,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
var character: Char var character: Char
for (element in text) { for (element in text) {
character = element character = element
val hexChar = character.toInt() val hexChar = character.code
if ( if (
hexChar in 0x20..0xD7FF || hexChar in 0x20..0xD7FF ||
hexChar == 0x9 || hexChar == 0x9 ||

View File

@@ -106,7 +106,7 @@ class SearchHelper {
} else if (TimeoutHelper.checkTime(context)) { } else if (TimeoutHelper.checkTime(context)) {
var searchWithoutUI = false var searchWithoutUI = false
if (PreferencesUtil.isAutofillAutoSearchEnable(context) if (PreferencesUtil.isAutofillAutoSearchEnable(context)
&& searchInfo != null && searchInfo != null && !searchInfo.manualSelection
&& !searchInfo.containsOnlyNullValues()) { && !searchInfo.containsOnlyNullValues()) {
// If search provide results // If search provide results
database.createVirtualGroupFromSearchInfo( database.createVirtualGroupFromSearchInfo(

View File

@@ -119,7 +119,7 @@ class GroupActivityEducation(activity: Activity)
.outerCircleColorInt(getCircleColor()) .outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha()) .outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor()) .textColorInt(getTextColor())
.tintTarget(true) .tintTarget(false)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {
override fun onTargetClick(view: TapTargetView) { override fun onTargetClick(view: TapTargetView) {

View File

@@ -38,9 +38,9 @@ class PasswordActivityEducation(activity: Activity)
activity.getString(R.string.education_unlock_summary)) activity.getString(R.string.education_unlock_summary))
.outerCircleColorInt(getCircleColor()) .outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha()) .outerCircleAlpha(getCircleAlpha())
.icon(ContextCompat.getDrawable(activity, R.mipmap.ic_launcher_round)) .icon(ContextCompat.getDrawable(activity, R.drawable.ic_lock_open_white_24dp))
.textColorInt(getTextColor()) .textColorInt(getTextColor())
.tintTarget(false) .tintTarget(true)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {
override fun onTargetClick(view: TapTargetView) { override fun onTargetClick(view: TapTargetView) {

View File

@@ -178,6 +178,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
assignKeyboardView() assignKeyboardView()
} }
override fun onEvaluateFullscreenMode(): Boolean {
return resources.getBoolean(R.bool.magikeyboard_allow_fullscreen_mode)
&& super.onEvaluateFullscreenMode()
}
private fun playVibration(keyCode: Int) { private fun playVibration(keyCode: Int) {
when (keyCode) { when (keyCode) {
Keyboard.KEYCODE_DELETE -> {} Keyboard.KEYCODE_DELETE -> {}
@@ -267,7 +272,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
if (entryInfoKey != null) { if (entryInfoKey != null) {
currentInputConnection.commitText(entryInfoKey!!.url, 1) currentInputConnection.commitText(entryInfoKey!!.url, 1)
} }
actionTabAutomatically() actionGoAutomatically()
} }
KEY_FIELDS -> { KEY_FIELDS -> {
if (entryInfoKey != null) { if (entryInfoKey != null) {

View File

@@ -40,6 +40,8 @@ class EntryInfo : NodeInfo {
var password: String = "" var password: String = ""
var url: String = "" var url: String = ""
var notes: String = "" var notes: String = ""
var backgroundColor: Int? = null
var foregroundColor: Int? = null
var customFields: MutableList<Field> = mutableListOf() var customFields: MutableList<Field> = mutableListOf()
var attachments: MutableList<Attachment> = mutableListOf() var attachments: MutableList<Attachment> = mutableListOf()
var otpModel: OtpModel? = null var otpModel: OtpModel? = null
@@ -53,6 +55,10 @@ class EntryInfo : NodeInfo {
password = parcel.readString() ?: password password = parcel.readString() ?: password
url = parcel.readString() ?: url url = parcel.readString() ?: url
notes = parcel.readString() ?: notes notes = parcel.readString() ?: notes
val readBgColor = parcel.readInt()
backgroundColor = if (readBgColor == -1) null else readBgColor
val readFgColor = parcel.readInt()
foregroundColor = if (readFgColor == -1) null else readFgColor
parcel.readList(customFields, Field::class.java.classLoader) parcel.readList(customFields, Field::class.java.classLoader)
parcel.readList(attachments, Attachment::class.java.classLoader) parcel.readList(attachments, Attachment::class.java.classLoader)
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
@@ -70,6 +76,8 @@ class EntryInfo : NodeInfo {
parcel.writeString(password) parcel.writeString(password)
parcel.writeString(url) parcel.writeString(url)
parcel.writeString(notes) parcel.writeString(notes)
parcel.writeInt(backgroundColor ?: -1)
parcel.writeInt(foregroundColor ?: -1)
parcel.writeList(customFields) parcel.writeList(customFields)
parcel.writeList(attachments) parcel.writeList(attachments)
parcel.writeParcelable(otpModel, flags) parcel.writeParcelable(otpModel, flags)
@@ -186,6 +194,43 @@ class EntryInfo : NodeInfo {
} }
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is EntryInfo) return false
if (!super.equals(other)) return false
if (id != other.id) return false
if (username != other.username) return false
if (password != other.password) return false
if (url != other.url) return false
if (notes != other.notes) return false
if (backgroundColor != other.backgroundColor) return false
if (foregroundColor != other.foregroundColor) return false
if (customFields != other.customFields) return false
if (attachments != other.attachments) return false
if (otpModel != other.otpModel) return false
if (isTemplate != other.isTemplate) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + id.hashCode()
result = 31 * result + username.hashCode()
result = 31 * result + password.hashCode()
result = 31 * result + url.hashCode()
result = 31 * result + notes.hashCode()
result = 31 * result + backgroundColor.hashCode()
result = 31 * result + foregroundColor.hashCode()
result = 31 * result + customFields.hashCode()
result = 31 * result + attachments.hashCode()
result = 31 * result + (otpModel?.hashCode() ?: 0)
result = 31 * result + isTemplate.hashCode()
return result
}
companion object { companion object {
const val WEB_DOMAIN_FIELD_NAME = "URL" const val WEB_DOMAIN_FIELD_NAME = "URL"

View File

@@ -1,12 +1,15 @@
package com.kunzisoft.keepass.model package com.kunzisoft.keepass.model
import android.os.Parcel import android.os.Parcel
import android.os.ParcelUuid
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.icon.IconImageStandard import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.FOLDER_ID import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.FOLDER_ID
import java.util.*
class GroupInfo : NodeInfo { class GroupInfo : NodeInfo {
var id: UUID? = null
var notes: String? = null var notes: String? = null
init { init {
@@ -16,14 +19,35 @@ class GroupInfo : NodeInfo {
constructor(): super() constructor(): super()
constructor(parcel: Parcel): super(parcel) { constructor(parcel: Parcel): super(parcel) {
id = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: id
notes = parcel.readString() notes = parcel.readString()
} }
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
super.writeToParcel(parcel, flags) super.writeToParcel(parcel, flags)
val uuid = if (id != null) ParcelUuid(id) else null
parcel.writeParcelable(uuid, flags)
parcel.writeString(notes) parcel.writeString(notes)
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is GroupInfo) return false
if (!super.equals(other)) return false
if (id != other.id) return false
if (notes != other.notes) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (id?.hashCode() ?: 0)
result = 31 * result + (notes?.hashCode() ?: 0)
return result
}
companion object CREATOR : Parcelable.Creator<GroupInfo> { companion object CREATOR : Parcelable.Creator<GroupInfo> {
override fun createFromParcel(parcel: Parcel): GroupInfo { override fun createFromParcel(parcel: Parcel): GroupInfo {
return GroupInfo(parcel) return GroupInfo(parcel)

View File

@@ -4,6 +4,8 @@ import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.DateInstant 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.utils.UuidUtil
import java.util.*
open class NodeInfo() : Parcelable { open class NodeInfo() : Parcelable {
@@ -36,6 +38,30 @@ open class NodeInfo() : Parcelable {
return 0 return 0
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is NodeInfo) return false
if (title != other.title) return false
if (icon != other.icon) return false
if (creationTime != other.creationTime) return false
if (lastModificationTime != other.lastModificationTime) return false
if (expires != other.expires) return false
if (expiryTime != other.expiryTime) return false
return true
}
override fun hashCode(): Int {
var result = title.hashCode()
result = 31 * result + icon.hashCode()
result = 31 * result + creationTime.hashCode()
result = 31 * result + lastModificationTime.hashCode()
result = 31 * result + expires.hashCode()
result = 31 * result + expiryTime.hashCode()
return result
}
companion object CREATOR : Parcelable.Creator<NodeInfo> { companion object CREATOR : Parcelable.Creator<NodeInfo> {
override fun createFromParcel(parcel: Parcel): NodeInfo { override fun createFromParcel(parcel: Parcel): NodeInfo {
return NodeInfo(parcel) return NodeInfo(parcel)

View File

@@ -14,7 +14,7 @@ import kotlinx.coroutines.launch
import mozilla.components.lib.publicsuffixlist.PublicSuffixList import mozilla.components.lib.publicsuffixlist.PublicSuffixList
class SearchInfo : ObjectNameResource, Parcelable { class SearchInfo : ObjectNameResource, Parcelable {
var manualSelection: Boolean = false
var applicationId: String? = null var applicationId: String? = null
set(value) { set(value) {
field = when { field = when {
@@ -42,6 +42,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
constructor() constructor()
constructor(toCopy: SearchInfo?) { constructor(toCopy: SearchInfo?) {
manualSelection = toCopy?.manualSelection ?: manualSelection
applicationId = toCopy?.applicationId applicationId = toCopy?.applicationId
webDomain = toCopy?.webDomain webDomain = toCopy?.webDomain
webScheme = toCopy?.webScheme webScheme = toCopy?.webScheme
@@ -49,6 +50,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
} }
private constructor(parcel: Parcel) { private constructor(parcel: Parcel) {
manualSelection = parcel.readByte().toInt() != 0
val readAppId = parcel.readString() val readAppId = parcel.readString()
applicationId = if (readAppId.isNullOrEmpty()) null else readAppId applicationId = if (readAppId.isNullOrEmpty()) null else readAppId
val readDomain = parcel.readString() val readDomain = parcel.readString()
@@ -64,6 +66,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
} }
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeByte((if (manualSelection) 1 else 0).toByte())
parcel.writeString(applicationId ?: "") parcel.writeString(applicationId ?: "")
parcel.writeString(webDomain ?: "") parcel.writeString(webDomain ?: "")
parcel.writeString(webScheme ?: "") parcel.writeString(webScheme ?: "")
@@ -88,10 +91,9 @@ class SearchInfo : ObjectNameResource, Parcelable {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (other !is SearchInfo) return false
other as SearchInfo
if (manualSelection != other.manualSelection) return false
if (applicationId != other.applicationId) return false if (applicationId != other.applicationId) return false
if (webDomain != other.webDomain) return false if (webDomain != other.webDomain) return false
if (webScheme != other.webScheme) return false if (webScheme != other.webScheme) return false
@@ -101,7 +103,8 @@ class SearchInfo : ObjectNameResource, Parcelable {
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = applicationId?.hashCode() ?: 0 var result = manualSelection.hashCode()
result = 31 * result + (applicationId?.hashCode() ?: 0)
result = 31 * result + (webDomain?.hashCode() ?: 0) result = 31 * result + (webDomain?.hashCode() ?: 0)
result = 31 * result + (webScheme?.hashCode() ?: 0) result = 31 * result + (webScheme?.hashCode() ?: 0)
result = 31 * result + (otpString?.hashCode() ?: 0) result = 31 * result + (otpString?.hashCode() ?: 0)

View File

@@ -234,7 +234,7 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
fun replaceBase32Chars(parameter: String): String { fun replaceBase32Chars(parameter: String): String {
// Add padding '=' at end if not Base32 length // Add padding '=' at end if not Base32 length
var parameterNewSize = parameter.toUpperCase(Locale.ENGLISH).removeSpaceChars() var parameterNewSize = parameter.uppercase(Locale.ENGLISH).removeSpaceChars()
while (parameterNewSize.length % 8 != 0) { while (parameterNewSize.length % 8 != 0) {
parameterNewSize += '=' parameterNewSize += '='
} }
@@ -264,7 +264,7 @@ enum class OtpTokenType {
companion object { companion object {
fun getFromString(tokenType: String): OtpTokenType { fun getFromString(tokenType: String): OtpTokenType {
return when (tokenType.toLowerCase(Locale.ENGLISH)) { return when (tokenType.lowercase(Locale.ENGLISH)) {
"s", "steam" -> STEAM "s", "steam" -> STEAM
"hotp" -> RFC4226 "hotp" -> RFC4226
else -> RFC6238 else -> RFC6238

View File

@@ -143,7 +143,7 @@ object OtpEntryFields {
if (otpPlainText != null && otpPlainText.isNotEmpty() && isOTPUri(otpPlainText)) { if (otpPlainText != null && otpPlainText.isNotEmpty() && isOTPUri(otpPlainText)) {
val uri = Uri.parse(otpPlainText.removeSpaceChars()) val uri = Uri.parse(otpPlainText.removeSpaceChars())
if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase(Locale.ENGLISH)) { if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.lowercase(Locale.ENGLISH)) {
Log.e(TAG, "Invalid or missing scheme in uri") Log.e(TAG, "Invalid or missing scheme in uri")
return false return false
} }
@@ -309,7 +309,7 @@ object OtpEntryFields {
} }
if (algorithmField != null) { if (algorithmField != null) {
otpElement.algorithm = otpElement.algorithm =
when (algorithmField.toUpperCase(Locale.ENGLISH)) { when (algorithmField.uppercase(Locale.ENGLISH)) {
TIMEOTP_ALGORITHM_SHA1_VALUE -> HashAlgorithm.SHA1 TIMEOTP_ALGORITHM_SHA1_VALUE -> HashAlgorithm.SHA1
TIMEOTP_ALGORITHM_SHA256_VALUE -> HashAlgorithm.SHA256 TIMEOTP_ALGORITHM_SHA256_VALUE -> HashAlgorithm.SHA256
TIMEOTP_ALGORITHM_SHA512_VALUE -> HashAlgorithm.SHA512 TIMEOTP_ALGORITHM_SHA512_VALUE -> HashAlgorithm.SHA512
@@ -417,7 +417,7 @@ object OtpEntryFields {
val output = HashMap<String, String>() val output = HashMap<String, String>()
for (element in elements) { for (element in elements) {
val pair = element.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() val pair = element.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
output[pair[0].toLowerCase(Locale.ENGLISH)] = pair[1] output[pair[0].lowercase(Locale.ENGLISH)] = pair[1]
} }
return output return output
} }

View File

@@ -0,0 +1,33 @@
package com.kunzisoft.keepass.receivers
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.util.Log
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.utils.DexUtil
import com.kunzisoft.keepass.utils.MagikeyboardUtil
class DexModeReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val enabled = when (intent?.action) {
"android.app.action.ENTER_KNOX_DESKTOP_MODE" -> {
Log.i(TAG, "Entered DeX mode")
false
}
"android.app.action.EXIT_KNOX_DESKTOP_MODE" -> {
Log.i(TAG, "Left DeX mode")
true
}
else -> return
}
MagikeyboardUtil.setEnabled(context!!, enabled)
}
companion object {
private val TAG = DexModeReceiver::class.java.name
}
}

View File

@@ -1,9 +1,11 @@
package com.kunzisoft.keepass.services package com.kunzisoft.keepass.services
import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
import android.content.* import android.content.*
import android.net.Uri import android.net.Uri
import android.os.Binder import android.os.Binder
import android.os.Build
import android.os.IBinder import android.os.IBinder
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
@@ -50,11 +52,20 @@ class AdvancedUnlockNotificationService : NotificationService() {
mTempCipherDao = ArrayList() mTempCipherDao = ArrayList()
} }
// It's simpler to use pendingIntent to perform REMOVE_ADVANCED_UNLOCK_KEY_ACTION
// because can be directly broadcast to another module or app
@SuppressLint("LaunchActivityFromNotification")
override fun onBind(intent: Intent): IBinder { override fun onBind(intent: Intent): IBinder {
super.onBind(intent) super.onBind(intent)
val pendingDeleteIntent = PendingIntent.getBroadcast(this, val pendingDeleteIntent = PendingIntent.getBroadcast(this,
4577, Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION), 0) 4577,
Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
})
val biometricUnlockEnabled = PreferencesUtil.isBiometricUnlockEnable(this) val biometricUnlockEnabled = PreferencesUtil.isBiometricUnlockEnable(this)
val notificationBuilder = buildNewNotification().apply { val notificationBuilder = buildNewNotification().apply {
setSmallIcon(if (biometricUnlockEnabled) { setSmallIcon(if (biometricUnlockEnabled) {

View File

@@ -24,6 +24,7 @@ import android.content.ContentResolver
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Binder import android.os.Binder
import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
@@ -188,20 +189,30 @@ class AttachmentFileNotificationService: LockNotificationService() {
private fun newNotification(attachmentNotification: AttachmentNotification) { private fun newNotification(attachmentNotification: AttachmentNotification) {
val pendingContentIntent = PendingIntent.getActivity(this, val pendingContentIntent = PendingIntent.getActivity(this,
0, 0,
Intent().apply { Intent().apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
setDataAndType(attachmentNotification.uri, setDataAndType(attachmentNotification.uri,
contentResolver.getType(attachmentNotification.uri)) contentResolver.getType(attachmentNotification.uri))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}, PendingIntent.FLAG_CANCEL_CURRENT) }, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
}
)
val pendingDeleteIntent = PendingIntent.getService(this, val pendingDeleteIntent = PendingIntent.getService(this,
0, 0,
Intent(this, AttachmentFileNotificationService::class.java).apply { Intent(this, AttachmentFileNotificationService::class.java).apply {
// No action to delete the service // No action to delete the service
putExtra(FILE_URI_KEY, attachmentNotification.uri) putExtra(FILE_URI_KEY, attachmentNotification.uri)
}, PendingIntent.FLAG_CANCEL_CURRENT) }, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
}
)
val fileName = UriUtil.getFileData(this, attachmentNotification.uri)?.name val fileName = UriUtil.getFileData(this, attachmentNotification.uri)?.name
?: attachmentNotification.uri.path ?: attachmentNotification.uri.path

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.services
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.os.Build
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
@@ -112,7 +113,13 @@ class ClipboardEntryNotificationService : LockNotificationService() {
putParcelableArrayListExtra(EXTRA_CLIPBOARD_FIELDS, fieldsToAdd) putParcelableArrayListExtra(EXTRA_CLIPBOARD_FIELDS, fieldsToAdd)
} }
return PendingIntent.getService( return PendingIntent.getService(
this, 0, copyIntent, PendingIntent.FLAG_UPDATE_CURRENT) this, 0, copyIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
} }
private fun newNotification(title: String?, fieldsToAdd: ArrayList<ClipboardEntryNotificationField>) { private fun newNotification(title: String?, fieldsToAdd: ArrayList<ClipboardEntryNotificationField>) {
@@ -162,7 +169,13 @@ class ClipboardEntryNotificationService : LockNotificationService() {
val cleanIntent = Intent(this, ClipboardEntryNotificationService::class.java) val cleanIntent = Intent(this, ClipboardEntryNotificationService::class.java)
cleanIntent.action = ACTION_CLEAN_CLIPBOARD cleanIntent.action = ACTION_CLEAN_CLIPBOARD
val cleanPendingIntent = PendingIntent.getService( val cleanPendingIntent = PendingIntent.getService(
this, 0, cleanIntent, PendingIntent.FLAG_UPDATE_CURRENT) this, 0, cleanIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
builder.setDeleteIntent(cleanPendingIntent) builder.setDeleteIntent(cleanPendingIntent)
//Get settings //Get settings

View File

@@ -24,6 +24,7 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.* import android.os.*
import android.util.Log import android.util.Log
import androidx.media.app.NotificationCompat
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.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
@@ -407,11 +408,21 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
this, this,
0, 0,
Intent(this, GroupActivity::class.java), Intent(this, GroupActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
) )
val pendingDeleteIntent = PendingIntent.getBroadcast( val pendingDeleteIntent = PendingIntent.getBroadcast(
this, this,
4576, Intent(LOCK_ACTION), 0 4576,
Intent(LOCK_ACTION),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
) )
// Add actions in notifications // Add actions in notifications
notificationBuilder.apply { notificationBuilder.apply {
@@ -420,9 +431,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
// Unfortunately swipe is disabled in lollipop+ // Unfortunately swipe is disabled in lollipop+
setDeleteIntent(pendingDeleteIntent) setDeleteIntent(pendingDeleteIntent)
addAction( addAction(
R.drawable.ic_lock_white_24dp, getString(R.string.lock), R.drawable.ic_lock_database_white_32dp, getString(R.string.lock),
pendingDeleteIntent pendingDeleteIntent
) )
// Won't work with Xiaomi and Kitkat
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
setStyle(
NotificationCompat.MediaStyle()
.setShowActionsInCompactView(0)
)
}
} }
} }
} }

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.services
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.os.Build
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@@ -93,7 +94,13 @@ class KeyboardEntryNotificationService : LockNotificationService() {
val deleteIntent = Intent(this, KeyboardEntryNotificationService::class.java).apply { val deleteIntent = Intent(this, KeyboardEntryNotificationService::class.java).apply {
action = ACTION_CLEAN_KEYBOARD_ENTRY action = ACTION_CLEAN_KEYBOARD_ENTRY
} }
pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT) pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
val builder = buildNewNotification() val builder = buildNewNotification()
.setSmallIcon(R.drawable.notification_ic_keyboard_key_24dp) .setSmallIcon(R.drawable.notification_ic_keyboard_key_24dp)

View File

@@ -57,6 +57,7 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
} }
if (dialogFragment != null) { if (dialogFragment != null) {
@Suppress("DEPRECATION")
dialogFragment.setTargetFragment(this, 0) dialogFragment.setTargetFragment(this, 0)
dialogFragment.show(parentFragmentManager, TAG_AUTOFILL_PREF_FRAGMENT) dialogFragment.show(parentFragmentManager, TAG_AUTOFILL_PREF_FRAGMENT)
} }

View File

@@ -48,6 +48,7 @@ class MagikeyboardSettingsFragment : PreferenceFragmentCompat() {
} }
if (dialogFragment != null) { if (dialogFragment != null) {
@Suppress("DEPRECATION")
dialogFragment.setTargetFragment(this, 0) dialogFragment.setTargetFragment(this, 0)
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT) dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
} }

View File

@@ -40,7 +40,6 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.ProFeatureDialogFragment import com.kunzisoft.keepass.activities.dialogs.ProFeatureDialogFragment
import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment
import com.kunzisoft.keepass.activities.stylish.Stylish import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.education.Education import com.kunzisoft.keepass.education.Education
@@ -157,7 +156,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE) val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
intent.data = Uri.parse("package:com.kunzisoft.keepass.autofill.KeeAutofillService") intent.data = Uri.parse("package:com.kunzisoft.keepass.autofill.KeeAutofillService")
Log.d(javaClass.name, "Autofill enable service: intent=$intent") Log.d(javaClass.name, "Autofill enable service: intent=$intent")
startActivityForResult(intent, REQUEST_CODE_AUTOFILL) startActivity(intent)
} else { } else {
Log.d(javaClass.name, "Autofill service already enabled.") Log.d(javaClass.name, "Autofill service already enabled.")
} }
@@ -366,26 +365,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
) { _, _ -> ) { _, _ ->
validate?.invoke() validate?.invoke()
deleteKeysAlertDialog?.setOnDismissListener(null) deleteKeysAlertDialog?.setOnDismissListener(null)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
AdvancedUnlockManager.deleteEntryKeyInKeystoreForBiometric(
activity,
object : AdvancedUnlockManager.AdvancedUnlockErrorCallback {
fun showException(e: Exception) {
Toast.makeText(context,
getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
Toast.LENGTH_SHORT).show()
}
override fun onInvalidKeyException(e: Exception) {
showException(e)
}
override fun onGenericException(e: Exception) {
showException(e)
}
})
}
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
} }
.setNegativeButton(resources.getString(android.R.string.cancel) .setNegativeButton(resources.getString(android.R.string.cancel)
) { _, _ ->} ) { _, _ ->}
@@ -472,7 +452,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
getString(R.string.show_uuid_key), getString(R.string.show_uuid_key),
getString(R.string.enable_education_screens_key), getString(R.string.enable_education_screens_key),
getString(R.string.reset_education_screens_key) -> { getString(R.string.reset_education_screens_key) -> {
DATABASE_APPEARANCE_PREFERENCE_CHANGED = true DATABASE_PREFERENCE_CHANGED = true
} }
} }
return super.onPreferenceTreeClick(preference) return super.onPreferenceTreeClick(preference)
@@ -494,6 +474,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
} }
if (dialogFragment != null) { if (dialogFragment != null) {
@Suppress("DEPRECATION")
dialogFragment.setTargetFragment(this, 0) dialogFragment.setTargetFragment(this, 0)
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT) dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
} }
@@ -533,9 +514,8 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
} }
companion object { companion object {
private const val REQUEST_CODE_AUTOFILL = 5201
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT" private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
var DATABASE_APPEARANCE_PREFERENCE_CHANGED = false var DATABASE_PREFERENCE_CHANGED = false
} }
} }

View File

@@ -30,8 +30,8 @@ import androidx.preference.PreferenceCategory
import androidx.preference.SwitchPreference import androidx.preference.SwitchPreference
import com.kunzisoft.androidclearchroma.ChromaUtil import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
@@ -57,7 +57,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
private var dbNamePref: InputTextPreference? = null private var dbNamePref: InputTextPreference? = null
private var dbDescriptionPref: InputTextPreference? = null private var dbDescriptionPref: InputTextPreference? = null
private var dbDefaultUsername: InputTextPreference? = null private var dbDefaultUsernamePref: InputTextPreference? = null
private var dbCustomColorPref: DialogColorPreference? = null private var dbCustomColorPref: DialogColorPreference? = null
private var dbDataCompressionPref: Preference? = null private var dbDataCompressionPref: Preference? = null
private var recycleBinGroupPref: DialogListExplanationPreference? = null private var recycleBinGroupPref: DialogListExplanationPreference? = null
@@ -164,29 +164,20 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} }
// Database default username // Database default username
dbDefaultUsername = findPreference(getString(R.string.database_default_username_key)) dbDefaultUsernamePref = findPreference(getString(R.string.database_default_username_key))
if (database.allowDefaultUsername) { dbDefaultUsernamePref?.summary = database.defaultUsername
dbDefaultUsername?.summary = database.defaultUsername
} else {
dbDefaultUsername?.isEnabled = false
// TODO dbGeneralPrefCategory?.removePreference(dbDefaultUsername)
}
// Database custom color // Database custom color
dbCustomColorPref = findPreference(getString(R.string.database_custom_color_key)) dbCustomColorPref = findPreference(getString(R.string.database_custom_color_key))
if (database.allowCustomColor) { dbCustomColorPref?.apply {
dbCustomColorPref?.apply { val customColor = database.customColor
try { if (customColor != null) {
color = Color.parseColor(database.customColor) color = customColor
summary = database.customColor summary = ChromaUtil.getFormattedColorString(customColor, false)
} catch (e: Exception) { } else{
color = DialogColorPreference.DISABLE_COLOR color = DialogColorPreference.DISABLE_COLOR
summary = "" summary = ""
}
} }
} else {
dbCustomColorPref?.isEnabled = false
// TODO dbGeneralPrefCategory?.removePreference(dbCustomColorPref)
} }
// Version // Version
@@ -348,12 +339,13 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} }
} }
private val colorSelectedListener: ((Boolean, Int)-> Unit) = { enable, color -> private val colorSelectedListener: ((Int?)-> Unit) = { color ->
dbCustomColorPref?.summary = ChromaUtil.getFormattedColorString(color, false) if (color != null) {
if (enable) {
dbCustomColorPref?.color = color dbCustomColorPref?.color = color
dbCustomColorPref?.summary = ChromaUtil.getFormattedColorString(color, false)
} else { } else {
dbCustomColorPref?.color = DialogColorPreference.DISABLE_COLOR dbCustomColorPref?.color = DialogColorPreference.DISABLE_COLOR
dbCustomColorPref?.summary = ""
} }
} }
@@ -416,7 +408,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
mDatabase?.defaultUsername = oldDefaultUsername mDatabase?.defaultUsername = oldDefaultUsername
oldDefaultUsername oldDefaultUsername
} }
dbDefaultUsername?.summary = defaultUsernameToShow dbDefaultUsernamePref?.summary = defaultUsernameToShow
} }
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_COLOR_TASK -> { DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_COLOR_TASK -> {
val oldColor = data.getString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY)!! val oldColor = data.getString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY)!!
@@ -426,7 +418,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newColor newColor
} else { } else {
mDatabase?.customColor = oldColor mDatabase?.customColor = Color.parseColor(oldColor)
oldColor oldColor
} }
dbCustomColorPref?.summary = defaultColorToShow dbCustomColorPref?.summary = defaultColorToShow
@@ -632,6 +624,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} }
if (dialogFragment != null && !mDatabaseReadOnly) { if (dialogFragment != null && !mDatabaseReadOnly) {
@Suppress("DEPRECATION")
dialogFragment.setTargetFragment(this, 0) dialogFragment.setTargetFragment(this, 0)
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT) dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
} }
@@ -680,6 +673,27 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} }
} }
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
// To reload group when database settings are modified
when (preference?.key) {
getString(R.string.database_name_key),
getString(R.string.database_description_key),
getString(R.string.database_default_username_key),
getString(R.string.database_custom_color_key),
getString(R.string.database_data_compression_key),
getString(R.string.database_data_remove_unlinked_attachments_key),
getString(R.string.recycle_bin_enable_key),
getString(R.string.recycle_bin_group_key),
getString(R.string.templates_group_enable_key),
getString(R.string.templates_group_uuid_key),
getString(R.string.max_history_items_key),
getString(R.string.max_history_size_key) -> {
NestedAppSettingsFragment.DATABASE_PREFERENCE_CHANGED = true
}
}
return super.onPreferenceTreeClick(preference)
}
companion object { companion object {
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT" private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
} }

View File

@@ -493,6 +493,12 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.autofill_inline_suggestions_default)) context.resources.getBoolean(R.bool.autofill_inline_suggestions_default))
} }
fun isAutofillManualSelectionEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.autofill_manual_selection_key),
context.resources.getBoolean(R.bool.autofill_manual_selection_default))
}
fun isAutofillSaveSearchInfoEnable(context: Context): Boolean { fun isAutofillSaveSearchInfoEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.autofill_save_search_info_key), return prefs.getBoolean(context.getString(R.string.autofill_save_search_info_key),
@@ -630,6 +636,7 @@ object PreferencesUtil {
context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_auto_search_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.autofill_auto_search_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_save_search_info_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.autofill_save_search_info_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_ask_to_save_data_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.autofill_ask_to_save_data_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_application_id_blocklist_key) -> editor.putStringSet(name, getStringSetFromProperties(value)) context.getString(R.string.autofill_application_id_blocklist_key) -> editor.putStringSet(name, getStringSetFromProperties(value))

View File

@@ -49,7 +49,6 @@ open class SettingsActivity
private var backupManager: BackupManager? = null private var backupManager: BackupManager? = null
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var appPropertiesFileCreationRequestCode: Int? = null
private var coordinatorLayout: CoordinatorLayout? = null private var coordinatorLayout: CoordinatorLayout? = null
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
@@ -64,6 +63,41 @@ open class SettingsActivity
toolbar = findViewById(R.id.toolbar) toolbar = findViewById(R.id.toolbar)
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { selectedFileUri ->
// Import app properties result
try {
selectedFileUri?.let { uri ->
val appProperties = Properties()
contentResolver?.openInputStream(uri)?.use { inputStream ->
appProperties.load(inputStream)
}
PreferencesUtil.setAppProperties(this, appProperties)
// Restart the current activity
reloadActivity()
Toast.makeText(this, R.string.success_import_app_properties, Toast.LENGTH_LONG).show()
}
} catch (e: Exception) {
Toast.makeText(this, R.string.error_import_app_properties, Toast.LENGTH_LONG).show()
Log.e(TAG, "Unable to import app properties", e)
}
}
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
// Export app properties result
try {
createdFileUri?.let { uri ->
contentResolver?.openOutputStream(uri)?.use { outputStream ->
PreferencesUtil
.getAppProperties(this)
.store(outputStream, getString(R.string.description_app_properties))
}
Toast.makeText(this, R.string.success_export_app_properties, Toast.LENGTH_LONG).show()
}
} catch (e: Exception) {
Toast.makeText(this, R.string.error_export_app_properties, Toast.LENGTH_LONG).show()
Log.e(DatabaseLockActivity.TAG, "Unable to export app properties", e)
}
}
if (savedInstanceState?.getString(TITLE_KEY).isNullOrEmpty()) if (savedInstanceState?.getString(TITLE_KEY).isNullOrEmpty())
toolbar?.setTitle(R.string.settings) toolbar?.setTitle(R.string.settings)
@@ -217,54 +251,10 @@ open class SettingsActivity
} }
fun exportAppProperties() { fun exportAppProperties() {
appPropertiesFileCreationRequestCode = mExternalFileHelper?.createDocument(getString(R.string.app_properties_file_name, mExternalFileHelper?.createDocument(getString(R.string.app_properties_file_name,
DateTime.now().toLocalDateTime().toString("yyyy-MM-dd'_'HH-mm"))) DateTime.now().toLocalDateTime().toString("yyyy-MM-dd'_'HH-mm")))
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// Import app properties result
try {
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { selectedFileUri ->
selectedFileUri?.let { uri ->
val appProperties = Properties()
contentResolver?.openInputStream(uri)?.use { inputStream ->
appProperties.load(inputStream)
}
PreferencesUtil.setAppProperties(this, appProperties)
// Restart the current activity
reloadActivity()
Toast.makeText(this, R.string.success_import_app_properties, Toast.LENGTH_LONG).show()
}
}
} catch (e: Exception) {
Toast.makeText(this, R.string.error_import_app_properties, Toast.LENGTH_LONG).show()
Log.e(TAG, "Unable to import app properties", e)
}
// Export app properties result
try {
if (requestCode == appPropertiesFileCreationRequestCode) {
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
createdFileUri?.let { uri ->
contentResolver?.openOutputStream(uri)?.use { outputStream ->
PreferencesUtil
.getAppProperties(this)
.store(outputStream, getString(R.string.description_app_properties))
}
Toast.makeText(this, R.string.success_export_app_properties, Toast.LENGTH_LONG).show()
}
}
appPropertiesFileCreationRequestCode = null
}
} catch (e: Exception) {
Toast.makeText(this, R.string.error_export_app_properties, Toast.LENGTH_LONG).show()
Log.e(DatabaseLockActivity.TAG, "Unable to export app properties", e)
}
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)

View File

@@ -41,7 +41,7 @@ class DialogColorPreference @JvmOverloads constructor(context: Context,
} }
override fun getDialogLayoutResource(): Int { override fun getDialogLayoutResource(): Int {
return R.layout.pref_dialog_input_color return R.layout.fragment_color_picker
} }
companion object { companion object {

View File

@@ -30,27 +30,57 @@ import android.view.Window
import android.widget.CompoundButton import android.widget.CompoundButton
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.androidclearchroma.IndicatorMode import com.kunzisoft.androidclearchroma.IndicatorMode
import com.kunzisoft.androidclearchroma.colormode.ColorMode import com.kunzisoft.androidclearchroma.colormode.ColorMode
import com.kunzisoft.androidclearchroma.fragment.ChromaColorFragment import com.kunzisoft.androidclearchroma.fragment.ChromaColorFragment
import com.kunzisoft.androidclearchroma.fragment.ChromaColorFragment.* import com.kunzisoft.androidclearchroma.fragment.ChromaColorFragment.*
import com.kunzisoft.androidclearchroma.view.ChromaColorView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.ColorPickerDialogFragment
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
private lateinit var rootView: View private lateinit var rootView: View
private lateinit var enableSwitchView: CompoundButton private lateinit var enableSwitchView: CompoundButton
private var chromaColorFragment: ChromaColorFragment? = null private lateinit var chromaColorView: ChromaColorView
var onColorSelectedListener: ((enable: Boolean, color: Int) -> Unit)? = null var onColorSelectedListener: ((color: Int?) -> Unit)? = null
private var mDefaultColor = Color.WHITE
private var mActivated = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val alertDialogBuilder = AlertDialog.Builder(requireActivity()) val alertDialogBuilder = AlertDialog.Builder(requireActivity())
rootView = requireActivity().layoutInflater.inflate(R.layout.pref_dialog_input_color, null) rootView = requireActivity().layoutInflater.inflate(R.layout.fragment_color_picker, null)
enableSwitchView = rootView.findViewById(R.id.switch_element) enableSwitchView = rootView.findViewById(R.id.switch_element)
chromaColorView = rootView.findViewById(R.id.chroma_color_view)
if (savedInstanceState != null) {
if (savedInstanceState.containsKey(ARG_INITIAL_COLOR)) {
mDefaultColor = savedInstanceState.getInt(ARG_INITIAL_COLOR)
}
if (savedInstanceState.containsKey(ARG_ACTIVATED)) {
mActivated = savedInstanceState.getBoolean(ARG_ACTIVATED)
}
} else {
arguments?.apply {
if (containsKey(ARG_INITIAL_COLOR)) {
mDefaultColor = getInt(ARG_INITIAL_COLOR)
}
if (containsKey(ARG_ACTIVATED)) {
mActivated = getBoolean(ARG_ACTIVATED)
}
}
}
enableSwitchView.isChecked = mActivated
chromaColorView.currentColor = mDefaultColor
chromaColorView.setOnColorChangedListener {
if (!enableSwitchView.isChecked)
enableSwitchView.isChecked = true
}
alertDialogBuilder.setPositiveButton(android.R.string.ok) { _, _ -> alertDialogBuilder.setPositiveButton(android.R.string.ok) { _, _ ->
onDialogClosed(true) onDialogClosed(true)
@@ -68,8 +98,6 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
// request a window without the title // request a window without the title
dialog.window?.requestFeature(Window.FEATURE_NO_TITLE) dialog.window?.requestFeature(Window.FEATURE_NO_TITLE)
dialog.setOnShowListener { measureLayout(it as Dialog) }
return dialog return dialog
} }
@@ -77,73 +105,48 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
database?.let { database?.let {
val initColor = try { var initColor = it.customColor
if (initColor != null) {
enableSwitchView.isChecked = true enableSwitchView.isChecked = true
Color.parseColor(it.customColor) } else {
} catch (e: Exception) {
enableSwitchView.isChecked = false enableSwitchView.isChecked = false
DEFAULT_COLOR initColor = DEFAULT_COLOR
} }
chromaColorView.currentColor = initColor
arguments?.putInt(ARG_INITIAL_COLOR, initColor) arguments?.putInt(ARG_INITIAL_COLOR, initColor)
} }
val fragmentManager = childFragmentManager
chromaColorFragment = fragmentManager.findFragmentByTag(TAG_FRAGMENT_COLORS) as ChromaColorFragment?
if (chromaColorFragment == null) {
chromaColorFragment = newInstance(arguments)
fragmentManager.beginTransaction().apply {
add(com.kunzisoft.androidclearchroma.R.id.color_dialog_container, chromaColorFragment!!, TAG_FRAGMENT_COLORS)
commit()
}
}
} }
override fun onDialogClosed(database: Database?, positiveResult: Boolean) { override fun onDialogClosed(database: Database?, positiveResult: Boolean) {
super.onDialogClosed(database, positiveResult) super.onDialogClosed(database, positiveResult)
if (positiveResult) { if (positiveResult) {
val customColorEnable = enableSwitchView.isChecked val newColor: Int? = if (enableSwitchView.isChecked)
chromaColorFragment?.currentColor?.let { currentColor -> chromaColorView.currentColor
onColorSelectedListener?.invoke(customColorEnable, currentColor) else
database?.let { null
val newColor = if (customColorEnable) { onColorSelectedListener?.invoke(newColor)
ChromaUtil.getFormattedColorString(currentColor, false) database?.let {
} else { val oldColor = database.customColor
"" database.customColor = newColor
} saveColor(oldColor, newColor)
val oldColor = database.customColor
database.customColor = newColor
saveColor(oldColor, newColor)
}
} }
} }
} }
/**
* Set new dimensions to dialog
* @param ad dialog
*/
private fun measureLayout(ad: Dialog) {
val typedValue = TypedValue()
resources.getValue(com.kunzisoft.androidclearchroma.R.dimen.chroma_dialog_height_multiplier, typedValue, true)
val heightMultiplier = typedValue.float
val height = (ad.context.resources.displayMetrics.heightPixels * heightMultiplier).toInt()
resources.getValue(com.kunzisoft.androidclearchroma.R.dimen.chroma_dialog_width_multiplier, typedValue, true)
val widthMultiplier = typedValue.float
val width = (ad.context.resources.displayMetrics.widthPixels * widthMultiplier).toInt()
ad.window?.setLayout(width, height)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
return rootView return rootView
} }
companion object { override fun onSaveInstanceState(outState: Bundle) {
private const val TAG_FRAGMENT_COLORS = "TAG_FRAGMENT_COLORS" super.onSaveInstanceState(outState)
outState.putInt(ARG_INITIAL_COLOR, chromaColorView.currentColor)
outState.putBoolean(ARG_ACTIVATED, mActivated)
}
companion object {
private const val ARG_INITIAL_COLOR = "ARG_INITIAL_COLOR"
private const val ARG_ACTIVATED = "ARG_ACTIVATED"
@ColorInt @ColorInt
const val DEFAULT_COLOR: Int = Color.WHITE const val DEFAULT_COLOR: Int = Color.WHITE
@@ -151,9 +154,7 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
val fragment = DatabaseColorPreferenceDialogFragmentCompat() val fragment = DatabaseColorPreferenceDialogFragmentCompat()
val bundle = Bundle(1) val bundle = Bundle(1)
bundle.putString(ARG_KEY, key) bundle.putString(ARG_KEY, key)
bundle.putInt(ARG_INITIAL_COLOR, Color.BLACK) bundle.putInt(ARG_INITIAL_COLOR, DEFAULT_COLOR)
bundle.putInt(ARG_COLOR_MODE, ColorMode.RGB.ordinal)
bundle.putInt(ARG_INDICATOR_MODE, IndicatorMode.HEX.ordinal)
fragment.arguments = bundle fragment.arguments = bundle
return fragment return fragment

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
@@ -76,9 +77,17 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
// To inherit to save element in database // To inherit to save element in database
} }
protected fun saveColor(oldColor: String, protected fun saveColor(oldColor: Int?,
newColor: String) { newColor: Int?) {
mDatabaseViewModel.saveColor(oldColor, newColor, mDatabaseAutoSaveEnable) val oldColorString = if (oldColor != null)
ChromaUtil.getFormattedColorString(oldColor, false)
else
""
val newColorString = if (newColor != null)
ChromaUtil.getFormattedColorString(newColor, false)
else
""
mDatabaseViewModel.saveColor(oldColorString, newColorString, mDatabaseAutoSaveEnable)
} }
protected fun saveCompression(oldCompression: CompressionAlgorithm, protected fun saveCompression(oldCompression: CompressionAlgorithm,

View File

@@ -19,7 +19,13 @@
*/ */
package com.kunzisoft.keepass.settings.preferencedialogfragment package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.app.AlarmManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
import android.util.Log
import android.view.View import android.view.View
import android.widget.NumberPicker import android.widget.NumberPicker
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
@@ -62,13 +68,39 @@ class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
} }
} }
override fun onResume() {
super.onResume()
(context?.applicationContext?.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
&& !alarmManager.canScheduleExactAlarms()) {
setExplanationText(R.string.warning_exact_alarm)
setExplanationButton(R.string.permission) {
// Open the exact alarm permission screen
try {
startActivity(Intent().apply {
action = ACTION_REQUEST_SCHEDULE_EXACT_ALARM
})
} catch (e: Exception) {
Log.e(TAG, "Unable to open exact alarm permission screen", e)
}
}
} else {
explanationText = ""
setExplanationButton("") {}
}
}
}
private fun durationToDaysHoursMinutesSeconds(duration: Long) { private fun durationToDaysHoursMinutesSeconds(duration: Long) {
if (duration < 0) { if (duration < 0) {
mEnabled = false
mDays = 0 mDays = 0
mHours = 0 mHours = 0
mMinutes = 0 mMinutes = 0
mSeconds = 0 mSeconds = 0
} else { } else {
mEnabled = true
mDays = (duration / (24L * 60L * 60L * 1000L)).toInt() mDays = (duration / (24L * 60L * 60L * 1000L)).toInt()
val daysMilliseconds = mDays * 24L * 60L * 60L * 1000L val daysMilliseconds = mDays * 24L * 60L * 60L * 1000L
mHours = ((duration - daysMilliseconds) / (60L * 60L * 1000L)).toInt() mHours = ((duration - daysMilliseconds) / (60L * 60L * 1000L)).toInt()
@@ -127,7 +159,7 @@ class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
setSwitchAction({ isChecked -> setSwitchAction({ isChecked ->
mEnabled = isChecked mEnabled = isChecked
}, mDays + mHours + mMinutes + mSeconds > 0) }, mEnabled)
assignValuesInViews() assignValuesInViews()
} }
@@ -162,6 +194,7 @@ class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
} }
companion object { companion object {
private const val TAG = "DurationDialogFrgCmpt"
private const val ENABLE_KEY = "ENABLE_KEY" private const val ENABLE_KEY = "ENABLE_KEY"
private const val DAYS_KEY = "DAYS_KEY" private const val DAYS_KEY = "DAYS_KEY"
private const val HOURS_KEY = "HOURS_KEY" private const val HOURS_KEY = "HOURS_KEY"

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.CompoundButton import android.widget.CompoundButton
import android.widget.EditText import android.widget.EditText
import android.widget.TextView import android.widget.TextView
@@ -35,6 +36,7 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
private var inputTextView: EditText? = null private var inputTextView: EditText? = null
private var textUnitView: TextView? = null private var textUnitView: TextView? = null
private var textExplanationView: TextView? = null private var textExplanationView: TextView? = null
private var explanationButton: Button? = null
private var switchElementView: CompoundButton? = null private var switchElementView: CompoundButton? = null
private var mOnInputTextEditorActionListener: TextView.OnEditorActionListener? = null private var mOnInputTextEditorActionListener: TextView.OnEditorActionListener? = null
@@ -100,6 +102,27 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
explanationText = getString(explanationTextId) explanationText = getString(explanationTextId)
} }
val explanationButtonText: String?
get() = explanationButton?.text?.toString() ?: ""
fun setExplanationButton(explanationButtonText: String?, clickListener: View.OnClickListener) {
explanationButton?.apply {
if (explanationButtonText != null && explanationButtonText.isNotEmpty()) {
text = explanationButtonText
visibility = View.VISIBLE
setOnClickListener(clickListener)
} else {
text = ""
visibility = View.GONE
setOnClickListener(null)
}
}
}
fun setExplanationButton(@StringRes explanationButtonTextId: Int, clickListener: View.OnClickListener) {
setExplanationButton(getString(explanationButtonTextId), clickListener)
}
override fun onBindDialogView(view: View) { override fun onBindDialogView(view: View) {
super.onBindDialogView(view) super.onBindDialogView(view)
@@ -128,6 +151,8 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
textUnitView?.visibility = View.GONE textUnitView?.visibility = View.GONE
textExplanationView = view.findViewById(R.id.explanation_text) textExplanationView = view.findViewById(R.id.explanation_text)
textExplanationView?.visibility = View.GONE textExplanationView?.visibility = View.GONE
explanationButton = view.findViewById(R.id.explanation_button)
explanationButton?.visibility = View.GONE
switchElementView = view.findViewById(R.id.switch_element) switchElementView = view.findViewById(R.id.switch_element)
switchElementView?.visibility = View.GONE switchElementView?.visibility = View.GONE
} }
@@ -155,6 +180,10 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
} }
} }
fun isSwitchActivated(): Boolean {
return switchElementView?.isChecked == true
}
fun activateSwitch() { fun activateSwitch() {
if (switchElementView?.isChecked != true) if (switchElementView?.isChecked != true)
switchElementView?.isChecked = true switchElementView?.isChecked = true

View File

@@ -31,9 +31,13 @@ abstract class ActionRunnable: Runnable {
var result: Result = Result() var result: Result = Result()
override fun run() { override fun run() {
onStartRun() try {
onActionRun() onStartRun()
onFinishRun() onActionRun()
onFinishRun()
} catch (runException: Exception) {
setError(runException)
}
} }
abstract fun onStartRun() abstract fun onStartRun()

View File

@@ -43,9 +43,14 @@ object TimeoutHelper {
private fun getLockPendingIntent(context: Context): PendingIntent { private fun getLockPendingIntent(context: Context): PendingIntent {
return PendingIntent.getBroadcast(context.applicationContext, return PendingIntent.getBroadcast(context.applicationContext,
REQUEST_ID, REQUEST_ID,
Intent(LOCK_ACTION), Intent(LOCK_ACTION),
PendingIntent.FLAG_CANCEL_CURRENT) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
}
)
} }
/** /**
@@ -61,9 +66,26 @@ object TimeoutHelper {
val triggerTime = System.currentTimeMillis() + timeout val triggerTime = System.currentTimeMillis() + timeout
Log.d(TAG, "TimeoutHelper start") Log.d(TAG, "TimeoutHelper start")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager.setExact(AlarmManager.RTC, triggerTime, getLockPendingIntent(context)) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
&& !alarmManager.canScheduleExactAlarms()) {
alarmManager.set(
AlarmManager.RTC,
triggerTime,
getLockPendingIntent(context)
)
} else {
alarmManager.setExact(
AlarmManager.RTC,
triggerTime,
getLockPendingIntent(context)
)
}
} else { } else {
alarmManager.set(AlarmManager.RTC, triggerTime, getLockPendingIntent(context)) alarmManager.set(
AlarmManager.RTC,
triggerTime,
getLockPendingIntent(context)
)
} }
} }
} }

View File

@@ -60,18 +60,41 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
Intent.ACTION_SCREEN_OFF -> { Intent.ACTION_SCREEN_OFF -> {
if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(context)) { if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(context)) {
mLockPendingIntent = PendingIntent.getBroadcast(context, mLockPendingIntent = PendingIntent.getBroadcast(context,
4575, 4575,
Intent(intent).apply { Intent(intent).apply {
action = LOCK_ACTION action = LOCK_ACTION
}, },
0) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
)
// Launch the effective action after a small time // Launch the effective action after a small time
val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong() val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong()
val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager? (context.getSystemService(ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager?.setExact(AlarmManager.RTC_WAKEUP, first, mLockPendingIntent) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
} else { && !alarmManager.canScheduleExactAlarms()) {
alarmManager?.set(AlarmManager.RTC_WAKEUP, first, mLockPendingIntent) alarmManager.set(
AlarmManager.RTC_WAKEUP,
first,
mLockPendingIntent
)
} else {
alarmManager.setExact(
AlarmManager.RTC_WAKEUP,
first,
mLockPendingIntent
)
}
} else {
alarmManager.set(
AlarmManager.RTC_WAKEUP,
first,
mLockPendingIntent
)
}
} }
} else { } else {
cancelLockPendingIntent(context) cancelLockPendingIntent(context)

View File

@@ -0,0 +1,27 @@
package com.kunzisoft.keepass.utils
import android.content.res.Configuration
import android.util.Log
object DexUtil {
private val TAG = DexUtil::class.java.name
// Determine if the current environment is in DeX mode. Always returns false on non-Samsung
// devices.
fun isDexMode(config: Configuration): Boolean {
// This is the documented way to check this: https://developer.samsung.com/samsung-dex/modify-optimizing.html
return try {
val configClass = config.javaClass
val enabledConstant = configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass)
val enabledField = configClass.getField("semDesktopModeEnabled").getInt(config)
val isEnabled = enabledConstant == enabledField
Log.d(TAG, "DeX currently enabled: $isEnabled")
isEnabled
} catch (e: Exception) {
Log.d(TAG, "Failed to check for DeX mode; likely not Samsung device: $e")
false
}
}
}

View File

@@ -0,0 +1,27 @@
package com.kunzisoft.keepass.utils
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.util.Log
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
object MagikeyboardUtil {
private val TAG = MagikeyboardUtil::class.java.name
// Set whether MagikeyboardService is enabled. This change is persistent and survives app
// crashes and device restarts. The state is changed immediately and does not require an app
// restart.
fun setEnabled(context: Context, enabled: Boolean) {
val componentState = if (enabled) {
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
} else {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
}
Log.d(TAG, "Setting service state: $enabled")
val component = ComponentName(context, MagikeyboardService::class.java)
context.packageManager.setComponentEnabledSetting(component, componentState, PackageManager.DONT_KILL_APP)
}
}

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