Compare commits

..

174 Commits
3.3.0 ... 3.4.0

Author SHA1 Message Date
J-Jamet
6b54dd9e0d Merge branch 'release/3.4.0' 2022-04-12 19:45:10 +02:00
J-Jamet
1f54e7752d Disable keyboard timeout by default 2022-04-12 15:04:12 +02:00
J-Jamet
6ac941f276 Upgrade to 3.4.0 2022-04-12 14:58:13 +02:00
J-Jamet
6c308483f7 Upgrade gradle and kotlin 2022-04-12 12:31:18 +02:00
J-Jamet
9d25fb74ec Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2022-04-12 12:14:23 +02:00
J-Jamet
d217b52744 Upgrade to 3.4.0_beta02
Fix #1282 with workaround
2022-04-12 12:12:05 +02:00
J-Jamet
319da4b174 Rollback openOutputStream in "rwt" 2022-04-12 11:32:49 +02:00
nautilusx
9bee467942 Translated using Weblate (German)
Currently translated at 99.6% (611 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-04-12 07:46:27 +02:00
VfBFan
44ac70fc97 Translated using Weblate (German)
Currently translated at 99.6% (611 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-04-12 07:46:26 +02:00
J-Jamet
d897611d62 Change main screenshot 2022-04-10 17:21:35 +02:00
J-Jamet
c2ae251e73 Change screenshots 2022-04-10 17:10:01 +02:00
J-Jamet
35ad285864 Fix education screen 2022-04-09 16:52:25 +02:00
J-Jamet
97bdae21eb Small change 2022-04-09 16:37:11 +02:00
J-Jamet
d6dc6e43c7 Update strings 2022-04-09 16:35:20 +02:00
Oğuz Ersen
01e6e530d5 Translated using Weblate (Turkish)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-04-09 16:28:35 +02:00
Kunzisoft
9ec0178beb Translated using Weblate (French)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2022-04-09 16:28:35 +02:00
Hosted Weblate
d66f2f6d24 Merge branch 'origin/develop' into Weblate. 2022-04-09 16:15:08 +02:00
Milo Ivir
79cd4004cc Translated using Weblate (Croatian)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2022-04-09 16:15:08 +02:00
VfBFan
991243e2df Translated using Weblate (German)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-04-09 16:15:07 +02:00
J-Jamet
b91cf11d86 Upgrade to 3.4 beta01 2022-04-09 16:14:15 +02:00
J-Jamet
d182ec09fa Upgrade CHANGELOG 2022-04-09 16:12:57 +02:00
J-Jamet
8641822358 Fix small bugs 2022-04-09 16:03:12 +02:00
J-Jamet
9665cbb428 Fix small visual bug 2022-04-08 18:37:06 +02:00
J-Jamet
a280dfaf3b Better magikeyboard views 2022-04-08 18:23:02 +02:00
J-Jamet
3e56521ea8 Empty Magikeyboard memory when the main service is killed #1261 2022-04-08 16:53:03 +02:00
SC
b205230ea9 Translated using Weblate (Portuguese)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2022-04-08 09:31:01 +02:00
Oğuz Ersen
51645ab126 Translated using Weblate (Turkish)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-04-08 09:31:00 +02:00
Eric
5d04897e75 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-04-08 09:30:59 +02:00
Ihor Hordiichuk
1ac0ea5cc6 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-04-08 09:30:59 +02:00
solokot
a07e8b51e5 Translated using Weblate (Russian)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-04-08 09:30:58 +02:00
Vitor Henrique
a81f0238f4 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-04-08 09:30:57 +02:00
Matthaiks
2b81eb8ec7 Translated using Weblate (Polish)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-04-08 09:30:56 +02:00
Retrial
e5eb642781 Translated using Weblate (Greek)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-04-08 09:30:56 +02:00
VfBFan
a4cbe25733 Translated using Weblate (German)
Currently translated at 99.8% (612 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-04-08 09:30:55 +02:00
J-Jamet
2042c85b22 Ask confirmation to lock if changes without save #970 2022-04-07 18:06:47 +02:00
J-Jamet
3149f8745c Fix passkey view 2022-04-07 17:39:46 +02:00
J-Jamet
15b9f1616f Fix keyboard search and selection 2022-04-07 14:28:45 +02:00
J-Jamet
94c02b7288 Fix selection mode instance issue 2022-04-07 13:26:07 +02:00
J-Jamet
7e70b59a59 Fix selection mode instance issue 2022-04-07 13:22:19 +02:00
Hosted Weblate
2c7f5e41ed Merge branch 'origin/develop' into Weblate. 2022-04-05 18:00:37 +02:00
J-Jamet
108b8df280 Fix CHANGELOG issue number 2022-04-05 15:32:42 +02:00
J-Jamet
553098f9be Manage package name from Magikeyboard #1010 2022-04-05 15:28:15 +02:00
J-Jamet
131eb78407 Setting to change the keyboard during a search #1254 2022-04-05 13:38:36 +02:00
J-Jamet
f956a279a5 Save search parameters #1254 2022-04-05 13:07:36 +02:00
J-Jamet
7150686b92 Fix search parameter parcelable 2022-04-05 11:59:02 +02:00
J-Jamet
4b1fb2c173 Upgrade CHANGELOG 2022-04-05 11:28:58 +02:00
J-Jamet
94464bf608 Save pass generator options in app.properties 2022-04-04 15:30:57 +02:00
J-Jamet
2faa88784a Update CHANGELOG 2022-04-04 15:09:32 +02:00
J-Jamet
e6607b53d8 Better search implementation #175 2022-04-04 15:08:11 +02:00
J-Jamet
3f6a6c864a Show TOTP in 3-digit grouping #1270 2022-04-04 13:25:01 +02:00
J-Jamet
30a578257d Update CHANGELOG 2022-04-04 13:07:38 +02:00
J-Jamet
8411134adf Save files with "wt" #1282 2022-04-04 12:21:35 +02:00
J-Jamet
f86a5d1a19 Fix small bug 2022-04-01 19:19:47 +02:00
J-Jamet
be72492537 Merge branch 'feature/Mnemonics' into develop 2022-04-01 19:14:07 +02:00
J-Jamet
76f9e8ec6e Generate passphrase #218 2022-04-01 19:08:50 +02:00
Milo Ivir
8fb1c44e58 Translated using Weblate (Croatian)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2022-04-01 13:08:19 +02:00
J-Jamet
f607b35cf3 Change max slider to 64 2022-04-01 11:01:00 +02:00
J-Jamet
0e56bec35a Key generator as tabs 2022-04-01 10:51:38 +02:00
Hosted Weblate
c890d10114 Merge branch 'origin/develop' into Weblate. 2022-03-31 12:09:22 +02:00
Milo Ivir
dee2fe5ce7 Translated using Weblate (Croatian)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2022-03-31 12:09:21 +02:00
J-Jamet
4a4d767bce Manage setting to hide password #696 2022-03-29 17:18:49 +02:00
J-Jamet
d57e0cf601 Add setting to colorize password 2022-03-29 16:41:23 +02:00
J-Jamet
7fa141dd1b Merge branch 'develop' into feature/Mnemonics 2022-03-29 16:01:50 +02:00
J-Jamet
c261a0cbca Smaller tab buttons 2022-03-29 16:01:20 +02:00
J-Jamet
66661cbd49 Add bold and change password colors 2022-03-29 15:54:37 +02:00
J-Jamet
100c126c3d Update CHANGELOG 2022-03-29 15:32:51 +02:00
J-Jamet
d466e3077d Add color for special password chars #454 2022-03-29 15:32:29 +02:00
J-Jamet
24587dc34e Small refactorization 2022-03-29 12:42:53 +02:00
J-Jamet
32cc57dd03 Add editable chars fields #539 2022-03-28 22:14:24 +02:00
J-Jamet
a55488846b Add advanced password filters #1052 2022-03-28 21:26:26 +02:00
J-Jamet
dcf61fd4e2 Update CHANGELOG 2022-03-28 16:34:37 +02:00
J-Jamet
5bf998468a Save password preferences dynamically 2022-03-28 16:24:25 +02:00
J-Jamet
01c9625c59 Dynamic filter change and fix bugs 2022-03-28 15:08:56 +02:00
Dixon Huang
772c378922 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2022-03-28 13:09:14 +02:00
J-Jamet
ee50a91379 Move entropy to the top 2022-03-27 21:40:13 +02:00
J-Jamet
9cfda3bad8 Fix small bugs 2022-03-27 21:33:44 +02:00
J-Jamet
aa19b08bd9 Fix password entropy and add chips 2022-03-27 20:41:51 +02:00
J-Jamet
87f69bb7e2 Entropy #869 2022-03-27 18:56:12 +02:00
J-Jamet
41c0aeedbe Show visual password strength indicator #631 2022-03-27 17:22:52 +02:00
Eric
3cbe53d76f Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-03-27 00:27:21 +01:00
Ihor Hordiichuk
aed60d6c1e Translated using Weblate (Ukrainian)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-03-27 00:27:21 +01:00
Stephan Paternotte
be7d35490d Translated using Weblate (Dutch)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2022-03-27 00:27:20 +01:00
VfBFan
d0ea997c63 Translated using Weblate (German)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-03-27 00:27:20 +01:00
J-Jamet
1fecffeba2 Merge branch 'master' of github.com:Kunzisoft/KeePassDX 2022-03-26 16:28:39 +01:00
J-Jamet
76319a56a2 Merge tag '3.3.3' into develop
3.3.3
2022-03-26 16:27:23 +01:00
J-Jamet
1d71de7031 Merge branch 'release/3.3.3' 2022-03-26 16:27:15 +01:00
Oğuz Ersen
e9a1cfea11 Translated using Weblate (Turkish)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-03-26 04:15:30 +01:00
solokot
9115856d19 Translated using Weblate (Russian)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-03-26 04:15:30 +01:00
Matthaiks
8c45266c18 Translated using Weblate (Polish)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-03-26 04:15:29 +01:00
Retrial
6b4130df89 Translated using Weblate (Greek)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-03-26 04:15:29 +01:00
J-Jamet
e3e10e7dfa Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2022-03-25 16:03:08 +01:00
Kunzisoft
cbf900004d Translated using Weblate (French)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2022-03-25 16:03:01 +01:00
J-Jamet
51b36dc460 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2022-03-25 16:02:20 +01:00
Hosted Weblate
9f8016afe2 Merge branch 'origin/develop' into Weblate. 2022-03-25 15:56:17 +01:00
abidin toumi
d5a36db50a Translated using Weblate (Arabic)
Currently translated at 77.0% (457 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2022-03-25 15:56:17 +01:00
J-Jamet
ecd458d8d0 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2022-03-25 15:52:56 +01:00
J-Jamet
9088297c41 Fix small bugs 2022-03-25 15:48:33 +01:00
J-Jamet
5282deb088 Tabs to show main and advanced content separately 2022-03-25 15:32:48 +01:00
J-Jamet
75d661f12b Add a warning to inform about KeyStore usage #1269 2022-03-25 13:09:08 +01:00
J-Jamet
83bc769d9e Upgrade CHANGELOG 2022-03-23 20:10:39 +01:00
J-Jamet
8324acadc8 Toast when trying to open another database 2022-03-23 18:31:20 +01:00
J-Jamet
6e42db41be Fix shared otpauth link if database not open #1274 2022-03-23 18:03:32 +01:00
J-Jamet
3917bfc9e6 Update CHANGELOG 2022-03-23 17:46:42 +01:00
J-Jamet
d11febb1ce Ellipsize attachment name #1253 2022-03-23 17:41:51 +01:00
J-Jamet
360eb6f9cc Fix URL color 2022-03-23 15:40:08 +01:00
J-Jamet
e4ac5d01d0 Fingerprint unlock no more by default 2022-03-23 15:01:07 +01:00
J-Jamet
6a51fc0668 Fix settings title 2022-03-23 14:18:26 +01:00
J-Jamet
ba03f07fbe Replace Libre UI logo when no biometric 2022-03-23 13:34:04 +01:00
J-Jamet
7bf7d63f64 Better UI when no biometric is available 2022-03-23 13:13:29 +01:00
J-Jamet
d3efaabc24 Rollback algorithm 2022-03-23 09:29:45 +01:00
J-Jamet
b4283ed98b Small improvement 2022-03-23 09:15:36 +01:00
J-Jamet
de407e4cf9 Remove unused package 2022-03-20 16:40:19 +01:00
J-Jamet
60ed3a9836 Upgrade to 3.3.3 2022-03-19 15:53:00 +01:00
Jérémy JAMET
7948358d85 Add spaces in issue template 2022-03-18 19:46:32 +01:00
Jérémy JAMET
96b82bb9b2 Remove "please report..." in issue template 2022-03-18 19:45:39 +01:00
Stephan Paternotte
699ccf13f0 Translated using Weblate (Dutch)
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2022-03-18 06:55:47 +01:00
Pavel Borecki
ae88aa4e42 Translated using Weblate (Czech)
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2022-03-18 06:55:47 +01:00
J-Jamet
bcce13b12f Merge tag '3.3.2' into develop
3.3.2
2022-03-15 20:21:53 +01:00
J-Jamet
4fd4f660a7 Merge branch 'release/3.3.2' 2022-03-15 20:21:45 +01:00
J-Jamet
518e59b33c Merge branch 'App_Store_Merge' into release/3.3.2 2022-03-15 18:56:44 +01:00
J-Jamet
165cdcc00d Upgrade to 3.3.2 2022-03-15 18:52:05 +01:00
J-Jamet
48c2115fdf Fix small bugs 2022-03-15 13:17:23 +01:00
Stephan Paternotte
d7d68ccdeb Translated using Weblate (Dutch)
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2022-03-11 00:00:32 +01:00
abidin toumi
5cf6362db4 Translated using Weblate (Arabic)
Currently translated at 77.5% (460 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2022-03-10 17:56:43 +01:00
Matthaiks
4efcc48160 Translated using Weblate (Polish)
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-03-10 17:56:43 +01:00
Raghav Kabra
383274ce0f Translated using Weblate (Hindi)
Currently translated at 25.1% (149 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hi/
2022-03-07 12:00:12 +01:00
abidin toumi
c9dec3a2f7 Translated using Weblate (Arabic)
Currently translated at 77.2% (458 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2022-03-07 12:00:11 +01:00
Wilker Santana da Silva
2d4bf2903b Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-03-07 12:00:11 +01:00
Nextross
2b88cfbda0 Translated using Weblate (Czech)
Currently translated at 98.6% (585 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2022-03-07 12:00:10 +01:00
Raghav Kabra
eb6ab7a156 Translated using Weblate (Hindi)
Currently translated at 23.9% (142 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hi/
2022-03-05 17:21:22 +01:00
J-Jamet
9bed7bf213 Remove Pro version in fastlane 2022-03-03 16:58:28 +01:00
J-Jamet
99c2796014 Change icons and remove pro version 2022-03-03 16:50:46 +01:00
J-Jamet
9ee9bf12ae Replace icons 2022-03-03 13:10:10 +01:00
SC
688cbe50f2 Translated using Weblate (Portuguese)
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2022-03-02 19:55:26 +01:00
VfBFan
e0577d1628 Translated using Weblate (German)
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-03-02 19:55:25 +01:00
J-Jamet
de70925f8a Upgrade Gradle 2022-03-02 16:46:12 +01:00
Stephan Paternotte
0f8dd17fde Translated using Weblate (Dutch)
Currently translated at 99.4% (590 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2022-02-27 22:57:39 +01:00
Óscar Fernández Díaz
4bc8a08606 Translated using Weblate (Spanish)
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2022-02-27 19:54:08 +01:00
Retrial
cf34433186 Translated using Weblate (Greek)
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-02-27 19:54:07 +01:00
J-Jamet
21113d6fc8 Merge tag '3.3.1' into develop
3.3.1
2022-02-26 18:17:07 +01:00
J-Jamet
d7decba3f5 Merge branch 'release/3.3.1' 2022-02-26 18:16:59 +01:00
Oğuz Ersen
671364f395 Translated using Weblate (Turkish)
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-02-26 17:20:50 +01:00
Eric
d0072237e7 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-02-26 17:20:50 +01:00
Ihor Hordiichuk
d11c001e0d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-02-26 17:20:50 +01:00
solokot
a197453ce0 Translated using Weblate (Russian)
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-02-26 17:20:49 +01:00
Matthaiks
d1586a8d80 Translated using Weblate (Polish)
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-02-26 17:20:49 +01:00
Kunzisoft
6e959e415f Translated using Weblate (French)
Currently translated at 100.0% (593 of 593 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2022-02-26 17:20:49 +01:00
J-Jamet
8a8e14701b Merge branch 'translations' into develop 2022-02-26 17:14:07 +01:00
J-Jamet
60bb144020 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2022-02-26 17:13:29 +01:00
J-Jamet
b561db3a67 Workaround to fill OTP token in multiple fields with Magikeyboard (long press) #1158 2022-02-26 15:19:06 +01:00
J-Jamet
b0e722acce Best autofill recognition #1250 2022-02-26 15:18:08 +01:00
J-Jamet
f79d32b22b Fix temp advanced unlocking #1245 2022-02-26 12:45:59 +01:00
Hosted Weblate
3579606d1e Merge branch 'origin/develop' into Weblate. 2022-02-26 12:31:54 +01:00
J-Jamet
b93c6266a6 Upgrade CHANGELOG 2022-02-26 12:30:28 +01:00
J-Jamet
ea9c530667 Fix "other" search filter 2022-02-26 12:29:07 +01:00
J-Jamet
6ea95c050a Fix filter string #1249 2022-02-26 12:14:04 +01:00
SC
5a99a28195 Translated using Weblate (Portuguese)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2022-02-25 20:55:58 +01:00
Vitor Henrique
0cd5351c07 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.8% (585 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-02-25 20:55:58 +01:00
VfBFan
148bed801b Translated using Weblate (German)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-02-25 20:55:57 +01:00
Larry Day
87a1a3ba1b Translated using Weblate (Czech)
Currently translated at 96.7% (573 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2022-02-25 20:55:57 +01:00
J-Jamet
20d2c10bba Upgrade room to 2.4.2 2022-02-25 12:50:01 +01:00
J-Jamet
3eed5e395e Upgrade to 3.3.1
Fix Japanese keyboard in search #1248
2022-02-25 12:44:38 +01:00
Oğuz Ersen
870fbaa05c Translated using Weblate (Turkish)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-02-22 01:54:58 +01:00
Eric
55868f68da Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-02-22 01:54:58 +01:00
Ihor Hordiichuk
e6f0fbeab5 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-02-22 01:54:58 +01:00
solokot
8a554349b5 Translated using Weblate (Russian)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-02-22 01:54:57 +01:00
Matthaiks
ab87d4e564 Translated using Weblate (Polish)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-02-22 01:54:57 +01:00
Éfrit
8400a83b70 Translated using Weblate (French)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2022-02-22 01:54:57 +01:00
Jeffree Romero
5d2caa37a9 Translated using Weblate (Spanish)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2022-02-22 01:54:57 +01:00
Retrial
a6b7cfc2d3 Translated using Weblate (Greek)
Currently translated at 100.0% (592 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-02-22 01:54:56 +01:00
J-Jamet
74107b90bb Max binary byte as 10 MB to prevent OOM #256 2022-02-20 17:16:36 +01:00
Hosted Weblate
9f4a915d43 Merge branch 'origin/develop' into Weblate. 2022-02-19 11:29:30 +01:00
Ismael Mirabile
3f1f22e1c3 Translated using Weblate (German)
Currently translated at 99.6% (590 of 592 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-02-19 11:29:30 +01:00
J-Jamet
1d98eb2b95 Merge tag '3.3.0' into develop
3.3.0
2022-02-19 11:01:11 +01:00
261 changed files with 6640 additions and 14819 deletions

View File

@@ -8,9 +8,11 @@ assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
@@ -18,9 +20,11 @@ Steps to reproduce the behavior:
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**KeePass Database**
- Created with: [e.g Windows KeePass 2.42]
- Version: [e.g. 2]
- Location: [e.g. Remote file retrieved with GDrive app]
@@ -28,15 +32,18 @@ A clear and concise description of what you expected to happen.
- Size: [e.g. 150Mo]
- Contains attachment: [e.g. Yes]
**KeePassDX (please complete the following information):**
**KeePassDX:**
- Version: [e.g. 2.5.0.0beta23]
- Build: [e.g. Free]
- Language: [e.g. French]
**Android (please complete the following information):**
**Android:**
- Device: [e.g. GalaxyS8]
- Version: [e.g. 8.1]
**Additional context**
Add any other context about the problem here.
- Browser for Autofill: [e.g. Chrome version X]

View File

@@ -1,3 +1,33 @@
KeePassDX(3.4.0)
* Passphrase implementation #218
* Show visual password strength indicator with entropy #631 #869 #454 #1270
* Dynamically save password generator configuration #618 #696
* Add advanced password filters #1052 #448 #983 #271 #539
* Better search implementation #175 #1254 #1267
* Manage package name from Magikeyboard #1010 #1261
* Ask confirmation to lock if changes without save #970
* Fix small bugs #1282
KeePassDX(3.3.3)
* Fix shared otpauth link if database not open #1274
* Ellipsize attachment name #1253
* Add a warning to inform about KeyStore usage #1269
* Fingerprint unlock no more by default #1273
* Tabs to show main and advanced content separately
* Fix URL color
KeePassDX(3.3.2)
* Merge KeePassDX & KeePassDX Pro #1257
* Create new Contributor Pro app
KeePassDX(3.3.1)
* Fix Japanese keyboard in search #1248
* Better OOM management #256
* Fix filters #1249
* Fix temp advanced unlocking #1245
* Best autofill recognition #1250
* Workaround to fill OTP token in multiple fields with Magikeyboard (long press) #1158
KeePassDX(3.3.0)
* Quick search and dynamic filters #163 #462 #521
* Keep search context #1141

View File

@@ -12,8 +12,8 @@ android {
applicationId "com.kunzisoft.keepass"
minSdkVersion 15
targetSdkVersion 31
versionCode = 102
versionName = "3.3.0"
versionCode = 108
versionName = "3.4.0"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
@@ -43,30 +43,20 @@ android {
dimension "version"
applicationIdSuffix = ".libre"
buildConfigField "String", "BUILD_VERSION", "\"libre\""
buildConfigField "boolean", "FULL_VERSION", "true"
buildConfigField "boolean", "CLOSED_STORE", "false"
buildConfigField "String[]", "STYLES_DISABLED",
"{\"KeepassDXStyle_Red\"," +
"\"KeepassDXStyle_Red_Night\"," +
"\"KeepassDXStyle_Reply\"," +
"\"KeepassDXStyle_Reply_Night\"," +
"\"KeepassDXStyle_Purple\"," +
"\"KeepassDXStyle_Purple_Dark\"}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
}
pro {
dimension "version"
applicationIdSuffix = ".pro"
buildConfigField "String", "BUILD_VERSION", "\"pro\""
buildConfigField "boolean", "FULL_VERSION", "true"
buildConfigField "boolean", "CLOSED_STORE", "true"
buildConfigField "String[]", "STYLES_DISABLED", "{}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIZiXvrQCzSV9LNI6-p7cjTKENZLHIrz_zaqZuQQ" ]
}
free {
dimension "version"
applicationIdSuffix = ".free"
buildConfigField "String", "BUILD_VERSION", "\"free\""
buildConfigField "boolean", "FULL_VERSION", "false"
buildConfigField "boolean", "CLOSED_STORE", "true"
buildConfigField "String[]", "STYLES_DISABLED",
"{\"KeepassDXStyle_Simple\"," +
@@ -86,7 +76,6 @@ android {
sourceSets {
libre.res.srcDir 'src/libre/res'
pro.res.srcDir 'src/pro/res'
free.res.srcDir 'src/free/res'
}
@@ -104,7 +93,7 @@ android {
}
}
def room_version = "2.4.1"
def room_version = "2.4.2"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
@@ -138,6 +127,8 @@ dependencies {
// Apache Commons
implementation 'commons-io:commons-io:2.8.0'
implementation 'commons-codec:commons-codec:1.15'
// Password generator
implementation 'me.gosimple:nbvcxz:1.5.0'
// Encrypt lib
implementation project(path: ':crypto')
// Icon pack

View File

@@ -1,61 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="108"
android:viewportHeight="108"
android:width="108dp"
android:height="108dp">
android:height="108dp"
android:viewportWidth="120"
android:viewportHeight="120">
<group
android:translateY="-332">
<group
android:translateY="332">
<path
android:pathData="M65.728516 32.791016L58.052734 35.904297 56.173828 48.380859 35.306641 69.267578 35.238281 73.759766 69.478516 108 108 108 108 70.810547 73.09375 35.904297 65.728516 32.791016Z"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:strokeMiterLimit="4" >
<aapt:attr name="android:fillColor">
<gradient
android:endColor="#0000"
android:endX="80"
android:endY="80"
android:startColor="#4e000000"
android:startX="0"
android:startY="0"
android:type="linear"/>
</aapt:attr>
</path>
</group>
<group
android:scaleX="0.3939503"
android:scaleY="0.3939503"
android:translateX="33.66343"
android:translateY="233.998">
<path
android:pathData="M88.76953 339.91602L4.1718754 424.59766 4.0000004 436 15.400391 435.82813 27.240234 424 40 424l0 -12 12 0 0 -12.73438 34.01172 -33.97656A8 8 0 0 1 84 360a8 8 0 0 1 8 -8 8 8 0 0 1 5.296882 2.01367l2.787098 -2.7832 -11.31445 -11.31445z"
android:fillColor="#eaeaea"
android:strokeWidth="1"
android:strokeColor="#58000000" />
</group>
<group
android:scaleX="0.3939503"
android:scaleY="0.3939503"
android:translateX="33.66343"
android:translateY="233.998">
<path
android:pathData="M4.0000004 340L4.1718754 351.40137 88.59863 435.82812 100 436 99.828122 424.59863 15.401367 340.17188Z"
android:fillColor="#81c784" />
</group>
<group
android:scaleX="0.3939503"
android:scaleY="0.3939503"
android:translateX="33.66343"
android:translateY="233.998">
<path
android:pathData="M81.39454 332.00195a27 27 0 0 0 -19.48634 7.90625 27 27 0 0 0 0 38.1836 27 27 0 0 0 38.1836 0 27 27 0 0 0 0 -38.1836 27 27 0 0 0 -18.69726 -7.90625zM92 352a8 8 0 0 1 8 8 8 8 0 0 1 -8 8 8 8 0 0 1 -8 -8 8 8 0 0 1 8 -8z"
android:fillColor="#eaeaea"
android:strokeWidth="1"
android:strokeColor="#58000000" />
</group>
android:translateX="6"
android:translateY="8">
<path
android:fillColor="#24000000"
android:strokeWidth="1.99999297"
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
<path
android:fillColor="#24000000"
android:strokeWidth="1.99999297"
android:pathData="M63.9961,34.0059 C61.5643,34.096,59.2564,35.102,57.5352,36.8223 C53.7682,40.589,53.7682,46.6982,57.5352,50.4649 C61.3017,54.232,67.4073,54.232,71.1739,50.4649 C74.9409,46.6982,74.9409,40.589,71.1739,36.8223 C69.2766,34.9258,66.6768,33.9054,63.9962,34.0059 Z M68.1992,40.6954 C69.8278,40.6958,71.148,42.016,71.1484,43.6446 C71.148,45.2732,69.8278,46.5934,68.1992,46.5938 C66.5706,46.5934,65.2504,45.2732,65.25,43.6446 C65.2504,42.016,66.5706,40.6958,68.1992,40.6954 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
</group>
<group
android:translateX="6"
android:translateY="6">
<path
android:fillColor="#ffa726"
android:strokeWidth="1.99999297"
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
<path
android:fillColor="#ffffff"
android:strokeWidth="1.99999297"
android:pathData="M63.9961,34.0059 C61.5643,34.096,59.2564,35.102,57.5352,36.8223 C53.7682,40.589,53.7682,46.6982,57.5352,50.4649 C61.3017,54.232,67.4073,54.232,71.1739,50.4649 C74.9409,46.6982,74.9409,40.589,71.1739,36.8223 C69.2766,34.9258,66.6768,33.9054,63.9962,34.0059 Z M68.1992,40.6954 C69.8278,40.6958,71.148,42.016,71.1484,43.6446 C71.148,45.2732,69.8278,46.5934,68.1992,46.5938 C66.5706,46.5934,65.2504,45.2732,65.25,43.6446 C65.2504,42.016,66.5706,40.6958,68.1992,40.6954 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
</group>
</vector>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="84"
android:viewportHeight="84">
<group
android:translateX="-12"
android:translateY="-12">
<path
android:fillColor="#ffa726"
android:strokeWidth="1.99999297"
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
<path
android:fillColor="#ffffff"
android:strokeWidth="1.99999297"
android:pathData="M63.9961,34.0059 C61.5643,34.096,59.2564,35.102,57.5352,36.8223 C53.7682,40.589,53.7682,46.6982,57.5352,50.4649 C61.3017,54.232,67.4073,54.232,71.1739,50.4649 C74.9409,46.6982,74.9409,40.589,71.1739,36.8223 C69.2766,34.9258,66.6768,33.9054,63.9962,34.0059 Z M68.1992,40.6954 C69.8278,40.6958,71.148,42.016,71.1484,43.6446 C71.148,45.2732,69.8278,46.5934,68.1992,46.5938 C66.5706,46.5934,65.2504,45.2732,65.25,43.6446 C65.2504,42.016,66.5706,40.6958,68.1992,40.6954 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
</group>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,61 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="108"
android:viewportHeight="108"
android:width="108dp"
android:height="108dp">
android:height="108dp"
android:viewportWidth="120"
android:viewportHeight="120">
<group
android:translateY="-332">
<group
android:translateY="332">
<path
android:pathData="M65.728516 32.791016L58.052734 35.904297 56.173828 48.380859 35.306641 69.267578 35.238281 73.759766 69.478516 108 108 108 108 70.810547 73.09375 35.904297 65.728516 32.791016Z"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:strokeMiterLimit="4" >
<aapt:attr name="android:fillColor">
<gradient
android:endColor="#0000"
android:endX="80"
android:endY="80"
android:startColor="#4e000000"
android:startX="0"
android:startY="0"
android:type="linear"/>
</aapt:attr>
</path>
</group>
<group
android:scaleX="0.3939503"
android:scaleY="0.3939503"
android:translateX="33.66343"
android:translateY="233.998">
<path
android:pathData="M88.76953 339.91602L4.1718754 424.59766 4.0000004 436 15.400391 435.82813 27.240234 424 40 424l0 -12 12 0 0 -12.73438 34.01172 -33.97656A8 8 0 0 1 84 360a8 8 0 0 1 8 -8 8 8 0 0 1 5.296882 2.01367l2.787098 -2.7832 -11.31445 -11.31445z"
android:fillColor="#eaeaea"
android:strokeWidth="1"
android:strokeColor="#58000000"/>
</group>
<group
android:scaleX="0.3939503"
android:scaleY="0.3939503"
android:translateX="33.66343"
android:translateY="233.998">
<path
android:pathData="M4.0000004 340L4.1718754 351.40137 88.59863 435.82812 100 436 99.828122 424.59863 15.401367 340.17188Z"
android:fillColor="#64b5f6" />
</group>
<group
android:scaleX="0.3939503"
android:scaleY="0.3939503"
android:translateX="33.66343"
android:translateY="233.998">
<path
android:pathData="M81.39454 332.00195a27 27 0 0 0 -19.48634 7.90625 27 27 0 0 0 0 38.1836 27 27 0 0 0 38.1836 0 27 27 0 0 0 0 -38.1836 27 27 0 0 0 -18.69726 -7.90625zM92 352a8 8 0 0 1 8 8 8 8 0 0 1 -8 8 8 8 0 0 1 -8 -8 8 8 0 0 1 8 -8z"
android:fillColor="#eaeaea"
android:strokeWidth="1"
android:strokeColor="#58000000" />
</group>
android:translateX="6"
android:translateY="8">
<path
android:fillColor="#24000000"
android:strokeWidth="1.99999297"
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
<path
android:fillColor="#24000000"
android:strokeWidth="1.99999297"
android:pathData="M64.501,35.0576 C63.7095,35.0576,62.918,35.3613,62.3115,35.9678 L55.0127,43.2666 C53.7998,44.4795,53.7998,46.4306,55.0127,47.6436 L62.3115,54.9424 C63.5244,56.1553,65.4775,56.1553,66.6904,54.9424 L73.9873,47.6436 C75.2002,46.4307,75.2002,44.4796,73.9873,43.2666 L66.6904,35.9678 C66.0839,35.3613,65.2924,35.0576,64.5009,35.0576 Z M67.6729,42.6006 C69.3298,42.6006,70.6729,43.9437,70.6729,45.6006 C70.6729,47.2575,69.3298,48.6006,67.6729,48.6006 C66.016,48.6006,64.6729,47.2575,64.6729,45.6006 C64.6729,43.9437,66.016,42.6006,67.6729,42.6006 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
</group>
<group
android:translateX="6"
android:translateY="6">
<path
android:fillColor="#ffa726"
android:strokeWidth="1.99999297"
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
<path
android:fillColor="#ffffff"
android:strokeWidth="1.99999297"
android:pathData="M64.501,35.0576 C63.7095,35.0576,62.918,35.3613,62.3115,35.9678 L55.0127,43.2666 C53.7998,44.4795,53.7998,46.4306,55.0127,47.6436 L62.3115,54.9424 C63.5244,56.1553,65.4775,56.1553,66.6904,54.9424 L73.9873,47.6436 C75.2002,46.4307,75.2002,44.4796,73.9873,43.2666 L66.6904,35.9678 C66.0839,35.3613,65.2924,35.0576,64.5009,35.0576 Z M67.6729,42.6006 C69.3298,42.6006,70.6729,43.9437,70.6729,45.6006 C70.6729,47.2575,69.3298,48.6006,67.6729,48.6006 C66.016,48.6006,64.6729,47.2575,64.6729,45.6006 C64.6729,43.9437,66.016,42.6006,67.6729,42.6006 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
</group>
</vector>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="84"
android:viewportHeight="84">
<group
android:translateX="-12"
android:translateY="-12">
<path
android:fillColor="#ffa726"
android:strokeWidth="1.99999297"
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
<path
android:fillColor="#ffffff"
android:strokeWidth="1.99999297"
android:pathData="M64.501,35.0576 C63.7095,35.0576,62.918,35.3613,62.3115,35.9678 L55.0127,43.2666 C53.7998,44.4795,53.7998,46.4306,55.0127,47.6436 L62.3115,54.9424 C63.5244,56.1553,65.4775,56.1553,66.6904,54.9424 L73.9873,47.6436 C75.2002,46.4307,75.2002,44.4796,73.9873,43.2666 L66.6904,35.9678 C66.0839,35.3613,65.2924,35.0576,64.5009,35.0576 Z M67.6729,42.6006 C69.3298,42.6006,70.6729,43.9437,70.6729,45.6006 C70.6729,47.2575,69.3298,48.6006,67.6729,48.6006 C66.016,48.6006,64.6729,47.2575,64.6729,45.6006 C64.6729,43.9437,66.016,42.6006,67.6729,42.6006 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
</group>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/green" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/green" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -131,6 +131,9 @@
<activity
android:name="com.kunzisoft.keepass.activities.IconPickerActivity"
android:configChanges="keyboardHidden" />
<activity
android:name="com.kunzisoft.keepass.activities.KeyGeneratorActivity"
android:configChanges="keyboardHidden" />
<activity
android:name="com.kunzisoft.keepass.activities.ImageViewerActivity"
android:configChanges="keyboardHidden" />
@@ -155,6 +158,7 @@
<activity
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
android:theme="@style/Theme.Transparent"
android:excludeFromRecents="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
@@ -169,9 +173,6 @@
<data android:scheme="otpauth" android:host="hotp" />
</intent-filter>
</activity>
<activity
android:name="com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity"
android:theme="@style/Theme.Transparent" />
<activity
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
android:label="@string/keyboard_setting_label"

View File

@@ -44,6 +44,7 @@ import androidx.core.graphics.ColorUtils
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.tabs.TabLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.fragments.EntryFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
@@ -83,6 +84,7 @@ class EntryActivity : DatabaseLockActivity() {
private var titleIconView: ImageView? = null
private var historyView: View? = null
private var tagsListView: RecyclerView? = null
private var entryContentTab: TabLayout? = null
private var tagsAdapter: TagsAdapter? = null
private var entryProgress: LinearProgressIndicator? = null
private var lockView: View? = null
@@ -133,6 +135,7 @@ class EntryActivity : DatabaseLockActivity() {
titleIconView = findViewById(R.id.entry_icon)
historyView = findViewById(R.id.history_container)
tagsListView = findViewById(R.id.entry_tags_list_view)
entryContentTab = findViewById(R.id.entry_content_tab)
entryProgress = findViewById(R.id.entry_progress)
lockView = findViewById(R.id.lock_button)
loadingView = findViewById(R.id.loading)
@@ -162,6 +165,19 @@ class EntryActivity : DatabaseLockActivity() {
adapter = tagsAdapter
}
// Init content tab
entryContentTab?.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
mEntryViewModel.selectSection(EntryViewModel.EntrySection.
getEntrySectionByPosition(tab?.position ?: 0)
)
}
override fun onTabUnselected(tab: TabLayout.Tab?) {}
override fun onTabReselected(tab: TabLayout.Tab?) {}
})
// Get Entry from UUID
try {
intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { mainEntryId ->
@@ -193,6 +209,10 @@ class EntryActivity : DatabaseLockActivity() {
lockAndExit()
}
mEntryViewModel.sectionSelected.observe(this) { entrySection ->
entryContentTab?.getTabAt(entrySection.position)?.select()
}
mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
if (entryInfoHistory != null) {
this.mMainEntryId = entryInfoHistory.mainEntryId

View File

@@ -58,9 +58,12 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.template.*
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
@@ -78,11 +81,9 @@ import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
import org.joda.time.DateTime
import java.util.*
import kotlin.collections.ArrayList
class EntryEditActivity : DatabaseLockActivity(),
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
GeneratePasswordDialogFragment.GeneratePasswordListener,
SetOTPDialogFragment.CreateOtpListener,
DatePickerDialog.OnDateSetListener,
TimePickerDialog.OnTimeSetListener,
@@ -119,6 +120,20 @@ class EntryEditActivity : DatabaseLockActivity(),
mEntryEditViewModel.selectIcon(icon)
}
private var mPasswordField: Field? = null
private var mKeyGeneratorResultLauncher = KeyGeneratorActivity.registerForGeneratedKeyResult(this) { keyGenerated ->
keyGenerated?.let {
mPasswordField?.let {
it.protectedValue.stringValue = keyGenerated
mEntryEditViewModel.selectPassword(it)
}
}
mPasswordField = null
Handler(Looper.getMainLooper()).post {
performedNextEducation()
}
}
// To ask data lost only one time
private var backPressedAlreadyApproved = false
@@ -268,9 +283,8 @@ class EntryEditActivity : DatabaseLockActivity(),
}
mEntryEditViewModel.requestPasswordSelection.observe(this) { passwordField ->
GeneratePasswordDialogFragment
.getInstance(passwordField)
.show(supportFragmentManager, "PasswordGeneratorFragment")
mPasswordField = passwordField
KeyGeneratorActivity.launch(this, mKeyGeneratorResultLauncher)
}
mEntryEditViewModel.requestCustomFieldEdition.observe(this) { field ->
@@ -656,17 +670,6 @@ class EntryEditActivity : DatabaseLockActivity(),
mEntryEditViewModel.selectTime(hours, minutes)
}
override fun acceptPassword(passwordField: Field) {
mEntryEditViewModel.selectPassword(passwordField)
Handler(Looper.getMainLooper()).post {
performedNextEducation()
}
}
override fun cancelPassword(passwordField: Field) {
// Do nothing here
}
override fun onBackPressed() {
onApprovedBackPressed {
super@EntryEditActivity.onBackPressed()

View File

@@ -20,8 +20,11 @@
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
@@ -50,142 +53,166 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
var sharedWebDomain: String? = null
var otpString: String? = null
when (intent?.action) {
Intent.ACTION_SEND -> {
if ("text/plain" == intent.type) {
// Retrieve web domain or OTP
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
else
sharedWebDomain = Uri.parse(extra).host
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
if (keySelectionBundle != null) {
// To manage package name
var searchInfo = SearchInfo()
keySelectionBundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { mSearchInfo ->
searchInfo = mSearchInfo
}
launch(database, searchInfo, true)
} else {
// To manage share
var sharedWebDomain: String? = null
var otpString: String? = null
when (intent?.action) {
Intent.ACTION_SEND -> {
if ("text/plain" == intent.type) {
// Retrieve web domain or OTP
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
else
sharedWebDomain = Uri.parse(extra).host
}
}
}
}
Intent.ACTION_VIEW -> {
// Retrieve OTP
intent.dataString?.let { extra ->
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
Intent.ACTION_VIEW -> {
// Retrieve OTP
intent.dataString?.let { extra ->
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
}
}
else -> {}
}
else -> {}
}
// Build domain search param
val searchInfo = SearchInfo().apply {
this.webDomain = sharedWebDomain
this.otpString = otpString
}
// Build domain search param
val searchInfo = SearchInfo().apply {
this.webDomain = sharedWebDomain
this.otpString = otpString
}
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launch(database, searchInfo)
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launch(database, searchInfo)
}
}
}
private fun launch(database: Database?,
searchInfo: SearchInfo) {
searchInfo: SearchInfo,
forceSelection: Boolean = false) {
if (!searchInfo.containsOnlyNullValues()) {
// Setting to integrate Magikeyboard
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
// Setting to integrate Magikeyboard
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
// If database is open
val readOnly = database?.isReadOnly != false
SearchHelper.checkAutoSearchInfo(this,
database,
searchInfo,
{ openedDatabase, items ->
// Items found
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(
this,
openedDatabase,
searchInfo,
false)
} else {
Toast.makeText(applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
}
} else if (searchShareForMagikeyboard) {
if (items.size == 1) {
// Automatically populate keyboard
val entryPopulate = items[0]
populateKeyboardAndMoveAppToBackground(
this,
entryPopulate,
intent)
} else {
// Select the one we want
GroupActivity.launchForKeyboardSelectionResult(this,
openedDatabase,
searchInfo,
true)
}
// If database is open
val readOnly = database?.isReadOnly != false
SearchHelper.checkAutoSearchInfo(this,
database,
searchInfo,
{ openedDatabase, items ->
// Items found
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(
this,
openedDatabase,
searchInfo,
false)
} else {
GroupActivity.launchForSearchResult(this,
Toast.makeText(applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
}
} else if (searchShareForMagikeyboard) {
if (items.size == 1 && !forceSelection) {
// Automatically populate keyboard
val entryPopulate = items[0]
populateKeyboardAndMoveAppToBackground(
this,
entryPopulate,
intent)
Log.e("TEST", "One item activity")
} else {
// Select the one we want
GroupActivity.launchForKeyboardSelectionResult(this,
openedDatabase,
searchInfo,
true)
}
},
{ openedDatabase ->
// Show the database UI to select the entry
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(this,
openedDatabase,
searchInfo,
false)
} else {
Toast.makeText(applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
}
} else if (readOnly || searchShareForMagikeyboard) {
GroupActivity.launchForKeyboardSelectionResult(this,
openedDatabase,
searchInfo,
false)
} else {
} else {
GroupActivity.launchForSearchResult(this,
openedDatabase,
searchInfo,
true)
}
},
{ openedDatabase ->
// Show the database UI to select the entry
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(this,
openedDatabase,
searchInfo,
false)
}
},
{
// If database not open
if (searchInfo.otpString != null) {
if (!readOnly) {
FileDatabaseSelectActivity.launchForSaveResult(this,
searchInfo)
} else {
Toast.makeText(applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
}
} else if (searchShareForMagikeyboard) {
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
searchInfo)
} else {
FileDatabaseSelectActivity.launchForSearchResult(this,
searchInfo)
Toast.makeText(applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
}
} else if (readOnly || searchShareForMagikeyboard) {
GroupActivity.launchForKeyboardSelectionResult(this,
openedDatabase,
searchInfo,
false)
} else {
GroupActivity.launchForSaveResult(this,
openedDatabase,
searchInfo,
false)
}
)
}
},
{
// If database not open
if (searchInfo.otpString != null) {
FileDatabaseSelectActivity.launchForSaveResult(this,
searchInfo)
} else if (searchShareForMagikeyboard) {
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
searchInfo)
} else {
FileDatabaseSelectActivity.launchForSearchResult(this,
searchInfo)
}
}
)
finish()
}
companion object {
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
fun launch(context: Context,
searchInfo: SearchInfo? = null) {
val intent = Intent(context, EntrySelectionLauncherActivity::class.java).apply {
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
putParcelable(KEY_SEARCH_INFO, searchInfo)
})
}
// New task needed because don't launch from an Activity context
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
}
}
fun populateKeyboardAndMoveAppToBackground(activity: Activity,

View File

@@ -37,6 +37,7 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
@@ -73,6 +74,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
// Views
private lateinit var coordinatorLayout: CoordinatorLayout
private var specialTitle: View? = null
private var createDatabaseButtonView: View? = null
private var openDatabaseButtonView: View? = null
@@ -112,6 +114,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
toolbar.title = ""
setSupportActionBar(toolbar)
// Special title
specialTitle = findViewById(R.id.file_selection_title_part_3)
// Create database button
createDatabaseButtonView = findViewById(R.id.create_database_button)
createDatabaseButtonView?.setOnClickListener { createNewFile() }
@@ -335,6 +340,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
override fun onResume() {
super.onResume()
// Define special title
specialTitle?.isVisible = UriUtil.contributingUser(this)
// Show open and create button or special mode
when (mSpecialMode) {
SpecialMode.DEFAULT -> {
@@ -391,7 +399,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
super.onCreateOptionsMenu(menu)
if (mSpecialMode == SpecialMode.DEFAULT) {
MenuUtil.defaultMenuInflater(menuInflater, menu)
MenuUtil.defaultMenuInflater(this, menuInflater, menu)
}
Handler(Looper.getMainLooper()).post {

View File

@@ -79,6 +79,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.settings.SettingsActivity
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.*
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
@@ -182,6 +183,11 @@ class GroupActivity : DatabaseLockActivity(),
addSearch()
//loadGroup()
// Back to previous keyboard
if (PreferencesUtil.isKeyboardPreviousSearchEnable(this@GroupActivity)) {
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
}
return true
}
@@ -200,7 +206,7 @@ class GroupActivity : DatabaseLockActivity(),
finishNodeAction()
if (mSearchState == null) {
mSearchState = SearchState(searchFiltersView?.searchParameters
?: SearchParameters(), 0)
?: PreferencesUtil.getDefaultSearchParameters(this), 0)
}
}
@@ -719,7 +725,7 @@ class GroupActivity : DatabaseLockActivity(),
val stringQuery = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
intent.action = Intent.ACTION_DEFAULT
intent.removeExtra(SearchManager.QUERY)
mSearchState = SearchState(SearchParameters().apply {
mSearchState = SearchState(PreferencesUtil.getDefaultSearchParameters(this).apply {
searchQuery = stringQuery
}, mSearchState?.firstVisibleItem ?: 0)
}
@@ -1126,6 +1132,7 @@ class GroupActivity : DatabaseLockActivity(),
finishNodeAction()
searchView?.setOnQueryTextListener(null)
searchFiltersView?.saveSearchParameters()
}
private fun addSearchQueryInSearchView(searchQuery: String) {

View File

@@ -0,0 +1,139 @@
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.commit
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.fragments.KeyGeneratorFragment
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.updateLockPaddingLeft
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
class KeyGeneratorActivity : DatabaseLockActivity() {
private lateinit var toolbar: Toolbar
private lateinit var coordinatorLayout: CoordinatorLayout
private lateinit var validationButton: View
private var lockView: View? = null
private val keyGeneratorViewModel: KeyGeneratorViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_key_generator)
toolbar = findViewById(R.id.toolbar)
toolbar.title = " "
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
coordinatorLayout = findViewById(R.id.key_generator_coordinator)
lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener {
lockAndExit()
}
validationButton = findViewById(R.id.key_generator_validation)
validationButton.setOnClickListener {
keyGeneratorViewModel.validateKeyGenerated()
}
supportFragmentManager.commit {
replace(R.id.key_generator_fragment, KeyGeneratorFragment.getInstance(
// Default selection tab
KeyGeneratorFragment.KeyGeneratorTab.PASSWORD
), KEY_GENERATED_FRAGMENT_TAG
)
}
keyGeneratorViewModel.keyGenerated.observe(this) { keyGenerated ->
setResult(Activity.RESULT_OK, Intent().apply {
putExtra(KEY_GENERATED, keyGenerated)
})
finish()
}
}
override fun viewToInvalidateTimeout(): View? {
return findViewById<ViewGroup>(R.id.key_generator_container)
}
override fun onResume() {
super.onResume()
// Show the lock button
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
View.VISIBLE
} else {
View.GONE
}
// Padding if lock button visible
toolbar.updateLockPaddingLeft()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.key_generator, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
}
R.id.menu_generate -> {
keyGeneratorViewModel.requireKeyGeneration()
}
}
return super.onOptionsItemSelected(item)
}
override fun onBackPressed() {
setResult(Activity.RESULT_CANCELED, Intent())
super.onBackPressed()
}
companion object {
private const val KEY_GENERATED = "KEY_GENERATED"
private const val KEY_GENERATED_FRAGMENT_TAG = "KEY_GENERATED_FRAGMENT_TAG"
fun registerForGeneratedKeyResult(activity: FragmentActivity,
keyGeneratedListener: (String?) -> Unit): ActivityResultLauncher<Intent> {
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
keyGeneratedListener.invoke(
result.data?.getStringExtra(KEY_GENERATED)
)
} else {
keyGeneratedListener.invoke(null)
}
}
}
fun launch(context: FragmentActivity,
resultLauncher: ActivityResultLauncher<Intent>) {
// Create an instance to return the picker icon
resultLauncher.launch(
Intent(context, KeyGeneratorActivity::class.java)
)
}
}
}

View File

@@ -1,59 +0,0 @@
/*
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper
/**
* Activity to select entry in database and populate it in Magikeyboard
*/
class MagikeyboardLauncherActivity : DatabaseModeActivity() {
override fun applyCustomStyle(): Boolean {
return false
}
override fun finishActivityIfReloadRequested(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
SearchHelper.checkAutoSearchInfo(this,
database,
null,
{ _, _ ->
// Not called
// if items found directly returns before calling this activity
},
{ openedDatabase ->
// Select if not found
GroupActivity.launchForKeyboardSelectionResult(this, openedDatabase)
},
{
// Pass extra to get entry
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
}
)
finish()
}
}

View File

@@ -34,11 +34,13 @@ import android.view.ViewGroup
import android.widget.Button
import android.widget.CompoundButton
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.biometric.BiometricManager
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar
@@ -52,6 +54,7 @@ import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
@@ -63,6 +66,7 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.MenuUtil
@@ -79,6 +83,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
// Views
private var toolbar: Toolbar? = null
private var filenameView: TextView? = null
private var advancedUnlockButton: View? = null
private var mainCredentialView: MainCredentialView? = null
private var confirmButtonView: Button? = null
private var infoContainerView: ViewGroup? = null
@@ -116,6 +121,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
supportActionBar?.setDisplayShowHomeEnabled(true)
filenameView = findViewById(R.id.filename)
advancedUnlockButton = findViewById(R.id.activity_password_advanced_unlock_button)
mainCredentialView = findViewById(R.id.activity_password_credentials)
confirmButtonView = findViewById(R.id.activity_password_open_button)
infoContainerView = findViewById(R.id.activity_password_info_container)
@@ -143,6 +149,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
getUriFromIntent(intent)
// Init Biometric elements
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
advancedUnlockButton?.setOnClickListener {
startActivity(Intent(this, SettingsAdvancedUnlockActivity::class.java))
}
}
advancedUnlockFragment = supportFragmentManager
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
if (advancedUnlockFragment == null) {
@@ -227,6 +238,15 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
if (database != null) {
// Trying to load another database
if (mDatabaseFileUri != null
&& database.fileUri != null
&& mDatabaseFileUri != database.fileUri) {
Toast.makeText(this,
R.string.warning_database_already_opened,
Toast.LENGTH_LONG
).show()
}
launchGroupActivityIfLoaded(database)
}
}
@@ -539,7 +559,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
}
if (mSpecialMode == SpecialMode.DEFAULT) {
MenuUtil.defaultMenuInflater(inflater, menu)
MenuUtil.defaultMenuInflater(this, inflater, menu)
}
super.onCreateOptionsMenu(menu)
@@ -588,15 +608,29 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
{
performedNextEducation(menu)
})
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& !readOnlyEducationPerformed) {
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(this)
if ((biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
&& advancedUnlockButton != null) {
mPasswordActivityEducation.checkAndPerformedBiometricEducation(
advancedUnlockButton!!,
{
startActivity(
Intent(
this,
SettingsAdvancedUnlockActivity::class.java
)
)
},
{
advancedUnlockFragment?.performEducation(mPasswordActivityEducation,
readOnlyEducationPerformed,
{
performedNextEducation(menu)
},
{
performedNextEducation(menu)
})
})
}
}
} catch (ignored: Exception) {}
}
}

View File

@@ -1,224 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.view.View
import android.widget.*
import androidx.appcompat.app.AlertDialog
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.applyFontVisibility
class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
private var mListener: GeneratePasswordListener? = null
private var root: View? = null
private var lengthTextView: EditText? = null
private var passwordInputLayoutView: TextInputLayout? = null
private var passwordView: EditText? = null
private var mPasswordField: Field? = null
private var uppercaseBox: CompoundButton? = null
private var lowercaseBox: CompoundButton? = null
private var digitsBox: CompoundButton? = null
private var minusBox: CompoundButton? = null
private var underlineBox: CompoundButton? = null
private var spaceBox: CompoundButton? = null
private var specialsBox: CompoundButton? = null
private var bracketsBox: CompoundButton? = null
private var extendedBox: CompoundButton? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as GeneratePasswordListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + GeneratePasswordListener::class.java.name)
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater
root = inflater.inflate(R.layout.fragment_generate_password, null)
passwordInputLayoutView = root?.findViewById(R.id.password_input_layout)
passwordView = root?.findViewById(R.id.password)
passwordView?.applyFontVisibility()
val passwordCopyView: ImageView? = root?.findViewById(R.id.password_copy_button)
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(activity))
View.VISIBLE else View.GONE
val clipboardHelper = ClipboardHelper(activity)
passwordCopyView?.setOnClickListener {
clipboardHelper.timeoutCopyToClipboard(passwordView!!.text.toString(),
getString(R.string.copy_field,
getString(R.string.entry_password)))
}
lengthTextView = root?.findViewById(R.id.length)
uppercaseBox = root?.findViewById(R.id.cb_uppercase)
lowercaseBox = root?.findViewById(R.id.cb_lowercase)
digitsBox = root?.findViewById(R.id.cb_digits)
minusBox = root?.findViewById(R.id.cb_minus)
underlineBox = root?.findViewById(R.id.cb_underline)
spaceBox = root?.findViewById(R.id.cb_space)
specialsBox = root?.findViewById(R.id.cb_specials)
bracketsBox = root?.findViewById(R.id.cb_brackets)
extendedBox = root?.findViewById(R.id.cb_extended)
mPasswordField = arguments?.getParcelable(KEY_PASSWORD_FIELD)
assignDefaultCharacters()
val seekBar = root?.findViewById<SeekBar>(R.id.seekbar_length)
seekBar?.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
lengthTextView?.setText(progress.toString())
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
context?.let { context ->
seekBar?.progress = PreferencesUtil.getDefaultPasswordLength(context)
}
root?.findViewById<Button>(R.id.generate_password_button)
?.setOnClickListener { fillPassword() }
builder.setView(root)
.setPositiveButton(R.string.accept) { _, _ ->
mPasswordField?.let { passwordField ->
passwordView?.text?.toString()?.let { passwordValue ->
passwordField.protectedValue.stringValue = passwordValue
}
mListener?.acceptPassword(passwordField)
}
dismiss()
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
mPasswordField?.let { passwordField ->
mListener?.cancelPassword(passwordField)
}
dismiss()
}
// Pre-populate a password to possibly save the user a few clicks
fillPassword()
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun assignDefaultCharacters() {
uppercaseBox?.isChecked = false
lowercaseBox?.isChecked = false
digitsBox?.isChecked = false
minusBox?.isChecked = false
underlineBox?.isChecked = false
spaceBox?.isChecked = false
specialsBox?.isChecked = false
bracketsBox?.isChecked = false
extendedBox?.isChecked = false
context?.let { context ->
PreferencesUtil.getDefaultPasswordCharacters(context)?.let { charSet ->
for (passwordChar in charSet) {
when (passwordChar) {
getString(R.string.value_password_uppercase) -> uppercaseBox?.isChecked = true
getString(R.string.value_password_lowercase) -> lowercaseBox?.isChecked = true
getString(R.string.value_password_digits) -> digitsBox?.isChecked = true
getString(R.string.value_password_minus) -> minusBox?.isChecked = true
getString(R.string.value_password_underline) -> underlineBox?.isChecked = true
getString(R.string.value_password_space) -> spaceBox?.isChecked = true
getString(R.string.value_password_special) -> specialsBox?.isChecked = true
getString(R.string.value_password_brackets) -> bracketsBox?.isChecked = true
getString(R.string.value_password_extended) -> extendedBox?.isChecked = true
}
}
}
}
}
private fun fillPassword() {
root?.findViewById<EditText>(R.id.password)?.setText(generatePassword())
}
fun generatePassword(): String {
var password = ""
try {
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
password = PasswordGenerator(resources).generatePassword(length,
uppercaseBox?.isChecked == true,
lowercaseBox?.isChecked == true,
digitsBox?.isChecked == true,
minusBox?.isChecked == true,
underlineBox?.isChecked == true,
spaceBox?.isChecked == true,
specialsBox?.isChecked == true,
bracketsBox?.isChecked == true,
extendedBox?.isChecked == true)
passwordInputLayoutView?.error = null
} catch (e: NumberFormatException) {
passwordInputLayoutView?.error = getString(R.string.error_wrong_length)
} catch (e: IllegalArgumentException) {
passwordInputLayoutView?.error = e.message
}
return password
}
interface GeneratePasswordListener {
fun acceptPassword(passwordField: Field)
fun cancelPassword(passwordField: Field)
}
companion object {
private const val KEY_PASSWORD_FIELD = "KEY_PASSWORD_FIELD"
fun getInstance(field: Field): GeneratePasswordDialogFragment {
return GeneratePasswordDialogFragment().apply {
arguments = Bundle().apply {
putParcelable(KEY_PASSWORD_FIELD, field)
}
}
}
}
}

View File

@@ -36,8 +36,11 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.PassKeyView
import com.kunzisoft.keepass.view.applyFontVisibility
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
@@ -48,8 +51,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private var passwordCheckBox: CompoundButton? = null
private var passwordTextInputLayout: TextInputLayout? = null
private var passwordView: TextView? = null
private var passKeyView: PassKeyView? = null
private var passwordRepeatTextInputLayout: TextInputLayout? = null
private var passwordRepeatView: TextView? = null
@@ -59,6 +61,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private var mListener: AssignMainCredentialDialogListener? = null
private var mExternalFileHelper: ExternalFileHelper? = null
private var mPasswordEntropyCalculator: PasswordEntropy? = null
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
private var mNoKeyConfirmationDialog: AlertDialog? = null
@@ -100,6 +103,13 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
super.onDetach()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create the password entropy object
mPasswordEntropyCalculator = PasswordEntropy()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
@@ -123,10 +133,10 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
}
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
passwordView = rootView?.findViewById(R.id.pass_password)
passKeyView = rootView?.findViewById(R.id.password_view)
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
passwordRepeatView = rootView?.findViewById(R.id.password_confirmation)
passwordRepeatView?.applyFontVisibility()
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
@@ -162,7 +172,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
if (allowNoMasterKey)
showNoKeyConfirmationDialog()
else {
passwordTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
passwordRepeatTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
}
}
if (!error) {
@@ -194,22 +204,22 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
super.onResume()
// To check checkboxes if a text is present
passwordView?.addTextChangedListener(passwordTextWatcher)
passKeyView?.addTextChangedListener(passwordTextWatcher)
}
override fun onPause() {
super.onPause()
passwordView?.removeTextChangedListener(passwordTextWatcher)
passKeyView?.removeTextChangedListener(passwordTextWatcher)
}
private fun verifyPassword(): Boolean {
var error = false
if (passwordCheckBox != null
&& passwordCheckBox!!.isChecked
&& passwordView != null
&& passKeyView != null
&& passwordRepeatView != null) {
mMasterPassword = passwordView!!.text.toString()
mMasterPassword = passKeyView!!.passwordString
val confPassword = passwordRepeatView!!.text.toString()
// Verify that passwords match

View File

@@ -204,9 +204,10 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
android.R.layout.simple_spinner_item, mHotpTokenTypeArray!!).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
// Proprietary only on closed and full version
// Proprietary only on full version
mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION)
UriUtil.contributingUser(activity)
)
totpTokenTypeAdapter = ArrayAdapter(activity,
android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)

View File

@@ -25,7 +25,6 @@ import android.text.SpannableStringBuilder
import androidx.appcompat.app.AlertDialog
import androidx.core.text.HtmlCompat
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.UriUtil
@@ -40,22 +39,12 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
val builder = AlertDialog.Builder(activity)
val stringBuilder = SpannableStringBuilder()
if (BuildConfig.CLOSED_STORE) {
if (BuildConfig.FULL_VERSION) {
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_work_hard), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
} else {
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_buy_pro), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.download) { _, _ ->
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
}
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
}
if (UriUtil.contributingUser(activity)) {
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_work_hard), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
} else {
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")

View File

@@ -16,7 +16,6 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo
@@ -25,6 +24,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.view.TemplateView
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.showByFading
import com.kunzisoft.keepass.viewmodels.EntryViewModel
import java.util.*
@@ -32,6 +32,9 @@ import java.util.*
class EntryFragment: DatabaseFragment() {
private lateinit var rootView: View
private lateinit var mainSection: View
private lateinit var advancedSection: View
private lateinit var templateView: TemplateView
private lateinit var creationDateView: TextView
@@ -72,6 +75,10 @@ class EntryFragment: DatabaseFragment() {
if (savedInstanceState == null) {
view.isVisible = false
}
mainSection = view.findViewById(R.id.entry_section_main)
advancedSection = view.findViewById(R.id.entry_section_advanced)
templateView = view.findViewById(R.id.entry_template)
loadTemplateSettings()
@@ -111,6 +118,19 @@ class EntryFragment: DatabaseFragment() {
}
}
}
mEntryViewModel.sectionSelected.observe(viewLifecycleOwner) { entrySection ->
when (entrySection ?: EntryViewModel.EntrySection.MAIN) {
EntryViewModel.EntrySection.MAIN -> {
mainSection.showByFading()
advancedSection.hideByFading()
}
EntryViewModel.EntrySection.ADVANCED -> {
mainSection.hideByFading()
advancedSection.showByFading()
}
}
}
}
override fun onDatabaseRetrieved(database: Database?) {

View File

@@ -26,14 +26,14 @@ class IconPickerFragment : DatabaseFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_icon_picker, container, false)
return inflater.inflate(R.layout.fragment_tabs_pagination, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewPager = view.findViewById(R.id.icon_picker_pager)
tabLayout = view.findViewById(R.id.icon_picker_tabs)
viewPager = view.findViewById(R.id.tabs_view_pager)
tabLayout = view.findViewById(R.id.tabs_layout)
resetAppTimeoutWhenViewFocusedOrChanged(view)
arguments?.apply {

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2022 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.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.fragment.app.activityViewModels
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.KeyGeneratorPagerAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
class KeyGeneratorFragment : DatabaseFragment() {
private var keyGeneratorPagerAdapter: KeyGeneratorPagerAdapter? = null
private lateinit var viewPager: ViewPager2
private lateinit var tabLayout: TabLayout
private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()
private var mSelectedTab = KeyGeneratorTab.PASSWORD
private var mOnPageChangeCallback: ViewPager2.OnPageChangeCallback = object:
ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
mSelectedTab = KeyGeneratorTab.getKeyGeneratorTabByPosition(position)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_tabs_pagination, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
keyGeneratorPagerAdapter = KeyGeneratorPagerAdapter(this, )
viewPager = view.findViewById(R.id.tabs_view_pager)
tabLayout = view.findViewById(R.id.tabs_layout)
viewPager.adapter = keyGeneratorPagerAdapter
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = getString(KeyGeneratorTab.getKeyGeneratorTabByPosition(position).stringId)
}.attach()
viewPager.registerOnPageChangeCallback(mOnPageChangeCallback)
resetAppTimeoutWhenViewFocusedOrChanged(view)
arguments?.apply {
if (containsKey(PASSWORD_TAB_ARG)) {
viewPager.currentItem = getInt(PASSWORD_TAB_ARG)
}
remove(PASSWORD_TAB_ARG)
}
mKeyGeneratorViewModel.requireKeyGeneration.observe(viewLifecycleOwner) {
when (mSelectedTab) {
KeyGeneratorTab.PASSWORD -> {
mKeyGeneratorViewModel.requirePasswordGeneration()
}
KeyGeneratorTab.PASSPHRASE -> {
mKeyGeneratorViewModel.requirePassphraseGeneration()
}
}
}
mKeyGeneratorViewModel.keyGeneratedValidated.observe(viewLifecycleOwner) {
when (mSelectedTab) {
KeyGeneratorTab.PASSWORD -> {
mKeyGeneratorViewModel.validatePasswordGenerated()
}
KeyGeneratorTab.PASSPHRASE -> {
mKeyGeneratorViewModel.validatePassphraseGenerated()
}
}
}
}
override fun onDestroyView() {
viewPager.unregisterOnPageChangeCallback(mOnPageChangeCallback)
super.onDestroyView()
}
override fun onDatabaseRetrieved(database: Database?) {
// Nothing here
}
enum class KeyGeneratorTab(@StringRes val stringId: Int) {
PASSWORD(R.string.password), PASSPHRASE(R.string.passphrase);
companion object {
fun getKeyGeneratorTabByPosition(position: Int): KeyGeneratorTab {
return when (position) {
0 -> PASSWORD
else -> PASSPHRASE
}
}
}
}
companion object {
private const val PASSWORD_TAB_ARG = "PASSWORD_TAB_ARG"
fun getInstance(keyGeneratorTab: KeyGeneratorTab): KeyGeneratorFragment {
val fragment = KeyGeneratorFragment()
fragment.arguments = Bundle().apply {
putInt(PASSWORD_TAB_ARG, keyGeneratorTab.ordinal)
}
return fragment
}
}
}

View File

@@ -0,0 +1,252 @@
/*
* Copyright 2022 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.fragments
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.activityViewModels
import com.google.android.material.slider.Slider
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.password.PassphraseGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.PassKeyView
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
class PassphraseGeneratorFragment : DatabaseFragment() {
private lateinit var passKeyView: PassKeyView
private lateinit var sliderWordCount: Slider
private lateinit var wordCountText: EditText
private lateinit var charactersCountText: TextView
private lateinit var wordSeparator: EditText
private lateinit var wordCaseSpinner: Spinner
private var minSliderWordCount: Int = 0
private var maxSliderWordCount: Int = 0
private var wordCaseAdapter: ArrayAdapter<String>? = null
private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_generate_passphrase, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
passKeyView = view.findViewById(R.id.passphrase_view)
val passphraseCopyView: ImageView? = view.findViewById(R.id.passphrase_copy_button)
sliderWordCount = view.findViewById(R.id.slider_word_count)
wordCountText = view.findViewById(R.id.word_count)
charactersCountText = view.findViewById(R.id.character_count)
wordSeparator = view.findViewById(R.id.word_separator)
wordCaseSpinner = view.findViewById(R.id.word_case)
minSliderWordCount = resources.getInteger(R.integer.passphrase_generator_word_count_min)
maxSliderWordCount = resources.getInteger(R.integer.passphrase_generator_word_count_max)
contextThemed?.let { context ->
passphraseCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(context))
View.VISIBLE else View.GONE
val clipboardHelper = ClipboardHelper(context)
passphraseCopyView?.setOnClickListener {
clipboardHelper.timeoutCopyToClipboard(passKeyView.passwordString,
getString(R.string.copy_field,
getString(R.string.entry_password)))
}
wordCaseAdapter = ArrayAdapter(context,
android.R.layout.simple_spinner_item, resources.getStringArray(R.array.word_case_array)).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
wordCaseSpinner.adapter = wordCaseAdapter
}
loadSettings()
var listenSlider = true
var listenEditText = true
sliderWordCount.addOnChangeListener { _, value, _ ->
try {
listenEditText = false
if (listenSlider) {
wordCountText.setText(value.toInt().toString())
}
} catch (e: Exception) {
Log.e(TAG, "Unable to set the word count value", e)
} finally {
listenEditText = true
}
}
sliderWordCount.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
// TODO upgrade material-components lib
// https://stackoverflow.com/questions/70873160/material-slider-onslidertouchlisteners-methods-can-only-be-called-from-within-t
@SuppressLint("RestrictedApi")
override fun onStartTrackingTouch(slider: Slider) {}
@SuppressLint("RestrictedApi")
override fun onStopTrackingTouch(slider: Slider) {
generatePassphrase()
}
})
wordCountText.doOnTextChanged { _, _, _, _ ->
if (listenEditText) {
try {
listenSlider = false
setSliderValue(getWordCount())
} catch (e: Exception) {
Log.e(TAG, "Unable to get the word count value", e)
} finally {
listenSlider = true
generatePassphrase()
}
}
}
wordSeparator.doOnTextChanged { _, _, _, _ ->
generatePassphrase()
}
wordCaseSpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
generatePassphrase()
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
generatePassphrase()
mKeyGeneratorViewModel.passphraseGeneratedValidated.observe(viewLifecycleOwner) {
mKeyGeneratorViewModel.setKeyGenerated(passKeyView.passwordString)
}
mKeyGeneratorViewModel.requirePassphraseGeneration.observe(viewLifecycleOwner) {
generatePassphrase()
}
resetAppTimeoutWhenViewFocusedOrChanged(view)
}
private fun getWordCount(): Int {
return try {
Integer.valueOf(wordCountText.text.toString())
} catch (numberException: NumberFormatException) {
minSliderWordCount
}
}
private fun setWordCount(wordCount: Int) {
setSliderValue(wordCount)
wordCountText.setText(wordCount.toString())
}
private fun setSliderValue(value: Int) {
when {
value < minSliderWordCount -> {
sliderWordCount.value = minSliderWordCount.toFloat()
}
value > maxSliderWordCount -> {
sliderWordCount.value = maxSliderWordCount.toFloat()
}
else -> {
sliderWordCount.value = value.toFloat()
}
}
}
private fun getWordSeparator(): String {
return wordSeparator.text.toString().ifEmpty { " " }
}
private fun getWordCase(): PassphraseGenerator.WordCase {
var wordCase = PassphraseGenerator.WordCase.LOWER_CASE
try {
wordCase = PassphraseGenerator.WordCase.getByOrdinal(wordCaseSpinner.selectedItemPosition)
} catch (caseException: Exception) {
Log.e(TAG, "Unable to retrieve the word case", caseException)
}
return wordCase
}
private fun setWordCase(wordCase: PassphraseGenerator.WordCase) {
wordCaseSpinner.setSelection(wordCase.ordinal)
}
private fun getSeparator(): String {
return wordSeparator.text?.toString() ?: ""
}
private fun setSeparator(separator: String) {
wordSeparator.setText(separator)
}
private fun generatePassphrase() {
var passphrase = ""
try {
passphrase = PassphraseGenerator().generatePassphrase(
getWordCount(),
getWordSeparator(),
getWordCase())
} catch (e: Exception) {
Log.e(TAG, "Unable to generate a passphrase", e)
}
passKeyView.passwordString = passphrase
charactersCountText.text = getString(R.string.character_count, passphrase.length)
}
override fun onDestroy() {
saveSettings()
super.onDestroy()
}
private fun saveSettings() {
context?.let { context ->
PreferencesUtil.setDefaultPassphraseWordCount(context, getWordCount())
PreferencesUtil.setDefaultPassphraseWordCase(context, getWordCase())
PreferencesUtil.setDefaultPassphraseSeparator(context, getSeparator())
}
}
private fun loadSettings() {
context?.let { context ->
setWordCount(PreferencesUtil.getDefaultPassphraseWordCount(context))
setWordCase(PreferencesUtil.getDefaultPassphraseWordCase(context))
setSeparator(PreferencesUtil.getDefaultPassphraseSeparator(context))
}
}
override fun onDatabaseRetrieved(database: Database?) {
// Nothing here
}
companion object {
private const val TAG = "PassphraseGnrtrFrgmt"
}
}

View File

@@ -0,0 +1,358 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.fragments
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.ImageView
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.activityViewModels
import com.google.android.material.slider.Slider
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.PassKeyView
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
class PasswordGeneratorFragment : DatabaseFragment() {
private lateinit var passKeyView: PassKeyView
private lateinit var sliderLength: Slider
private lateinit var lengthEditView: EditText
private lateinit var uppercaseCompound: CompoundButton
private lateinit var lowercaseCompound: CompoundButton
private lateinit var digitsCompound: CompoundButton
private lateinit var minusCompound: CompoundButton
private lateinit var underlineCompound: CompoundButton
private lateinit var spaceCompound: CompoundButton
private lateinit var specialsCompound: CompoundButton
private lateinit var bracketsCompound: CompoundButton
private lateinit var extendedCompound: CompoundButton
private lateinit var considerCharsEditText: EditText
private lateinit var ignoreCharsEditText: EditText
private lateinit var atLeastOneCompound: CompoundButton
private lateinit var excludeAmbiguousCompound: CompoundButton
private var minLengthSlider: Int = 0
private var maxLengthSlider: Int = 0
private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_generate_password, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
passKeyView = view.findViewById(R.id.password_view)
val passwordCopyView: ImageView? = view.findViewById(R.id.password_copy_button)
sliderLength = view.findViewById(R.id.slider_length)
lengthEditView = view.findViewById(R.id.length)
uppercaseCompound = view.findViewById(R.id.upperCase_filter)
lowercaseCompound = view.findViewById(R.id.lowerCase_filter)
digitsCompound = view.findViewById(R.id.digits_filter)
minusCompound = view.findViewById(R.id.minus_filter)
underlineCompound = view.findViewById(R.id.underline_filter)
spaceCompound = view.findViewById(R.id.space_filter)
specialsCompound = view.findViewById(R.id.special_filter)
bracketsCompound = view.findViewById(R.id.brackets_filter)
extendedCompound = view.findViewById(R.id.extendedASCII_filter)
considerCharsEditText = view.findViewById(R.id.consider_chars_filter)
ignoreCharsEditText = view.findViewById(R.id.ignore_chars_filter)
atLeastOneCompound = view.findViewById(R.id.atLeastOne_filter)
excludeAmbiguousCompound = view.findViewById(R.id.excludeAmbiguous_filter)
contextThemed?.let { context ->
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(context))
View.VISIBLE else View.GONE
val clipboardHelper = ClipboardHelper(context)
passwordCopyView?.setOnClickListener {
clipboardHelper.timeoutCopyToClipboard(passKeyView.passwordString,
getString(R.string.copy_field,
getString(R.string.entry_password)))
}
}
minLengthSlider = resources.getInteger(R.integer.password_generator_length_min)
maxLengthSlider = resources.getInteger(R.integer.password_generator_length_max)
loadSettings()
uppercaseCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
lowercaseCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
digitsCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
minusCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
underlineCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
spaceCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
specialsCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
bracketsCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
extendedCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
considerCharsEditText.doOnTextChanged { _, _, _, _ ->
generatePassword()
}
ignoreCharsEditText.doOnTextChanged { _, _, _, _ ->
generatePassword()
}
atLeastOneCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
excludeAmbiguousCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
var listenSlider = true
var listenEditText = true
sliderLength.addOnChangeListener { _, value, _ ->
try {
listenEditText = false
if (listenSlider) {
lengthEditView.setText(value.toInt().toString())
}
} catch (e: Exception) {
Log.e(TAG, "Unable to set the length value", e)
} finally {
listenEditText = true
}
}
sliderLength.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
// TODO upgrade material-components lib
// https://stackoverflow.com/questions/70873160/material-slider-onslidertouchlisteners-methods-can-only-be-called-from-within-t
@SuppressLint("RestrictedApi")
override fun onStartTrackingTouch(slider: Slider) {}
@SuppressLint("RestrictedApi")
override fun onStopTrackingTouch(slider: Slider) {
generatePassword()
}
})
lengthEditView.doOnTextChanged { _, _, _, _ ->
if (listenEditText) {
try {
listenSlider = false
setSliderValue(getPasswordLength())
} catch (e: Exception) {
Log.e(TAG, "Unable to get the length value", e)
} finally {
listenSlider = true
generatePassword()
}
}
}
// Pre-populate a password to possibly save the user a few clicks
generatePassword()
mKeyGeneratorViewModel.passwordGeneratedValidated.observe(viewLifecycleOwner) {
mKeyGeneratorViewModel.setKeyGenerated(passKeyView.passwordString)
}
mKeyGeneratorViewModel.requirePasswordGeneration.observe(viewLifecycleOwner) {
generatePassword()
}
resetAppTimeoutWhenViewFocusedOrChanged(view)
}
private fun getPasswordLength(): Int {
return try {
Integer.valueOf(lengthEditView.text.toString())
} catch (numberException: NumberFormatException) {
minLengthSlider
}
}
private fun setPasswordLength(passwordLength: Int) {
setSliderValue(passwordLength)
lengthEditView.setText(passwordLength.toString())
}
private fun getOptions(): Set<String> {
val optionsSet = mutableSetOf<String>()
if (uppercaseCompound.isChecked)
optionsSet.add(getString(R.string.value_password_uppercase))
if (lowercaseCompound.isChecked)
optionsSet.add(getString(R.string.value_password_lowercase))
if (digitsCompound.isChecked)
optionsSet.add(getString(R.string.value_password_digits))
if (minusCompound.isChecked)
optionsSet.add(getString(R.string.value_password_minus))
if (underlineCompound.isChecked)
optionsSet.add(getString(R.string.value_password_underline))
if (spaceCompound.isChecked)
optionsSet.add(getString(R.string.value_password_space))
if (specialsCompound.isChecked)
optionsSet.add(getString(R.string.value_password_special))
if (bracketsCompound.isChecked)
optionsSet.add(getString(R.string.value_password_brackets))
if (extendedCompound.isChecked)
optionsSet.add(getString(R.string.value_password_extended))
if (atLeastOneCompound.isChecked)
optionsSet.add(getString(R.string.value_password_atLeastOne))
if (excludeAmbiguousCompound.isChecked)
optionsSet.add(getString(R.string.value_password_excludeAmbiguous))
return optionsSet
}
private fun setOptions(options: Set<String>) {
uppercaseCompound.isChecked = false
lowercaseCompound.isChecked = false
digitsCompound.isChecked = false
minusCompound.isChecked = false
underlineCompound.isChecked = false
spaceCompound.isChecked = false
specialsCompound.isChecked = false
bracketsCompound.isChecked = false
extendedCompound.isChecked = false
atLeastOneCompound.isChecked = false
excludeAmbiguousCompound.isChecked = false
for (option in options) {
when (option) {
getString(R.string.value_password_uppercase) -> uppercaseCompound.isChecked = true
getString(R.string.value_password_lowercase) -> lowercaseCompound.isChecked = true
getString(R.string.value_password_digits) -> digitsCompound.isChecked = true
getString(R.string.value_password_minus) -> minusCompound.isChecked = true
getString(R.string.value_password_underline) -> underlineCompound.isChecked = true
getString(R.string.value_password_space) -> spaceCompound.isChecked = true
getString(R.string.value_password_special) -> specialsCompound.isChecked = true
getString(R.string.value_password_brackets) -> bracketsCompound.isChecked = true
getString(R.string.value_password_extended) -> extendedCompound.isChecked = true
getString(R.string.value_password_atLeastOne) -> atLeastOneCompound.isChecked = true
getString(R.string.value_password_excludeAmbiguous) -> excludeAmbiguousCompound.isChecked = true
}
}
}
private fun getConsiderChars(): String {
return considerCharsEditText.text.toString()
}
private fun setConsiderChars(chars: String) {
considerCharsEditText.setText(chars)
}
private fun getIgnoreChars(): String {
return ignoreCharsEditText.text.toString()
}
private fun setIgnoreChars(chars: String) {
ignoreCharsEditText.setText(chars)
}
private fun generatePassword() {
var password = ""
try {
password = PasswordGenerator(resources).generatePassword(getPasswordLength(),
uppercaseCompound.isChecked,
lowercaseCompound.isChecked,
digitsCompound.isChecked,
minusCompound.isChecked,
underlineCompound.isChecked,
spaceCompound.isChecked,
specialsCompound.isChecked,
bracketsCompound.isChecked,
extendedCompound.isChecked,
getConsiderChars(),
getIgnoreChars(),
atLeastOneCompound.isChecked,
excludeAmbiguousCompound.isChecked)
} catch (e: Exception) {
Log.e(TAG, "Unable to generate a password", e)
}
passKeyView.passwordString = password
}
override fun onDestroy() {
saveSettings()
super.onDestroy()
}
override fun onDatabaseRetrieved(database: Database?) {
// Nothing here
}
private fun saveSettings() {
context?.let { context ->
PreferencesUtil.setDefaultPasswordOptions(context, getOptions())
PreferencesUtil.setDefaultPasswordLength(context, getPasswordLength())
PreferencesUtil.setDefaultPasswordConsiderChars(context, getConsiderChars())
PreferencesUtil.setDefaultPasswordIgnoreChars(context, getIgnoreChars())
}
}
private fun loadSettings() {
context?.let { context ->
setOptions(PreferencesUtil.getDefaultPasswordOptions(context))
setPasswordLength(PreferencesUtil.getDefaultPasswordLength(context))
setConsiderChars(PreferencesUtil.getDefaultPasswordConsiderChars(context))
setIgnoreChars(PreferencesUtil.getDefaultPasswordIgnoreChars(context))
}
}
private fun setSliderValue(value: Int) {
when {
value < minLengthSlider -> {
sliderLength.value = minLengthSlider.toFloat()
}
value > maxLengthSlider -> {
sliderLength.value = maxLengthSlider.toFloat()
}
else -> {
sliderLength.value = value.toFloat()
}
}
}
companion object {
private const val TAG = "PasswordGeneratorFrgmt"
}
}

View File

@@ -29,6 +29,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
@@ -431,7 +432,19 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
protected fun lockAndExit() {
sendBroadcast(Intent(LOCK_ACTION))
// Ask confirmation if modification not saved
if (mDatabase?.isReadOnly == false
&& mDatabase?.dataModifiedSinceLastLoading == true
&& !PreferencesUtil.isAutoSaveDatabaseEnabled(this)) {
AlertDialog.Builder(this)
.setMessage(R.string.discard_changes)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.lock) { _, _ ->
sendBroadcast(Intent(LOCK_ACTION))
}.create().show()
} else {
sendBroadcast(Intent(LOCK_ACTION))
}
}
fun resetAppTimeout() {

View File

@@ -1,5 +1,6 @@
package com.kunzisoft.keepass.activities.legacy
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
@@ -95,11 +96,9 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
private fun backToTheMainAppAndFinish() {
// To move the app in background and return to the main app
// Not visible as opened with FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
moveTaskToBack(true)
// To remove this instance in the OS app selector
Handler(Looper.getMainLooper()).postDelayed({
finish()
}, 500)
// Not finish() to prevent service kill
}
override fun onCreate(savedInstanceState: Bundle?) {

View File

@@ -0,0 +1,25 @@
package com.kunzisoft.keepass.adapters
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.kunzisoft.keepass.activities.fragments.PassphraseGeneratorFragment
import com.kunzisoft.keepass.activities.fragments.PasswordGeneratorFragment
import com.kunzisoft.keepass.activities.fragments.KeyGeneratorFragment
class KeyGeneratorPagerAdapter(fragment: Fragment)
: FragmentStateAdapter(fragment) {
private val passwordGeneratorFragment = PasswordGeneratorFragment()
private val passphraseGeneratorFragment = PassphraseGeneratorFragment()
override fun getItemCount(): Int {
return KeyGeneratorFragment.KeyGeneratorTab.values().size
}
override fun createFragment(position: Int): Fragment {
return when (KeyGeneratorFragment.KeyGeneratorTab.getKeyGeneratorTabByPosition(position)) {
KeyGeneratorFragment.KeyGeneratorTab.PASSWORD -> passwordGeneratorFragment
KeyGeneratorFragment.KeyGeneratorTab.PASSPHRASE -> passphraseGeneratorFragment
}
}
}

View File

@@ -516,7 +516,7 @@ class NodesAdapter (private val context: Context,
null -> {}
}
holder?.otpToken?.apply {
text = otpElement?.token
text = otpElement?.tokenString
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
}
holder?.otpContainer?.setOnClickListener {

View File

@@ -130,8 +130,9 @@ class CipherDatabaseAction(context: Context) {
cipherDatabaseResultListener: (CipherEncryptDatabase?) -> Unit) {
if (useTempDao) {
serviceActionTask {
var cipherDatabase: CipherEncryptDatabase? = null
mBinder?.getCipherDatabase(databaseUri)?.let { cipherDatabaseEntity ->
val cipherDatabase = CipherEncryptDatabase().apply {
cipherDatabase = CipherEncryptDatabase().apply {
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
this.encryptedValue = Base64.decode(
cipherDatabaseEntity.encryptedValue,
@@ -142,8 +143,8 @@ class CipherDatabaseAction(context: Context) {
Base64.NO_WRAP
)
}
cipherDatabaseResultListener.invoke(cipherDatabase)
}
cipherDatabaseResultListener.invoke(cipherDatabase)
}
} else {
IOActionTask(

View File

@@ -26,7 +26,7 @@ import kotlinx.coroutines.*
*/
class IOActionTask<T>(
private val action: () -> T ,
private val afterActionDatabaseListener: ((T?) -> Unit)? = null) {
private val afterActionListener: ((T?) -> Unit)? = null) {
private val mainScope = CoroutineScope(Dispatchers.Main)
@@ -42,7 +42,7 @@ class IOActionTask<T>(
}
}
withContext(Dispatchers.Main) {
afterActionDatabaseListener?.invoke(asyncResult.await())
afterActionListener?.invoke(asyncResult.await())
}
}
}

View File

@@ -377,9 +377,11 @@ class StructureParser(private val structure: AssistStructure) {
when {
inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate android number type: ${showHexInputType(inputType)}")
if (usernameIdCandidate == null) {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate android number type: ${showHexInputType(inputType)}")
}
}
inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {

View File

@@ -45,6 +45,8 @@ import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.CredentialStorage
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.showByFading
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -579,10 +581,13 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
private fun showViews(show: Boolean) {
lifecycleScope.launch(Dispatchers.Main) {
mAdvancedUnlockInfoView?.visibility = if (show)
View.VISIBLE
if (show) {
if (mAdvancedUnlockInfoView?.visibility != View.VISIBLE)
mAdvancedUnlockInfoView?.showByFading()
}
else {
View.GONE
if (mAdvancedUnlockInfoView?.visibility == View.VISIBLE)
mAdvancedUnlockInfoView?.hideByFading()
}
}
}
@@ -608,26 +613,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
}
}
fun performEducation(passwordActivityEducation: PasswordActivityEducation,
readOnlyEducationPerformed: Boolean,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& !readOnlyEducationPerformed) {
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext())
PreferencesUtil.isAdvancedUnlockEnable(requireContext())
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
&& mAdvancedUnlockInfoView != null && mAdvancedUnlockInfoView?.visibility == View.VISIBLE
&& mAdvancedUnlockInfoView?.unlockIconImageView != null
&& passwordActivityEducation.checkAndPerformedBiometricEducation(mAdvancedUnlockInfoView!!.unlockIconImageView!!,
onEducationViewClick,
onOuterViewClick)
}
} catch (ignored: Exception) {}
}
enum class Mode {
BIOMETRIC_UNAVAILABLE,
BIOMETRIC_SECURITY_UPDATE_REQUIRED,

View File

@@ -124,7 +124,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
}
}
private fun getSecretKey(): SecretKey? {
@Synchronized private fun getSecretKey(): SecretKey? {
if (!isKeyManagerInitialized) {
return null
}
@@ -141,8 +141,8 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
KeyGenParameterSpec.Builder(
ADVANCED_UNLOCK_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setBlockModes(ADVANCED_UNLOCK_BLOCKS_MODES)
.setEncryptionPaddings(ADVANCED_UNLOCK_ENCRYPTION_PADDING)
.apply {
// 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
@@ -173,11 +173,11 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
return null
}
fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) {
@Synchronized fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) {
initEncryptData(actionIfCypherInit, true)
}
private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
@Synchronized private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
firstLaunch: Boolean) {
if (!isKeyManagerInitialized) {
return
@@ -213,7 +213,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
}
}
fun encryptData(value: ByteArray) {
@Synchronized fun encryptData(value: ByteArray) {
if (!isKeyManagerInitialized) {
return
}
@@ -229,12 +229,12 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
}
}
fun initDecryptData(ivSpecValue: ByteArray,
@Synchronized fun initDecryptData(ivSpecValue: ByteArray,
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
initDecryptData(ivSpecValue, actionIfCypherInit, true)
}
private fun initDecryptData(ivSpecValue: ByteArray,
@Synchronized private fun initDecryptData(ivSpecValue: ByteArray,
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
firstLaunch: Boolean = true) {
if (!isKeyManagerInitialized) {
@@ -278,7 +278,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
}
}
fun decryptData(encryptedValue: ByteArray) {
@Synchronized fun decryptData(encryptedValue: ByteArray) {
if (!isKeyManagerInitialized) {
return
}
@@ -296,7 +296,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
}
}
fun deleteKeystoreKey() {
@Synchronized fun deleteKeystoreKey() {
try {
keyStore?.load(null)
keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
@@ -306,7 +306,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
}
}
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt,
@Synchronized fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt,
deviceCredentialResultLauncher: ActivityResultLauncher<Intent>
) {
// Init advanced unlock prompt
@@ -346,7 +346,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
}
}
fun closeBiometricPrompt() {
@Synchronized fun closeBiometricPrompt() {
biometricPrompt?.cancelAuthentication()
}

View File

@@ -913,53 +913,24 @@ class Database {
try {
val saveUri = databaseCopyUri ?: this.fileUri
if (saveUri != null) {
if (saveUri.scheme == "file") {
saveUri.path?.let { filename ->
val tempFile = File("$filename.tmp")
var fileOutputStream: FileOutputStream? = null
try {
fileOutputStream = FileOutputStream(tempFile)
val pmo = mDatabaseKDB?.let { DatabaseOutputKDB(it, fileOutputStream) }
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, fileOutputStream) }
pmo?.output()
} catch (e: Exception) {
throw IOException(e)
} finally {
fileOutputStream?.close()
}
// Force data to disk before continuing
try {
fileOutputStream?.fd?.sync()
} catch (e: SyncFailedException) {
// Ignore if fsync fails. We tried.
}
if (!tempFile.renameTo(File(filename))) {
throw IOException()
}
}
} else {
var outputStream: OutputStream? = null
try {
outputStream = contentResolver.openOutputStream(saveUri, "rwt")
outputStream?.let { definedOutputStream ->
val databaseOutput =
mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
?: mDatabaseKDBX?.let {
DatabaseOutputKDBX(
it,
definedOutputStream
)
}
databaseOutput?.output()
}
} catch (e: Exception) {
throw IOException(e)
} finally {
outputStream?.close()
var outputStream: OutputStream? = null
try {
outputStream = UriUtil.getUriOutputStream(contentResolver, saveUri)
outputStream?.let { definedOutputStream ->
val databaseOutput =
mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
?: mDatabaseKDBX?.let {
DatabaseOutputKDBX(
it,
definedOutputStream
)
}
databaseOutput?.output()
}
} catch (e: Exception) {
throw IOException(e)
} finally {
outputStream?.close()
}
if (databaseCopyUri == null) {
this.dataModifiedSinceLastLoading = false

View File

@@ -180,12 +180,16 @@ abstract class BinaryData : Parcelable {
companion object {
private val TAG = BinaryData::class.java.name
private const val MAX_BINARY_BYTE = 10485760 // 10 MB
fun canMemoryBeAllocatedInRAM(context: Context, memoryWanted: Long): Boolean {
if (memoryWanted > MAX_BINARY_BYTE)
return false
val memoryInfo = ActivityManager.MemoryInfo()
(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(memoryInfo)
(context.getSystemService(Context.ACTIVITY_SERVICE)
as? ActivityManager?)?.getMemoryInfo(memoryInfo)
val availableMemory = memoryInfo.availMem
return availableMemory > memoryWanted * 3
return availableMemory > (memoryWanted * 5)
}
}

View File

@@ -60,6 +60,11 @@ object TemplateField {
const val LABEL_SECURE_NOTE = "Secure Note"
const val LABEL_MEMBERSHIP = "Membership"
fun isStandardPasswordName(context: Context, name: String): Boolean {
return name.equals(LABEL_PASSWORD, true)
|| name == getLocalizedName(context, LABEL_PASSWORD)
}
fun isStandardFieldName(name: String): Boolean {
return arrayOf(
LABEL_TITLE,

View File

@@ -181,7 +181,7 @@ class SearchHelper {
return false
// Exclude entry expired
if (searchParameters.excludeExpired) {
if (!searchParameters.searchInExpired) {
if (entry.isCurrentlyExpires)
return false
}
@@ -237,14 +237,20 @@ class SearchHelper {
return false
return if (searchParameters.isRegex) {
val regex = if (searchParameters.caseSensitive) {
searchParameters.searchQuery.toRegex(RegexOption.DOT_MATCHES_ALL)
searchParameters.searchQuery
.toRegex(RegexOption.DOT_MATCHES_ALL)
} else {
searchParameters.searchQuery
.toRegex(setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.IGNORE_CASE))
}
regex.matches(stringToCheck)
} else {
stringToCheck.contains(searchParameters.searchQuery, !searchParameters.caseSensitive)
var searchFound = true
searchParameters.searchQuery.split(" ").forEach { word ->
searchFound = searchFound
&& stringToCheck.contains(word, !searchParameters.caseSensitive)
}
searchFound
}
}
}

View File

@@ -34,7 +34,7 @@ class SearchParameters() : Parcelable{
var searchInUsernames = true
var searchInPasswords = false
var searchInUrls = true
var excludeExpired = false
var searchInExpired = false
var searchInNotes = true
var searchInOTP = false
var searchInOther = true
@@ -49,11 +49,12 @@ class SearchParameters() : Parcelable{
constructor(parcel: Parcel) : this() {
searchQuery = parcel.readString() ?: searchQuery
caseSensitive = parcel.readByte() != 0.toByte()
isRegex = parcel.readByte() != 0.toByte()
searchInTitles = parcel.readByte() != 0.toByte()
searchInUsernames = parcel.readByte() != 0.toByte()
searchInPasswords = parcel.readByte() != 0.toByte()
searchInUrls = parcel.readByte() != 0.toByte()
excludeExpired = parcel.readByte() != 0.toByte()
searchInExpired = parcel.readByte() != 0.toByte()
searchInNotes = parcel.readByte() != 0.toByte()
searchInOTP = parcel.readByte() != 0.toByte()
searchInOther = parcel.readByte() != 0.toByte()
@@ -68,11 +69,12 @@ class SearchParameters() : Parcelable{
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(searchQuery)
parcel.writeByte(if (caseSensitive) 1 else 0)
parcel.writeByte(if (isRegex) 1 else 0)
parcel.writeByte(if (searchInTitles) 1 else 0)
parcel.writeByte(if (searchInUsernames) 1 else 0)
parcel.writeByte(if (searchInPasswords) 1 else 0)
parcel.writeByte(if (searchInUrls) 1 else 0)
parcel.writeByte(if (excludeExpired) 1 else 0)
parcel.writeByte(if (searchInExpired) 1 else 0)
parcel.writeByte(if (searchInNotes) 1 else 0)
parcel.writeByte(if (searchInOTP) 1 else 0)
parcel.writeByte(if (searchInOther) 1 else 0)

View File

@@ -360,6 +360,13 @@ open class Education(val activity: Activity) {
context.resources.getBoolean(R.bool.education_setup_OTP_default))
}
fun setEducationScreenReclickedPerformed(context: Context) {
getEducationSharedPreferences(context)
.edit()
.putBoolean(context.getString(R.string.education_screen_reclicked_key), true)
.apply()
}
/**
* Defines if the reset education preference has been reclicked
*

View File

@@ -18,6 +18,10 @@ package com.kunzisoft.keepass.magikeyboard;
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY;
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP;
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
import android.content.Context;
import android.content.res.TypedArray;
@@ -1047,6 +1051,12 @@ public class KeyboardView extends View implements View.OnClickListener {
if (popupKey.codes[0] == KEY_BACK_KEYBOARD) {
mKeyboardActionListener.onKey(KEY_CHANGE_KEYBOARD, popupKey.codes);
return true;
} else if (popupKey.codes[0] == KEY_ENTRY) {
mKeyboardActionListener.onKey(KEY_ENTRY_ALT, popupKey.codes);
return true;
} else if (popupKey.codes[0] == KEY_OTP) {
mKeyboardActionListener.onKey(KEY_OTP_ALT, popupKey.codes);
return true;
} else {
int popupKeyboardId = popupKey.popupResId;
if (popupKeyboardId != 0) {

View File

@@ -30,19 +30,27 @@ import android.view.*
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.PopupWindow
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity
import com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity
import com.kunzisoft.keepass.adapters.FieldsAdapter
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.*
import java.util.*
class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionListener {
@@ -50,13 +58,19 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
private var mDatabase: Database? = null
private var keyboardView: KeyboardView? = null
private var entryContainer: View? = null
private var entryText: TextView? = null
private var databaseText: TextView? = null
private var databaseColorView: ImageView? = null
private var packageText: TextView? = null
private var keyboard: Keyboard? = null
private var keyboardEntry: Keyboard? = null
private var popupCustomKeys: PopupWindow? = null
private var fieldsAdapter: FieldsAdapter? = null
private var playSoundDuringCLick: Boolean = false
private var mFormPackageName: String? = null
private var lockReceiver: LockReceiver? = null
override fun onCreate() {
@@ -66,6 +80,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
mDatabaseTaskProvider?.registerProgressTask()
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
this.mDatabase = database
assignKeyboardView()
}
// Remove the entry and lock the keyboard when the lock signal is receive
lockReceiver = LockReceiver {
@@ -82,7 +97,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
override fun onCreateInputView(): View {
val rootKeyboardView = layoutInflater.inflate(R.layout.keyboard_container, null)
entryContainer = rootKeyboardView.findViewById(R.id.magikeyboard_entry_container)
entryText = rootKeyboardView.findViewById(R.id.magikeyboard_entry_text)
databaseText = rootKeyboardView.findViewById(R.id.magikeyboard_database_text)
databaseColorView = rootKeyboardView.findViewById(R.id.magikeyboard_database_color)
packageText = rootKeyboardView.findViewById(R.id.magikeyboard_package_text)
keyboardView = rootKeyboardView.findViewById(R.id.magikeyboard_view)
if (keyboardView != null) {
@@ -94,7 +113,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
.inflate(R.layout.keyboard_popup_fields, FrameLayout(context))
popupCustomKeys = PopupWindow(context).apply {
width = WindowManager.LayoutParams.WRAP_CONTENT
width = WindowManager.LayoutParams.MATCH_PARENT
height = WindowManager.LayoutParams.WRAP_CONTENT
inputMethodMode = PopupWindow.INPUT_METHOD_NEEDED
contentView = popupFieldsView
@@ -104,7 +123,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
fieldsAdapter = FieldsAdapter(this)
fieldsAdapter?.onItemClickListener = object : FieldsAdapter.OnItemClickListener {
override fun onItemClick(item: Field) {
currentInputConnection.commitText(entryInfoKey?.getGeneratedFieldValue(item.name) , 1)
currentInputConnection.commitText(getEntryInfo()?.getGeneratedFieldValue(item.name) , 1)
actionTabAutomatically()
}
}
@@ -114,17 +133,6 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
val closeView = popupFieldsView.findViewById<View>(R.id.keyboard_popup_close)
closeView.setOnClickListener { popupCustomKeys?.dismiss() }
// Remove entry info if the database is not loaded
// or if entry info timestamp is before database loaded timestamp
val databaseTime = mDatabase?.loadTimestamp
val entryTime = entryInfoTimestamp
if (mDatabase == null
|| mDatabase?.loaded != true
|| databaseTime == null
|| entryTime == null
|| entryTime < databaseTime) {
removeEntryInfo()
}
assignKeyboardView()
keyboardView?.onKeyboardActionListener = this
@@ -134,17 +142,27 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
return super.onCreateInputView()
}
private fun getEntryInfo(): EntryInfo? {
var entryInfoRetrieved: EntryInfo? = null
entryUUID?.let { entryId ->
entryInfoRetrieved = mDatabase
?.getEntryById(NodeIdUUID(entryId))
?.getEntryInfo(mDatabase)
}
return entryInfoRetrieved
}
private fun assignKeyboardView() {
dismissCustomKeys()
if (keyboardView != null) {
if (entryInfoKey != null) {
val entryInfo = getEntryInfo()
populateEntryInfoInView(entryInfo)
if (entryInfo != null) {
if (keyboardEntry != null) {
populateEntryInfoInView()
keyboardView?.keyboard = keyboardEntry
}
} else {
if (keyboard != null) {
hideEntryInfo()
keyboardView?.keyboard = keyboard
}
}
@@ -153,23 +171,45 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
keyboardView?.isHapticFeedbackEnabled = PreferencesUtil.isKeyboardVibrationEnable(this)
playSoundDuringCLick = PreferencesUtil.isKeyboardSoundEnable(this)
}
setDatabaseViews()
}
private fun populateEntryInfoInView() {
entryText?.visibility = View.VISIBLE
if (entryInfoKey?.title?.isNotEmpty() == true) {
entryText?.text = entryInfoKey?.title
private fun setDatabaseViews() {
if (mDatabase == null || mDatabase?.loaded != true) {
entryContainer?.visibility = View.GONE
} else {
hideEntryInfo()
entryContainer?.visibility = View.VISIBLE
}
databaseText?.text = mDatabase?.name ?: ""
val databaseColor = mDatabase?.customColor
if (databaseColor != null) {
databaseColorView?.drawable?.colorFilter = BlendModeColorFilterCompat
.createBlendModeColorFilterCompat(databaseColor, BlendModeCompat.SRC_IN)
databaseColorView?.visibility = View.VISIBLE
} else {
databaseColorView?.visibility = View.GONE
}
}
private fun hideEntryInfo() {
entryText?.visibility = View.GONE
private fun populateEntryInfoInView(entryInfo: EntryInfo?) {
if (entryInfo == null) {
entryText?.text = ""
entryText?.visibility = View.GONE
} else {
entryText?.text = entryInfo.getVisualTitle()
entryText?.visibility = View.VISIBLE
}
}
override fun onStartInputView(info: EditorInfo, restarting: Boolean) {
super.onStartInputView(info, restarting)
mFormPackageName = info.packageName
if (!mFormPackageName.isNullOrEmpty()) {
packageText?.text = mFormPackageName
packageText?.visibility = View.VISIBLE
} else {
packageText?.visibility = View.GONE
}
assignKeyboardView()
}
@@ -228,16 +268,17 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
(getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?)
?.showInputMethodPicker()
}
KEY_UNLOCK -> {
}
KEY_ENTRY -> {
// Stop current service and reinit entry
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
removeEntryInfo()
val intent = Intent(this, MagikeyboardLauncherActivity::class.java)
// New task needed because don't launch from an Activity context
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
var searchInfo: SearchInfo? = null
if (mFormPackageName != null) {
searchInfo = SearchInfo().apply {
applicationId = mFormPackageName
}
}
actionKeyEntry(searchInfo)
}
KEY_ENTRY_ALT -> {
actionKeyEntry()
}
KEY_LOCK -> {
removeEntryInfo()
@@ -245,39 +286,58 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
dismissCustomKeys()
}
KEY_USERNAME -> {
if (entryInfoKey != null) {
currentInputConnection.commitText(entryInfoKey!!.username, 1)
getEntryInfo()?.username?.let { username ->
currentInputConnection.commitText(username, 1)
}
actionTabAutomatically()
}
KEY_PASSWORD -> {
if (entryInfoKey != null) {
currentInputConnection.commitText(entryInfoKey!!.password, 1)
val entryInfoKey = getEntryInfo()
entryInfoKey?.password?.let { password ->
currentInputConnection.commitText(password, 1)
}
val otpFieldExists = entryInfoKey?.containsCustomField(OTP_TOKEN_FIELD) ?: false
actionGoAutomatically(!otpFieldExists)
}
KEY_OTP -> {
if (entryInfoKey != null) {
getEntryInfo()?.let { entryInfo ->
currentInputConnection.commitText(
entryInfoKey!!.getGeneratedFieldValue(OTP_TOKEN_FIELD), 1)
entryInfo.getGeneratedFieldValue(OTP_TOKEN_FIELD), 1)
}
actionGoAutomatically()
}
KEY_OTP_ALT -> {
getEntryInfo()?.let { entryInfo ->
val otpToken = entryInfo.getGeneratedFieldValue(OTP_TOKEN_FIELD)
if (otpToken.isNotEmpty()) {
// Cut to fill each digit separatelyKeyEvent.KEYCODE_TAB
val otpTokenChars = otpToken.chunked(1)
otpTokenChars.forEachIndexed { index, char ->
currentInputConnection.commitText(char, 1)
if (index < (otpTokenChars.size-1))
currentInputConnection.sendKeyEvent(
KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB)
)
}
}
}
actionGoAutomatically()
}
KEY_URL -> {
if (entryInfoKey != null) {
currentInputConnection.commitText(entryInfoKey!!.url, 1)
getEntryInfo()?.url?.let { url ->
currentInputConnection.commitText(url, 1)
}
actionGoAutomatically()
}
KEY_FIELDS -> {
if (entryInfoKey != null) {
getEntryInfo()?.customFields?.let { customFields ->
fieldsAdapter?.apply {
setFields(entryInfoKey!!.customFields.filter { it.name != OTP_TOKEN_FIELD})
setFields(customFields.filter { it.name != OTP_TOKEN_FIELD})
notifyDataSetChanged()
}
}
popupCustomKeys?.showAtLocation(keyboardView, Gravity.END or Gravity.TOP, 0, 0)
popupCustomKeys?.showAtLocation(keyboardView,
Gravity.END or Gravity.TOP, 0, 180)
}
Keyboard.KEYCODE_DELETE -> {
inputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
@@ -286,6 +346,46 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
}
}
private fun actionKeyEntry(searchInfo: SearchInfo? = null) {
SearchHelper.checkAutoSearchInfo(this,
mDatabase,
searchInfo,
{ _, items ->
if (items.size == 1) {
if (entryUUID == null) {
// Automatically populate keyboard
removeEntryInfo()
addEntryAndLaunchNotificationIfAllowed(
this,
items[0],
true
)
assignKeyboardView()
} else {
// Choose another one
launchEntrySelection(null)
}
} else {
// Select if multiple
launchEntrySelection(searchInfo)
}
},
{ _ ->
// Select if not found
launchEntrySelection(searchInfo)
},
{
// Select if database not opened
launchEntrySelection(searchInfo)
}
)
}
private fun launchEntrySelection(searchInfo: SearchInfo?) {
removeEntryInfo()
EntrySelectionLauncherActivity.launch(this, searchInfo)
}
private fun actionTabAutomatically() {
if (PreferencesUtil.isAutoGoActionEnable(this))
currentInputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB))
@@ -342,22 +442,20 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
const val KEY_BACK_KEYBOARD = 600
const val KEY_CHANGE_KEYBOARD = 601
private const val KEY_UNLOCK = 610
private const val KEY_LOCK = 611
private const val KEY_ENTRY = 620
private const val KEY_USERNAME = 500
private const val KEY_PASSWORD = 510
private const val KEY_OTP = 515
private const val KEY_URL = 520
private const val KEY_FIELDS = 530
const val KEY_LOCK = 611
const val KEY_ENTRY = 620
const val KEY_ENTRY_ALT = 621
const val KEY_USERNAME = 500
const val KEY_PASSWORD = 510
const val KEY_OTP = 515
const val KEY_OTP_ALT = 516
const val KEY_URL = 520
const val KEY_FIELDS = 530
// TODO Retrieve entry info from id and service when database is open
private var entryInfoKey: EntryInfo? = null
private var entryInfoTimestamp: Long? = null
private var entryUUID: UUID? = null
private fun removeEntryInfo() {
entryInfoKey = null
entryInfoTimestamp = null
entryUUID = null
}
fun removeEntry(context: Context) {
@@ -366,10 +464,17 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
fun addEntryAndLaunchNotificationIfAllowed(context: Context, entry: EntryInfo, toast: Boolean = false) {
// Add a new entry
entryInfoKey = entry
entryInfoTimestamp = System.currentTimeMillis()
entryUUID = entry.id
// Launch notification if allowed
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
}
fun activatedInSettings(context: Context): Boolean {
return ContextCompat.getSystemService(context, InputMethodManager::class.java)
?.enabledInputMethodList
?.any {
it.packageName == context.packageName
} ?: false
}
}
}

View File

@@ -225,6 +225,14 @@ class EntryInfo : NodeInfo {
}
}
fun getVisualTitle(): String {
return title.ifEmpty {
url.ifEmpty {
username.ifEmpty { id.toString() }
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is EntryInfo) return false

View File

@@ -178,6 +178,14 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
}
}
/**
* Token with space each 3 digits
*/
val tokenString: String
get() {
return token.replace("...".toRegex(), "$0 ")
}
val secondsRemaining: Int
get() = otpModel.period - (System.currentTimeMillis() / 1000 % otpModel.period).toInt()

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.password
import me.gosimple.nbvcxz.resources.Generator
class PassphraseGenerator {
@Throws(IllegalArgumentException::class)
fun generatePassphrase(wordCount: Int,
wordSeparator: String,
wordCase: WordCase): String {
// From eff_large dictionary
return when (wordCase) {
WordCase.LOWER_CASE -> {
Generator.generatePassphrase(wordSeparator, wordCount)
}
WordCase.UPPER_CASE -> {
applyWordCase(wordCount, wordSeparator) { word ->
word.uppercase()
}
}
WordCase.TITLE_CASE -> {
applyWordCase(wordCount, wordSeparator) { word ->
word.replaceFirstChar { char -> char.uppercaseChar() }
}
}
}
}
private fun applyWordCase(wordCount: Int,
wordSeparator: String,
wordAction: (word: String) -> String): String {
val splitWords = Generator.generatePassphrase(TEMP_SPLIT, wordCount).split(TEMP_SPLIT)
val stringBuilder = StringBuilder()
splitWords.forEach {
stringBuilder
.append(wordAction(it))
.append(wordSeparator)
}
return stringBuilder.toString().removeSuffix(wordSeparator)
}
enum class WordCase {
LOWER_CASE,
UPPER_CASE,
TITLE_CASE;
companion object {
fun getByOrdinal(position: Int): WordCase {
return when (position) {
0 -> LOWER_CASE
1 -> UPPER_CASE
else -> TITLE_CASE
}
}
}
}
companion object {
private const val TEMP_SPLIT = "-"
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright 2022 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.password
import android.content.res.Resources
import android.graphics.Color
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.IOActionTask
import kotlinx.coroutines.*
import me.gosimple.nbvcxz.Nbvcxz
import me.gosimple.nbvcxz.resources.Configuration
import me.gosimple.nbvcxz.resources.ConfigurationBuilder
import java.util.*
import kotlin.math.min
class PasswordEntropy(actionOnInitFinished: (() -> Unit)? = null) {
private var mPasswordEntropyCalculator: Nbvcxz? = null
private var entropyJob: Job? = null
init {
IOActionTask({
// Create the password generator object
val configuration: Configuration = ConfigurationBuilder()
.setLocale(Locale.getDefault())
.setMinimumEntropy(80.0)
.createConfiguration()
mPasswordEntropyCalculator = Nbvcxz(configuration)
}, {
actionOnInitFinished?.invoke()
}).execute()
}
enum class Strength(val color: Int) {
RISKY(Color.rgb(224, 56, 56)),
VERY_GUESSABLE(Color.rgb(196, 63, 49)),
SOMEWHAT_GUESSABLE(Color.rgb(219, 152, 55)),
SAFELY_UNGUESSABLE(Color.rgb(118, 168, 24)),
VERY_UNGUESSABLE(Color.rgb(37, 152, 41))
}
data class EntropyStrength(val strength: Strength,
val entropy: Double,
val estimationPercent: Int) {
override fun toString(): String {
return "EntropyStrength(strength=$strength, entropy=$entropy, estimationPercent=$estimationPercent)"
}
}
fun getEntropyStrength(passwordString: String,
entropyStrengthResult: (EntropyStrength) -> Unit) {
entropyStrengthResult.invoke(EntropyStrength(Strength.RISKY, CALCULATE_ENTROPY, 0))
entropyJob?.cancel()
entropyJob = CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.IO) {
val asyncResult: Deferred<EntropyStrength?> = async {
try {
if (passwordString.length <= MAX_PASSWORD_LENGTH) {
val estimate = mPasswordEntropyCalculator?.estimate(passwordString)
val basicScore = estimate?.basicScore ?: 0
val entropy = estimate?.entropy ?: 0.0
val percentScore = min(entropy * 100 / 200, 100.0).toInt()
val strength =
if (basicScore == 0 || percentScore < 10) {
Strength.RISKY
} else if (basicScore == 1 || percentScore < 20) {
Strength.VERY_GUESSABLE
} else if (basicScore == 2 || percentScore < 33) {
Strength.SOMEWHAT_GUESSABLE
} else if (basicScore == 3 || percentScore < 50) {
Strength.SAFELY_UNGUESSABLE
} else if (basicScore == 4) {
Strength.VERY_UNGUESSABLE
} else {
Strength.RISKY
}
EntropyStrength(strength, entropy, percentScore)
} else {
EntropyStrength(Strength.VERY_UNGUESSABLE, HIGH_ENTROPY, 100)
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
withContext(Dispatchers.Main) {
asyncResult.await()?.let { entropyStrength ->
entropyStrengthResult.invoke(entropyStrength)
}
}
}
}
}
companion object {
private const val MAX_PASSWORD_LENGTH = 128
private const val CALCULATE_ENTROPY = -1.0
private const val HIGH_ENTROPY = 1000.0
fun getStringEntropy(resources: Resources, entropy: Double): String {
return when (entropy) {
CALCULATE_ENTROPY -> {
resources.getString(R.string.entropy_calculate)
}
HIGH_ENTROPY -> {
resources.getString(R.string.entropy_high)
}
else -> {
resources.getString(
R.string.entropy,
"%.${2}f".format(entropy)
)
}
}
}
}
}

View File

@@ -20,33 +20,17 @@
package com.kunzisoft.keepass.password
import android.content.res.Resources
import android.graphics.Color
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import com.kunzisoft.keepass.R
import java.security.SecureRandom
import java.util.*
class PasswordGenerator(private val resources: Resources) {
// From KeePassXC code https://github.com/keepassxreboot/keepassxc/pull/538
private fun extendedChars(): String {
val charSet = StringBuilder()
// [U+0080, U+009F] are C1 control characters,
// U+00A0 is non-breaking space
run {
var ch = '\u00A1'
while (ch <= '\u00AC') {
charSet.append(ch)
++ch
}
}
// U+00AD is soft hyphen (format character)
var ch = '\u00AE'
while (ch < '\u00FF') {
charSet.append(ch)
++ch
}
charSet.append('\u00FF')
return charSet.toString()
}
@Throws(IllegalArgumentException::class)
fun generatePassword(length: Int,
upperCase: Boolean,
@@ -57,7 +41,11 @@ class PasswordGenerator(private val resources: Resources) {
space: Boolean,
specials: Boolean,
brackets: Boolean,
extended: Boolean): String {
extended: Boolean,
considerChars: String,
ignoreChars: String,
atLeastOneFromEach: Boolean,
excludeAmbiguousChar: Boolean): String {
// Desired password length is 0 or less
if (length <= 0) {
throw IllegalArgumentException(resources.getString(R.string.error_wrong_length))
@@ -72,74 +60,164 @@ class PasswordGenerator(private val resources: Resources) {
&& !space
&& !specials
&& !brackets
&& !extended) {
&& !extended
&& considerChars.isEmpty()) {
throw IllegalArgumentException(resources.getString(R.string.error_pass_gen_type))
}
val characterSet = getCharacterSet(
upperCase,
lowerCase,
digits,
minus,
underline,
space,
specials,
brackets,
extended)
val size = characterSet.length
val buffer = StringBuilder()
val random = SecureRandom() // use more secure variant of Random!
if (size > 0) {
for (i in 0 until length) {
buffer.append(characterSet[random.nextInt(size)])
// Filter builder
val passwordFilters = PasswordFilters().apply {
this.length = length
this.ignoreChars = ignoreChars
if (excludeAmbiguousChar)
this.ignoreChars += AMBIGUOUS_CHARS
if (upperCase) {
addFilter(
UPPERCASE_CHARS,
if (atLeastOneFromEach) 1 else 0
)
}
if (lowerCase) {
addFilter(
LOWERCASE_CHARS,
if (atLeastOneFromEach) 1 else 0
)
}
if (digits) {
addFilter(
DIGIT_CHARS,
if (atLeastOneFromEach) 1 else 0
)
}
if (minus) {
addFilter(
MINUS_CHAR,
if (atLeastOneFromEach) 1 else 0
)
}
if (underline) {
addFilter(
UNDERLINE_CHAR,
if (atLeastOneFromEach) 1 else 0
)
}
if (space) {
addFilter(
SPACE_CHAR,
if (atLeastOneFromEach) 1 else 0
)
}
if (specials) {
addFilter(
SPECIAL_CHARS,
if (atLeastOneFromEach) 1 else 0
)
}
if (brackets) {
addFilter(
BRACKET_CHARS,
if (atLeastOneFromEach) 1 else 0
)
}
if (extended) {
addFilter(
extendedChars(),
if (atLeastOneFromEach) 1 else 0
)
}
if (considerChars.isNotEmpty()) {
addFilter(
considerChars,
if (atLeastOneFromEach) 1 else 0
)
}
}
return buffer.toString()
return generateRandomString(SecureRandom(), passwordFilters)
}
private fun getCharacterSet(upperCase: Boolean,
lowerCase: Boolean,
digits: Boolean,
minus: Boolean,
underline: Boolean,
space: Boolean,
specials: Boolean,
brackets: Boolean,
extended: Boolean): String {
val charSet = StringBuilder()
private fun generateRandomString(random: Random, passwordFilters: PasswordFilters): String {
val randomString = StringBuilder()
if (upperCase) {
charSet.append(UPPERCASE_CHARS)
// Allocate appropriate memory for the password.
var requiredCharactersLeft = passwordFilters.getRequiredCharactersLeft()
// Build the password.
for (i in 0 until passwordFilters.length) {
var selectableChars: String = if (requiredCharactersLeft < passwordFilters.length - i) {
// choose from any group at random
passwordFilters.getSelectableChars()
} else {
// choose only from a group that we need to satisfy a minimum for.
passwordFilters.getSelectableCharsForNeed()
}
passwordFilters.ignoreChars.forEach {
selectableChars = selectableChars.replace(it.toString(), "")
}
// Now that the string is built, get the next random character.
val selectableCharsMaxIndex = selectableChars.length
val randomSelectableCharsIndex = if (selectableCharsMaxIndex > 0) random.nextInt(selectableCharsMaxIndex) else 0
val nextChar = selectableChars[randomSelectableCharsIndex]
// Put at random position
val randomStringMaxIndex = randomString.length
val randomStringIndex = if (randomStringMaxIndex > 0) random.nextInt(randomStringMaxIndex) else 0
randomString.insert(randomStringIndex, nextChar)
// Now figure out where it came from, and decrement the appropriate minimum value
passwordFilters.getFilterThatContainsChar(nextChar)?.let {
if (it.minCharsNeeded > 0) {
it.minCharsNeeded--
requiredCharactersLeft--
}
}
}
if (lowerCase) {
charSet.append(LOWERCASE_CHARS)
}
if (digits) {
charSet.append(DIGIT_CHARS)
}
if (minus) {
charSet.append(MINUS_CHAR)
}
if (underline) {
charSet.append(UNDERLINE_CHAR)
}
if (space) {
charSet.append(SPACE_CHAR)
}
if (specials) {
charSet.append(SPECIAL_CHARS)
}
if (brackets) {
charSet.append(BRACKET_CHARS)
}
if (extended) {
charSet.append(extendedChars())
return randomString.toString()
}
private data class Filter(var chars: String,
var minCharsNeeded: Int)
private class PasswordFilters {
var length: Int = 0
var ignoreChars = ""
val filters = mutableListOf<Filter>()
fun addFilter(chars: String, minCharsNeeded: Int) {
filters.add(Filter(chars, minCharsNeeded))
}
return charSet.toString()
fun getRequiredCharactersLeft(): Int {
var charsRequired = 0
filters.forEach {
charsRequired += it.minCharsNeeded
}
return charsRequired
}
fun getSelectableChars(): String {
val stringBuilder = StringBuilder()
filters.forEach {
stringBuilder.append(it.chars)
}
return stringBuilder.toString()
}
fun getFilterThatContainsChar(char: Char): Filter? {
return filters.find { it.chars.contains(char) }
}
fun getSelectableCharsForNeed(): String {
val selectableChars = StringBuilder()
// choose only from a group that we need to satisfy a minimum for.
filters.forEach {
if (it.minCharsNeeded > 0) {
selectableChars.append(it.chars)
}
}
return selectableChars.toString()
}
}
companion object {
@@ -151,5 +229,75 @@ class PasswordGenerator(private val resources: Resources) {
private const val SPACE_CHAR = " "
private const val SPECIAL_CHARS = "!\"#$%&'*+,./:;=?@\\^`"
private const val BRACKET_CHARS = "[]{}()<>"
private const val AMBIGUOUS_CHARS = "iI|lLoO01"
// From KeePassXC code https://github.com/keepassxreboot/keepassxc/pull/538
private fun extendedChars(): String {
val charSet = StringBuilder()
// [U+0080, U+009F] are C1 control characters,
// U+00A0 is non-breaking space
run {
var ch = '\u00A1'
while (ch <= '\u00AC') {
charSet.append(ch)
++ch
}
}
// U+00AD is soft hyphen (format character)
var ch = '\u00AE'
while (ch < '\u00FF') {
charSet.append(ch)
++ch
}
charSet.append('\u00FF')
return charSet.toString()
}
fun getColorizedPassword(password: String): Spannable {
val spannableString = SpannableStringBuilder()
if (password.isNotEmpty()) {
password.forEach {
when {
UPPERCASE_CHARS.contains(it)||
LOWERCASE_CHARS.contains(it) -> {
spannableString.append(it)
}
DIGIT_CHARS.contains(it) -> {
// RED
spannableString.append(colorizeChar(it, Color.rgb(246, 79, 62)))
}
SPECIAL_CHARS.contains(it) -> {
// Blue
spannableString.append(colorizeChar(it, Color.rgb(39, 166, 228)))
}
MINUS_CHAR.contains(it)||
UNDERLINE_CHAR.contains(it)||
BRACKET_CHARS.contains(it) -> {
// Purple
spannableString.append(colorizeChar(it, Color.rgb(185, 38, 209)))
}
extendedChars().contains(it) -> {
// Green
spannableString.append(colorizeChar(it, Color.rgb(44, 181, 50)))
}
else -> {
spannableString.append(it)
}
}
}
}
return spannableString
}
private fun colorizeChar(char: Char, color: Int): Spannable {
val spannableColorChar = SpannableString(char.toString())
spannableColorChar.setSpan(
ForegroundColorSpan(color),
0,
1,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
return spannableColorChar
}
}
}

View File

@@ -23,7 +23,6 @@ import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.preferencedialogfragment.DurationDialogFragmentCompat

View File

@@ -52,7 +52,7 @@ import com.kunzisoft.keepass.utils.UriUtil
class NestedAppSettingsFragment : NestedSettingsFragment() {
private var deleteKeysAlertDialog: AlertDialog? = null
private var warningAlertDialog: AlertDialog? = null
override fun onCreateScreenPreference(screen: Screen, savedInstanceState: Bundle?, rootKey: String?) {
@@ -262,7 +262,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
val deviceCredentialChecked = deviceCredentialUnlockEnablePreference?.isChecked ?: false
if (!biometricChecked) {
biometricUnlockEnablePreference.isChecked = true
deleteKeysMessage(activity) {
warningMessage(activity, keystoreWarning = false, deleteKeys = true) {
biometricUnlockEnablePreference.isChecked = false
autoOpenPromptPreference?.isEnabled = deviceCredentialChecked
tempAdvancedUnlockPreference?.isEnabled = deviceCredentialChecked
@@ -270,13 +270,17 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
} else {
if (deviceCredentialChecked) {
biometricUnlockEnablePreference.isChecked = false
deleteKeysMessage(activity) {
warningMessage(activity, keystoreWarning = true, deleteKeys = true) {
biometricUnlockEnablePreference.isChecked = true
deviceCredentialUnlockEnablePreference?.isChecked = false
}
} else {
autoOpenPromptPreference?.isEnabled = true
tempAdvancedUnlockPreference?.isEnabled = true
biometricUnlockEnablePreference.isChecked = false
warningMessage(activity, keystoreWarning = true, deleteKeys = false) {
biometricUnlockEnablePreference.isChecked = true
autoOpenPromptPreference?.isEnabled = true
tempAdvancedUnlockPreference?.isEnabled = true
}
}
}
true
@@ -305,7 +309,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
val biometricChecked = biometricUnlockEnablePreference?.isChecked ?: false
if (!deviceCredentialChecked) {
deviceCredentialUnlockEnablePreference.isChecked = true
deleteKeysMessage(activity) {
warningMessage(activity, keystoreWarning = false, deleteKeys = true) {
deviceCredentialUnlockEnablePreference.isChecked = false
autoOpenPromptPreference?.isEnabled = biometricChecked
tempAdvancedUnlockPreference?.isEnabled = biometricChecked
@@ -313,13 +317,17 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
} else {
if (biometricChecked) {
deviceCredentialUnlockEnablePreference.isChecked = false
deleteKeysMessage(activity) {
warningMessage(activity, keystoreWarning = true, deleteKeys = true) {
deviceCredentialUnlockEnablePreference.isChecked = true
biometricUnlockEnablePreference?.isChecked = false
}
} else {
autoOpenPromptPreference?.isEnabled = true
tempAdvancedUnlockPreference?.isEnabled = true
deviceCredentialUnlockEnablePreference.isChecked = false
warningMessage(activity, keystoreWarning = true, deleteKeys = false) {
deviceCredentialUnlockEnablePreference.isChecked = true
autoOpenPromptPreference?.isEnabled = true
tempAdvancedUnlockPreference?.isEnabled = true
}
}
}
true
@@ -334,7 +342,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
tempAdvancedUnlockPreference?.setOnPreferenceClickListener {
tempAdvancedUnlockPreference.isChecked = !tempAdvancedUnlockPreference.isChecked
deleteKeysMessage(activity) {
warningMessage(activity, keystoreWarning = false, deleteKeys = true) {
tempAdvancedUnlockPreference.isChecked = !tempAdvancedUnlockPreference.isChecked
}
true
@@ -343,7 +351,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
val deleteKeysFingerprints: Preference? = findPreference(getString(R.string.biometric_delete_all_key_key))
if (biometricUnlockSupported || deviceCredentialUnlockSupported) {
deleteKeysFingerprints?.setOnPreferenceClickListener {
deleteKeysMessage(activity)
warningMessage(activity, keystoreWarning = false, deleteKeys = true)
false
}
} else {
@@ -357,22 +365,36 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
}
private fun deleteKeysMessage(activity: FragmentActivity, validate: (()->Unit)? = null) {
deleteKeysAlertDialog = AlertDialog.Builder(activity)
.setMessage(resources.getString(R.string.advanced_unlock_delete_all_key_warning))
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(resources.getString(android.R.string.ok)
) { _, _ ->
validate?.invoke()
deleteKeysAlertDialog?.setOnDismissListener(null)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
}
private fun warningMessage(activity: FragmentActivity,
keystoreWarning: Boolean,
deleteKeys: Boolean,
validate: (()->Unit)? = null) {
var message = ""
if (keystoreWarning) {
message += resources.getString(R.string.advanced_unlock_prompt_store_credential_message)
message += "\n\n" + resources.getString(R.string.advanced_unlock_keystore_warning)
}
if (keystoreWarning && deleteKeys) {
message += "\n\n"
}
if (deleteKeys) {
message += resources.getString(R.string.advanced_unlock_delete_all_key_warning)
}
warningAlertDialog = AlertDialog.Builder(activity)
.setMessage(message)
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(resources.getString(android.R.string.ok)
) { _, _ ->
validate?.invoke()
warningAlertDialog?.setOnDismissListener(null)
if (deleteKeys && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
}
.setNegativeButton(resources.getString(android.R.string.cancel)
) { _, _ ->}
.create()
deleteKeysAlertDialog?.show()
}
.setNegativeButton(resources.getString(android.R.string.cancel)
) { _, _ ->}
.create()
warningAlertDialog?.show()
}
private fun onCreateAppearancePreferences(rootKey: String?) {
@@ -382,13 +404,17 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
findPreference<ListPreference>(getString(R.string.setting_style_key))?.setOnPreferenceChangeListener { _, newValue ->
var styleEnabled = true
val styleIdString = newValue as String
if (BuildConfig.CLOSED_STORE || !Education.isEducationScreenReclickedPerformed(activity))
if (!UriUtil.contributingUser(activity)) {
for (themeIdDisabled in BuildConfig.STYLES_DISABLED) {
if (themeIdDisabled == styleIdString) {
styleEnabled = false
ProFeatureDialogFragment().show(parentFragmentManager, "pro_feature_dialog")
ProFeatureDialogFragment().show(
parentFragmentManager,
"pro_feature_dialog"
)
}
}
}
if (styleEnabled) {
Stylish.assignStyle(activity, styleIdString)
// Relaunch the current activity to redraw theme
@@ -409,13 +435,17 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
findPreference<IconPackListPreference>(getString(R.string.setting_icon_pack_choose_key))?.setOnPreferenceChangeListener { _, newValue ->
var iconPackEnabled = true
val iconPackId = newValue as String
if (BuildConfig.CLOSED_STORE || !Education.isEducationScreenReclickedPerformed(activity))
if (!UriUtil.contributingUser(activity)) {
for (iconPackIdDisabled in BuildConfig.ICON_PACKS_DISABLED) {
if (iconPackIdDisabled == iconPackId) {
iconPackEnabled = false
ProFeatureDialogFragment().show(parentFragmentManager, "pro_feature_dialog")
ProFeatureDialogFragment().show(
parentFragmentManager,
"pro_feature_dialog"
)
}
}
}
if (iconPackEnabled) {
IconPackChooser.setSelectedIconPack(iconPackId)
}
@@ -501,7 +531,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
override fun onPause() {
deleteKeysAlertDialog?.dismiss()
warningAlertDialog?.dismiss()
super.onPause()
}
@@ -509,9 +539,8 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
override fun onStop() {
super.onStop()
activity?.let { activity ->
if (mCount == 10) {
Education.getEducationSharedPreferences(activity).edit()
.putBoolean(getString(R.string.education_screen_reclicked_key), true).apply()
if (mCount == 10 && !BuildConfig.CLOSED_STORE) {
Education.setEducationScreenReclickedPerformed(activity)
}
}
}

View File

@@ -31,8 +31,12 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.password.PassphraseGenerator
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.UriUtil
import java.util.*
object PreferencesUtil {
@@ -110,6 +114,18 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.show_entry_colors_default))
}
fun hideProtectedValue(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.hide_password_key),
context.resources.getBoolean(R.bool.hide_password_default))
}
fun colorizePassword(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.colorize_password_key),
context.resources.getBoolean(R.bool.colorize_password_default))
}
fun showUsernamesListEntries(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.list_entries_show_username_key),
@@ -151,7 +167,7 @@ object PreferencesUtil {
fun setStyle(context: Context, styleString: String) {
var tempThemeString = styleString
if (BuildConfig.CLOSED_STORE || !Education.isEducationScreenReclickedPerformed(context)) {
if (!UriUtil.contributingUser(context)) {
if (tempThemeString in BuildConfig.STYLES_DISABLED) {
tempThemeString = Stylish.defaultStyle(context)
}
@@ -191,15 +207,194 @@ object PreferencesUtil {
fun getDefaultPasswordLength(context: Context): Int {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getInt(context.getString(R.string.password_length_key),
Integer.parseInt(context.getString(R.string.default_password_length)))
return prefs.getInt(context.getString(R.string.password_generator_length_key),
context.resources.getInteger(R.integer.password_generator_length_default))
}
fun getDefaultPasswordCharacters(context: Context): Set<String>? {
fun setDefaultPasswordLength(context: Context, passwordLength: Int) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putInt(
context.getString(R.string.password_generator_length_key),
passwordLength
)
apply()
}
}
fun getDefaultPasswordOptions(context: Context): Set<String> {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getStringSet(context.getString(R.string.list_password_generator_options_key),
HashSet(listOf(*context.resources
.getStringArray(R.array.list_password_generator_options_default_values))))
return prefs.getStringSet(context.getString(R.string.password_generator_options_key),
HashSet(listOf(*context.resources
.getStringArray(R.array.list_password_generator_options_default_values)))) ?: setOf()
}
fun setDefaultPasswordOptions(context: Context, passwordOptionsSet: Set<String>) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putStringSet(
context.getString(R.string.password_generator_options_key),
passwordOptionsSet
)
apply()
}
}
fun getDefaultPasswordConsiderChars(context: Context): String {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getString(context.getString(R.string.password_generator_consider_chars_key),
context.getString(R.string.password_generator_consider_chars_default)) ?: ""
}
fun setDefaultPasswordConsiderChars(context: Context, considerChars: String) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putString(
context.getString(R.string.password_generator_consider_chars_key),
considerChars
)
apply()
}
}
fun getDefaultPasswordIgnoreChars(context: Context): String {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getString(context.getString(R.string.password_generator_ignore_chars_key),
context.getString(R.string.password_generator_ignore_chars_default)) ?: ""
}
fun setDefaultPasswordIgnoreChars(context: Context, ignoreChars: String) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putString(
context.getString(R.string.password_generator_ignore_chars_key),
ignoreChars
)
apply()
}
}
fun getDefaultPassphraseWordCount(context: Context): Int {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getInt(context.getString(R.string.passphrase_generator_word_count_key),
context.resources.getInteger(R.integer.passphrase_generator_word_count_default))
}
fun setDefaultPassphraseWordCount(context: Context, passphraseWordCount: Int) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putInt(
context.getString(R.string.passphrase_generator_word_count_key),
passphraseWordCount
)
apply()
}
}
fun getDefaultPassphraseWordCase(context: Context): PassphraseGenerator.WordCase {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return PassphraseGenerator.WordCase
.getByOrdinal(prefs.getInt(context
.getString(R.string.passphrase_generator_word_case_key),
0)
)
}
fun setDefaultPassphraseWordCase(context: Context, wordCase: PassphraseGenerator.WordCase) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putInt(
context.getString(R.string.passphrase_generator_word_case_key),
wordCase.ordinal
)
apply()
}
}
fun getDefaultPassphraseSeparator(context: Context): String {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getString(context.getString(R.string.passphrase_generator_separator_key),
context.getString(R.string.passphrase_generator_separator_default)) ?: ""
}
fun setDefaultPassphraseSeparator(context: Context, separator: String) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putString(
context.getString(R.string.passphrase_generator_separator_key),
separator
)
apply()
}
}
fun getDefaultSearchParameters(context: Context): SearchParameters {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return SearchParameters().apply {
caseSensitive = prefs.getBoolean(context.getString(R.string.search_option_case_sensitive_key),
context.resources.getBoolean(R.bool.search_option_case_sensitive_default))
isRegex = prefs.getBoolean(context.getString(R.string.search_option_regex_key),
context.resources.getBoolean(R.bool.search_option_regex_default))
searchInTitles = prefs.getBoolean(context.getString(R.string.search_option_title_key),
context.resources.getBoolean(R.bool.search_option_title_default))
searchInUsernames = prefs.getBoolean(context.getString(R.string.search_option_username_key),
context.resources.getBoolean(R.bool.search_option_username_default))
searchInPasswords = prefs.getBoolean(context.getString(R.string.search_option_password_key),
context.resources.getBoolean(R.bool.search_option_password_default))
searchInUrls = prefs.getBoolean(context.getString(R.string.search_option_url_key),
context.resources.getBoolean(R.bool.search_option_url_default))
searchInExpired = prefs.getBoolean(context.getString(R.string.search_option_expired_key),
context.resources.getBoolean(R.bool.search_option_expired_default))
searchInNotes = prefs.getBoolean(context.getString(R.string.search_option_note_key),
context.resources.getBoolean(R.bool.search_option_note_default))
searchInOTP = prefs.getBoolean(context.getString(R.string.search_option_otp_key),
context.resources.getBoolean(R.bool.search_option_otp_default))
searchInOther = prefs.getBoolean(context.getString(R.string.search_option_other_key),
context.resources.getBoolean(R.bool.search_option_other_default))
searchInUUIDs = prefs.getBoolean(context.getString(R.string.search_option_uuid_key),
context.resources.getBoolean(R.bool.search_option_uuid_default))
searchInTags = prefs.getBoolean(context.getString(R.string.search_option_tag_key),
context.resources.getBoolean(R.bool.search_option_tag_default))
searchInCurrentGroup = prefs.getBoolean(context.getString(R.string.search_option_current_group_key),
context.resources.getBoolean(R.bool.search_option_current_group_default))
searchInSearchableGroup = prefs.getBoolean(context.getString(R.string.search_option_searchable_group_key),
context.resources.getBoolean(R.bool.search_option_searchable_group_default))
searchInRecycleBin = prefs.getBoolean(context.getString(R.string.search_option_recycle_bin_key),
context.resources.getBoolean(R.bool.search_option_recycle_bin_default))
searchInTemplates = prefs.getBoolean(context.getString(R.string.search_option_templates_key),
context.resources.getBoolean(R.bool.search_option_templates_default))
}
}
fun setDefaultSearchParameters(context: Context, searchParameters: SearchParameters) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putBoolean(context.getString(R.string.search_option_case_sensitive_key),
searchParameters.caseSensitive)
putBoolean(context.getString(R.string.search_option_regex_key),
searchParameters.isRegex)
putBoolean(context.getString(R.string.search_option_title_key),
searchParameters.searchInTitles)
putBoolean(context.getString(R.string.search_option_username_key),
searchParameters.searchInUsernames)
putBoolean(context.getString(R.string.search_option_password_key),
searchParameters.searchInPasswords)
putBoolean(context.getString(R.string.search_option_url_key),
searchParameters.searchInUrls)
putBoolean(context.getString(R.string.search_option_expired_key),
searchParameters.searchInExpired)
putBoolean(context.getString(R.string.search_option_note_key),
searchParameters.searchInNotes)
putBoolean(context.getString(R.string.search_option_otp_key),
searchParameters.searchInOTP)
putBoolean(context.getString(R.string.search_option_other_key),
searchParameters.searchInOther)
putBoolean(context.getString(R.string.search_option_uuid_key),
searchParameters.searchInUUIDs)
putBoolean(context.getString(R.string.search_option_tag_key),
searchParameters.searchInTags)
putBoolean(context.getString(R.string.search_option_current_group_key),
searchParameters.searchInCurrentGroup)
putBoolean(context.getString(R.string.search_option_searchable_group_key),
searchParameters.searchInSearchableGroup)
putBoolean(context.getString(R.string.search_option_recycle_bin_key),
searchParameters.searchInRecycleBin)
putBoolean(context.getString(R.string.search_option_templates_key),
searchParameters.searchInTemplates)
apply()
}
}
fun isClipboardNotificationsEnable(context: Context): Boolean {
@@ -350,12 +545,6 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.sort_recycle_bin_bottom_default))
}
fun hideProtectedValue(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.hide_password_key),
context.resources.getBoolean(R.bool.hide_password_default))
}
fun fieldFontIsInVisibility(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.monospace_font_fields_enable_key),
@@ -435,6 +624,7 @@ object PreferencesUtil {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.keyboard_search_share_key),
context.resources.getBoolean(R.bool.keyboard_search_share_default))
&& MagikeyboardService.activatedInSettings(context)
}
fun isKeyboardSaveSearchInfoEnable(context: Context): Boolean {
@@ -469,6 +659,12 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.keyboard_previous_database_credentials_default))
}
fun isKeyboardPreviousSearchEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.keyboard_previous_search_key),
context.resources.getBoolean(R.bool.keyboard_previous_search_default))
}
fun isKeyboardPreviousFillInEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.keyboard_previous_fill_in_key),
@@ -608,9 +804,6 @@ object PreferencesUtil {
context.getString(R.string.lock_database_screen_off_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.lock_database_back_root_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.lock_database_show_button_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.password_length_key) -> editor.putInt(name, value.toInt())
context.getString(R.string.list_password_generator_options_key) -> editor.putStringSet(name, getStringSetFromProperties(value))
context.getString(R.string.hide_password_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.allow_copy_password_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.remember_database_locations_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.show_recent_files_key) -> editor.putBoolean(name, value.toBoolean())
@@ -637,6 +830,7 @@ object PreferencesUtil {
context.getString(R.string.keyboard_key_vibrate_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_key_sound_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_previous_database_credentials_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_previous_search_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_previous_fill_in_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_previous_lock_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean())
@@ -652,6 +846,8 @@ object PreferencesUtil {
context.getString(R.string.setting_style_brightness_key) -> editor.putString(name, value)
context.getString(R.string.setting_icon_pack_choose_key) -> editor.putString(name, value)
context.getString(R.string.show_entry_colors_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.hide_password_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.colorize_password_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.list_entries_show_username_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.list_groups_show_number_entries_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.show_otp_token_key) -> editor.putBoolean(name, value.toBoolean())
@@ -661,6 +857,14 @@ object PreferencesUtil {
context.getString(R.string.hide_expired_entries_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.enable_education_screens_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.password_generator_length_key) -> editor.putInt(name, value.toInt())
context.getString(R.string.password_generator_options_key) -> editor.putStringSet(name, getStringSetFromProperties(value))
context.getString(R.string.password_generator_consider_chars_key) -> editor.putString(name, value)
context.getString(R.string.password_generator_ignore_chars_key) -> editor.putString(name, value)
context.getString(R.string.passphrase_generator_word_count_key) -> editor.putInt(name, value.toInt())
context.getString(R.string.passphrase_generator_word_case_key) -> editor.putInt(name, value.toInt())
context.getString(R.string.passphrase_generator_separator_key) -> editor.putString(name, value)
context.getString(R.string.sort_node_key) -> editor.putString(name, value)
context.getString(R.string.sort_group_before_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.sort_ascending_key) -> editor.putBoolean(name, value.toBoolean())

View File

@@ -225,10 +225,14 @@ open class SettingsActivity
commit()
}
toolbar?.title = NestedSettingsFragment.retrieveTitle(resources, key)
setTitle(key)
hideOrShowLockButton(key)
}
protected fun setTitle(key: NestedSettingsFragment.Screen) {
toolbar?.title = NestedSettingsFragment.retrieveTitle(resources, key)
}
/**
* To keep the current screen when activity is reloaded
*/

View File

@@ -28,6 +28,7 @@ class SettingsAdvancedUnlockActivity : SettingsActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mTimeoutEnable = false
setTitle(NestedSettingsFragment.Screen.ADVANCED_UNLOCK)
}
override fun retrieveMainFragment(): Fragment {

View File

@@ -25,17 +25,16 @@ import android.content.Intent
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AboutActivity
import com.kunzisoft.keepass.settings.SettingsActivity
object MenuUtil {
fun defaultMenuInflater(inflater: MenuInflater, menu: Menu) {
fun defaultMenuInflater(context: Context, inflater: MenuInflater, menu: Menu) {
inflater.inflate(R.menu.settings, menu)
inflater.inflate(R.menu.about, menu)
if (!(BuildConfig.FULL_VERSION && BuildConfig.CLOSED_STORE))
if (!UriUtil.contributingUser(context))
menu.findItem(R.id.menu_contribute)?.isVisible = false
}

View File

@@ -30,6 +30,7 @@ import android.widget.Toast
import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.education.Education
import java.io.*
import java.util.*
@@ -68,7 +69,18 @@ object UriUtil {
return null
return when {
isFileScheme(fileUri) -> fileUri.path?.let { FileOutputStream(it) }
isContentScheme(fileUri) -> contentResolver.openOutputStream(fileUri, "rwt")
isContentScheme(fileUri) -> {
try {
contentResolver.openOutputStream(fileUri, "wt")
} catch (e: FileNotFoundException) {
Log.e(TAG, "Unable to open stream in `wt` mode, retry in `rwt` mode.", e)
// https://issuetracker.google.com/issues/180526528
// Try with rwt to fix content provider issue
val outStream = contentResolver.openOutputStream(fileUri, "rwt")
Log.w(TAG, "`rwt` mode used.")
outStream
}
}
else -> null
}
}
@@ -255,9 +267,16 @@ object UriUtil {
gotoUrl(context, context.getString(resId))
}
fun isExternalAppInstalled(context: Context, packageName: String): Boolean {
fun contributingUser(context: Context): Boolean {
return (Education.isEducationScreenReclickedPerformed(context)
|| isExternalAppInstalled(context, "com.kunzisoft.keepass.pro")
)
}
private fun isExternalAppInstalled(context: Context, packageName: String): Boolean {
try {
context.applicationContext.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
Education.setEducationScreenReclickedPerformed(context)
return true
} catch (e: Exception) {
Log.e(TAG, "App not accessible", e)

View File

@@ -42,7 +42,7 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
private var unlockAnimatedVector: FingerPrintAnimatedVector? = null
private var unlockTitleTextView: TextView? = null
private var unlockMessageTextView: TextView? = null
var unlockIconImageView: ImageView? = null
private var unlockIconImageView: ImageView? = null
init {

View File

@@ -46,7 +46,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) {
private var passwordView: EditText
private var passwordTextView: EditText
private var keyFileSelectionView: KeyFileSelectionView
private var checkboxPasswordView: CompoundButton
private var checkboxKeyFileView: CompoundButton
@@ -60,7 +60,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_main_credentials, this)
passwordView = findViewById(R.id.password)
passwordTextView = findViewById(R.id.password_text_view)
keyFileSelectionView = findViewById(R.id.keyfile_selection)
checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
@@ -75,8 +75,8 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
}
}
passwordView.setOnEditorActionListener(onEditorActionListener)
passwordView.addTextChangedListener(object : TextWatcher {
passwordTextView.setOnEditorActionListener(onEditorActionListener)
passwordTextView.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
@@ -86,7 +86,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
checkboxPasswordView.isChecked = true
}
})
passwordView.setOnKeyListener { _, _, keyEvent ->
passwordTextView.setOnKeyListener { _, _, keyEvent ->
var handled = false
if (keyEvent.action == KeyEvent.ACTION_DOWN
&& keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
@@ -108,11 +108,11 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
fun populatePasswordTextView(text: String?) {
if (text == null || text.isEmpty()) {
passwordView.setText("")
passwordTextView.setText("")
if (checkboxPasswordView.isChecked)
checkboxPasswordView.isChecked = false
} else {
passwordView.setText(text)
passwordTextView.setText(text)
if (checkboxPasswordView.isChecked)
checkboxPasswordView.isChecked = true
}
@@ -137,7 +137,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
fun getMainCredential(): MainCredential {
return MainCredential().apply {
this.masterPassword = if (checkboxPasswordView.isChecked)
passwordView.text?.toString() else null
passwordTextView.text?.toString() else null
this.keyFileUri = if (checkboxKeyFileView.isChecked)
keyFileSelectionView.uri else null
}
@@ -163,7 +163,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
*/
fun retrieveCredentialForStorage(listener: CredentialStorageListener): ByteArray? {
return when (mCredentialStorage) {
CredentialStorage.PASSWORD -> listener.passwordToStore(passwordView.text?.toString())
CredentialStorage.PASSWORD -> listener.passwordToStore(passwordTextView.text?.toString())
CredentialStorage.KEY_FILE -> listener.keyfileToStore(keyFileSelectionView.uri)
CredentialStorage.HARDWARE_KEY -> listener.hardwareKeyToStore()
}
@@ -176,15 +176,15 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
}
fun requestPasswordFocus() {
passwordView.requestFocusFromTouch()
passwordTextView.requestFocusFromTouch()
}
// Auto select the password field and open keyboard
fun focusPasswordFieldAndOpenKeyboard() {
passwordView.postDelayed({
passwordView.requestFocusFromTouch()
passwordTextView.postDelayed({
passwordTextView.requestFocusFromTouch()
val inputMethodManager = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as? InputMethodManager?
inputMethodManager?.showSoftInput(passwordView, InputMethodManager.SHOW_IMPLICIT)
inputMethodManager?.showSoftInput(passwordTextView, InputMethodManager.SHOW_IMPLICIT)
}, 100)
}

View File

@@ -0,0 +1,156 @@
/*
* Copyright 2022 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.view
import android.content.Context
import android.text.Editable
import android.text.InputType
import android.text.SpannableString
import android.text.TextWatcher
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.FrameLayout
import android.widget.TextView
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.settings.PreferencesUtil
class PassKeyView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) {
private var mPasswordEntropyCalculator: PasswordEntropy? = null
private val passwordInputLayout: TextInputLayout
private val passwordText: TextView
private val passwordStrengthProgress: LinearProgressIndicator
private val passwordEntropy: TextView
private var mViewHint: String = ""
private var mMaxLines: Int = 3
private var mShowPassword: Boolean = false
private var mPasswordTextWatcher: MutableList<TextWatcher> = mutableListOf()
private val passwordTextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
mPasswordTextWatcher.forEach {
it.beforeTextChanged(charSequence, i, i1, i2)
}
}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
mPasswordTextWatcher.forEach {
it.onTextChanged(charSequence, i, i1, i2)
}
}
override fun afterTextChanged(editable: Editable) {
mPasswordTextWatcher.forEach {
it.afterTextChanged(editable)
}
getEntropyStrength(editable.toString())
}
}
init {
context.theme.obtainStyledAttributes(
attrs,
R.styleable.PassKeyView,
0, 0).apply {
try {
mViewHint = getString(R.styleable.PassKeyView_passKeyHint)
?: context.getString(R.string.password)
mMaxLines = getInteger(R.styleable.PassKeyView_passKeyMaxLines, mMaxLines)
mShowPassword = getBoolean(R.styleable.PassKeyView_passKeyVisible,
!PreferencesUtil.hideProtectedValue(context))
} finally {
recycle()
}
}
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_passkey, this)
passwordInputLayout = findViewById(R.id.password_input_layout)
passwordInputLayout?.hint = mViewHint
passwordText = findViewById(R.id.password_text)
if (mShowPassword) {
passwordText?.inputType = passwordText.inputType or
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
}
passwordText?.maxLines = mMaxLines
passwordText?.applyFontVisibility()
passwordText.addTextChangedListener(passwordTextWatcher)
passwordStrengthProgress = findViewById(R.id.password_strength_progress)
passwordStrengthProgress?.apply {
setIndicatorColor(PasswordEntropy.Strength.RISKY.color)
progress = 0
max = 100
}
passwordEntropy = findViewById(R.id.password_entropy)
mPasswordEntropyCalculator = PasswordEntropy {
passwordText?.text?.toString()?.let { firstPassword ->
getEntropyStrength(firstPassword)
}
}
}
private fun getEntropyStrength(passwordText: String) {
mPasswordEntropyCalculator?.getEntropyStrength(passwordText) { entropyStrength ->
passwordStrengthProgress.apply {
post {
setIndicatorColor(entropyStrength.strength.color)
setProgressCompat(entropyStrength.estimationPercent, true)
}
}
passwordEntropy.apply {
post {
text = PasswordEntropy.getStringEntropy(resources, entropyStrength.entropy)
}
}
}
}
fun addTextChangedListener(textWatcher: TextWatcher) {
mPasswordTextWatcher.add(textWatcher)
}
fun removeTextChangedListener(textWatcher: TextWatcher) {
mPasswordTextWatcher.remove(textWatcher)
}
var passwordString: String
get() {
return passwordText.text.toString()
}
set(value) {
val spannableString =
if (PreferencesUtil.colorizePassword(context))
PasswordGenerator.getColorizedPassword(value)
else
SpannableString(value)
passwordText.text = spannableString
}
}

View File

@@ -13,6 +13,7 @@ import androidx.core.view.isVisible
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.settings.PreferencesUtil
class SearchFiltersView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
@@ -30,7 +31,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
private var searchUsername: CompoundButton
private var searchPassword: CompoundButton
private var searchURL: CompoundButton
private var searchExpires: CompoundButton
private var searchExpired: CompoundButton
private var searchNotes: CompoundButton
private var searchOther: CompoundButton
private var searchUUID: CompoundButton
@@ -49,7 +50,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
this.searchInUsernames = searchUsername.isChecked
this.searchInPasswords = searchPassword.isChecked
this.searchInUrls = searchURL.isChecked
this.excludeExpired = !(searchExpires.isChecked)
this.searchInExpired = searchExpired.isChecked
this.searchInNotes = searchNotes.isChecked
this.searchInOther = searchOther.isChecked
this.searchInUUIDs = searchUUID.isChecked
@@ -69,12 +70,12 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchUsername.isChecked = value.searchInUsernames
searchPassword.isChecked = value.searchInPasswords
searchURL.isChecked = value.searchInUrls
searchExpires.isChecked = !value.excludeExpired
searchExpired.isChecked = value.searchInExpired
searchNotes.isChecked = value.searchInNotes
searchOther.isChecked = value.searchInOther
searchUUID.isChecked = value.searchInUUIDs
searchTag.isChecked = value.searchInTags
searchGroupSearchable.isChecked = value.searchInRecycleBin
searchGroupSearchable.isChecked = value.searchInSearchableGroup
searchRecycleBin.isChecked = value.searchInRecycleBin
searchTemplate.isChecked = value.searchInTemplates
mOnParametersChangeListener = tempListener
@@ -107,7 +108,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchUsername = findViewById(R.id.search_chip_username)
searchPassword = findViewById(R.id.search_chip_password)
searchURL = findViewById(R.id.search_chip_url)
searchExpires = findViewById(R.id.search_chip_expires)
searchExpired = findViewById(R.id.search_chip_expires)
searchNotes = findViewById(R.id.search_chip_note)
searchUUID = findViewById(R.id.search_chip_uuid)
searchOther = findViewById(R.id.search_chip_other)
@@ -116,6 +117,9 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchRecycleBin = findViewById(R.id.search_chip_recycle_bin)
searchTemplate = findViewById(R.id.search_chip_template)
// Set search
searchParameters = PreferencesUtil.getDefaultSearchParameters(context)
// Expand menu with button
searchExpandButton.setOnClickListener {
val isVisible = searchAdvanceFiltersContainer?.visibility == View.VISIBLE
@@ -153,8 +157,8 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchParameters.searchInUrls = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchExpires.setOnCheckedChangeListener { _, isChecked ->
searchParameters.excludeExpired = !isChecked
searchExpired.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInExpired = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchNotes.setOnCheckedChangeListener { _, isChecked ->
@@ -249,4 +253,8 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
}
}
}
fun saveSearchParameters() {
PreferencesUtil.setDefaultSearchParameters(context, searchParameters)
}
}

View File

@@ -162,7 +162,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
if (templateAttribute.options.isAssociatedWithPasswordGenerator()) {
setOnActionClickListener({
mOnPasswordGenerationActionClickListener?.invoke(field)
}, R.drawable.ic_generate_password_white_24dp)
}, R.drawable.ic_random_white_24dp)
}
}
}

View File

@@ -165,7 +165,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
setCopyButtonState(TextFieldView.ButtonState.GONE)
} else {
label = otpElement.type.name
value = otpElement.token
value = otpElement.tokenString
setCopyButtonState(TextFieldView.ButtonState.ACTIVATE)
setCopyButtonClickListener { _, _ ->
mOnCopyActionClickListener?.invoke(Field(
@@ -175,7 +175,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
mLastOtpTokenView = this
mOtpRunnable = Runnable {
if (otpElement.shouldRefreshToken()) {
value = otpElement.token
value = otpElement.tokenString
}
if (mLastOtpTokenView == null) {
mOnOtpElementUpdated?.invoke(null)

View File

@@ -1,9 +1,13 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.graphics.Typeface
import android.os.Build
import android.text.InputFilter
import android.text.InputType
import android.text.Spannable
import android.text.SpannableString
import android.text.style.StyleSpan
import android.util.AttributeSet
import android.util.TypedValue
import android.view.ContextThemeWrapper
@@ -19,6 +23,9 @@ import androidx.core.view.isVisible
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
class TextEditFieldView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
@@ -123,7 +130,13 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
return valueView.text?.toString() ?: ""
}
set(value) {
valueView.setText(value)
val spannableString =
if (PreferencesUtil.colorizePassword(context)
&& TemplateField.isStandardPasswordName(context, label))
PasswordGenerator.getColorizedPassword(value)
else
SpannableString(value)
valueView.setText(spannableString)
}
override var default: String = ""
@@ -164,7 +177,11 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
fun setProtection(protection: Boolean) {
if (protection) {
labelView.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
valueView.inputType = valueView.inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
val visibilityTag = if (PreferencesUtil.hideProtectedValue(context))
InputType.TYPE_TEXT_VARIATION_PASSWORD
else
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
valueView.inputType = valueView.inputType or visibilityTag
}
}

View File

@@ -20,13 +20,18 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.graphics.Typeface
import android.os.Build
import android.text.InputFilter
import android.text.Spannable
import android.text.SpannableString
import android.text.style.StyleSpan
import android.text.util.Linkify
import android.util.AttributeSet
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.View
import android.view.View.OnClickListener
import android.widget.RelativeLayout
import androidx.annotation.StringRes
import androidx.appcompat.widget.AppCompatImageButton
@@ -36,7 +41,10 @@ import androidx.core.text.util.LinkifyCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.model.EntryInfo.Companion.APPLICATION_ID_FIELD_NAME
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
@@ -191,7 +199,13 @@ class TextFieldView @JvmOverloads constructor(context: Context,
return valueView.text.toString()
}
set(value) {
valueView.text = value
val spannableString =
if (PreferencesUtil.colorizePassword(context)
&& TemplateField.isStandardPasswordName(context, label))
PasswordGenerator.getColorizedPassword(value)
else
SpannableString(value)
valueView.text = spannableString
changeProtectedValueParameters()
}

View File

@@ -53,6 +53,9 @@ class EntryViewModel: ViewModel() {
val onAttachmentAction : LiveData<EntryAttachmentState?> get() = _onAttachmentAction
private val _onAttachmentAction = MutableLiveData<EntryAttachmentState?>()
val sectionSelected : LiveData<EntrySection> get() = _sectionSelected
private val _sectionSelected = MutableLiveData<EntrySection>()
val historySelected : LiveData<EntryHistory> get() = _historySelected
private val _historySelected = SingleLiveEvent<EntryHistory>()
@@ -124,6 +127,10 @@ class EntryViewModel: ViewModel() {
_historySelected.value = EntryHistory(NodeIdUUID(item.id), null, item, position)
}
fun selectSection(section: EntrySection) {
_sectionSelected.value = section
}
data class EntryInfoHistory(var mainEntryId: NodeId<UUID>,
var historyPosition: Int,
val template: Template,
@@ -135,6 +142,16 @@ class EntryViewModel: ViewModel() {
var entryInfo: EntryInfo,
var historyPosition: Int = -1)
enum class EntrySection(var position: Int) {
MAIN(0), ADVANCED(1);
companion object {
fun getEntrySectionByPosition(position: Int): EntrySection {
return if (position == 1) ADVANCED else MAIN
}
}
}
companion object {
private val TAG = EntryViewModel::class.java.name
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2022 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.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class KeyGeneratorViewModel: ViewModel() {
val keyGenerated : LiveData<String> get() = _keyGenerated
private val _keyGenerated = MutableLiveData<String>()
val keyGeneratedValidated : LiveData<Void?> get() = _keyGeneratedValidated
private val _keyGeneratedValidated = SingleLiveEvent<Void?>()
val requireKeyGeneration : LiveData<Void?> get() = _requireKeyGeneration
private val _requireKeyGeneration = SingleLiveEvent<Void?>()
val passwordGeneratedValidated : LiveData<Void?> get() = _passwordGeneratedValidated
private val _passwordGeneratedValidated = SingleLiveEvent<Void?>()
val requirePasswordGeneration : LiveData<Void?> get() = _requirePasswordGeneration
private val _requirePasswordGeneration = SingleLiveEvent<Void?>()
val passphraseGeneratedValidated : LiveData<Void?> get() = _passphraseGeneratedValidated
private val _passphraseGeneratedValidated = SingleLiveEvent<Void?>()
val requirePassphraseGeneration : LiveData<Void?> get() = _requirePassphraseGeneration
private val _requirePassphraseGeneration = SingleLiveEvent<Void?>()
fun setKeyGenerated(passKey: String) {
_keyGenerated.value = passKey
}
fun validateKeyGenerated() {
_keyGeneratedValidated.call()
}
fun validatePasswordGenerated() {
_passwordGeneratedValidated.call()
}
fun validatePassphraseGenerated() {
_passphraseGeneratedValidated.call()
}
fun requireKeyGeneration() {
_requireKeyGeneration.call()
}
fun requirePasswordGeneration() {
_requirePasswordGeneration.call()
}
fun requirePassphraseGeneration() {
_requirePassphraseGeneration.call()
}
}

View File

@@ -19,11 +19,11 @@ class SingleLiveEvent<T> : MutableLiveData<T>() {
}
// Observe the internal MutableLiveData
super.observe(owner, { t ->
super.observe(owner) { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
}
@MainThread

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:color="@color/white"
tools:targetApi="lollipop">
<item>
<shape
android:shape="oval">
<solid android:color="?attr/colorPrimary"/>
</shape>
</item>
</ripple>

View File

@@ -1,12 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:color="@color/white"
tools:targetApi="lollipop">
<item>
<shape
android:shape="oval">
<solid android:color="?attr/colorAccent"/>
</shape>
</item>
</ripple>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape
android:shape="oval">
<solid android:color="@color/green_lighter"/>
</shape>
</item>
<item>
<shape
android:shape="oval">
<solid android:color="@color/green_light"/>
</shape>
</item>
</selector>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M3,14h4v-4H3V14zM3,19h4v-4H3V19zM3,9h4V5H3V9zM8,14h13v-4H8V14zM8,19h13v-4H8V19zM8,5v4h13V5H8z"/>
</vector>

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