Compare commits

..

381 Commits
2.8.6 ... 2.9.5

Author SHA1 Message Date
J-Jamet
774dddca54 Merge branch 'release/2.9.5' 2020-12-16 17:05:47 +01:00
J-Jamet
de980d030a Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2020-12-16 11:13:48 +01:00
J-Jamet
0e859646fe Fix timeout reset #817 2020-12-15 19:40:27 +01:00
christopher robert
059c7b7713 Translated using Weblate (German)
Currently translated at 98.3% (490 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-12-15 13:29:10 +01:00
J-Jamet
5fb7bf71c8 Prevent auto switch back to previous keyboard if otp field exists #814 2020-12-15 11:47:57 +01:00
J-Jamet
8b0133ff7f Update CHANGELOG 2020-12-14 19:25:34 +01:00
J-Jamet
8d834946b8 Fix view flickering 2020-12-14 19:12:45 +01:00
J-Jamet
2f646395d4 Merge branch 'feature/Device_Unlock' into develop 2020-12-14 18:31:28 +01:00
J-Jamet
f6e79ba37b Add advanced unlock education hint 2020-12-14 18:23:41 +01:00
J-Jamet
e633c7a861 Biometric unlock in priority and device unlock when biometric not available 2020-12-14 17:51:18 +01:00
J-Jamet
dc02a8d78c Rollback to fix bug after orientation change 2020-12-14 16:59:38 +01:00
J-Jamet
baa9b88512 Check if current database is the same after activity result 2020-12-14 16:56:15 +01:00
J-Jamet
c522e87da8 Fix multiple methods in settings 2020-12-14 16:41:14 +01:00
Paul
ef5ebf2c15 Translated using Weblate (German)
Currently translated at 98.3% (490 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-12-14 13:23:34 +01:00
christopher robert
4b147e770c Translated using Weblate (German)
Currently translated at 98.3% (490 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-12-14 13:23:34 +01:00
J-Jamet
157a5c0b05 Refactor view visibility method 2020-12-13 23:39:17 +01:00
J-Jamet
f2288b0c64 Fix biometric prompt during orientation change 2020-12-13 23:25:44 +01:00
J-Jamet
d8506450aa Fix biometric orientation change 2020-12-13 23:19:04 +01:00
J-Jamet
f9b085e73f Fix min setting version and condition in Android R 2020-12-13 22:44:30 +01:00
J-Jamet
388cf6a91b Fix device credential condition and keep connexion if ActivityForResult requested 2020-12-13 22:33:58 +01:00
Stephan Paternotte
9e6e77b363 Translated using Weblate (Dutch)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2020-12-13 18:48:46 +01:00
J-Jamet
ec33ca8173 Refactoring Advanced unlock fragment and manager 2020-12-13 17:09:41 +01:00
J-Jamet
6be0457947 Add fragment 2020-12-13 14:18:21 +01:00
WaldiS
f3b84aa845 Translated using Weblate (Polish)
Currently translated at 98.1% (489 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-12-12 17:29:08 +01:00
J-Jamet
bd0b5b0954 Fix exception message 2020-12-12 14:53:23 +01:00
J-Jamet
7dc93604ad Refactoring AdvancedUnlockManager.kt 2020-12-12 14:13:13 +01:00
J-Jamet
0ab22698a6 Refactoring classes 2020-12-11 15:15:52 +01:00
J-Jamet
c885ce7aaf Refactoring of advanced unlock 2020-12-11 13:36:51 +01:00
J-Jamet
92d1a7b901 Fix string 2020-12-11 11:11:48 +01:00
J-Jamet
6119054b45 Upgrade to version 2.9.5 2020-12-11 11:03:00 +01:00
Eric
e7aed72398 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-12-11 01:55:56 +01:00
solokot
cee7fa50f5 Translated using Weblate (Russian)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-12-11 01:55:55 +01:00
Stephan Paternotte
39a38bb223 Translated using Weblate (Dutch)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2020-12-11 01:55:55 +01:00
Oliver Cervera
7159a993db Translated using Weblate (Italian)
Currently translated at 99.7% (497 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-12-11 01:55:54 +01:00
J-Jamet
23933e80e3 Merge tag '2.9.4' into develop
2.9.4
2020-12-10 22:27:38 +01:00
J-Jamet
abc971b5cc Merge branch 'release/2.9.4' 2020-12-10 22:27:30 +01:00
J-Jamet
7dedcc8a21 Argon2_id implementation #791 2020-12-10 22:15:08 +01:00
J-Jamet
10d46e5dee Remove default device credential in Android R to prevent update bug #812 2020-12-10 14:58:33 +01:00
J-Jamet
139f7eb36d Upgrade version to 2.9.4 2020-12-09 16:50:32 +01:00
J-Jamet
1ddfa894b6 Merge tag '2.9.3' into develop
2.9.3
2020-12-09 16:13:56 +01:00
J-Jamet
d1695ab8c2 Merge branch 'release/2.9.3' 2020-12-09 16:13:39 +01:00
Milo Ivir
f27979e729 Translated using Weblate (Croatian)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-12-09 15:36:20 +01:00
Milo Ivir
6e61e8172a Translated using Weblate (Croatian)
Currently translated at 99.7% (497 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-12-09 14:37:24 +01:00
Oğuz Ersen
21890894ae Translated using Weblate (Turkish)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-12-09 14:37:24 +01:00
Ihor Hordiichuk
1feecd559d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-12-09 14:37:24 +01:00
solokot
6ea4afe75b Translated using Weblate (Russian)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-12-09 14:37:21 +01:00
HARADA Hiroyuki
fd96f6367d Translated using Weblate (Japanese)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-12-09 14:37:21 +01:00
Kunzisoft
8ce183c4c9 Translated using Weblate (French)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-12-09 14:37:20 +01:00
Retrial
407a1db101 Translated using Weblate (Greek)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2020-12-09 14:37:20 +01:00
J-Jamet
622d096e31 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2020-12-08 13:01:48 +01:00
C. Rüdinger
bf27fb1f89 Translated using Weblate (German)
Currently translated at 94.9% (466 of 491 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-12-08 12:53:03 +01:00
Paul
860b9055c5 Translated using Weblate (German)
Currently translated at 94.9% (466 of 491 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-12-08 12:53:03 +01:00
J-Jamet
b3ae3a4148 Merge branch 'feature/Temp_Advanced_Unlock' into develop #102 #437 #566 2020-12-08 12:08:48 +01:00
J-Jamet
0abd7d5762 Update CHANGELOG 2020-12-08 12:08:28 +01:00
J-Jamet
0aac2bc55b Fix settings 2020-12-08 12:05:35 +01:00
J-Jamet
fa08dc5cfb Change notification icon 2020-12-08 11:17:39 +01:00
J-Jamet
8d18970b4c Fix advanced unlock notification 2020-12-07 20:34:23 +01:00
J-Jamet
173f5ce979 Add advanced unlock timeout 2020-12-07 20:21:39 +01:00
J-Jamet
2e7088310a Add listeners to refresh unlocking state 2020-12-07 19:07:10 +01:00
J-Jamet
c75d99030c Better service implementation 2020-12-07 18:22:04 +01:00
J-Jamet
e4ba1d9bae Better service implementation 2020-12-07 17:40:12 +01:00
J-Jamet
e2886c342a Add temp advanced service to store encrypted elements 2020-12-07 17:03:26 +01:00
J-Jamet
e600d8a56c Add temp advanced unlocking settings 2020-12-07 14:08:44 +01:00
J-Jamet
caeb305475 Fix deletion keystore key 2020-12-07 12:17:07 +01:00
Milo Ivir
3d3a9d9bad Translated using Weblate (Croatian)
Currently translated at 100.0% (491 of 491 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-12-06 15:29:09 +01:00
Oğuz Ersen
5499ad5b94 Translated using Weblate (Turkish)
Currently translated at 100.0% (491 of 491 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-12-06 15:29:08 +01:00
Eric
0e29cd0cee Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (491 of 491 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-12-06 15:29:08 +01:00
Ihor Hordiichuk
24fb1b1a8f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (491 of 491 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-12-06 15:29:07 +01:00
solokot
03fb4cbf0c Translated using Weblate (Russian)
Currently translated at 100.0% (491 of 491 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-12-06 15:29:07 +01:00
WaldiS
e909280d5b Translated using Weblate (Polish)
Currently translated at 98.7% (485 of 491 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-12-06 15:29:07 +01:00
Kunzisoft
d41ddf60b4 Translated using Weblate (French)
Currently translated at 100.0% (491 of 491 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-12-06 15:29:06 +01:00
Retrial
1e01a74986 Translated using Weblate (Greek)
Currently translated at 100.0% (491 of 491 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2020-12-06 15:29:06 +01:00
zeritti
96a007aace Translated using Weblate (Czech)
Currently translated at 100.0% (491 of 491 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-12-06 15:29:06 +01:00
J-Jamet
9f23bb6129 Device credential as default unlock method in Android R+ 2020-12-05 15:03:23 +01:00
J-Jamet
b7e8559773 Remove unused code 2020-12-05 14:42:45 +01:00
J-Jamet
5b247575c8 Fix small bugs #805 2020-12-05 12:09:07 +01:00
HARADA Hiroyuki
eb0e5b478f Translated using Weblate (Japanese)
Currently translated at 100.0% (491 of 491 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-12-04 03:46:13 +01:00
HARADA Hiroyuki
08906ae1da Translated using Weblate (Japanese)
Currently translated at 100.0% (491 of 491 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-12-04 02:53:11 +01:00
Hosted Weblate
395a5efecd Merge branch 'origin/develop' into Weblate. 2020-12-03 23:58:52 +01:00
Milo Ivir
0452dd14f6 Translated using Weblate (Croatian)
Currently translated at 97.9% (476 of 486 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-12-03 23:58:52 +01:00
Oğuz Ersen
3906df314d Translated using Weblate (Turkish)
Currently translated at 100.0% (486 of 486 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-12-03 23:58:52 +01:00
Allan Nordhøy
ce49aa2ebd Translated using Weblate (Norwegian Bokmål)
Currently translated at 70.7% (344 of 486 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-12-03 23:58:51 +01:00
Eric
f2cb062b1e Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (486 of 486 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-12-03 23:58:50 +01:00
Ihor Hordiichuk
f25819a940 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (486 of 486 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-12-03 23:58:50 +01:00
Kunzisoft
3075a9f9f4 Translated using Weblate (Japanese)
Currently translated at 96.0% (467 of 486 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-12-03 23:58:49 +01:00
Kunzisoft
52f1a672c8 Translated using Weblate (French)
Currently translated at 100.0% (486 of 486 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-12-03 23:58:49 +01:00
Retrial
45785fde1c Translated using Weblate (Greek)
Currently translated at 96.0% (467 of 486 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2020-12-03 23:58:49 +01:00
J-Jamet
6a7649e1d7 Tooltips for Magikeyboard #586 2020-12-03 23:56:42 +01:00
J-Jamet
8b3831eb2b Move OTP button to the first view level in Magikeyboard #587 2020-12-03 23:21:59 +01:00
J-Jamet
73e7f4669c Remove lifecycle observer import 2020-12-03 15:45:03 +01:00
J-Jamet
c9f7bbbd25 Remove default database parameter when the file is no longer accessible #803 2020-12-03 15:41:13 +01:00
solokot
ee67238133 Translated using Weblate (Russian)
Currently translated at 100.0% (486 of 486 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-12-03 08:23:35 +01:00
J-Jamet
b425da8d0f Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2020-12-02 13:45:08 +01:00
J-Jamet
754a7f70bc Remove unused translation 2020-12-02 13:22:18 +01:00
Oğuz Ersen
a857ffa987 Translated using Weblate (Turkish)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-12-02 13:12:41 +01:00
Kunzisoft
391ce2ebba Translated using Weblate (Galician)
Currently translated at 6.4% (31 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2020-12-02 13:12:40 +01:00
Wilker Santana da Silva
086723adf4 Translated using Weblate (English)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-12-02 13:12:40 +01:00
Kunzisoft
e993279c35 Translated using Weblate (English)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-12-02 13:12:40 +01:00
Ihor Hordiichuk
aa64310875 Translated using Weblate (Ukrainian)
Currently translated at 99.3% (480 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-12-02 13:12:39 +01:00
Kunzisoft
795baf2c01 Translated using Weblate (Slovak)
Currently translated at 17.8% (86 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2020-12-02 13:12:39 +01:00
solokot
68ac453100 Translated using Weblate (Russian)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-12-02 13:12:39 +01:00
Kunzisoft
79d1f512e5 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 14.9% (72 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nn/
2020-12-02 13:12:39 +01:00
Kunzisoft
e739211314 Translated using Weblate (Latvian)
Currently translated at 14.6% (71 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lv/
2020-12-02 13:12:38 +01:00
HARADA Hiroyuki
d3f6374bb4 Translated using Weblate (Japanese)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-12-02 13:12:38 +01:00
Kunzisoft
5add632cbc Translated using Weblate (Hebrew)
Currently translated at 13.8% (67 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2020-12-02 13:12:38 +01:00
Kunzisoft
d210d1bcce Translated using Weblate (French)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-12-02 13:12:38 +01:00
J-Jamet
6d6422cd63 Merge branch 'translations' into develop 2020-12-02 10:06:28 +01:00
J-Jamet
66e8b7702b Default backup API key to unused 2020-12-02 09:54:47 +01:00
J-Jamet
b75502ad87 Replace strong tag 2020-12-02 09:41:17 +01:00
J-Jamet
3fba96d11f Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2020-12-02 09:39:11 +01:00
Kunzisoft
3571905705 Translated using Weblate (English)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-12-02 09:15:20 +01:00
vachan-maker
acf0e2a1cb Translated using Weblate (English)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-12-02 09:15:19 +01:00
Wilker Santana da Silva
9e7dcb0d7c Translated using Weblate (English)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-12-02 09:15:19 +01:00
Kunzisoft
3c261e3cf7 Deleted translation using Weblate (Abkhazian) 2020-12-02 08:54:49 +01:00
x
b6f324f399 Translated using Weblate (Italian)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-12-02 08:38:03 +01:00
Kunzisoft
f2459489fa Translated using Weblate (Turkish)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-12-02 08:38:03 +01:00
Kunzisoft
f8691cf285 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-12-02 08:38:03 +01:00
Ihor Hordiichuk
2e631d3c42 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-12-02 08:38:03 +01:00
Filippo De Bortoli
1044dca936 Translated using Weblate (Italian)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-12-02 08:38:03 +01:00
x
56c3f495d5 Translated using Weblate (Italian)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-12-02 08:38:03 +01:00
x
0f3036dd9c Translated using Weblate (Italian)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-12-02 08:38:03 +01:00
Filippo De Bortoli
af445ef157 Translated using Weblate (Italian)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-12-02 08:38:03 +01:00
Kunzisoft
25eb09f11c Translated using Weblate (English)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-12-02 08:38:03 +01:00
Oğuz Ersen
16f255aeca Translated using Weblate (Turkish)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-12-02 08:38:03 +01:00
Jennifer Kitts
d0b340837d Translated using Weblate (English)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-12-02 08:38:03 +01:00
Eric
893828ac44 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-12-02 08:38:03 +01:00
Ihor Hordiichuk
a3ca03636a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-12-02 08:38:03 +01:00
WaldiS
582ffe3f23 Translated using Weblate (Polish)
Currently translated at 99.5% (481 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-12-02 08:38:03 +01:00
Milo Ivir
3caad2cceb Translated using Weblate (Croatian)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-12-02 08:38:03 +01:00
Jennifer Kitts
618dcf014d Added translation using Weblate (Abkhazian) 2020-12-02 08:38:03 +01:00
zeritti
d88e20bb56 Translated using Weblate (Czech)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-12-02 08:38:03 +01:00
Miguel
8a8b2b027e Translated using Weblate (Portuguese (Portugal))
Currently translated at 94.8% (458 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2020-12-02 08:38:03 +01:00
Bruno Guerreiro
41cb223099 Translated using Weblate (Portuguese (Portugal))
Currently translated at 94.8% (458 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2020-12-02 08:38:03 +01:00
J. Lavoie
b93ea5e662 Translated using Weblate (Italian)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-12-02 08:38:03 +01:00
J. Lavoie
31c35939fd Translated using Weblate (French)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-12-02 08:38:03 +01:00
J. Lavoie
20a35f4221 Translated using Weblate (Finnish)
Currently translated at 60.0% (290 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fi/
2020-12-02 08:38:03 +01:00
C. Rüdinger
bc6aeb2e93 Translated using Weblate (German)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-12-02 08:38:03 +01:00
J. Lavoie
a561299809 Translated using Weblate (German)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-12-02 08:38:03 +01:00
Filippo De Bortoli
76efb938ab Translated using Weblate (Italian)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-12-02 08:38:03 +01:00
Filippo De Bortoli
56abf73eaf Translated using Weblate (Italian)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-12-02 08:38:03 +01:00
J. Lavoie
2bc068d65a Translated using Weblate (Italian)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-12-02 08:38:03 +01:00
J. Lavoie
f191259f37 Translated using Weblate (German)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-12-02 08:38:03 +01:00
C. Rüdinger
1384c6661d Translated using Weblate (German)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-12-02 08:38:03 +01:00
Oğuz Ersen
c047621548 Translated using Weblate (Turkish)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-12-02 08:38:03 +01:00
Eric
017aaf2e54 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-12-02 08:38:03 +01:00
Ihor Hordiichuk
e4b2b930af Translated using Weblate (Ukrainian)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-12-02 08:38:03 +01:00
solokot
2646c0f0ee Translated using Weblate (Russian)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-12-02 08:38:03 +01:00
HARADA Hiroyuki
4afadb779c Translated using Weblate (Japanese)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-12-02 08:38:03 +01:00
Retrial
ad6e4daa22 Translated using Weblate (Greek)
Currently translated at 100.0% (483 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2020-12-02 08:38:03 +01:00
jan madsen
d5cd07fe76 Translated using Weblate (Danish)
Currently translated at 98.7% (477 of 483 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-12-02 08:38:03 +01:00
abidin toumi
364065ed51 Translated using Weblate (Arabic)
Currently translated at 74.6% (360 of 482 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2020-12-02 08:38:03 +01:00
Stephan Paternotte
b4f05d4da7 Translated using Weblate (Dutch)
Currently translated at 100.0% (482 of 482 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2020-12-02 08:38:03 +01:00
J-Jamet
3d12a0e8e9 Fix fingerprint none enrolled detection 2020-12-01 13:48:08 +01:00
J-Jamet
e29f3194f3 Upgrade to 2.9.3 and update CHANGELOG 2020-11-30 17:50:47 +01:00
J-Jamet
6bac86638b Change fingerprint pref icon by a bolt 2020-11-30 17:45:58 +01:00
J-Jamet
d6ee1cdf6e Merge branch 'feature/Device_Credential_Unlock' into develop #779 2020-11-30 17:24:59 +01:00
J-Jamet
e9d0efaf93 Fix advanced unlock setting events 2020-11-30 17:20:58 +01:00
J-Jamet
85467fa15b Change biometric to advanced unlock 2020-11-30 13:56:14 +01:00
J-Jamet
84bb47aa53 Better unlock settings 2020-11-23 20:47:50 +01:00
J-Jamet
75f245c7dc Add device credential unlock 2020-11-23 19:14:44 +01:00
J-Jamet
6c5be88432 Fix biometric error message when the keystore is not accessible 2020-11-23 16:52:36 +01:00
J-Jamet
590b22de69 Merge tag '2.9.2' into develop
2.9.2
2020-11-22 20:37:23 +01:00
J-Jamet
4770269f6f Merge branch 'release/2.9.2' 2020-11-22 20:37:14 +01:00
J-Jamet
823f591aa8 Fix small UI issue 2020-11-22 20:06:39 +01:00
J-Jamet
c88c489633 Maximal search elements to 10 #793 2020-11-22 19:06:35 +01:00
J-Jamet
8afb58a044 Fix translations 2020-11-22 18:32:48 +01:00
J-Jamet
428fa8a61b Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2020-11-22 18:15:31 +01:00
J-Jamet
3ba5e1ee79 Upgrade gradle and libs 2020-11-21 19:09:54 +01:00
J-Jamet
d46d0cb384 Better custom field name verification #718 2020-11-21 17:20:54 +01:00
J-Jamet
f841884557 Fix first custom field #718 2020-11-21 17:06:43 +01:00
J-Jamet
d381ab5316 Merge branch 'master' into develop 2020-11-21 14:20:13 +01:00
J-Jamet
a3e8e7ae77 Remove not required files 2020-11-21 14:19:40 +01:00
Jérémy JAMET
a405753827 Licensing organization 2020-11-21 14:11:23 +01:00
J-Jamet
8df74d2c4b Harmonization of KeePass names 2020-11-21 11:51:20 +01:00
J-Jamet
cbe0ffe52a 2 lines to show title in lists #534 #617 2020-11-20 20:09:47 +01:00
J-Jamet
a5631a0476 Fix read only #792 2020-11-20 18:36:44 +01:00
Michal Čihař
e4bd704a53 Deleted translation using Weblate (_EN (generated)) 2020-11-19 12:57:02 +01:00
J-Jamet
81a53440bc Change database edition color #719 2020-11-19 11:57:46 +01:00
J-Jamet
3466de1990 Update CHANGELOG 2020-11-19 11:38:08 +01:00
J-Jamet
253b053c2c Different channels for each type of notification #688 2020-11-19 11:31:03 +01:00
Aman Alam
928d012046 Translated using Weblate (Punjabi)
Currently translated at 74.4% (359 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pa/
2020-11-18 21:28:54 +01:00
Ihor Hordiichuk
19367406c6 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (482 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-11-18 21:28:52 +01:00
Balázs Meskó
f4e3717dd3 Translated using Weblate (Hungarian)
Currently translated at 93.3% (450 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2020-11-18 21:28:52 +01:00
Retrial
c01e1d91c5 Translated using Weblate (Greek)
Currently translated at 100.0% (482 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2020-11-18 21:28:51 +01:00
J-Jamet
6983f9f0b6 Harmonization of field names #789 2020-11-18 20:23:05 +01:00
J-Jamet
c13cf1a86c Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2020-11-18 19:40:14 +01:00
J-Jamet
2a3dafe07f Prevent manual creation of existing field name #718 2020-11-18 19:39:53 +01:00
vachan-maker
0104d02442 Translated using Weblate (Malayalam)
Currently translated at 82.5% (398 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2020-11-16 07:28:51 +01:00
solokot
8cef4fde82 Translated using Weblate (Russian)
Currently translated at 100.0% (482 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-11-13 15:17:29 +01:00
J-Jamet
3dc02516ea Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2020-11-13 10:03:45 +01:00
J-Jamet
3614529fda Add new browsers to Autofill compatibility list #769 2020-11-13 10:03:36 +01:00
Jennifer Kitts
80d4f06e56 Added translation using Weblate (_EN (generated)) 2020-11-13 06:22:19 +01:00
J-Jamet
9ff4f395b5 Merge branch 'feature/OTP_Links' into develop 2020-11-12 15:15:43 +01:00
J-Jamet
61b8fa116a Fix small chars issue 2020-11-12 15:14:14 +01:00
J-Jamet
22a3541b7b Auto populate Title and Username from OTP Auth 2020-11-12 15:08:27 +01:00
J-Jamet
c57515fed5 Fix small OTP Auth syntax #556 2020-11-12 14:59:39 +01:00
J-Jamet
eec6199413 Fix decoding OTP Auth #556 2020-11-12 14:39:33 +01:00
J-Jamet
fe48955b94 Check OTP Auth URI #556 2020-11-12 13:19:05 +01:00
J-Jamet
03047ae6dd Replace OTP when already exists #556 2020-11-12 12:30:25 +01:00
J-Jamet
856d4867b4 Manage OTP links #556 2020-11-10 15:00:42 +01:00
J-Jamet
b1b1aa0e13 Change behavior #783 2020-11-10 13:05:07 +01:00
WaldiS
ab68472698 Translated using Weblate (Polish)
Currently translated at 100.0% (482 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-11-09 21:22:11 +01:00
J-Jamet
5ae4ee1411 Fix switch back to previous keyboard #782 2020-11-09 17:04:43 +01:00
Aman Alam
f1989bac21 Translated using Weblate (Punjabi)
Currently translated at 62.8% (303 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pa/
2020-11-09 14:19:18 +01:00
J-Jamet
1b20188b98 Fix links in pro description #771 2020-11-09 14:14:09 +01:00
J-Jamet
140a79d18c Fix same save shared info #783 2020-11-09 11:53:42 +01:00
J-Jamet
a9610ced0e Fix empty OTP field after selection #781 2020-11-09 11:07:23 +01:00
J-Jamet
17faee7719 Fix search in OTP #780 2020-11-09 10:30:58 +01:00
jan madsen
120ca1c02c Translated using Weblate (Danish)
Currently translated at 94.3% (455 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-11-07 20:52:16 +01:00
Srdjan Todorovic
ef2d1ebe4f Translated using Weblate (Serbian (latin))
Currently translated at 54.7% (264 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sr_Latn/
2020-11-06 22:21:23 +01:00
Milo Ivir
b9c931c97f Translated using Weblate (Croatian)
Currently translated at 100.0% (482 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-11-06 22:21:18 +01:00
Milo Ivir
60dd963d7d Translated using Weblate (German)
Currently translated at 100.0% (482 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-11-06 22:21:18 +01:00
J-Jamet
c414fac815 Change default AES Key rounds to 500000 2020-11-06 13:08:46 +01:00
Srdjan Todorovic
95041d6a0c Translated using Weblate (Serbian (latin))
Currently translated at 3.3% (16 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sr_Latn/
2020-11-04 19:27:07 +01:00
Oğuz Ersen
1fab9c3279 Translated using Weblate (Turkish)
Currently translated at 100.0% (482 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-11-04 19:27:06 +01:00
Eric
3b8661249e Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (482 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-11-04 19:27:05 +01:00
Ihor Hordiichuk
2fba831851 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (482 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-11-04 19:27:03 +01:00
solokot
fd4ac14ab3 Translated using Weblate (Russian)
Currently translated at 100.0% (482 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-11-04 19:27:03 +01:00
HARADA Hiroyuki
1d9b9e9bfa Translated using Weblate (Japanese)
Currently translated at 100.0% (482 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-11-04 19:27:02 +01:00
Kunzisoft
14b2277313 Translated using Weblate (French)
Currently translated at 100.0% (482 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-11-04 19:27:02 +01:00
nautilusx
0f57bf5235 Translated using Weblate (German)
Currently translated at 98.3% (474 of 482 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-11-04 19:26:59 +01:00
J-Jamet
4bb7fae1e3 Upgrade to version 2.9.2 2020-11-03 12:28:04 +01:00
J-Jamet
ec0b8ebc92 Merge tag '2.9.1' into develop
2.9.1
2020-11-03 12:17:23 +01:00
J-Jamet
1727633f5d Merge branch 'release/2.9.1' 2020-11-03 12:17:15 +01:00
J-Jamet
668a77cb5a Remove empty string resources 2020-11-03 12:00:31 +01:00
J-Jamet
e87ee5e091 Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2020-11-03 11:57:10 +01:00
J-Jamet
b7acc0e6da Update CHANGELOG 2020-11-02 22:15:22 +01:00
J-Jamet
109f34680f Fix crash in Android 21/23 #774 2020-11-02 21:20:15 +01:00
J-Jamet
fd57ccc1a7 Copy password from generator #697 2020-11-02 17:31:35 +01:00
J-Jamet
e3dfae246a Fix change font size #770 2020-11-02 16:52:59 +01:00
Srdjan Todorovic
7186bbbe6c Added translation using Weblate (Serbian (latin)) 2020-11-02 15:06:36 +01:00
J-Jamet
c7b2ce37b1 Update CHANGELOG 2020-11-02 13:42:24 +01:00
J-Jamet
5b3a602911 Replace constraint layout by linear layout to fix crop #772 2020-11-02 13:38:54 +01:00
J-Jamet
6d51edd94d Upgrade CHANGELOG 2020-11-02 12:45:16 +01:00
J-Jamet
dcb68a6538 Fix full description links #771 2020-11-02 12:39:13 +01:00
J-Jamet
d549b86c81 Upgrade to version 2.9.1 2020-11-01 20:54:53 +01:00
J-Jamet
ac415f1384 Remove files after deploy pro 2020-11-01 20:52:25 +01:00
Hosted Weblate
80ed5800a0 Merge branch 'origin/master' into Weblate. 2020-11-01 20:38:50 +01:00
J-Jamet
291ed44621 Fix fastlane metadata 2020-11-01 20:30:50 +01:00
J-Jamet
a7e8915ea0 Merge tag '2.9' into develop
2.9
2020-11-01 19:44:36 +01:00
J-Jamet
a027c76af3 Merge branch 'release/2.9' 2020-11-01 19:44:28 +01:00
J-Jamet
ec375bd068 Fix subDomain when WebDomain is an IP #767 2020-11-01 15:24:16 +01:00
J-Jamet
f5a28c83f0 Check opening database in read only to saving data 2020-11-01 12:52:02 +01:00
J-Jamet
813240e233 Allow IP addresses instead of strict WebDomain #767 2020-11-01 11:43:16 +01:00
J-Jamet
051ac0e669 Fix discard entry edition in registration mode 2020-11-01 11:23:31 +01:00
J-Jamet
5c798c4569 Fix search domain in open database 2020-11-01 11:11:03 +01:00
Jennifer Kitts
cc2146e397 Added translation using Weblate (English (Developer)) 2020-11-01 02:32:59 +01:00
J-Jamet
8356514bf8 Replace <strong> tags 2020-10-31 18:31:05 +01:00
J-Jamet
464bc10860 Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations
# Conflicts:
#	app/src/main/res/values-el/strings.xml
#	app/src/main/res/values-es/strings.xml
#	app/src/main/res/values-fr/strings.xml
2020-10-31 18:21:36 +01:00
J-Jamet
5436c8bed1 Save scheme in WebDomain 2020-10-31 17:55:58 +01:00
J-Jamet
1b163b161b To ask data lost only one time 2020-10-31 17:22:28 +01:00
J-Jamet
f0ee5fd946 Fix read only for registration 2020-10-31 14:48:33 +01:00
J-Jamet
066a9f871c Set entry result in EntryEditActivity only if requested by the caller 2020-10-30 15:15:38 +01:00
J-Jamet
e23841f5bb Fix lock database after autofill 2020-10-30 15:10:35 +01:00
J-Jamet
571e66fae5 Fix registration callback, to upgrade with #765 2020-10-30 12:45:21 +01:00
J-Jamet
402aa280e0 Upgrade ContraintLayout lib and fix custom edit field 2020-10-30 11:47:42 +01:00
J-Jamet
65de5df319 Change UI to prevent unwanted manual file deletion 2020-10-30 11:34:53 +01:00
J-Jamet
63168afc85 Small UI fix when file modification hidden 2020-10-29 13:48:20 +01:00
J-Jamet
1c61f54df6 Fix expand file info view 2020-10-29 13:33:49 +01:00
J-Jamet
50b5ad1799 Fix setting color 2020-10-29 11:43:08 +01:00
J-Jamet
6457c02a35 Upgrade core lib 2020-10-29 09:20:55 +01:00
J-Jamet
7b242c9733 Links to open external applications 2020-10-28 18:20:53 +01:00
J-Jamet
e0ab6137e7 Fix search info field value in WebDomain[x] 2020-10-28 16:39:51 +01:00
J-Jamet
49b23a33e7 Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop
# Conflicts:
#	app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt
#	app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt
2020-10-28 15:18:11 +01:00
J-Jamet
c5e9d14199 Fix special modes and add search/save modes 2020-10-27 16:01:40 +01:00
J-Jamet
f7e8662bdf Fix search info 2020-10-26 18:49:37 +01:00
J-Jamet
7d356d1e34 Fix magikeyboard selection 2020-10-26 18:26:14 +01:00
J-Jamet
f9bb70f395 Fix autofill response 2020-10-26 16:06:43 +01:00
Aman ALam
4335809468 Translated using Weblate (Punjabi)
Currently translated at 61.9% (285 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pa/
2020-10-25 18:26:51 +01:00
Vibo Lavida
2a1b1d28bd Translated using Weblate (Spanish)
Currently translated at 83.4% (384 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-10-25 18:26:50 +01:00
J-Jamet
38d69428d8 Merge branch 'hokonch-i18n-ja' into develop 2020-10-25 18:01:20 +01:00
J-Jamet
0c22a8135d Merge branch 'i18n-ja' of git://github.com/hokonch/KeePassDX into hokonch-i18n-ja 2020-10-25 18:00:57 +01:00
J-Jamet
2b50665f9e Merge branch 'teemue-patch-1' into develop 2020-10-25 17:51:49 +01:00
J-Jamet
59607efa62 Fix backup search #759 2020-10-25 17:48:16 +01:00
J-Jamet
cd5cfbe009 Fix logo in password UI 2020-10-25 16:17:43 +01:00
J-Jamet
84e6d96ce0 Fix logo in password UI 2020-10-25 15:58:06 +01:00
hokonch
8f00b53fab Reorder and update strings (ja) 2020-10-25 12:59:56 +09:00
hokonch
e2164a1a9c Add fastlane screenshots (ja) 2020-10-25 12:59:56 +09:00
hokonch
6e5dcaf08d Add fastlane descriptions (ja) 2020-10-25 12:59:35 +09:00
J-Jamet
18a2aae66a Rollback password UI and add logotype 2020-10-23 23:10:50 +02:00
J-Jamet
4c0aab15fa Fix biometric state and string 2020-10-23 17:44:24 +02:00
J-Jamet
75a7d4188b Fix biometric education hint 2020-10-23 16:59:51 +02:00
J-Jamet
ff0da57aeb Fix crash in KitKat 2020-10-23 16:37:57 +02:00
J-Jamet
d716cba46b Fix password view in landscape 2020-10-23 16:00:39 +02:00
J-Jamet
5d41e44141 Change password layout to fix biometric view flickering 2020-10-23 14:54:07 +02:00
J-Jamet
281936ecd0 Fix biometric view flickering 2020-10-23 13:13:12 +02:00
J-Jamet
da23321c0d Add mozilla license and upgrade CHNAGELOG 2020-10-23 10:01:12 +02:00
J-Jamet
110e2b7580 Fix subdomain search #728 2020-10-22 21:16:44 +02:00
SeerLite
546d4353e2 Translated using Weblate (Spanish)
Currently translated at 76.9% (354 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-10-22 16:26:54 +02:00
Vibo Lavida
15b5d36cd6 Translated using Weblate (Spanish)
Currently translated at 76.9% (354 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-10-22 16:26:54 +02:00
J-Jamet
6a935a49ea Setting to hide UUID #757 2020-10-21 21:28:52 +02:00
J-Jamet
f7f515481f Fix biometric flickering 2020-10-19 17:26:13 +02:00
J-Jamet
3aab37c0c0 Refactor biometric variables 2020-10-19 17:20:29 +02:00
J-Jamet
123e626df6 Fix onValidateSpecialMode() 2020-10-19 16:53:44 +02:00
J-Jamet
758985675d Encapsulate activities launch methods 2020-10-19 16:51:23 +02:00
J-Jamet
631ebc657b Fix special mode in app background 2020-10-19 16:01:11 +02:00
J-Jamet
d0ca714482 Setting to switch keyboard when database is locked #625 2020-10-19 15:04:12 +02:00
J-Jamet
1fe3787186 Fix keyboard selection 2020-10-19 14:24:12 +02:00
J-Jamet
c8a952616f Setting to close database after Autofill selection #755 2020-10-19 14:10:16 +02:00
J-Jamet
efcbecc218 Fix load database when launch autofill or share in background #738 2020-10-19 12:45:34 +02:00
J-Jamet
487bafa5cf Fix biometric prompt auto opened #738 2020-10-19 12:26:07 +02:00
teemue
93aed33e2a Update fi translation
Typo fixes
2020-10-18 17:28:05 +03:00
J-Jamet
a7765cb635 Fix warning at emptying recycle bin #742 2020-10-18 13:24:47 +02:00
J-Jamet
e6a5be6c66 Save new credentials with Autofill #524 2020-10-18 12:58:22 +02:00
Devin Williams
5443532266 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 15.8% (73 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nn/
2020-10-17 22:26:42 +02:00
J-Jamet
990aca2e1a Merge branch 'feature/Autofill_Save' into develop #524 2020-10-17 16:18:40 +02:00
J-Jamet
49297adf97 Fix read only mode and back pressed 2020-10-17 15:35:01 +02:00
J-Jamet
b4d26fd35a Manually save search in selection mode 2020-10-16 15:40:01 +02:00
J-Jamet
b31f580760 Save web scheme to save URL 2020-10-16 12:39:30 +02:00
José Elias Júnior
7dc33f1956 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2020-10-15 01:49:34 +02:00
Éfrit
e3470dd68b Translated using Weblate (French)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-10-15 01:49:33 +02:00
J-Jamet
410c653654 Fix save last access time 2020-10-13 15:04:59 +02:00
J-Jamet
bef3cf4c93 Fix save search info 2020-10-13 15:04:12 +02:00
J-Jamet
d8961d2acb Save search info settings 2020-10-13 14:20:12 +02:00
J-Jamet
17873ef7aa Merge branch 'develop' into feature/Autofill_Save 2020-10-13 12:55:05 +02:00
random r
1d6e7eabc1 Translated using Weblate (Italian)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-10-13 12:26:41 +02:00
J-Jamet
ba956abb1c Fix populate custom field in KDB database 2020-10-12 22:33:57 +02:00
J-Jamet
7a2c2df89e Change app caller in special mode, remove home button from regular toolbar in special mode 2020-10-12 21:59:12 +02:00
J-Jamet
bd44e659a8 Add verification to save data 2020-10-12 15:47:28 +02:00
J-Jamet
8a1a27f5a1 Fix callback result 2020-10-12 15:40:40 +02:00
J-Jamet
7825071f61 Fix keyboard selection result 2020-10-12 15:04:17 +02:00
J-Jamet
7047bcbb1e Register username and password in entry 2020-10-12 14:24:52 +02:00
J-Jamet
193ef74e63 Register WebDomain or ApplicationId 2020-10-12 12:50:14 +02:00
Jennifer Kitts
8a193d4dcd Added translation using Weblate (English (United States)) 2020-10-10 17:22:00 +02:00
marklin0913da248e4cdada422a
35cc662b26 Translated using Weblate (Chinese (Traditional))
Currently translated at 65.4% (301 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2020-10-10 16:26:41 +02:00
J-Jamet
9173bcc742 Fix biometric authentication when weak security is not present 2020-10-08 23:17:36 +02:00
J-Jamet
267f4273ee Try to add new biometric states from the doc #724 2020-10-08 23:10:28 +02:00
J-Jamet
f1e675d662 Upgrade biometric lib to 1.1.0-beta01 #724 2020-10-08 22:28:20 +02:00
J-Jamet
c228534218 Merge branch 'hbiel-feature/api-upgrade-icon-packs' into develop 2020-10-08 22:13:44 +02:00
J-Jamet
181fa5f32a Create registration mode 2020-10-08 21:58:32 +02:00
hbiel
7d5c37ec33 upgrade icon packs to API 30 2020-10-08 21:16:10 +02:00
J-Jamet
2eb9736d23 Merge branch 'develop' into feature/Autofill_Save 2020-10-08 18:16:21 +02:00
Allan Nordhøy
9f89ad2a08 Translated using Weblate (Indonesian)
Currently translated at 50.4% (232 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2020-10-08 07:26:12 +02:00
J-Jamet
490db3a026 Add a lock to fix autofill in Firefox #725 2020-10-07 16:23:54 +02:00
J-Jamet
2428f073bc Update CHANGELOG 2020-10-07 13:46:43 +02:00
J-Jamet
e1231585ce Merge branch 'develop' into feature/Autofill_Save 2020-10-07 13:39:36 +02:00
J-Jamet
839adaf559 Merge branch 'feature/BIOMETRIC_SECURITY' into develop 2020-10-07 13:38:51 +02:00
J-Jamet
839e004c08 Merge branch 'develop' into feature/Autofill_Save 2020-10-07 13:38:04 +02:00
J-Jamet
ecf98828ff Merge branch 'jdambron-add-missing-French-translation' into develop 2020-10-07 13:35:37 +02:00
J-Jamet
4dbc3c2353 Merge branch 'develop' of git://github.com/wishawa/KeePassDX into wishawa-develop 2020-10-07 13:33:24 +02:00
J-Jamet
c953a337fe Setting to disable save autofill data 2020-10-05 20:39:33 +02:00
J-Jamet
6f026e6043 Merge branch 'develop' into feature/Autofill_Save 2020-10-05 19:31:11 +02:00
J-Jamet
a775e29aef Fix webDomain with Autofill compatibility mode #551 2020-10-05 19:29:19 +02:00
J-Jamet
cb7b37fca4 Fix crash in autofill save workflow 2020-10-05 18:12:30 +02:00
arwansel
cc79a67a9f Translated using Weblate (Indonesian)
Currently translated at 50.4% (232 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2020-10-05 12:35:31 +02:00
Lucas Nunes
72e7e3a3c4 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.3% (457 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2020-10-05 12:35:29 +02:00
ssantos
4ffbeebd85 Translated using Weblate (Portuguese)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2020-10-02 23:35:38 +02:00
abidin toumi
2e4b4e4736 Translated using Weblate (Arabic)
Currently translated at 77.3% (356 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2020-10-02 23:35:37 +02:00
Retrial
a0acb0b658 Translated using Weblate (Greek)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2020-10-02 23:34:57 +02:00
cloudy-dev
feeaea4d64 Translated using Weblate (German)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-10-02 23:34:56 +02:00
Wisha Wa
0b7eb96e48 Use textNoSuggestions type for username input instead. 2020-10-02 06:48:43 +00:00
jdambron
f0c4b628bf Add missing French translations 2020-10-02 08:28:12 +02:00
J-Jamet
c66fc102ee First commit for save 2020-10-01 18:55:29 +02:00
J-Jamet
9947a23343 Try to fix add/update database history #732 2020-10-01 12:47:52 +02:00
J-Jamet
abc204b313 Replace Toast by Log.e 2020-09-30 22:47:20 +02:00
J-Jamet
425a0812bd Try to fix variable BIOMETRIC_STRONG recognition #724 2020-09-30 19:55:43 +02:00
J-Jamet
030d466b11 Try to fix variable BIOMETRIC_STRONG recognition #724 2020-09-30 15:58:12 +02:00
J-Jamet
27e9cd04a9 Fix password activity education exception 2020-09-29 22:32:18 +02:00
J-Jamet
e42aea9444 Fix biometric button animation after key deletion 2020-09-29 17:34:10 +02:00
J-Jamet
3e1ee720f1 Fix biometric button animation after key deletion 2020-09-29 17:13:18 +02:00
J-Jamet
cb9f12482e Fix biometric clear button menu 2020-09-29 16:47:02 +02:00
J-Jamet
bcf273a435 Fix button state after clearing key 2020-09-29 16:35:56 +02:00
J-Jamet
6230ada2cc Upgrade biometric lib to 1.1.0-alpha02
Use BIOMETRIC_STRONG option
Fix biometricPrompt opening after clearing key
2020-09-29 16:21:32 +02:00
J-Jamet
db4de65683 Refactor canAuthenticate 2020-09-29 15:12:32 +02:00
J-Jamet
931f7f07ca Update CHANGELOG 2020-09-29 13:37:40 +02:00
J-Jamet
0f764c9400 Fix text size 2020-09-29 13:30:21 +02:00
J-Jamet
89c98ab257 Add variable 'm' suffix 2020-09-29 13:13:19 +02:00
J-Jamet
a49255c471 Fix crash with API 30 #723 2020-09-29 13:10:24 +02:00
J-Jamet
39d5a4908f Upgrade to version 2.9 2020-09-29 12:46:39 +02:00
J-Jamet
fbaeaddff7 Upgrade to API 30 2020-09-29 12:44:51 +02:00
abidin toumi
bc97db982e Translated using Weblate (Arabic)
Currently translated at 69.3% (319 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2020-09-29 11:40:10 +02:00
abidin toumi
9a2f260bc5 Translated using Weblate (Arabic)
Currently translated at 66.0% (304 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2020-09-28 11:41:00 +02:00
vachan-maker
954f1e3c7f Translated using Weblate (Malayalam)
Currently translated at 83.2% (383 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2020-09-27 11:01:07 +02:00
abidin toumi
0626f8d678 Translated using Weblate (Arabic)
Currently translated at 61.9% (285 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2020-09-27 11:01:06 +02:00
WaldiS
aa8c9676fe Translated using Weblate (Polish)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-09-27 11:01:06 +02:00
Kornelijus Tvarijanavičius
bb2ab768a8 Translated using Weblate (Lithuanian)
Currently translated at 16.7% (77 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2020-09-27 11:01:05 +02:00
J-Jamet
8192c68f38 Merge tag '2.8.7' into develop
2.8.7
2020-09-26 10:31:54 +02:00
J-Jamet
454561c2a1 Merge branch 'release/2.8.7' 2020-09-26 10:31:47 +02:00
J-Jamet
deabcc9605 Update CHANGELOG and version to 2.8.7 2020-09-26 10:22:00 +02:00
J-Jamet
c3bdb9dd16 Downgrade to API 29 #722 2020-09-26 10:17:19 +02:00
J-Jamet
86d77c908f Merge tag '2.8.6' into develop
2.8.6
2020-09-25 22:18:25 +02:00
233 changed files with 9544 additions and 4464 deletions

View File

@@ -20,7 +20,7 @@ Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
** Keepass Database **
**KeePass Database**
- Created with: [e.g Windows KeePass 2.42]
- Version: [e.g. 2]
- Location: [e.g. Remote file retrieved with GDrive app]

View File

@@ -1,3 +1,53 @@
KeePassDX(2.9.5)
* Unlock database by device credentials (PIN/Password/Pattern) with Android M+ #102 #152 #811
* Prevent auto switch back to previous keyboard if otp field exists #814
* Fix timeout reset #817
KeePassDX(2.9.4)
* Fix small bugs #812
* Argon2ID implementation #791
KeePassDX(2.9.3)
* Unlock database by device credentials (PIN/Password/Pattern) #779 #102
* Advanced unlock with timeout #102 #437 #566
* Remove default database parameter when the file is no longer accessible #803
* Move OTP button to the first view level in Magikeyboard #587
* Tooltips for Magikeyboard #586
* Fix small bugs #805
KeePassDX(2.9.2)
* Managing OTP links from QR applications #556
* Prevent manual creation of existing field name #718
* Harmonization of field names #789
* Different channels for each type of notification #688
* Fix OTP #780 #781
* Fix same save shared info #783
* Fix switch back to previous keyboard #782
* Fix read only #792
* Better UI #719 #534 #617 #793
KeePassDX(2.9.1)
* Copy password from generator #697
* Fix Magikeyboard not fully visible #772
* Fix change font size #770
* Fix crash #774
* Small fixes #771
KeePassDX(2.9)
* Upgrade to Android API 30 #723
* Save new credentials with Autofill #524
* Setting to close database after Autofill selection #755
* Setting to switch keyboard when database is locked #625
* Fix biometric issues #724 #740 #731
* Fix autofill #725 #551
* Fix subdomain search #728
* Fix backup search #759
* Small fixes and translations #732 #736 #737 #738 #742 #767
KeePassDX(2.8.7)
* Downgrade to Android API 29 (crash on startup with API 30 on some devices)
Sorry for the inconvenience
KeePassDX(2.8.6)
* Fix Autofill recognition #712
* Keep value after renaming custom field #709

1013
LICENSE

File diff suppressed because it is too large Load Diff

202
LICENSES/LICENSE_APACHE Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,7 @@
Copyright (c) 2000 - 2011 The Legion Of The Bouncy Castle (http://www.bouncycastle.org)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,111 @@
Files ic00.png to ic61.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
TITLE: NUVOLA ICON THEME for KDE 3.x
AUTHOR: David Vignoni | ICON KING
SITE: http://www.icon-king.com
MAILING LIST: http://mail.icon-king.com/mailman/listinfo/nuvola_icon-king.com
Copyright (c) 2003-2004 David Vignoni.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation,
version 2.1 of the License.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library (see the the license.txt file); if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#######**** NOTE THIS ADD-ON ****#######
The GNU Lesser General Public License or LGPL is written for software libraries
in the first place. The LGPL has to be considered valid for this artwork
library too.
Nuvola icon theme for KDE 3.x is a special kind of software library, it is an
artwork library, it's elements can be used in a Graphical User Interface, or
GUI.
Source code, for this library means:
- raster png image* .
The LGPL in some sections obliges you to make the files carry
notices. With images this is in some cases impossible or hardly usefull.
With this library a notice is placed at a prominent place in the directory
containing the elements. You may follow this practice.
The exception in section 6 of the GNU Lesser General Public License covers
the use of elements of this art library in a GUI.
dave [at] icon-king.com
Date: 15 october 2004
Version: 1.0
DESCRIPTION:
Icon theme for KDE 3.x.
Icons where designed using Adobe Illustrator, and then exported to PNG format.
Icons shadows and minor corrections were done using Adobe Photoshop.
Kiconedit was used to correct some 16x16 and 22x22 icons.
LICENSE
Released under GNU Lesser General Public License (LGPL)
Look at the license.txt file.
CONTACT
David Vignoni
e-mail : david [at] icon-king.com
ICQ : 117761009
http: http://www.icon-king.com
---
Files ic62.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
Based on http://de.wikipedia.org/w/index.php?title=Datei:Tux.svg&filetimestamp=20090927073505
The copyright holder of this file allows anyone to use it for any purpose,
provided that the copyright holders Larry Ewing, Simon Budig and
Anja Gerwinski are mentioned.
---
Files ic63.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
Based on http://en.wikipedia.org/wiki/File:ASF-logo.svg
Apache logo
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may
obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.
---
Files ic64.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
Created by Jeremy JAMET and licensed under the terms of the GPLv3.
---
Files ic65.png, ic67.png and ic68.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
Created by Tobias Selig and licensed under the terms of the GPLv2 or GPLv3.
---
File ic66.png under under res/drawable, res/drawable-hdpi and res/drawable-ldpi
Based on http://commons.wikimedia.org/wiki/File:Dollar_symbol_gold.svg
Author: Rugby471
Permission is granted to copy, distribute and/or modify this document under
the terms of the GNU Free Documentation License, Version 1.2 or any later
version published by the Free Software Foundation; with no Invariant Sections,
no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is
included in the section entitled "GNU Free Documentation License".
---

View File

@@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@@ -1,6 +1,6 @@
# Android KeepassDX
# Android KeePassDX
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> KeepassDX is a **multi-format KeePass manager for Android devices**. The app allows creating keys and passwords in a secure way by integrating with the Android design standards.
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> KeePassDX is a **multi-format KeePass manager for Android devices**. The app allows creating keys and passwords in a secure way by integrating with the Android design standards.
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
@@ -19,7 +19,7 @@
- Precise management of **settings**.
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
KeepassDX is **open source** and **ad-free**.
KeePassDX is **open source** and **ad-free**.
## What is KeePassDX?
@@ -88,4 +88,4 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*This project is a fork of [KeepassDroid](https://github.com/bpellin/keepassdroid) by bpellin.*
*This project is a fork of [KeePassDroid](https://github.com/bpellin/keepassdroid) by bpellin.*

View File

@@ -1 +0,0 @@
theme: jekyll-theme-cayman

View File

@@ -6,20 +6,21 @@ apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 30
buildToolsVersion '30.0.2'
ndkVersion '21.3.6528147'
defaultConfig {
applicationId "com.kunzisoft.keepass"
minSdkVersion 14
targetSdkVersion 30
versionCode = 42
versionName = "2.8.6"
versionCode = 49
versionName = "2.9.5"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
manifestPlaceholders = [ googleAndroidBackupAPIKey:"" ]
manifestPlaceholders = [ googleAndroidBackupAPIKey:"unused" ]
kapt {
arguments {
@@ -96,14 +97,13 @@ def room_version = "2.2.5"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.cardview:cardview:1.0.0'
// WARNING: Bug with extra field
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.0.1'
implementation 'androidx.biometric:biometric:1.1.0-rc01'
// Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:1.3.1"
implementation "androidx.core:core-ktx:1.3.2"
implementation 'androidx.fragment:fragment-ktx:1.2.5'
// WARNING: To upgrade with style, bug in edit text
implementation 'com.google.android.material:material:1.0.0'
@@ -115,7 +115,7 @@ dependencies {
// Time
implementation 'joda-time:joda-time:2.10.6'
// Color
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3'
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
// Education
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
// Apache Commons Collections

View File

@@ -1,19 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportWidth="108"
android:viewportHeight="108"
android:width="108dp"
android:height="108dp">
xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="108"
android:viewportHeight="108"
android:width="108dp"
android:height="108dp">
<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:fillColor="@color/long_shadow"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:strokeMiterLimit="4" />
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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -1,19 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportWidth="108"
android:viewportHeight="108"
android:width="108dp"
android:height="108dp">
xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="108"
android:viewportHeight="108"
android:width="108dp"
android:height="108dp">
<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:fillColor="@color/long_shadow"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:strokeMiterLimit="4" />
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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -16,6 +16,8 @@
android:name="android.permission.VIBRATE"/>
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"/>
<application
android:label="@string/app_name"
@@ -47,7 +49,7 @@
<activity
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustResize|stateUnchanged">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
@@ -148,6 +150,13 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="otpauth" android:host="totp" />
<data android:scheme="otpauth" android:host="hotp" />
</intent-filter>
</activity>
<activity
android:name="com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity"
@@ -165,13 +174,17 @@
android:enabled="true"
android:exported="false" />
<service
android:name=".notifications.AttachmentFileNotificationService"
android:name="com.kunzisoft.keepass.notifications.AttachmentFileNotificationService"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService"
android:enabled="true"
android:exported="false" />
<!-- Receiver for Autofill -->
<service
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"

Binary file not shown.

View File

@@ -30,39 +30,75 @@ import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.autofill.KeeAutofillService
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.UriUtil
@RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Build search param
val searchInfo = SearchInfo().apply {
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
// Retrieve selection mode
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
when (specialMode) {
SpecialMode.SELECTION -> {
// Build search param
val searchInfo = SearchInfo().apply {
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
}
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launchSelection(searchInfo)
}
}
SpecialMode.REGISTRATION -> {
// To register info
val registerInfo = intent.getParcelableExtra<RegisterInfo>(KEY_REGISTER_INFO)
val searchInfo = SearchInfo(registerInfo?.searchInfo)
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launchRegistration(searchInfo, registerInfo)
}
}
else -> {
// Not an autofill call
setResult(Activity.RESULT_CANCELED)
finish()
}
}
}
super.onCreate(savedInstanceState)
}
private fun launchSelection(searchInfo: SearchInfo) {
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
if (assistStructure == null) {
setResult(Activity.RESULT_CANCELED)
finish()
} else if (!KeeAutofillService.searchAllowedFor(searchInfo.applicationId,
} else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
PreferencesUtil.applicationIdBlocklist(this))
|| !KeeAutofillService.searchAllowedFor(searchInfo.webDomain,
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
PreferencesUtil.webDomainBlocklist(this))) {
// If item not allowed, show a toast
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
showBlockRestartMessage()
setResult(Activity.RESULT_CANCELED)
finish()
} else {
val database = Database.getInstance()
val readOnly = database.isReadOnly
// If database is open
SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(),
@@ -75,9 +111,10 @@ class AutofillLauncherActivity : AppCompatActivity() {
{
// Show the database UI to select the entry
GroupActivity.launchForAutofillResult(this,
readOnly,
assistStructure,
false,
searchInfo)
searchInfo,
false)
},
{
// If database not open
@@ -87,12 +124,66 @@ class AutofillLauncherActivity : AppCompatActivity() {
}
)
}
}
super.onCreate(savedInstanceState)
private fun launchRegistration(searchInfo: SearchInfo, registerInfo: RegisterInfo?) {
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
PreferencesUtil.applicationIdBlocklist(this))
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
PreferencesUtil.webDomainBlocklist(this))) {
showBlockRestartMessage()
setResult(Activity.RESULT_CANCELED)
} else {
val database = Database.getInstance()
val readOnly = database.isReadOnly
SearchHelper.checkAutoSearchInfo(this,
database,
searchInfo,
{ _ ->
if (!readOnly) {
// Show the database UI to select the entry
GroupActivity.launchForRegistration(this,
registerInfo)
} else {
showReadOnlySaveMessage()
}
},
{
if (!readOnly) {
// Show the database UI to select the entry
GroupActivity.launchForRegistration(this,
registerInfo)
} else {
showReadOnlySaveMessage()
}
},
{
// If database not open
FileDatabaseSelectActivity.launchForRegistration(this,
registerInfo)
}
)
}
finish()
}
private fun showBlockRestartMessage() {
// If item not allowed, show a toast
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
}
private fun showReadOnlySaveMessage() {
Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
if (PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
// Close the database
sendBroadcast(Intent(LOCK_ACTION))
}
super.onActivityResult(requestCode, resultCode, data)
}
@@ -100,18 +191,41 @@ class AutofillLauncherActivity : AppCompatActivity() {
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
fun getAuthIntentSenderForResponse(context: Context,
searchInfo: SearchInfo? = null): IntentSender {
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
fun getAuthIntentSenderForSelection(context: Context,
searchInfo: SearchInfo? = null): IntentSender {
return PendingIntent.getActivity(context, 0,
// Doesn't work with Parcelable (don't know why?)
Intent(context, AutofillLauncherActivity::class.java).apply {
searchInfo?.let {
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
}
},
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
}
fun getAuthIntentSenderForRegistration(context: Context,
registerInfo: RegisterInfo): IntentSender {
return PendingIntent.getActivity(context, 0,
Intent(context, AutofillLauncherActivity::class.java).apply {
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
putExtra(KEY_REGISTER_INFO, registerInfo)
},
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
}
fun launchForRegistration(context: Context,
registerInfo: RegisterInfo) {
val intent = Intent(context, AutofillLauncherActivity::class.java)
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
intent.putExtra(KEY_REGISTER_INFO, registerInfo)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}
}

View File

@@ -40,6 +40,7 @@ import com.google.android.material.appbar.CollapsingToolbarLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
@@ -133,7 +134,7 @@ class EntryActivity : LockingActivity() {
}
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
// Init the clipboard helper
clipboardHelper = ClipboardHelper(this)

View File

@@ -21,6 +21,8 @@ package com.kunzisoft.keepass.activities
import android.app.Activity
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.app.assist.AssistStructure
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
@@ -33,6 +35,7 @@ import android.view.MenuItem
import android.view.View
import android.widget.DatePicker
import android.widget.TimePicker
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
@@ -42,8 +45,11 @@ import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
@@ -64,7 +70,6 @@ import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.closeDatabase
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.showActionError
import com.kunzisoft.keepass.view.updateLockPaddingLeft
@@ -106,6 +111,9 @@ class EntryEditActivity : LockingActivity(),
// Education
private var entryEditActivityEducation: EntryEditActivityEducation? = null
// To ask data lost only one time
private var backPressedAlreadyApproved = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_entry_edit)
@@ -127,7 +135,7 @@ class EntryEditActivity : LockingActivity(),
}
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
@@ -172,6 +180,20 @@ class EntryEditActivity : LockingActivity(),
tempEntryInfo?.username = mDatabase?.defaultUsername ?: ""
}
// Retrieve data from registration
val registerInfo = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)
val searchInfo: SearchInfo? = registerInfo?.searchInfo
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
registerInfo?.username?.let {
tempEntryInfo?.username = it
}
registerInfo?.password?.let {
tempEntryInfo?.password = it
}
searchInfo?.let { tempSearchInfo ->
tempEntryInfo?.saveSearchInfo(mDatabase, tempSearchInfo)
}
// Build fragment to manage entry modification
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
if (entryEditFragment == null) {
@@ -283,8 +305,30 @@ class EntryEditActivity : LockingActivity(),
}
}
if (newNodes.size == 1) {
mEntry = newNodes[0] as Entry?
finish()
(newNodes[0] as? Entry?)?.let { entry ->
mEntry = entry
EntrySelectionHelper.doSpecialAction(intent,
{
// Finish naturally
finishForEntryResult()
},
{
// Nothing when search retrieved
},
{
entryValidatedForSave()
},
{
entryValidatedForKeyboardSelection(entry)
},
{ _, _ ->
entryValidatedForAutofillSelection(entry)
},
{
entryValidatedForAutofillRegistration()
}
)
}
}
}
} catch (e: Exception) {
@@ -296,6 +340,39 @@ class EntryEditActivity : LockingActivity(),
}
}
private fun entryValidatedForSave() {
onValidateSpecialMode()
finishForEntryResult()
}
private fun entryValidatedForKeyboardSelection(entry: Entry) {
// Populate Magikeyboard with entry
mDatabase?.let { database ->
populateKeyboardAndMoveAppToBackground(this,
entry.getEntryInfo(database),
intent)
}
onValidateSpecialMode()
// Don't keep activity history for entry edition
finishForEntryResult()
}
private fun entryValidatedForAutofillSelection(entry: Entry) {
// Build Autofill response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mDatabase?.let { database ->
AutofillHelper.buildResponse(this@EntryEditActivity,
entry.getEntryInfo(database))
}
}
onValidateSpecialMode()
}
private fun entryValidatedForAutofillRegistration() {
onValidateSpecialMode()
finishForEntryResult()
}
override fun onResume() {
super.onResume()
@@ -376,14 +453,35 @@ class EntryEditActivity : LockingActivity(),
EntryCustomFieldDialogFragment.getInstance(field).show(supportFragmentManager, "customFieldDialog")
}
private fun verifyNameField(field: Field,
actionIfNewName: () -> Unit) {
var extraFieldAlreadyContainsName = false
entryEditFragment?.getExtraFields()?.forEach {
if (it.name.equals(field.name, true))
extraFieldAlreadyContainsName = true
}
if (!extraFieldAlreadyContainsName
&& Entry.newExtraFieldNameAllowed(field)) {
actionIfNewName.invoke()
} else {
Log.e(TAG, "Unable to create the new field, field name already exists")
coordinatorLayout?.let {
Snackbar.make(it, R.string.error_field_name_already_exists, Snackbar.LENGTH_LONG).asError().show()
}
}
}
override fun onNewCustomFieldApproved(newField: Field) {
entryEditFragment?.apply {
putExtraField(newField)
verifyNameField(newField) {
entryEditFragment?.putExtraField(newField)
}
}
override fun onEditCustomFieldApproved(oldField: Field, newField: Field) {
entryEditFragment?.replaceExtraField(oldField, newField)
verifyNameField(newField) {
entryEditFragment?.replaceExtraField(oldField, newField)
}
}
override fun onDeleteCustomFieldApproved(oldField: Field) {
@@ -476,10 +574,8 @@ class EntryEditActivity : LockingActivity(),
Entry(mEntry!!)
}?.let { newEntry ->
newEntry.setEntryInfo(mDatabase, newEntryInfo)
// Build info
newEntry.lastAccessTime = DateInstant()
newEntry.lastModificationTime = DateInstant()
newEntry.setEntryInfo(mDatabase, newEntryInfo)
// Delete temp attachment if not used
mTempAttachments.forEach {
@@ -673,16 +769,34 @@ class EntryEditActivity : LockingActivity(),
}
override fun onBackPressed() {
AlertDialog.Builder(this)
.setMessage(R.string.discard_changes)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.discard) { _, _ ->
super@EntryEditActivity.onBackPressed()
}.create().show()
onApprovedBackPressed {
super@EntryEditActivity.onBackPressed()
}
}
override fun finish() {
// Assign entry callback as a result in all case
override fun onCancelSpecialMode() {
onApprovedBackPressed {
super@EntryEditActivity.onCancelSpecialMode()
finish()
}
}
private fun onApprovedBackPressed(approved: () -> Unit) {
if (!backPressedAlreadyApproved) {
AlertDialog.Builder(this)
.setMessage(R.string.discard_changes)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.discard) { _, _ ->
backPressedAlreadyApproved = true
approved.invoke()
}.create().show()
} else {
approved.invoke()
}
}
private fun finishForEntryResult() {
// Assign entry callback as a result
try {
mEntry?.let { entry ->
val bundle = Bundle()
@@ -725,12 +839,13 @@ class EntryEditActivity : LockingActivity(),
* Launch EntryEditActivity to update an existing entry
*
* @param activity from activity
* @param pwEntry Entry to update
* @param entry Entry to update
*/
fun launch(activity: Activity, pwEntry: Entry) {
fun launch(activity: Activity,
entry: Entry) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, pwEntry.nodeId)
intent.putExtra(KEY_ENTRY, entry.nodeId)
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
}
}
@@ -739,14 +854,102 @@ class EntryEditActivity : LockingActivity(),
* Launch EntryEditActivity to add a new entry
*
* @param activity from activity
* @param pwGroup Group who will contains new entry
* @param group Group who will contains new entry
*/
fun launch(activity: Activity, pwGroup: Group) {
fun launch(activity: Activity,
group: Group) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, pwGroup.nodeId)
intent.putExtra(KEY_PARENT, group.nodeId)
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
}
}
fun launchForSave(context: Context,
entry: Entry,
searchInfo: SearchInfo) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, entry.nodeId)
EntrySelectionHelper.startActivityForSaveModeResult(context,
intent,
searchInfo)
}
}
fun launchForSave(context: Context,
group: Group,
searchInfo: SearchInfo) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, group.nodeId)
EntrySelectionHelper.startActivityForSaveModeResult(context,
intent,
searchInfo)
}
}
/**
* Launch EntryEditActivity to add a new entry in keyboard selection
*/
fun launchForKeyboardSelectionResult(context: Context,
group: Group,
searchInfo: SearchInfo? = null) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, group.nodeId)
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(context,
intent,
searchInfo)
}
}
/**
* Launch EntryEditActivity to add a new entry in autofill selection
*/
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity,
assistStructure: AssistStructure,
group: Group,
searchInfo: SearchInfo? = null) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, group.nodeId)
AutofillHelper.startActivityForAutofillResult(activity,
intent,
assistStructure,
searchInfo)
}
}
/**
* Launch EntryEditActivity to register an updated entry (from autofill)
*/
fun launchForRegistration(context: Context,
entry: Entry,
registerInfo: RegisterInfo? = null) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, entry.nodeId)
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
intent,
registerInfo)
}
}
/**
* Launch EntryEditActivity to register a new entry (from autofill)
*/
fun launchForRegistration(context: Context,
group: Group,
registerInfo: RegisterInfo? = null) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, group.nodeId)
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
intent,
registerInfo)
}
}
}
}

View File

@@ -37,6 +37,7 @@ import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment
@@ -148,6 +149,8 @@ class EntryEditFragment: StylishFragment() {
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
taIconColor?.recycle()
rootView?.resetAppTimeoutWhenViewFocusedOrChanged(requireContext())
// Retrieve the new entry after an orientation change
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo

View File

@@ -23,6 +23,7 @@ import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
@@ -31,6 +32,7 @@ import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil
/**
@@ -42,79 +44,137 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
var sharedWebDomain: String? = null
var otpString: String? = null
when (intent?.action) {
Intent.ACTION_SEND -> {
if ("text/plain" == intent.type) {
// Retrieve web domain
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
sharedWebDomain = Uri.parse(it).host
// 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
}
}
else -> {}
}
// Setting to integrate Magikeyboard
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
// Build search param
// Build domain search param
val searchInfo = SearchInfo().apply {
webDomain = sharedWebDomain
this.webDomain = sharedWebDomain
this.otpString = otpString
}
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launch(searchInfo)
}
// If database is open
SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(),
searchInfo,
{ items ->
// Items found
if (searchShareForMagikeyboard) {
if (items.size == 1) {
// Automatically populate keyboard
val entryPopulate = items[0]
populateKeyboardAndMoveAppToBackground(this,
entryPopulate,
intent)
super.onCreate(savedInstanceState)
}
private fun launch(searchInfo: SearchInfo) {
if (!searchInfo.containsOnlyNullValues()) {
// Setting to integrate Magikeyboard
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
// If database is open
val database = Database.getInstance()
val readOnly = database.isReadOnly
SearchHelper.checkAutoSearchInfo(this,
database,
searchInfo,
{ items ->
// Items found
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(this,
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,
readOnly,
searchInfo,
true)
}
} else {
// Select the one we want
GroupActivity.launchForEntrySelectionResult(this,
true,
GroupActivity.launchForSearchResult(this,
readOnly,
searchInfo,
true)
}
},
{
// Show the database UI to select the entry
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(this,
searchInfo,
false)
} else {
Toast.makeText(applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
}
} else if (readOnly || searchShareForMagikeyboard) {
GroupActivity.launchForKeyboardSelectionResult(this,
readOnly,
searchInfo,
false)
} else {
GroupActivity.launchForSaveResult(this,
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)
}
} else {
GroupActivity.launch(this,
true,
searchInfo)
}
},
{
// Show the database UI to select the entry
if (searchShareForMagikeyboard) {
GroupActivity.launchForEntrySelectionResult(this,
false,
searchInfo)
} else {
GroupActivity.launch(this,
false,
searchInfo)
}
},
{
// If database not open
if (searchShareForMagikeyboard) {
FileDatabaseSelectActivity.launchForEntrySelectionResult(this,
searchInfo)
} else {
FileDatabaseSelectActivity.launch(this,
searchInfo)
}
}
)
)
}
finish()
super.onCreate(savedInstanceState)
}
}
@@ -125,6 +185,6 @@ fun populateKeyboardAndMoveAppToBackground(activity: Activity,
// Populate Magikeyboard with entry
MagikIME.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
// Consume the selection mode
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
EntrySelectionHelper.removeModesFromIntent(intent)
activity.moveTaskToBack(true)
}

View File

@@ -19,7 +19,6 @@
*/
package com.kunzisoft.keepass.activities
import android.annotation.SuppressLint
import android.app.Activity
import android.app.assist.AssistStructure
import android.content.Context
@@ -37,7 +36,6 @@ import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
@@ -45,8 +43,8 @@ import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
@@ -54,8 +52,10 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil
@@ -161,7 +161,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
}
// Observe list of databases
databaseFilesViewModel.databaseFilesLoaded.observe(this, Observer { databaseFiles ->
databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles ->
when (databaseFiles.databaseFileAction) {
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
@@ -170,7 +170,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
}
GroupActivity.launch(this@FileDatabaseSelectActivity)
GroupActivity.launch(this@FileDatabaseSelectActivity,
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
}
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
@@ -184,13 +185,13 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
}
}
databaseFilesViewModel.consumeAction()
})
}
// Observe default database
databaseFilesViewModel.defaultDatabase.observe(this, Observer {
databaseFilesViewModel.defaultDatabase.observe(this) {
// Retrieve settings for default database
mAdapterDatabaseHistory?.setDefaultDatabase(it)
})
}
// Attach the dialog thread to this activity
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
@@ -202,6 +203,24 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
databaseFilesViewModel.addDatabaseFile(databaseUri, keyFileUri)
}
}
ACTION_DATABASE_LOAD_TASK -> {
val database = Database.getInstance()
if (result.isSuccess
&& database.loaded) {
launchGroupActivity(database)
} else {
var resultError = ""
val resultMessage = result.message
// Show error message
if (resultMessage != null && resultMessage.isNotEmpty()) {
resultError = "$resultError $resultMessage"
}
Log.e(TAG, resultError)
Snackbar.make(activity_file_selection_coordinator_layout,
resultError,
Snackbar.LENGTH_LONG).asError().show()
}
}
}
}
}
@@ -217,78 +236,39 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
private fun fileNoFoundAction(e: FileNotFoundException) {
val error = getString(R.string.file_not_found_content)
Log.e(TAG, error, e)
coordinatorLayout?.let {
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
}
Log.e(TAG, error, e)
}
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
EntrySelectionHelper.doEntrySelectionAction(intent,
{
try {
PasswordActivity.launch(this@FileDatabaseSelectActivity,
databaseUri, keyFile,
searchInfo)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
// Remove the search info from intent
if (searchInfo != null) {
finish()
}
PasswordActivity.launch(this,
databaseUri,
keyFile,
{ exception ->
fileNoFoundAction(exception)
},
{
try {
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
databaseUri, keyFile,
searchInfo)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
finish()
},
{ assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
databaseUri, keyFile,
assistStructure,
searchInfo)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
}
})
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() })
}
private fun launchGroupActivity(readOnly: Boolean) {
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
EntrySelectionHelper.doEntrySelectionAction(intent,
{
GroupActivity.launch(this@FileDatabaseSelectActivity,
false,
searchInfo,
readOnly)
},
{
GroupActivity.launchForEntrySelectionResult(this@FileDatabaseSelectActivity,
false,
searchInfo,
readOnly)
// Do not keep history
finish()
},
{ assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
assistStructure,
false,
searchInfo,
readOnly)
}
})
private fun launchGroupActivity(database: Database) {
GroupActivity.launch(this,
database.isReadOnly,
{ onValidateSpecialMode() },
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() })
}
override fun onValidateSpecialMode() {
super.onValidateSpecialMode()
finish()
}
override fun onCancelSpecialMode() {
super.onCancelSpecialMode()
finish()
}
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
@@ -302,22 +282,25 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
super.onResume()
// Show open and create button or special mode
if (mSelectionMode) {
// Disable create button if in selection mode or request for autofill
createDatabaseButtonView?.visibility = View.GONE
} else {
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
// There is an activity which can handle this intent.
createDatabaseButtonView?.visibility = View.VISIBLE
} else{
// No Activity found that can handle this intent.
when (mSpecialMode) {
SpecialMode.DEFAULT -> {
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
// There is an activity which can handle this intent.
createDatabaseButtonView?.visibility = View.VISIBLE
} else{
// No Activity found that can handle this intent.
createDatabaseButtonView?.visibility = View.GONE
}
}
else -> {
// Disable create button if in selection mode or request for autofill
createDatabaseButtonView?.visibility = View.GONE
}
}
val database = Database.getInstance()
if (database.loaded) {
launchGroupActivity(database.isReadOnly)
launchGroupActivity(database)
} else {
// Construct adapter with listeners
if (PreferencesUtil.showRecentFiles(this)) {
@@ -408,7 +391,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
if (!mSelectionMode) {
if (mSpecialMode == SpecialMode.DEFAULT) {
MenuUtil.defaultMenuInflater(menuInflater, menu)
}
@@ -451,8 +434,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
when (item.itemId) {
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
}
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
return super.onOptionsItemSelected(item)
}
companion object {
@@ -463,17 +446,38 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
/*
* -------------------------
* Launch only to standard search, else pass by PasswordActivity
* Standard Launch
* -------------------------
*/
fun launch(context: Context,
searchInfo: SearchInfo? = null) {
val intent = Intent(context, FileDatabaseSelectActivity::class.java)
searchInfo?.let {
intent.putExtra(KEY_SEARCH_INFO, it)
}
context.startActivity(intent)
fun launch(context: Context) {
context.startActivity(Intent(context, FileDatabaseSelectActivity::class.java))
}
/*
* -------------------------
* Search Launch
* -------------------------
*/
fun launchForSearchResult(context: Context,
searchInfo: SearchInfo) {
EntrySelectionHelper.startActivityForSearchModeResult(context,
Intent(context, FileDatabaseSelectActivity::class.java),
searchInfo)
}
/*
* -------------------------
* Save Launch
* -------------------------
*/
fun launchForSaveResult(context: Context,
searchInfo: SearchInfo) {
EntrySelectionHelper.startActivityForSaveModeResult(context,
Intent(context, FileDatabaseSelectActivity::class.java),
searchInfo)
}
/*
@@ -482,9 +486,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
* -------------------------
*/
fun launchForEntrySelectionResult(activity: Activity,
searchInfo: SearchInfo? = null) {
EntrySelectionHelper.startActivityForEntrySelectionResult(activity,
fun launchForKeyboardSelectionResult(activity: Activity,
searchInfo: SearchInfo? = null) {
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
searchInfo)
}
@@ -504,5 +508,17 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
assistStructure,
searchInfo)
}
/*
* -------------------------
* Registration Launch
* -------------------------
*/
fun launchForRegistration(context: Context,
registerInfo: RegisterInfo? = null) {
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
Intent(context, FileDatabaseSelectActivity::class.java),
registerInfo)
}
}
}

View File

@@ -36,6 +36,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
@@ -44,14 +45,12 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.FragmentManager
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.Database
@@ -62,14 +61,16 @@ import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.model.getSearchString
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY
@@ -153,7 +154,7 @@ class GroupActivity : LockingActivity(),
taTextColor.recycle()
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(rootContainerView)
rootContainerView?.resetAppTimeoutWhenViewFocusedOrChanged(this)
// Retrieve elements after an orientation change
if (savedInstanceState != null) {
@@ -203,16 +204,48 @@ class GroupActivity : LockingActivity(),
}
// Add listeners to the add buttons
addNodeButtonView?.setAddGroupClickListener(View.OnClickListener {
addNodeButtonView?.setAddGroupClickListener {
GroupEditDialogFragment.build()
.show(supportFragmentManager,
GroupEditDialogFragment.TAG_CREATE_GROUP)
})
addNodeButtonView?.setAddEntryClickListener(View.OnClickListener {
}
addNodeButtonView?.setAddEntryClickListener {
mCurrentGroup?.let { currentGroup ->
EntryEditActivity.launch(this@GroupActivity, currentGroup)
EntrySelectionHelper.doSpecialAction(intent,
{
EntryEditActivity.launch(this@GroupActivity, currentGroup)
},
{
// Search not used
},
{ searchInfo ->
EntryEditActivity.launchForSave(this@GroupActivity,
currentGroup, searchInfo)
onLaunchActivitySpecialMode()
},
{ searchInfo ->
EntryEditActivity.launchForKeyboardSelectionResult(this@GroupActivity,
currentGroup, searchInfo)
onLaunchActivitySpecialMode()
},
{ searchInfo, assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
EntryEditActivity.launchForAutofillResult(this@GroupActivity,
assistStructure,
currentGroup, searchInfo)
onLaunchActivitySpecialMode()
} else {
onCancelSpecialMode()
}
},
{ searchInfo ->
EntryEditActivity.launchForRegistration(this@GroupActivity,
currentGroup, searchInfo)
onLaunchActivitySpecialMode()
}
)
}
})
}
mDatabase?.let { database ->
// Search suggestion
@@ -233,6 +266,41 @@ class GroupActivity : LockingActivity(),
refreshSearchGroup()
when (actionTask) {
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
if (result.isSuccess) {
mListNodesFragment?.updateNodes(oldNodes, newNodes)
EntrySelectionHelper.doSpecialAction(intent,
{
// Standard not used after task
},
{
// Search not used
},
{
// Save not used
},
{
try {
val entry = newNodes[0] as Entry
entrySelectedForKeyboardSelection(entry)
} catch (e: Exception) {
Log.e(TAG, "Unable to perform action for keyboard selection after entry update", e)
}
},
{ _, _ ->
try {
val entry = newNodes[0] as Entry
entrySelectedForAutofillSelection(entry)
} catch (e: Exception) {
Log.e(TAG, "Unable to perform action for autofill selection after entry update", e)
}
},
{
// Not use
}
)
}
}
ACTION_DATABASE_UPDATE_GROUP_TASK -> {
if (result.isSuccess) {
mListNodesFragment?.updateNodes(oldNodes, newNodes)
@@ -306,11 +374,11 @@ class GroupActivity : LockingActivity(),
*/
private fun manageSearchInfoIntent(intent: Intent): Boolean {
// To relaunch the activity as ACTION_SEARCH
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
val autoSearch = intent.getBooleanExtra(AUTO_SEARCH_KEY, false)
if (searchInfo != null && autoSearch) {
intent.action = Intent.ACTION_SEARCH
intent.putExtra(SearchManager.QUERY, searchInfo.getSearchString(this))
intent.putExtra(SearchManager.QUERY, searchInfo.toString())
return true
}
return false
@@ -355,7 +423,7 @@ class GroupActivity : LockingActivity(),
fragmentTransaction.addToBackStack(fragmentTag)
fragmentTransaction.commit()
if (mSelectionMode)
if (mSpecialMode != SpecialMode.DEFAULT)
mSelectionModeCountBackStack++
// Update last access time.
@@ -483,20 +551,6 @@ class GroupActivity : LockingActivity(),
}
}
override fun onCancelSpecialMode() {
// To remove the navigation history and
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
val fragmentManager = supportFragmentManager
if (mSelectionModeCountBackStack > 0) {
for (selectionMode in 0 .. mSelectionModeCountBackStack) {
fragmentManager.popBackStack()
}
}
// Reinit the counter for navigation history
mSelectionModeCountBackStack = 0
backToTheAppCaller()
}
private fun refreshNumberOfChildren() {
numberChildrenView?.apply {
if (PreferencesUtil.showNumberEntries(context)) {
@@ -524,28 +578,58 @@ class GroupActivity : LockingActivity(),
Type.ENTRY -> try {
val entryVersioned = node as Entry
EntrySelectionHelper.doEntrySelectionAction(intent,
EntrySelectionHelper.doSpecialAction(intent,
{
EntryActivity.launch(this@GroupActivity, entryVersioned, mReadOnly)
},
{
rebuildListNodes()
// Populate Magikeyboard with entry
mDatabase?.let { database ->
populateKeyboardAndMoveAppToBackground(this@GroupActivity,
entryVersioned.getEntryInfo(database),
intent)
// Nothing here, a search is simply performed
},
{ searchInfo ->
if (!mReadOnly)
entrySelectedForSave(entryVersioned, searchInfo)
else
finish()
},
{ searchInfo ->
// Recheck search, only to fix #783 because workflow allows to open multiple search elements
SearchHelper.checkAutoSearchInfo(this,
mDatabase!!,
searchInfo,
{ _ ->
// Item in search, don't save
entrySelectedForKeyboardSelection(entryVersioned)
},
{
// Item not found, save it if required
if (!mReadOnly
&& searchInfo != null
&& PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity)) {
updateEntryWithSearchInfo(entryVersioned, searchInfo)
} else {
entrySelectedForKeyboardSelection(entryVersioned)
}
},
{
// Normally not append
finish()
}
)
},
{ searchInfo, _ ->
if (!mReadOnly
&& searchInfo != null
&& PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)) {
updateEntryWithSearchInfo(entryVersioned, searchInfo)
} else {
entrySelectedForAutofillSelection(entryVersioned)
}
},
{
// Build response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
mDatabase?.let { database ->
AutofillHelper.buildResponse(this@GroupActivity,
entryVersioned.getEntryInfo(database))
}
}
finish()
{ registerInfo ->
if (!mReadOnly)
entrySelectedForRegistration(entryVersioned, registerInfo)
else
finish()
})
} catch (e: ClassCastException) {
Log.e(TAG, "Node can't be cast in Entry")
@@ -553,6 +637,57 @@ class GroupActivity : LockingActivity(),
}
}
private fun entrySelectedForSave(entry: Entry, searchInfo: SearchInfo) {
rebuildListNodes()
// Save to update the entry
EntryEditActivity.launchForSave(this@GroupActivity,
entry, searchInfo)
onLaunchActivitySpecialMode()
}
private fun entrySelectedForKeyboardSelection(entry: Entry) {
rebuildListNodes()
// Populate Magikeyboard with entry
mDatabase?.let { database ->
populateKeyboardAndMoveAppToBackground(this,
entry.getEntryInfo(database),
intent)
}
onValidateSpecialMode()
}
private fun entrySelectedForAutofillSelection(entry: Entry) {
// Build response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
mDatabase?.let { database ->
AutofillHelper.buildResponse(this,
entry.getEntryInfo(database))
}
}
onValidateSpecialMode()
}
private fun entrySelectedForRegistration(entry: Entry, registerInfo: RegisterInfo?) {
rebuildListNodes()
// Registration to update the entry
EntryEditActivity.launchForRegistration(this@GroupActivity,
entry, registerInfo)
onLaunchActivitySpecialMode()
}
private fun updateEntryWithSearchInfo(entry: Entry, searchInfo: SearchInfo) {
val newEntry = Entry(entry)
newEntry.setEntryInfo(mDatabase, newEntry.getEntryInfo(mDatabase, true).apply {
saveSearchInfo(mDatabase, searchInfo)
})
// In selection mode, it's forced read-only, so update not allowed
mProgressDatabaseTaskProvider?.startDatabaseUpdateEntry(
entry,
newEntry,
!mReadOnly && mAutoSaveEnable
)
}
private fun finishNodeAction() {
actionNodeMode?.finish()
}
@@ -662,7 +797,7 @@ class GroupActivity : LockingActivity(),
return true
}
override fun onDeleteMenuClick(nodes: List<Node>): Boolean {
private fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false): Boolean {
val database = mDatabase
// If recycle bin enabled, ensure it exists
@@ -682,13 +817,22 @@ class GroupActivity : LockingActivity(),
}
// else open the dialog to confirm deletion
else {
DeleteNodesDialogFragment.getInstance(nodes)
.show(supportFragmentManager, "deleteNodesDialogFragment")
val deleteNodesDialogFragment: DeleteNodesDialogFragment =
if (recycleBin) {
EmptyRecycleBinDialogFragment.getInstance(nodes)
} else {
DeleteNodesDialogFragment.getInstance(nodes)
}
deleteNodesDialogFragment.show(supportFragmentManager, "deleteNodesDialogFragment")
}
finishNodeAction()
return true
}
override fun onDeleteMenuClick(nodes: List<Node>): Boolean {
return deleteNodes(nodes)
}
override fun permanentlyDeleteNodes(nodes: List<Node>) {
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
nodes,
@@ -727,7 +871,7 @@ class GroupActivity : LockingActivity(),
if (mReadOnly) {
menu.findItem(R.id.menu_save_database)?.isVisible = false
}
if (!mSelectionMode) {
if (mSpecialMode == SpecialMode.DEFAULT) {
MenuUtil.defaultMenuInflater(inflater, menu)
}
@@ -857,7 +1001,7 @@ class GroupActivity : LockingActivity(),
R.id.menu_empty_recycle_bin -> {
mCurrentGroup?.getChildren()?.let { listChildren ->
// Automatically delete all elements
onDeleteMenuClick(listChildren)
deleteNodes(listChildren, true)
}
return true
}
@@ -993,41 +1137,51 @@ class GroupActivity : LockingActivity(),
assignGroupViewElements()
}
private fun backToTheAppCaller() {
if (mAutofillSelection) {
// To get the app caller, only for autofill
super.onBackPressed()
} else {
// To move the app in background
moveTaskToBack(true)
}
}
override fun onBackPressed() {
if (mListNodesFragment?.nodeActionSelectionMode == true) {
finishNodeAction()
} else {
// Normal way when we are not in root
if (mRootGroup != null && mRootGroup != mCurrentGroup) {
super.onBackPressed()
super.onRegularBackPressed()
rebuildListNodes()
}
// Else in root, lock if needed
else {
intent.removeExtra(AUTO_SEARCH_KEY)
intent.removeExtra(KEY_SEARCH_INFO)
EntrySelectionHelper.removeModesFromIntent(intent)
EntrySelectionHelper.removeInfoFromIntent(intent)
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
lockAndExit()
super.onBackPressed()
super.onRegularBackPressed()
} else {
// To restore standard mode
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
backToTheAppCaller()
}
}
}
}
private fun removeFragmentHistory() {
val fragmentManager = supportFragmentManager
if (mSelectionModeCountBackStack > 0) {
for (selectionMode in 0 .. mSelectionModeCountBackStack) {
fragmentManager.popBackStack()
}
}
// Reinit the counter for navigation history
mSelectionModeCountBackStack = 0
}
override fun onValidateSpecialMode() {
removeFragmentHistory()
super.onValidateSpecialMode()
}
override fun onCancelSpecialMode() {
removeFragmentHistory()
super.onCancelSpecialMode()
}
companion object {
private val TAG = GroupActivity::class.java.name
@@ -1075,30 +1229,62 @@ class GroupActivity : LockingActivity(),
* -------------------------
*/
fun launch(context: Context,
autoSearch: Boolean = false,
searchInfo: SearchInfo? = null,
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
readOnly: Boolean,
autoSearch: Boolean = false) {
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
searchInfo?.let {
intent.putExtra(KEY_SEARCH_INFO, it)
}
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
context.startActivity(intent)
}
}
/*
* -------------------------
* Search Launch
* -------------------------
*/
fun launchForSearchResult(context: Context,
readOnly: Boolean,
searchInfo: SearchInfo,
autoSearch: Boolean = false) {
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
EntrySelectionHelper.addSearchInfoInIntent(
intent,
searchInfo)
context.startActivity(intent)
}
}
/*
* -------------------------
* Search save Launch
* -------------------------
*/
fun launchForSaveResult(context: Context,
searchInfo: SearchInfo,
autoSearch: Boolean = false) {
checkTimeAndBuildIntent(context, null, false) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
EntrySelectionHelper.startActivityForSaveModeResult(context,
intent,
searchInfo)
}
}
/*
* -------------------------
* Keyboard Launch
* -------------------------
*/
fun launchForEntrySelectionResult(context: Context,
autoSearch: Boolean = false,
searchInfo: SearchInfo? = null,
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
fun launchForKeyboardSelectionResult(context: Context,
readOnly: Boolean,
searchInfo: SearchInfo? = null,
autoSearch: Boolean = false) {
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
EntrySelectionHelper.startActivityForEntrySelectionResult(context, intent, searchInfo)
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(context,
intent,
searchInfo)
}
}
@@ -1109,14 +1295,189 @@ class GroupActivity : LockingActivity(),
*/
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity,
readOnly: Boolean,
assistStructure: AssistStructure,
autoSearch: Boolean = false,
searchInfo: SearchInfo? = null,
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) {
autoSearch: Boolean = false) {
checkTimeAndBuildIntent(activity, null, readOnly) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure, searchInfo)
AutofillHelper.startActivityForAutofillResult(activity,
intent,
assistStructure,
searchInfo)
}
}
/*
* -------------------------
* Registration Launch
* -------------------------
*/
fun launchForRegistration(context: Context,
registerInfo: RegisterInfo? = null) {
checkTimeAndBuildIntent(context, null, false) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, false)
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
intent,
registerInfo)
}
}
/*
* -------------------------
* Global Launch
* -------------------------
*/
fun launch(activity: Activity,
readOnly: Boolean,
onValidateSpecialMode: () -> Unit,
onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit) {
EntrySelectionHelper.doSpecialAction(activity.intent,
{
GroupActivity.launch(activity,
readOnly,
true)
},
{ searchInfo ->
SearchHelper.checkAutoSearchInfo(activity,
Database.getInstance(),
searchInfo,
{ _ ->
// Response is build
GroupActivity.launchForSearchResult(activity,
readOnly,
searchInfo,
true)
onLaunchActivitySpecialMode()
},
{
// Here no search info found
if (readOnly) {
GroupActivity.launchForSearchResult(activity,
readOnly,
searchInfo,
false)
} else {
GroupActivity.launchForSaveResult(activity,
searchInfo,
false)
}
onLaunchActivitySpecialMode()
},
{
// Simply close if database not opened, normally not happened
onCancelSpecialMode()
}
)
},
{ searchInfo ->
// Save info used with OTP
if (!readOnly) {
GroupActivity.launchForSaveResult(activity,
searchInfo,
false)
onLaunchActivitySpecialMode()
} else {
Toast.makeText(activity.applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
onCancelSpecialMode()
}
},
{ searchInfo ->
SearchHelper.checkAutoSearchInfo(activity,
Database.getInstance(),
searchInfo,
{ items ->
// Response is build
if (items.size == 1) {
populateKeyboardAndMoveAppToBackground(activity,
items[0],
activity.intent)
onValidateSpecialMode()
} else {
// Select the one we want
GroupActivity.launchForKeyboardSelectionResult(activity,
readOnly,
searchInfo,
true)
onLaunchActivitySpecialMode()
}
},
{
// Here no search info found, disable auto search
GroupActivity.launchForKeyboardSelectionResult(activity,
readOnly,
searchInfo,
false)
onLaunchActivitySpecialMode()
},
{
// Simply close if database not opened, normally not happened
onCancelSpecialMode()
}
)
},
{ searchInfo, assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
SearchHelper.checkAutoSearchInfo(activity,
Database.getInstance(),
searchInfo,
{ items ->
// Response is build
AutofillHelper.buildResponse(activity, items)
onValidateSpecialMode()
},
{
// Here no search info found, disable auto search
GroupActivity.launchForAutofillResult(activity,
readOnly,
assistStructure,
searchInfo,
false)
onLaunchActivitySpecialMode()
},
{
// Simply close if database not opened, normally not happened
onCancelSpecialMode()
}
)
} else {
onCancelSpecialMode()
}
},
{ registerInfo ->
if (!readOnly) {
SearchHelper.checkAutoSearchInfo(activity,
Database.getInstance(),
registerInfo?.searchInfo,
{ _ ->
// No auto search, it's a registration
GroupActivity.launchForRegistration(activity,
registerInfo)
onLaunchActivitySpecialMode()
},
{
// Here no search info found, disable auto search
GroupActivity.launchForRegistration(activity,
registerInfo)
onLaunchActivitySpecialMode()
},
{
// Simply close if database not opened, normally not happened
onCancelSpecialMode()
}
)
} else {
Toast.makeText(activity.applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
onCancelSpecialMode()
}
})
}
}
}

View File

@@ -43,6 +43,7 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.node.Type
import java.util.*
@@ -69,10 +70,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
private var readOnly: Boolean = false
get() {
return field || selectionMode
}
private var selectionMode: Boolean = false
private var specialMode: SpecialMode = SpecialMode.DEFAULT
val isEmpty: Boolean
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
@@ -195,7 +193,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
super.onResume()
activity?.intent?.let {
selectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(it)
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
}
// Refresh data

View File

@@ -30,16 +30,22 @@ import com.kunzisoft.keepass.database.search.SearchHelper
class MagikeyboardLauncherActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val database = Database.getInstance()
val readOnly = database.isReadOnly
SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(),
database,
null,
{},
{
GroupActivity.launchForEntrySelectionResult(this)
// Not called
// if items found directly returns before calling this activity
},
{
// Select if not found
GroupActivity.launchForKeyboardSelectionResult(this, readOnly)
},
{
// Pass extra to get entry
FileDatabaseSelectActivity.launchForEntrySelectionResult(this)
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
}
)
finish()

View File

@@ -37,26 +37,26 @@ import android.widget.*
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar
import androidx.biometric.BiometricManager
import androidx.core.app.ActivityCompat
import androidx.lifecycle.Observer
import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
@@ -68,15 +68,13 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.closeDatabase
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import kotlinx.android.synthetic.main.activity_password.*
import java.io.FileNotFoundException
open class PasswordActivity : SpecialModeActivity() {
open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
// Views
private var toolbar: Toolbar? = null
@@ -86,12 +84,12 @@ open class PasswordActivity : SpecialModeActivity() {
private var confirmButtonView: Button? = null
private var checkboxPasswordView: CompoundButton? = null
private var checkboxKeyFileView: CompoundButton? = null
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
private var infoContainerView: ViewGroup? = null
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
private var mDefaultDatabase: Boolean = false
private var mDatabaseFileUri: Uri? = null
private var mDatabaseKeyFileUri: Uri? = null
@@ -113,7 +111,6 @@ open class PasswordActivity : SpecialModeActivity() {
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
private var mAllowAutoOpenBiometricPrompt: Boolean = true
override fun onCreate(savedInstanceState: Bundle?) {
@@ -133,7 +130,6 @@ open class PasswordActivity : SpecialModeActivity() {
keyFileSelectionView = findViewById(R.id.keyfile_selection)
checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
advancedUnlockInfoView = findViewById(R.id.biometric_info)
infoContainerView = findViewById(R.id.activity_password_info_container)
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
@@ -160,10 +156,6 @@ open class PasswordActivity : SpecialModeActivity() {
}
})
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
enableOrNotTheConfirmationButton()
}
// If is a view intent
getUriFromIntent(intent)
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
@@ -173,8 +165,31 @@ open class PasswordActivity : SpecialModeActivity() {
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
}
// Init Biometric elements
advancedUnlockFragment = supportFragmentManager
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
if (advancedUnlockFragment == null) {
advancedUnlockFragment = AdvancedUnlockFragment()
supportFragmentManager.commit {
replace(R.id.fragment_advanced_unlock_container_view,
advancedUnlockFragment!!,
UNLOCK_FRAGMENT_TAG)
}
}
// Listen password checkbox to init advanced unlock and confirmation button
checkboxPasswordView?.setOnCheckedChangeListener { _, _ ->
advancedUnlockFragment?.checkUnlockAvailability()
enableOrNotTheConfirmationButton()
}
// Observe if default database
databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
mDefaultDatabase = isDefaultDatabase
}
// Observe database file change
databaseFileViewModel.databaseFileLoaded.observe(this, Observer { databaseFile ->
databaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
// Force read only if the file does not exists
mForceReadOnly = databaseFile?.let {
!it.databaseFileExists
@@ -194,19 +209,14 @@ open class PasswordActivity : SpecialModeActivity() {
filenameView?.text = databaseFile?.databaseAlias ?: ""
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
})
}
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
onActionFinish = { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> {
// Recheck biometric if error
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) {
// Stay with the same mode and init it
advancedUnlockedManager?.initBiometricMode()
}
}
// Recheck advanced unlock if error
advancedUnlockFragment?.initAdvancedUnlockMode()
if (result.isSuccess) {
mDatabaseKeyFileUri = null
@@ -220,32 +230,40 @@ open class PasswordActivity : SpecialModeActivity() {
if (resultException != null) {
resultError = resultException.getLocalizedMessage(resources)
// Relaunch loading if we need to fix UUID
if (resultException is DuplicateUuidDatabaseException) {
showLoadDatabaseDuplicateUuidMessage {
when (resultException) {
is DuplicateUuidDatabaseException -> {
// Relaunch loading if we need to fix UUID
showLoadDatabaseDuplicateUuidMessage {
var databaseUri: Uri? = null
var masterPassword: String? = null
var keyFileUri: Uri? = null
var readOnly = true
var cipherEntity: CipherDatabaseEntity? = null
var databaseUri: Uri? = null
var masterPassword: String? = null
var keyFileUri: Uri? = null
var readOnly = true
var cipherEntity: CipherDatabaseEntity? = null
result.data?.let { resultData ->
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
keyFileUri = resultData.getParcelable(KEY_FILE_URI_KEY)
readOnly = resultData.getBoolean(READ_ONLY_KEY)
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
result.data?.let { resultData ->
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
keyFileUri = resultData.getParcelable(KEY_FILE_URI_KEY)
readOnly = resultData.getBoolean(READ_ONLY_KEY)
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
}
databaseUri?.let { databaseFileUri ->
showProgressDialogAndLoadDatabase(
databaseFileUri,
masterPassword,
keyFileUri,
readOnly,
cipherEntity,
true)
}
}
databaseUri?.let { databaseFileUri ->
showProgressDialogAndLoadDatabase(
databaseFileUri,
masterPassword,
keyFileUri,
readOnly,
cipherEntity,
true)
}
is FileNotFoundDatabaseException -> {
// Remove this default database inaccessible
if (mDefaultDatabase) {
databaseFileViewModel.removeDefaultDatabase()
}
}
}
@@ -277,6 +295,9 @@ open class PasswordActivity : SpecialModeActivity() {
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
}
mDatabaseFileUri?.let {
databaseFileViewModel.checkIfIsDefaultDatabase(it)
}
}
override fun onNewIntent(intent: Intent?) {
@@ -285,74 +306,49 @@ open class PasswordActivity : SpecialModeActivity() {
}
private fun launchGroupActivity() {
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
EntrySelectionHelper.doEntrySelectionAction(intent,
{
GroupActivity.launch(this@PasswordActivity,
true,
searchInfo,
readOnly)
// Finish activity if no search info
if (searchInfo != null) {
finish()
}
},
{
SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(),
searchInfo,
{ items ->
// Response is build
if (items.size == 1) {
populateKeyboardAndMoveAppToBackground(this@PasswordActivity,
items[0],
intent)
} else {
// Select the one we want
GroupActivity.launchForEntrySelectionResult(this,
true,
searchInfo)
}
},
{
// Here no search info found, disable auto search
GroupActivity.launchForEntrySelectionResult(this@PasswordActivity,
false,
searchInfo,
readOnly)
},
{
// Simply close if database not opened, normally not happened
}
)
// Do not keep history
finish()
},
{ assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(),
searchInfo,
{ items ->
// Response is build
AutofillHelper.buildResponse(this, items)
finish()
},
{
// Here no search info found, disable auto search
GroupActivity.launchForAutofillResult(this@PasswordActivity,
assistStructure,
false,
searchInfo,
readOnly)
},
{
// Simply close if database not opened, normally not happened
finish()
}
)
}
})
GroupActivity.launch(this,
readOnly,
{ onValidateSpecialMode() },
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() }
)
}
override fun onValidateSpecialMode() {
super.onValidateSpecialMode()
finish()
}
override fun onCancelSpecialMode() {
super.onCancelSpecialMode()
finish()
}
override fun retrieveCredentialForEncryption(): String {
return passwordView?.text?.toString() ?: ""
}
override fun conditionToStoreCredential(): Boolean {
return checkboxPasswordView?.isChecked == true
}
override fun onCredentialEncrypted(databaseUri: Uri,
encryptedCredential: String,
ivSpec: String) {
// Load the database if password is registered with biometric
verifyCheckboxesAndLoadDatabase(
CipherDatabaseEntity(
databaseUri.toString(),
encryptedCredential,
ivSpec)
)
}
override fun onCredentialDecrypted(databaseUri: Uri,
decryptedCredential: String) {
// Load the database if password is retrieve from biometric
// Retrieve from biometric
verifyKeyFileCheckboxAndLoadDatabase(decryptedCredential)
}
private val onEditorActionListener = object : TextView.OnEditorActionListener {
@@ -391,7 +387,6 @@ open class PasswordActivity : SpecialModeActivity() {
false
else
mAllowAutoOpenBiometricPrompt
mDatabaseFileUri?.let { databaseFileUri ->
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
}
@@ -422,47 +417,9 @@ open class PasswordActivity : SpecialModeActivity() {
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
} else {
// Init Biometric elements
var biometricInitialize = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
if (advancedUnlockedManager == null && databaseFileUri != null) {
advancedUnlockedManager = AdvancedUnlockedManager(this,
databaseFileUri,
advancedUnlockInfoView,
checkboxPasswordView,
enableButtonOnCheckedChangeListener,
passwordView,
{ passwordEncrypted, ivSpec ->
// Load the database if password is registered with biometric
if (passwordEncrypted != null && ivSpec != null) {
verifyCheckboxesAndLoadDatabase(
CipherDatabaseEntity(
databaseFileUri.toString(),
passwordEncrypted,
ivSpec)
)
}
},
{ passwordDecrypted ->
// Load the database if password is retrieve from biometric
passwordDecrypted?.let {
// Retrieve from biometric
verifyKeyFileCheckboxAndLoadDatabase(it)
}
})
}
advancedUnlockedManager?.isBiometricPromptAutoOpenEnable = mAllowAutoOpenBiometricPrompt
advancedUnlockedManager?.checkBiometricAvailability()
biometricInitialize = true
} else {
advancedUnlockedManager?.destroy()
advancedUnlockInfoView?.visibility = View.GONE
}
}
if (!biometricInitialize) {
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
}
checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
advancedUnlockFragment?.loadDatabase(databaseFileUri,
mAllowAutoOpenBiometricPrompt
&& mProgressDatabaseTaskProvider?.isBinded() != true)
}
enableOrNotTheConfirmationButton()
@@ -514,11 +471,6 @@ open class PasswordActivity : SpecialModeActivity() {
override fun onPause() {
mProgressDatabaseTaskProvider?.unregisterProgressTask()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
advancedUnlockedManager?.destroy()
advancedUnlockedManager = null
}
// Reinit locking activity UI variable
LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
mAllowAutoOpenBiometricPrompt = true
@@ -569,15 +521,25 @@ open class PasswordActivity : SpecialModeActivity() {
clearCredentialsViews()
}
databaseFileUri?.let { databaseUri ->
// Show the progress dialog and load the database
showProgressDialogAndLoadDatabase(
databaseUri,
password,
keyFileUri,
readOnly,
cipherDatabaseEntity,
false)
if (readOnly && (
mSpecialMode == SpecialMode.SAVE
|| mSpecialMode == SpecialMode.REGISTRATION)
) {
Log.e(TAG, getString(R.string.autofill_read_only_save))
Snackbar.make(activity_password_coordinator_layout,
R.string.autofill_read_only_save,
Snackbar.LENGTH_LONG).asError().show()
} else {
databaseFileUri?.let { databaseUri ->
// Show the progress dialog and load the database
showProgressDialogAndLoadDatabase(
databaseUri,
password,
keyFileUri,
readOnly,
cipherDatabaseEntity,
false)
}
}
}
@@ -607,21 +569,16 @@ open class PasswordActivity : SpecialModeActivity() {
val inflater = menuInflater
// Read menu
inflater.inflate(R.menu.open_file, menu)
if (mSelectionMode || mForceReadOnly) {
if (mForceReadOnly) {
menu.removeItem(R.id.menu_open_file_read_mode_key)
} else {
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
}
if (!mSelectionMode) {
if (mSpecialMode == SpecialMode.DEFAULT) {
MenuUtil.defaultMenuInflater(inflater, menu)
}
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// biometric menu
advancedUnlockedManager?.inflateOptionsMenu(inflater, menu)
}
super.onCreateOptionsMenu(menu)
launchEducation(menu)
@@ -686,27 +643,25 @@ open class PasswordActivity : SpecialModeActivity() {
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
{
onOptionsItemSelected(menu.findItem(R.id.menu_open_file_read_mode_key))
try {
menu.findItem(R.id.menu_open_file_read_mode_key)
} catch (e: Exception) {
Log.e(TAG, "Unable to find read mode menu")
}
performedNextEducation(passwordActivityEducation, menu)
},
{
performedNextEducation(passwordActivityEducation, menu)
})
if (!readOnlyEducationPerformed) {
val biometricCanAuthenticate = BiometricManager.from(this).canAuthenticate()
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& PreferencesUtil.isBiometricUnlockEnable(applicationContext)
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.unlockIconImageView != null
&& passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!,
{
performedNextEducation(passwordActivityEducation, menu)
},
{
performedNextEducation(passwordActivityEducation, menu)
})
}
advancedUnlockFragment?.performEducation(passwordActivityEducation,
readOnlyEducationPerformed,
{
performedNextEducation(passwordActivityEducation, menu)
},
{
performedNextEducation(passwordActivityEducation, menu)
})
}
}
@@ -728,10 +683,7 @@ open class PasswordActivity : SpecialModeActivity() {
readOnly = !readOnly
changeOpenFileReadIcon(item)
}
R.id.menu_biometric_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
advancedUnlockedManager?.deleteEntryKey()
}
else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
}
return super.onOptionsItemSelected(item)
@@ -745,6 +697,9 @@ open class PasswordActivity : SpecialModeActivity() {
mAllowAutoOpenBiometricPrompt = false
// To get device credential unlock result
advancedUnlockFragment?.onActivityResult(requestCode, resultCode, data)
// To get entry in result
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
@@ -778,6 +733,8 @@ open class PasswordActivity : SpecialModeActivity() {
private val TAG = PasswordActivity::class.java.name
private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG"
private const val KEY_FILENAME = "fileName"
private const val KEY_KEYFILE = "keyFile"
private const val VIEW_INTENT = "android.intent.action.VIEW"
@@ -805,19 +762,52 @@ open class PasswordActivity : SpecialModeActivity() {
*/
@Throws(FileNotFoundException::class)
fun launch(
activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
searchInfo: SearchInfo?) {
fun launch(activity: Activity,
databaseFile: Uri,
keyFile: Uri?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
searchInfo?.let {
intent.putExtra(KEY_SEARCH_INFO, it)
}
activity.startActivity(intent)
}
}
/*
* -------------------------
* Share Launch
* -------------------------
*/
@Throws(FileNotFoundException::class)
fun launchForSearchResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
searchInfo: SearchInfo) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
EntrySelectionHelper.startActivityForSearchModeResult(
activity,
intent,
searchInfo)
}
}
/*
* -------------------------
* Save Launch
* -------------------------
*/
@Throws(FileNotFoundException::class)
fun launchForSaveResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
searchInfo: SearchInfo) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
EntrySelectionHelper.startActivityForSaveModeResult(
activity,
intent,
searchInfo)
}
}
/*
* -------------------------
* Keyboard Launch
@@ -825,13 +815,12 @@ open class PasswordActivity : SpecialModeActivity() {
*/
@Throws(FileNotFoundException::class)
fun launchForKeyboardResult(
activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
searchInfo: SearchInfo?) {
fun launchForKeyboardResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
EntrySelectionHelper.startActivityForEntrySelectionResult(
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
activity,
intent,
searchInfo)
@@ -846,22 +835,93 @@ open class PasswordActivity : SpecialModeActivity() {
@RequiresApi(api = Build.VERSION_CODES.O)
@Throws(FileNotFoundException::class)
fun launchForAutofillResult(
activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
assistStructure: AssistStructure?,
searchInfo: SearchInfo?) {
if (assistStructure != null) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
AutofillHelper.startActivityForAutofillResult(
activity,
intent,
assistStructure,
searchInfo)
}
} else {
launch(activity, databaseFile, keyFile, searchInfo)
fun launchForAutofillResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
assistStructure: AssistStructure,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
AutofillHelper.startActivityForAutofillResult(
activity,
intent,
assistStructure,
searchInfo)
}
}
/*
* -------------------------
* Registration Launch
* -------------------------
*/
fun launchForRegistration(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
registerInfo: RegisterInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
EntrySelectionHelper.startActivityForRegistrationModeResult(
activity,
intent,
registerInfo)
}
}
/*
* -------------------------
* Global Launch
* -------------------------
*/
fun launch(activity: Activity,
databaseUri: Uri,
keyFile: Uri?,
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit) {
try {
EntrySelectionHelper.doSpecialAction(activity.intent,
{
PasswordActivity.launch(activity,
databaseUri, keyFile)
},
{ searchInfo -> // Search Action
PasswordActivity.launchForSearchResult(activity,
databaseUri, keyFile,
searchInfo)
onLaunchActivitySpecialMode()
},
{ searchInfo -> // Save Action
PasswordActivity.launchForSaveResult(activity,
databaseUri, keyFile,
searchInfo)
onLaunchActivitySpecialMode()
},
{ searchInfo -> // Keyboard Selection Action
PasswordActivity.launchForKeyboardResult(activity,
databaseUri, keyFile,
searchInfo)
onLaunchActivitySpecialMode()
},
{ searchInfo, assistStructure -> // Autofill Selection Action
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PasswordActivity.launchForAutofillResult(activity,
databaseUri, keyFile,
assistStructure,
searchInfo)
onLaunchActivitySpecialMode()
} else {
onCancelSpecialMode()
}
},
{ registerInfo -> // Registration Action
PasswordActivity.launchForRegistration(activity,
databaseUri, keyFile,
registerInfo)
onLaunchActivitySpecialMode()
}
)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
}
}

View File

@@ -31,7 +31,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
class DeleteNodesDialogFragment : DialogFragment() {
open class DeleteNodesDialogFragment : DialogFragment() {
private var mNodesToDelete: List<Node> = ArrayList()
private var mListener: DeleteNodeListener? = null
@@ -51,6 +51,10 @@ class DeleteNodesDialogFragment : DialogFragment() {
super.onDetach()
}
protected open fun retrieveMessage(): String {
return getString(R.string.warning_permanently_delete_nodes)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
arguments?.apply {
@@ -68,7 +72,7 @@ class DeleteNodesDialogFragment : DialogFragment() {
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity)
builder.setMessage(getString(R.string.warning_permanently_delete_nodes))
builder.setMessage(retrieveMessage())
builder.setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.permanentlyDeleteNodes(mNodesToDelete)
}

View File

@@ -0,0 +1,39 @@
/*
* 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.dialogs
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
class EmptyRecycleBinDialogFragment : DeleteNodesDialogFragment() {
override fun retrieveMessage(): String {
return getString(R.string.warning_empty_recycle_bin)
}
companion object {
fun getInstance(nodesToDelete: List<Node>): EmptyRecycleBinDialogFragment {
return EmptyRecycleBinDialogFragment().apply {
arguments = getBundleFromListNodes(nodesToDelete)
}
}
}
}

View File

@@ -26,13 +26,11 @@ import com.google.android.material.textfield.TextInputLayout
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.view.View
import android.widget.Button
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.SeekBar
import android.widget.*
import com.kunzisoft.keepass.R
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 : DialogFragment() {
@@ -78,6 +76,15 @@ class GeneratePasswordDialogFragment : DialogFragment() {
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.allowCopyPasswordAndProtectedFields(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)

View File

@@ -90,12 +90,12 @@ class UnavailableFeatureDialogFragment : DialogFragment() {
}
}
if (apiName.isEmpty()) {
val mapper = arrayOf("ANDROID BASE", "ANDROID BASE 1.1", "CUPCAKE", "DONUT", "ECLAIR", "ECLAIR_0_1", "ECLAIR_MR1", "FROYO", "GINGERBREAD", "GINGERBREAD_MR1", "HONEYCOMB", "HONEYCOMB_MR1", "HONEYCOMB_MR2", "ICE_CREAM_SANDWICH", "ICE_CREAM_SANDWICH_MR1", "JELLY_BEAN", "JELLY_BEAN", "JELLY_BEAN", "KITKAT", "KITKAT", "LOLLIPOOP", "LOLLIPOOP_MR1", "MARSHMALLOW", "NOUGAT", "NOUGAT", "OREO", "OREO")
val mapper = arrayOf("ANDROID BASE", "ANDROID BASE 1.1", "CUPCAKE", "DONUT", "ECLAIR", "ECLAIR_0_1", "ECLAIR_MR1", "FROYO", "GINGERBREAD", "GINGERBREAD_MR1", "HONEYCOMB", "HONEYCOMB_MR1", "HONEYCOMB_MR2", "ICE_CREAM_SANDWICH", "ICE_CREAM_SANDWICH_MR1", "JELLY_BEAN", "JELLY_BEAN", "JELLY_BEAN", "KITKAT", "KITKAT", "LOLLIPOOP", "LOLLIPOOP_MR1", "MARSHMALLOW", "NOUGAT", "NOUGAT", "OREO", "OREO", "PIE", "", "")
val index = apiNumber - 1
apiName = if (index < mapper.size) mapper[index] else "UNKNOWN_VERSION"
}
if (version.isEmpty()) {
val versions = arrayOf("1.0", "1.1", "1.5", "1.6", "2.0", "2.0.1", "2.1", "2.2.X", "2.3", "2.3.3", "3.0", "3.1", "3.2.0", "4.0.1", "4.0.3", "4.1.0", "4.2.0", "4.3.0", "4.4", "4.4", "5.0", "5.1", "6.0", "7.0", "7.1", "8.0.0", "8.1.0")
val versions = arrayOf("1.0", "1.1", "1.5", "1.6", "2.0", "2.0.1", "2.1", "2.2.X", "2.3", "2.3.3", "3.0", "3.1", "3.2.0", "4.0.1", "4.0.3", "4.1.0", "4.2.0", "4.3.0", "4.4", "4.4", "5.0", "5.1", "6.0", "7.0", "7.1", "8.0.0", "8.1.0", "9", "10", "11")
val index = apiNumber - 1
version = if (index < versions.size) versions[index] else "UNKNOWN_VERSION"
}

View File

@@ -24,54 +24,186 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import java.io.Serializable
object EntrySelectionHelper {
private const val EXTRA_ENTRY_SELECTION_MODE = "com.kunzisoft.keepass.extra.ENTRY_SELECTION_MODE"
private const val DEFAULT_ENTRY_SELECTION_MODE = false
// Key to retrieve search in intent
const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
private const val KEY_SPECIAL_MODE = "com.kunzisoft.keepass.extra.SPECIAL_MODE"
private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
fun startActivityForEntrySelectionResult(context: Context,
intent: Intent,
searchInfo: SearchInfo?) {
addEntrySelectionModeExtraInIntent(intent)
searchInfo?.let {
intent.putExtra(KEY_SEARCH_INFO, it)
}
fun startActivityForSearchModeResult(context: Context,
intent: Intent,
searchInfo: SearchInfo) {
addSpecialModeInIntent(intent, SpecialMode.SEARCH)
addSearchInfoInIntent(intent, searchInfo)
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
fun addEntrySelectionModeExtraInIntent(intent: Intent) {
intent.putExtra(EXTRA_ENTRY_SELECTION_MODE, true)
fun startActivityForSaveModeResult(context: Context,
intent: Intent,
searchInfo: SearchInfo) {
addSpecialModeInIntent(intent, SpecialMode.SAVE)
addTypeModeInIntent(intent, TypeMode.DEFAULT)
addSearchInfoInIntent(intent, searchInfo)
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
fun removeEntrySelectionModeFromIntent(intent: Intent) {
intent.removeExtra(EXTRA_ENTRY_SELECTION_MODE)
fun startActivityForKeyboardSelectionModeResult(context: Context,
intent: Intent,
searchInfo: SearchInfo?) {
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
addTypeModeInIntent(intent, TypeMode.MAGIKEYBOARD)
addSearchInfoInIntent(intent, searchInfo)
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
fun retrieveEntrySelectionModeFromIntent(intent: Intent): Boolean {
return intent.getBooleanExtra(EXTRA_ENTRY_SELECTION_MODE, DEFAULT_ENTRY_SELECTION_MODE)
fun startActivityForRegistrationModeResult(context: Context,
intent: Intent,
registerInfo: RegisterInfo?) {
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
// At the moment, only autofill for registration
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
addRegisterInfoInIntent(intent, registerInfo)
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
fun doEntrySelectionAction(intent: Intent,
standardAction: () -> Unit,
keyboardAction: () -> Unit,
autofillAction: (assistStructure: AssistStructure) -> Unit) {
var assistStructureInit = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.retrieveAssistStructure(intent)?.let { assistStructure ->
autofillAction.invoke(assistStructure)
assistStructureInit = true
}
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
searchInfo?.let {
intent.putExtra(KEY_SEARCH_INFO, it)
}
if (!assistStructureInit) {
if (intent.getBooleanExtra(EXTRA_ENTRY_SELECTION_MODE, DEFAULT_ENTRY_SELECTION_MODE)) {
intent.removeExtra(EXTRA_ENTRY_SELECTION_MODE)
keyboardAction.invoke()
} else {
standardAction.invoke()
}
fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? {
return intent.getParcelableExtra(KEY_SEARCH_INFO)
}
fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
registerInfo?.let {
intent.putExtra(KEY_REGISTER_INFO, it)
}
}
fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? {
return intent.getParcelableExtra(KEY_REGISTER_INFO)
}
fun removeInfoFromIntent(intent: Intent) {
intent.removeExtra(KEY_SEARCH_INFO)
intent.removeExtra(KEY_REGISTER_INFO)
}
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
intent.putExtra(KEY_SPECIAL_MODE, specialMode as Serializable)
}
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (AutofillHelper.retrieveAssistStructure(intent) != null)
return SpecialMode.SELECTION
}
return intent.getSerializableExtra(KEY_SPECIAL_MODE) as SpecialMode?
?: SpecialMode.DEFAULT
}
fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
intent.putExtra(KEY_TYPE_MODE, typeMode as Serializable)
}
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (AutofillHelper.retrieveAssistStructure(intent) != null)
return TypeMode.AUTOFILL
}
return intent.getSerializableExtra(KEY_TYPE_MODE) as TypeMode? ?: TypeMode.DEFAULT
}
fun removeModesFromIntent(intent: Intent) {
intent.removeExtra(KEY_SPECIAL_MODE)
intent.removeExtra(KEY_TYPE_MODE)
}
fun doSpecialAction(intent: Intent,
defaultAction: () -> Unit,
searchAction: (searchInfo: SearchInfo) -> Unit,
saveAction: (searchInfo: SearchInfo) -> Unit,
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
autofillSelectionAction: (searchInfo: SearchInfo?,
assistStructure: AssistStructure) -> Unit,
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
when (retrieveSpecialModeFromIntent(intent)) {
SpecialMode.DEFAULT -> {
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
defaultAction.invoke()
}
SpecialMode.SEARCH -> {
val searchInfo = retrieveSearchInfoFromIntent(intent)
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
if (searchInfo != null)
searchAction.invoke(searchInfo)
else {
defaultAction.invoke()
}
}
SpecialMode.SAVE -> {
val searchInfo = retrieveSearchInfoFromIntent(intent)
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
if (searchInfo != null)
saveAction.invoke(searchInfo)
else {
defaultAction.invoke()
}
}
SpecialMode.SELECTION -> {
val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent)
var assistStructureInit = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.retrieveAssistStructure(intent)?.let { assistStructure ->
autofillSelectionAction.invoke(searchInfo, assistStructure)
assistStructureInit = true
}
}
if (!assistStructureInit) {
if (intent.getSerializableExtra(KEY_SPECIAL_MODE) != null) {
when (retrieveTypeModeFromIntent(intent)) {
TypeMode.DEFAULT -> {
removeModesFromIntent(intent)
if (searchInfo != null)
searchAction.invoke(searchInfo)
else
defaultAction.invoke()
}
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
else -> {
// In this case, error
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
}
}
} else {
if (searchInfo != null)
searchAction.invoke(searchInfo)
else
defaultAction.invoke()
}
}
}
SpecialMode.REGISTRATION -> {
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
autofillRegistrationAction.invoke(registerInfo)
}
}
}

View File

@@ -0,0 +1,9 @@
package com.kunzisoft.keepass.activities.helpers
enum class SpecialMode {
DEFAULT,
SEARCH,
SAVE,
SELECTION,
REGISTRATION;
}

View File

@@ -0,0 +1,5 @@
package com.kunzisoft.keepass.activities.helpers
enum class TypeMode {
DEFAULT, MAGIKEYBOARD, AUTOFILL
}

View File

@@ -20,12 +20,17 @@
package com.kunzisoft.keepass.activities.lock
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
@@ -43,7 +48,7 @@ abstract class LockingActivity : SpecialModeActivity() {
// Force readOnly if Entry Selection mode
protected var mReadOnly: Boolean
get() {
return mReadOnlyToSave || mSelectionMode
return mReadOnlyToSave
}
set(value) {
mReadOnlyToSave = value
@@ -96,6 +101,15 @@ abstract class LockingActivity : SpecialModeActivity() {
override fun onResume() {
super.onResume()
// If in ave or registration mode, don't allow read only
if ((mSpecialMode == SpecialMode.SAVE
|| mSpecialMode == SpecialMode.REGISTRATION)
&& mReadOnly) {
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
EntrySelectionHelper.removeModesFromIntent(intent)
finish()
}
mProgressDatabaseTaskProvider?.registerProgressTask()
// To refresh when back to normal workflow from selection workflow
@@ -150,35 +164,6 @@ abstract class LockingActivity : SpecialModeActivity() {
sendBroadcast(Intent(LOCK_ACTION))
}
/**
* To reset the app timeout when a view is focused or changed
*/
@SuppressLint("ClickableViewAccessibility")
protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View?) {
views.forEach {
it?.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// Log.d(TAG, "View touched, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
}
}
false
}
it?.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
// Log.d(TAG, "View focused, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
}
}
if (it is ViewGroup) {
for (i in 0..it.childCount) {
resetAppTimeoutWhenViewFocusedOrChanged(it.getChildAt(i))
}
}
}
}
override fun onBackPressed() {
if (mTimeoutEnable) {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
@@ -191,7 +176,7 @@ abstract class LockingActivity : SpecialModeActivity() {
companion object {
private const val TAG = "LockingActivity"
const val TAG = "LockingActivity"
const val RESULT_EXIT_LOCK = 1450
@@ -202,3 +187,28 @@ abstract class LockingActivity : SpecialModeActivity() {
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
}
}
/**
* To reset the app timeout when a view is focused or changed
*/
@SuppressLint("ClickableViewAccessibility")
fun View.resetAppTimeoutWhenViewFocusedOrChanged(context: Context) {
setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
//Log.d(LockingActivity.TAG, "View touched, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
}
}
false
}
setOnFocusChangeListener { _, _ ->
//Log.d(LockingActivity.TAG, "View focused, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
}
if (this is ViewGroup) {
for (i in 0..childCount) {
getChildAt(i)?.resetAppTimeoutWhenViewFocusedOrChanged(context)
}
}
}

View File

@@ -1,12 +1,13 @@
package com.kunzisoft.keepass.activities.selection
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.helpers.TypeMode
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.SpecialModeView
@@ -16,40 +17,128 @@ import com.kunzisoft.keepass.view.SpecialModeView
*/
abstract class SpecialModeActivity : StylishActivity() {
protected var mSelectionMode: Boolean = false
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
protected var mTypeMode: TypeMode = TypeMode.DEFAULT
protected var mAutofillSelection: Boolean = false
private var mSpecialModeView: SpecialModeView? = null
private var specialModeView: SpecialModeView? = null
override fun onBackPressed() {
if (mSpecialMode != SpecialMode.DEFAULT)
onCancelSpecialMode()
else
super.onBackPressed()
}
/**
* To call the regular onBackPressed() method in special mode
*/
protected fun onRegularBackPressed() {
super.onBackPressed()
}
/**
* Intent sender uses special retains data in callback
*/
private fun isIntentSender(): Boolean {
return (mSpecialMode == SpecialMode.SELECTION
&& mTypeMode == TypeMode.AUTOFILL)
/* TODO Registration callback #765
|| (mSpecialMode == SpecialMode.REGISTRATION
&& mTypeMode == TypeMode.AUTOFILL
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
*/
}
fun onLaunchActivitySpecialMode() {
if (!isIntentSender()) {
EntrySelectionHelper.removeModesFromIntent(intent)
EntrySelectionHelper.removeInfoFromIntent(intent)
finish()
}
}
open fun onValidateSpecialMode() {
if (isIntentSender()) {
super.finish()
} else {
EntrySelectionHelper.removeModesFromIntent(intent)
EntrySelectionHelper.removeInfoFromIntent(intent)
if (mSpecialMode != SpecialMode.DEFAULT) {
// To move the app in background
moveTaskToBack(true)
}
}
}
open fun onCancelSpecialMode() {
onBackPressed()
if (isIntentSender()) {
// To get the app caller, only for IntentSender
super.onBackPressed()
} else {
EntrySelectionHelper.removeModesFromIntent(intent)
EntrySelectionHelper.removeInfoFromIntent(intent)
if (mSpecialMode != SpecialMode.DEFAULT) {
// To move the app in background
moveTaskToBack(true)
}
}
}
protected fun backToTheAppCaller() {
if (isIntentSender()) {
// To get the app caller, only for IntentSender
super.onBackPressed()
} else {
// To move the app in background
moveTaskToBack(true)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
}
override fun onResume() {
super.onResume()
mSelectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mAutofillSelection = AutofillHelper.retrieveAssistStructure(intent) != null
}
val searchInfo: SearchInfo? = intent.getParcelableExtra(EntrySelectionHelper.KEY_SEARCH_INFO)
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)?.searchInfo
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
// To show the selection mode
specialModeView = findViewById(R.id.special_mode_view)
specialModeView?.apply {
mSpecialModeView = findViewById(R.id.special_mode_view)
mSpecialModeView?.apply {
// Populate title
val typeModeId = if (mAutofillSelection)
R.string.autofill
else
R.string.magic_keyboard_title
title = "${resources.getString(R.string.selection_mode)} (${getString(typeModeId)})"
val selectionModeStringId = when (mSpecialMode) {
SpecialMode.DEFAULT, // Not important because hidden
SpecialMode.SEARCH -> R.string.search_mode
SpecialMode.SAVE -> R.string.save_mode
SpecialMode.SELECTION -> R.string.selection_mode
SpecialMode.REGISTRATION -> R.string.registration_mode
}
val typeModeStringId = when (mTypeMode) {
TypeMode.DEFAULT, // Not important because hidden
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
TypeMode.AUTOFILL -> R.string.autofill
}
title = getString(selectionModeStringId)
if (mTypeMode != TypeMode.DEFAULT)
title = "$title (${getString(typeModeStringId)})"
// Populate subtitle
subtitle = searchInfo?.getName(resources)
// Show the toolbar or not
visible = mSelectionMode
visible = when (mSpecialMode) {
SpecialMode.DEFAULT -> false
SpecialMode.SEARCH -> true
SpecialMode.SAVE -> true
SpecialMode.SELECTION -> true
SpecialMode.REGISTRATION -> true
}
// Add back listener
onCancelButtonClickListener = View.OnClickListener {
@@ -58,7 +147,7 @@ abstract class SpecialModeActivity : StylishActivity() {
// Create menu
menu.clear()
if (mAutofillSelection) {
if (mTypeMode == TypeMode.AUTOFILL) {
menuInflater.inflate(R.menu.autofill, menu)
setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
@@ -70,9 +159,15 @@ abstract class SpecialModeActivity : StylishActivity() {
}
}
}
// To hide home button from the regular toolbar in special mode
if (mSpecialMode != SpecialMode.DEFAULT) {
supportActionBar?.setDisplayHomeAsUpEnabled(false)
supportActionBar?.setDisplayShowHomeEnabled(false)
}
}
fun blockAutofill(searchInfo: SearchInfo?) {
private fun blockAutofill(searchInfo: SearchInfo?) {
val webDomain = searchInfo?.webDomain
val applicationId = searchInfo?.applicationId
if (webDomain != null) {

View File

@@ -52,72 +52,69 @@ import java.util.*
class NodeAdapter (private val context: Context)
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
private var nodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
private val nodeSortedListCallback: NodeSortedListCallback
private val nodeSortedList: SortedList<Node>
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
private val mNodeSortedListCallback: NodeSortedListCallback
private val mNodeSortedList: SortedList<Node>
private val mInflater: LayoutInflater = LayoutInflater.from(context)
private var calculateViewTypeTextSize = Array(2) { true} // number of view type
private var textSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
private var prefSizeMultiplier: Float = 0F
private var subtextDefaultDimension: Float = 0F
private var infoTextDefaultDimension: Float = 0F
private var numberChildrenTextDefaultDimension: Float = 0F
private var iconDefaultDimension: Float = 0F
private var mCalculateViewTypeTextSize = Array(2) { true} // number of view type
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
private var mPrefSizeMultiplier: Float = 0F
private var mSubtextDefaultDimension: Float = 0F
private var mInfoTextDefaultDimension: Float = 0F
private var mNumberChildrenTextDefaultDimension: Float = 0F
private var mIconDefaultDimension: Float = 0F
private var showUserNames: Boolean = true
private var showNumberEntries: Boolean = true
private var entryFilters = arrayOf<Group.ChildFilter>()
private var mShowUserNames: Boolean = true
private var mShowNumberEntries: Boolean = true
private var mEntryFilters = arrayOf<Group.ChildFilter>()
private var actionNodesList = LinkedList<Node>()
private var nodeClickCallback: NodeClickCallback? = null
private var mActionNodesList = LinkedList<Node>()
private var mNodeClickCallback: NodeClickCallback? = null
private val mDatabase: Database
@ColorInt
private val contentSelectionColor: Int
private val mContentSelectionColor: Int
@ColorInt
private val iconGroupColor: Int
private val mIconGroupColor: Int
@ColorInt
private val iconEntryColor: Int
private val mIconEntryColor: Int
/**
* Determine if the adapter contains or not any element
* @return true if the list is empty
*/
val isEmpty: Boolean
get() = nodeSortedList.size() <= 0
get() = mNodeSortedList.size() <= 0
init {
this.infoTextDefaultDimension = context.resources.getDimension(R.dimen.list_medium_size_default)
this.subtextDefaultDimension = context.resources.getDimension(R.dimen.list_small_size_default)
this.numberChildrenTextDefaultDimension = context.resources.getDimension(R.dimen.list_tiny_size_default)
this.iconDefaultDimension = context.resources.getDimension(R.dimen.list_icon_size_default)
this.mIconDefaultDimension = context.resources.getDimension(R.dimen.list_icon_size_default)
assignPreferences()
this.nodeSortedListCallback = NodeSortedListCallback()
this.nodeSortedList = SortedList(Node::class.java, nodeSortedListCallback)
this.mNodeSortedListCallback = NodeSortedListCallback()
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
// Database
this.mDatabase = Database.getInstance()
// Color of content selection
val taContentSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
this.contentSelectionColor = taContentSelectionColor.getColor(0, Color.WHITE)
this.mContentSelectionColor = taContentSelectionColor.getColor(0, Color.WHITE)
taContentSelectionColor.recycle()
// Retrieve the color to tint the icon
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
this.iconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
taTextColorPrimary.recycle()
// In two times to fix bug compilation
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
this.iconEntryColor = taTextColor.getColor(0, Color.BLACK)
this.mIconEntryColor = taTextColor.getColor(0, Color.BLACK)
taTextColor.recycle()
}
fun assignPreferences() {
this.prefSizeMultiplier = PreferencesUtil.getListTextSize(context)
this.mPrefSizeMultiplier = PreferencesUtil.getListTextSize(context)
notifyChangeSort(
PreferencesUtil.getListSort(context),
@@ -128,13 +125,13 @@ class NodeAdapter (private val context: Context)
)
)
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
this.showNumberEntries = PreferencesUtil.showNumberEntries(context)
this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context)
this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
this.entryFilters = Group.ChildFilter.getDefaults(context)
this.mEntryFilters = Group.ChildFilter.getDefaults(context)
// Reinit textSize for all view type
calculateViewTypeTextSize.forEachIndexed { index, _ -> calculateViewTypeTextSize[index] = true }
mCalculateViewTypeTextSize.forEachIndexed { index, _ -> mCalculateViewTypeTextSize[index] = true }
}
/**
@@ -142,12 +139,12 @@ class NodeAdapter (private val context: Context)
*/
fun rebuildList(group: Group) {
assignPreferences()
nodeSortedList.replaceAll(group.getFilteredChildren(entryFilters))
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
}
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
override fun compare(item1: Node, item2: Node): Int {
return nodeComparator!!.compare(item1, item2)
return mNodeComparator!!.compare(item1, item2)
}
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
@@ -162,7 +159,7 @@ class NodeAdapter (private val context: Context)
}
fun contains(node: Node): Boolean {
return nodeSortedList.indexOf(node) != SortedList.INVALID_POSITION
return mNodeSortedList.indexOf(node) != SortedList.INVALID_POSITION
}
/**
@@ -170,7 +167,7 @@ class NodeAdapter (private val context: Context)
* @param node Node to add
*/
fun addNode(node: Node) {
nodeSortedList.add(node)
mNodeSortedList.add(node)
}
/**
@@ -178,7 +175,7 @@ class NodeAdapter (private val context: Context)
* @param nodes Nodes to add
*/
fun addNodes(nodes: List<Node>) {
nodeSortedList.addAll(nodes)
mNodeSortedList.addAll(nodes)
}
/**
@@ -186,7 +183,7 @@ class NodeAdapter (private val context: Context)
* @param node Node to delete
*/
fun removeNode(node: Node) {
nodeSortedList.remove(node)
mNodeSortedList.remove(node)
}
/**
@@ -195,7 +192,7 @@ class NodeAdapter (private val context: Context)
*/
fun removeNodes(nodes: List<Node>) {
nodes.forEach { node ->
nodeSortedList.remove(node)
mNodeSortedList.remove(node)
}
}
@@ -203,9 +200,9 @@ class NodeAdapter (private val context: Context)
* Remove a node at [position] in the list
*/
fun removeNodeAt(position: Int) {
nodeSortedList.removeItemAt(position)
mNodeSortedList.removeItemAt(position)
// Refresh all the next items
notifyItemRangeChanged(position, nodeSortedList.size() - position)
notifyItemRangeChanged(position, mNodeSortedList.size() - position)
}
/**
@@ -226,10 +223,10 @@ class NodeAdapter (private val context: Context)
* @param newNode Node after the update
*/
fun updateNode(oldNode: Node, newNode: Node) {
nodeSortedList.beginBatchedUpdates()
nodeSortedList.remove(oldNode)
nodeSortedList.add(newNode)
nodeSortedList.endBatchedUpdates()
mNodeSortedList.beginBatchedUpdates()
mNodeSortedList.remove(oldNode)
mNodeSortedList.add(newNode)
mNodeSortedList.endBatchedUpdates()
}
/**
@@ -238,30 +235,30 @@ class NodeAdapter (private val context: Context)
* @param newNodes Node after the update
*/
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
nodeSortedList.beginBatchedUpdates()
mNodeSortedList.beginBatchedUpdates()
oldNodes.forEach { oldNode ->
nodeSortedList.remove(oldNode)
mNodeSortedList.remove(oldNode)
}
nodeSortedList.addAll(newNodes)
nodeSortedList.endBatchedUpdates()
mNodeSortedList.addAll(newNodes)
mNodeSortedList.endBatchedUpdates()
}
fun notifyNodeChanged(node: Node) {
notifyItemChanged(nodeSortedList.indexOf(node))
notifyItemChanged(mNodeSortedList.indexOf(node))
}
fun setActionNodes(actionNodes: List<Node>) {
this.actionNodesList.apply {
this.mActionNodesList.apply {
clear()
addAll(actionNodes)
}
}
fun unselectActionNodes() {
actionNodesList.forEach {
notifyItemChanged(nodeSortedList.indexOf(it))
mActionNodesList.forEach {
notifyItemChanged(mNodeSortedList.indexOf(it))
}
this.actionNodesList.apply {
this.mActionNodesList.apply {
clear()
}
}
@@ -271,49 +268,55 @@ class NodeAdapter (private val context: Context)
*/
fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
this.nodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
this.mNodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
}
override fun getItemViewType(position: Int): Int {
return nodeSortedList.get(position).type.ordinal
return mNodeSortedList.get(position).type.ordinal
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NodeViewHolder {
val view: View = if (viewType == Type.GROUP.ordinal) {
inflater.inflate(R.layout.item_list_nodes_group, parent, false)
mInflater.inflate(R.layout.item_list_nodes_group, parent, false)
} else {
inflater.inflate(R.layout.item_list_nodes_entry, parent, false)
mInflater.inflate(R.layout.item_list_nodes_entry, parent, false)
}
return NodeViewHolder(view)
val nodeViewHolder = NodeViewHolder(view)
mInfoTextDefaultDimension = nodeViewHolder.text.textSize
mSubtextDefaultDimension = nodeViewHolder.subText.textSize
nodeViewHolder.numberChildren?.let {
mNumberChildrenTextDefaultDimension = it.textSize
}
return nodeViewHolder
}
override fun onBindViewHolder(holder: NodeViewHolder, position: Int) {
val subNode = nodeSortedList.get(position)
val subNode = mNodeSortedList.get(position)
// Node selection
holder.container.isSelected = actionNodesList.contains(subNode)
holder.container.isSelected = mActionNodesList.contains(subNode)
// Assign image
val iconColor = if (holder.container.isSelected)
contentSelectionColor
mContentSelectionColor
else when (subNode.type) {
Type.GROUP -> iconGroupColor
Type.ENTRY -> iconEntryColor
Type.GROUP -> mIconGroupColor
Type.ENTRY -> mIconEntryColor
}
holder.imageIdentifier?.setColorFilter(iconColor)
holder.icon.apply {
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
// Relative size of the icon
layoutParams?.apply {
height = (iconDefaultDimension * prefSizeMultiplier).toInt()
width = (iconDefaultDimension * prefSizeMultiplier).toInt()
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
width = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
}
}
// Assign text
holder.text.apply {
text = subNode.title
setTextSize(textSizeUnit, infoTextDefaultDimension, prefSizeMultiplier)
setTextSize(mTextSizeUnit, mInfoTextDefaultDimension, mPrefSizeMultiplier)
strikeOut(subNode.isCurrentlyExpires)
}
// Add subText with username
@@ -331,10 +334,10 @@ class NodeAdapter (private val context: Context)
holder.text.text = entry.getVisualTitle()
holder.subText.apply {
val username = entry.username
if (showUserNames && username.isNotEmpty()) {
if (mShowUserNames && username.isNotEmpty()) {
visibility = View.VISIBLE
text = username
setTextSize(textSizeUnit, subtextDefaultDimension, prefSizeMultiplier)
setTextSize(mTextSizeUnit, mSubtextDefaultDimension, mPrefSizeMultiplier)
}
}
@@ -346,12 +349,12 @@ class NodeAdapter (private val context: Context)
// Add number of entries in groups
if (subNode.type == Type.GROUP) {
if (showNumberEntries) {
if (mShowNumberEntries) {
holder.numberChildren?.apply {
text = (subNode as Group)
.getNumberOfChildEntries(entryFilters)
.getNumberOfChildEntries(mEntryFilters)
.toString()
setTextSize(textSizeUnit, numberChildrenTextDefaultDimension, prefSizeMultiplier)
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
visibility = View.VISIBLE
}
} else {
@@ -361,22 +364,22 @@ class NodeAdapter (private val context: Context)
// Assign click
holder.container.setOnClickListener {
nodeClickCallback?.onNodeClick(subNode)
mNodeClickCallback?.onNodeClick(subNode)
}
holder.container.setOnLongClickListener {
nodeClickCallback?.onNodeLongClick(subNode) ?: false
mNodeClickCallback?.onNodeLongClick(subNode) ?: false
}
}
override fun getItemCount(): Int {
return nodeSortedList.size()
return mNodeSortedList.size()
}
/**
* Assign a listener when a node is clicked
*/
fun setOnNodeClickListener(nodeClickCallback: NodeClickCallback?) {
this.nodeClickCallback = nodeClickCallback
this.mNodeClickCallback = nodeClickCallback
}
/**

View File

@@ -100,6 +100,7 @@ class SearchEntryCursorAdapter(private val context: Context,
} else {
""
}
visibility = if (text.isEmpty()) View.GONE else View.VISIBLE
strikeOut(currentEntry.isCurrentlyExpires)
}
}
@@ -160,8 +161,8 @@ class SearchEntryCursorAdapter(private val context: Context,
}
private class ViewHolder {
internal var imageViewIcon: ImageView? = null
internal var textViewTitle: TextView? = null
internal var textViewSubTitle: TextView? = null
var imageViewIcon: ImageView? = null
var textViewTitle: TextView? = null
var textViewSubTitle: TextView? = null
}
}

View File

@@ -19,27 +19,96 @@
*/
package com.kunzisoft.keepass.app.database
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri
import android.os.IBinder
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.SingletonHolderParameter
import java.util.*
class CipherDatabaseAction(applicationContext: Context) {
class CipherDatabaseAction(context: Context) {
private val applicationContext = context.applicationContext
private val cipherDatabaseDao =
AppDatabase
.getDatabase(applicationContext)
.cipherDatabaseDao()
// Temp DAO to easily remove content if object no longer in memory
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
private val mIntentAdvancedUnlockService = Intent(applicationContext,
AdvancedUnlockNotificationService::class.java)
private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null
private var mServiceConnection: ServiceConnection? = null
private var mDatabaseListeners = LinkedList<DatabaseListener>()
fun reloadPreferences() {
useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
}
@Synchronized
private fun attachService(performedAction: () -> Unit) {
// Check if a service is currently running else do nothing
if (mBinder != null) {
performedAction.invoke()
} else if (mServiceConnection == null) {
mServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as AdvancedUnlockNotificationService.AdvancedUnlockBinder)
performedAction.invoke()
}
override fun onServiceDisconnected(name: ComponentName?) {
mBinder = null
mServiceConnection = null
mDatabaseListeners.forEach {
it.onDatabaseCleared()
}
}
}
applicationContext.bindService(mIntentAdvancedUnlockService,
mServiceConnection!!,
Context.BIND_ABOVE_CLIENT)
if (mBinder == null) {
applicationContext.startService(mIntentAdvancedUnlockService)
}
}
}
fun registerDatabaseListener(listener: DatabaseListener) {
mDatabaseListeners.add(listener)
}
fun unregisterDatabaseListener(listener: DatabaseListener) {
mDatabaseListeners.remove(listener)
}
interface DatabaseListener {
fun onDatabaseCleared()
}
fun getCipherDatabase(databaseUri: Uri,
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
IOActionTask(
{
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
},
{
cipherDatabaseResultListener.invoke(it)
}
).execute()
if (useTempDao) {
attachService {
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri))
}
} else {
IOActionTask(
{
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
},
{
cipherDatabaseResultListener.invoke(it)
}
).execute()
}
}
fun containsCipherDatabase(databaseUri: Uri,
@@ -51,36 +120,52 @@ class CipherDatabaseAction(applicationContext: Context) {
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
cipherDatabaseResultListener: (() -> Unit)? = null) {
IOActionTask(
{
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
// Update values if element not yet in the database
if (cipherDatabaseRetrieve == null) {
cipherDatabaseDao.add(cipherDatabaseEntity)
} else {
cipherDatabaseDao.update(cipherDatabaseEntity)
if (useTempDao) {
attachService {
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
cipherDatabaseResultListener?.invoke()
}
} else {
IOActionTask(
{
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
// Update values if element not yet in the database
if (cipherDatabaseRetrieve == null) {
cipherDatabaseDao.add(cipherDatabaseEntity)
} else {
cipherDatabaseDao.update(cipherDatabaseEntity)
}
},
{
cipherDatabaseResultListener?.invoke()
}
},
{
cipherDatabaseResultListener?.invoke()
}
).execute()
).execute()
}
}
fun deleteByDatabaseUri(databaseUri: Uri,
cipherDatabaseResultListener: (() -> Unit)? = null) {
IOActionTask(
{
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
},
{
cipherDatabaseResultListener?.invoke()
}
).execute()
if (useTempDao) {
attachService {
mBinder?.deleteByDatabaseUri(databaseUri)
cipherDatabaseResultListener?.invoke()
}
} else {
IOActionTask(
{
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
},
{
cipherDatabaseResultListener?.invoke()
}
).execute()
}
}
fun deleteAll() {
attachService {
mBinder?.deleteAll()
}
IOActionTask(
{
cipherDatabaseDao.deleteAll()

View File

@@ -43,6 +43,11 @@ data class CipherDatabaseEntity(
parcel.readString()!!,
parcel.readString()!!)
fun replaceContent(copy: CipherDatabaseEntity) {
this.encryptedValue = copy.encryptedValue
this.specParameters = copy.specParameters
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(databaseUri)
parcel.writeString(encryptedValue)

View File

@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.app.database
import android.content.Context
import android.net.Uri
import android.util.Log
import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.SingletonHolderParameter
@@ -133,10 +134,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
)
// Update values if history element not yet in the database
if (fileDatabaseHistoryRetrieve == null) {
databaseFileHistoryDao.add(fileDatabaseHistory)
} else {
databaseFileHistoryDao.update(fileDatabaseHistory)
try {
if (fileDatabaseHistoryRetrieve == null) {
databaseFileHistoryDao.add(fileDatabaseHistory)
} else {
databaseFileHistoryDao.update(fileDatabaseHistory)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to add or update database history", e)
}
val fileDatabaseInfo = FileDatabaseInfo(applicationContext,
@@ -208,5 +213,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
).execute()
}
companion object : SingletonHolderParameter<FileDatabaseHistoryAction, Context>(::FileDatabaseHistoryAction)
companion object : SingletonHolderParameter<FileDatabaseHistoryAction, Context>(::FileDatabaseHistoryAction) {
private val TAG = FileDatabaseHistoryAction::class.java.name
}
}

View File

@@ -34,7 +34,7 @@ import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.icons.assignDatabaseIcon
@@ -157,11 +157,9 @@ object AutofillHelper {
intent: Intent,
assistStructure: AssistStructure,
searchInfo: SearchInfo?) {
EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent)
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
intent.putExtra(ASSIST_STRUCTURE, assistStructure)
searchInfo?.let {
intent.putExtra(KEY_SEARCH_INFO, it)
}
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
}

View File

@@ -23,72 +23,91 @@ import android.os.Build
import android.os.CancellationSignal
import android.service.autofill.*
import android.util.Log
import android.view.autofill.AutofillId
import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
import java.util.concurrent.atomic.AtomicBoolean
@RequiresApi(api = Build.VERSION_CODES.O)
class KeeAutofillService : AutofillService() {
var applicationIdBlocklist: Set<String>? = null
var webDomainBlocklist: Set<String>? = null
var askToSaveData: Boolean = false
private var mLock = AtomicBoolean()
override fun onCreate() {
super.onCreate()
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
askToSaveData = PreferencesUtil.askToSaveAutofillData(this) // TODO apply when changed
}
override fun onFillRequest(request: FillRequest,
cancellationSignal: CancellationSignal,
callback: FillCallback) {
val fillContexts = request.fillContexts
val latestStructure = fillContexts[fillContexts.size - 1].structure
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
// Check user's settings for authenticating Responses and Datasets.
StructureParser(latestStructure).parse()?.let { parseResult ->
// Lock
if (!mLock.get()) {
mLock.set(true)
// Check user's settings for authenticating Responses and Datasets.
val latestStructure = request.fillContexts.last().structure
StructureParser(latestStructure).parse()?.let { parseResult ->
// Build search info only if applicationId or webDomain are not blocked
if (searchAllowedFor(parseResult.applicationId, applicationIdBlocklist)
&& searchAllowedFor(parseResult.domain, webDomainBlocklist)) {
val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.domain
// Build search info only if applicationId or webDomain are not blocked
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
}
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
searchInfo.webDomain = webDomainWithoutSubDomain
launchSelection(searchInfo, parseResult, callback)
}
}
SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(),
searchInfo,
{ items ->
val responseBuilder = FillResponse.Builder()
AutofillHelper.addHeader(responseBuilder, packageName,
parseResult.domain, parseResult.applicationId)
items.forEach {
responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult))
}
callback.onSuccess(responseBuilder.build())
},
{
// Show UI if no search result
showUIForEntrySelection(parseResult, searchInfo, callback)
},
{
// Show UI if database not open
showUIForEntrySelection(parseResult, searchInfo, callback)
}
)
}
}
}
private fun launchSelection(searchInfo: SearchInfo,
parseResult: StructureParser.Result,
callback: FillCallback) {
SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(),
searchInfo,
{ items ->
val responseBuilder = FillResponse.Builder()
AutofillHelper.addHeader(responseBuilder, packageName,
parseResult.webDomain, parseResult.applicationId)
items.forEach {
responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult))
}
callback.onSuccess(responseBuilder.build())
},
{
// Show UI if no search result
showUIForEntrySelection(parseResult, searchInfo, callback)
},
{
// Show UI if database not open
showUIForEntrySelection(parseResult, searchInfo, callback)
}
)
}
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
searchInfo: SearchInfo,
callback: FillCallback) {
@@ -96,12 +115,12 @@ class KeeAutofillService : AutofillService() {
if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response.
val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this,
val intentSender = AutofillLauncherActivity.getAuthIntentSenderForSelection(this,
searchInfo)
val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (!parseResult.domain.isNullOrEmpty()) {
val remoteViewsUnlock: RemoteViews = if (!parseResult.webDomain.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
setTextViewText(R.id.autofill_web_domain_text, parseResult.domain)
setTextViewText(R.id.autofill_web_domain_text, parseResult.webDomain)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
@@ -110,15 +129,63 @@ class KeeAutofillService : AutofillService() {
} else {
RemoteViews(packageName, R.layout.item_autofill_unlock)
}
responseBuilder.setAuthentication(autofillIds, sender, remoteViewsUnlock)
// Tell to service the interest to save credentials
if (askToSaveData) {
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
val info = ArrayList<AutofillId>()
// Only if at least a password
parseResult.passwordId?.let { passwordInfo ->
parseResult.usernameId?.let { usernameInfo ->
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
info.add(usernameInfo)
}
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
info.add(passwordInfo)
}
if (info.isNotEmpty()) {
responseBuilder.setSaveInfo(
SaveInfo.Builder(types, info.toTypedArray()).build()
)
}
}
// Build response
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
callback.onSuccess(responseBuilder.build())
}
}
}
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
// TODO Save autofill
//callback.onFailure(getString(R.string.autofill_not_support_save));
if (askToSaveData) {
val latestStructure = request.fillContexts.last().structure
StructureParser(latestStructure).parse(true)?.let { parseResult ->
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
Log.d(TAG, "autofill onSaveRequest password")
// Show UI to save data
val registerInfo = RegisterInfo(SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
},
parseResult.usernameValue?.textValue?.toString(),
parseResult.passwordValue?.textValue?.toString())
// TODO Callback in each activity #765
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
// registerInfo))
//} else {
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
callback.onSuccess()
//}
return
}
}
}
callback.onFailure("Saving form values is not allowed")
}
override fun onConnected() {
@@ -126,13 +193,14 @@ class KeeAutofillService : AutofillService() {
}
override fun onDisconnected() {
mLock.set(false)
Log.d(TAG, "onDisconnected")
}
companion object {
private val TAG = KeeAutofillService::class.java.name
fun searchAllowedFor(element: String?, blockList: Set<String>?): Boolean {
fun autofillAllowedFor(element: String?, blockList: Set<String>?): Boolean {
element?.let { elementNotNull ->
if (blockList?.any { appIdBlocked ->
elementNotNull.contains(appIdBlocked)

View File

@@ -25,6 +25,7 @@ import androidx.annotation.RequiresApi
import android.util.Log
import android.view.View
import android.view.autofill.AutofillId
import android.view.autofill.AutofillValue
import java.util.*
@@ -34,14 +35,19 @@ import java.util.*
@RequiresApi(api = Build.VERSION_CODES.O)
internal class StructureParser(private val structure: AssistStructure) {
private var result: Result? = null
private var usernameCandidate: AutofillId? = null
private var usernameNeeded = true
fun parse(): Result? {
private var usernameCandidate: AutofillId? = null
private var usernameValueCandidate: AutofillValue? = null
fun parse(saveValue: Boolean = false): Result? {
try {
result = Result()
result?.apply {
allowSaveValues = saveValue
usernameCandidate = null
usernameValueCandidate = null
mainLoop@ for (i in 0 until structure.windowNodeCount) {
val windowNode = structure.getWindowNodeAt(i)
applicationId = windowNode.title.toString().split("/")[0]
@@ -51,8 +57,12 @@ internal class StructureParser(private val structure: AssistStructure) {
break@mainLoop
}
// If not explicit username field found, add the field just before password field.
if (usernameId == null && passwordId != null && usernameCandidate != null)
if (usernameId == null && passwordId != null && usernameCandidate != null) {
usernameId = usernameCandidate
if (allowSaveValues) {
usernameValue = usernameValueCandidate
}
}
}
// Return the result only if password field is retrieved
@@ -70,11 +80,21 @@ internal class StructureParser(private val structure: AssistStructure) {
// Get the domain of a web app
node.webDomain?.let { webDomain ->
if (webDomain.isNotEmpty()) {
result?.domain = webDomain
result?.webDomain = webDomain
Log.d(TAG, "Autofill domain: $webDomain")
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
node.webScheme?.let { webScheme ->
if (webScheme.isNotEmpty()) {
result?.webScheme = webScheme
Log.d(TAG, "Autofill scheme: $webScheme")
}
}
}
val domainNotEmpty = result?.webDomain?.isNotEmpty() == true
var returnValue = false
// Only parse visible nodes
if (node.visibility == View.VISIBLE) {
if (node.autofillId != null
@@ -83,19 +103,24 @@ internal class StructureParser(private val structure: AssistStructure) {
val hints = node.autofillHints
if (hints != null && hints.isNotEmpty()) {
if (parseNodeByAutofillHint(node))
return true
returnValue = true
} else if (parseNodeByHtmlAttributes(node))
return true
returnValue = true
else if (parseNodeByAndroidInput(node))
return true
returnValue = true
}
// Optimized return but only if domain not empty
if (domainNotEmpty && returnValue)
return true
// Recursive method to process each node
for (i in 0 until node.childCount) {
if (parseViewNode(node.getChildAt(i)))
returnValue = true
if (domainNotEmpty && returnValue)
return true
}
}
return false
return returnValue
}
private fun parseNodeByAutofillHint(node: AssistStructure.ViewNode): Boolean {
@@ -107,10 +132,12 @@ internal class StructureParser(private val structure: AssistStructure) {
|| it.contains("email", true)
|| it.contains(View.AUTOFILL_HINT_PHONE, true)-> {
result?.usernameId = autofillId
result?.usernameValue = node.autofillValue
Log.d(TAG, "Autofill username hint")
}
it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password hint")
// Username not needed in this case
usernameNeeded = false
@@ -140,14 +167,17 @@ internal class StructureParser(private val structure: AssistStructure) {
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
"tel", "email" -> {
result?.usernameId = autofillId
result?.usernameValue = node.autofillValue
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
"text" -> {
usernameCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
"password" -> {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
return true
}
@@ -182,6 +212,7 @@ internal class StructureParser(private val structure: AssistStructure) {
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS) -> {
result?.usernameId = autofillId
result?.usernameValue = node.autofillValue
Log.d(TAG, "Autofill username android text type: ${showHexInputType(inputType)}")
}
inputIsVariationType(inputType,
@@ -189,6 +220,7 @@ internal class StructureParser(private val structure: AssistStructure) {
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
usernameCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
}
inputIsVariationType(inputType,
@@ -196,6 +228,7 @@ internal class StructureParser(private val structure: AssistStructure) {
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
usernameNeeded = false
return true
@@ -220,11 +253,13 @@ internal class StructureParser(private val structure: AssistStructure) {
inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
usernameCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill usernale candidate android number type: ${showHexInputType(inputType)}")
}
inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
usernameNeeded = false
return true
@@ -241,7 +276,14 @@ internal class StructureParser(private val structure: AssistStructure) {
@RequiresApi(api = Build.VERSION_CODES.O)
internal class Result {
var applicationId: String? = null
var domain: String? = null
var webDomain: String? = null
set(value) {
if (field == null)
field = value
}
var webScheme: String? = null
set(value) {
if (field == null)
field = value
@@ -269,6 +311,21 @@ internal class StructureParser(private val structure: AssistStructure) {
}
return all.toTypedArray()
}
// Only in registration mode
var allowSaveValues = false
var usernameValue: AutofillValue? = null
set(value) {
if (allowSaveValues && field == null)
field = value
}
var passwordValue: AutofillValue? = null
set(value) {
if (allowSaveValues && field == null)
field = value
}
}
companion object {

View File

@@ -0,0 +1,10 @@
package com.kunzisoft.keepass.biometric
import androidx.annotation.StringRes
import javax.crypto.Cipher
data class AdvancedUnlockCryptoPrompt(var cipher: Cipher,
@StringRes var promptTitleId: Int,
@StringRes var promptDescriptionId: Int? = null,
var isDeviceCredentialOperation: Boolean,
var isBiometricOperation: Boolean)

View File

@@ -0,0 +1,620 @@
/*
* 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.biometric
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.view.*
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import com.getkeepsafe.taptargetview.TapTargetView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.database.exception.IODatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedUnlockCallback {
private var mBuilderListener: BuilderListener? = null
private var mAdvancedUnlockEnabled = false
private var mAutoOpenPromptEnabled = false
private var advancedUnlockManager: AdvancedUnlockManager? = null
private var biometricMode: Mode = Mode.BIOMETRIC_UNAVAILABLE
private var mAdvancedUnlockInfoView: AdvancedUnlockInfoView? = null
var databaseFileUri: Uri? = null
private set
/**
* Manage setting to auto open biometric prompt
*/
private var mAutoOpenPrompt: Boolean = false
get() {
return field && mAutoOpenPromptEnabled
}
// Variable to check if the prompt can be open (if the right activity is currently shown)
// checkBiometricAvailability() allows open biometric prompt and onDestroy() removes the authorization
private var allowOpenBiometricPrompt = false
private lateinit var cipherDatabaseAction : CipherDatabaseAction
private var cipherDatabaseListener: CipherDatabaseAction.DatabaseListener? = null
// Only to fix multiple fingerprint menu #332
private var mAllowAdvancedUnlockMenu = false
private var mAddBiometricMenuInProgress = false
// Only keep connection when we request a device credential activity
private var keepConnection = false
override fun onAttach(context: Context) {
super.onAttach(context)
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(context)
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(context)
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mBuilderListener = context as BuilderListener
}
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + BuilderListener::class.java.name)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
setHasOptionsMenu(true)
cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
val rootView = inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_advanced_unlock, container, false)
mAdvancedUnlockInfoView = rootView.findViewById(R.id.advanced_unlock_view)
return rootView
}
private data class ActivityResult(var requestCode: Int, var resultCode: Int, var data: Intent?)
private var activityResult: ActivityResult? = null
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// To wait resume
activityResult = ActivityResult(requestCode, resultCode, data)
keepConnection = false
super.onActivityResult(requestCode, resultCode, data)
}
override fun onResume() {
super.onResume()
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(requireContext())
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext())
keepConnection = false
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// biometric menu
if (mAllowAdvancedUnlockMenu)
inflater.inflate(R.menu.advanced_unlock, menu)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
deleteEncryptedDatabaseKey()
}
}
return super.onOptionsItemSelected(item)
}
fun loadDatabase(databaseUri: Uri?, autoOpenPrompt: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// To get device credential unlock result, only if same database uri
if (databaseUri != null
&& mAdvancedUnlockEnabled) {
activityResult?.let {
if (databaseUri == databaseFileUri) {
advancedUnlockManager?.onActivityResult(it.requestCode, it.resultCode)
} else {
disconnect()
}
} ?: run {
connect(databaseUri)
this.mAutoOpenPrompt = autoOpenPrompt
}
} else {
disconnect()
}
activityResult = null
}
}
/**
* Check unlock availability and change the current mode depending of device's state
*/
fun checkUnlockAvailability() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
allowOpenBiometricPrompt = true
if (PreferencesUtil.isBiometricUnlockEnable(requireContext())) {
mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
// biometric not supported (by API level or hardware) so keep option hidden
// or manually disable
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext())
if (!PreferencesUtil.isAdvancedUnlockEnable(requireContext())
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
} else {
// biometric is available but not configured, show icon but in disabled state with some information
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
} else {
selectMode()
}
}
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(requireContext())) {
mAdvancedUnlockInfoView?.setIconResource(R.drawable.bolt)
if (AdvancedUnlockManager.isDeviceSecure(requireContext())) {
selectMode()
} else {
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
}
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun selectMode() {
// Check if fingerprint well init (be called the first time the fingerprint is configured
// and the activity still active)
if (advancedUnlockManager?.isKeyManagerInitialized != true) {
advancedUnlockManager = AdvancedUnlockManager { requireActivity() }
// callback for fingerprint findings
advancedUnlockManager?.advancedUnlockCallback = this
}
// Recheck to change the mode
if (advancedUnlockManager?.isKeyManagerInitialized != true) {
toggleMode(Mode.KEY_MANAGER_UNAVAILABLE)
} else {
if (mBuilderListener?.conditionToStoreCredential() == true) {
// listen for encryption
toggleMode(Mode.STORE_CREDENTIAL)
} else {
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
// biometric available but no stored password found yet for this DB so show info don't listen
toggleMode(if (containsCipher) {
// listen for decryption
Mode.EXTRACT_CREDENTIAL
} else {
// wait for typing
Mode.WAIT_CREDENTIAL
})
}
} ?: throw IODatabaseException()
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun toggleMode(newBiometricMode: Mode) {
if (newBiometricMode != biometricMode) {
biometricMode = newBiometricMode
initAdvancedUnlockMode()
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initNotAvailable() {
showViews(false)
mAdvancedUnlockInfoView?.setIconViewClickListener(false, null)
}
@RequiresApi(Build.VERSION_CODES.M)
private fun openBiometricSetting() {
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
requireContext().startActivity(Intent(Settings.ACTION_SETTINGS))
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initSecurityUpdateRequired() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
openBiometricSetting()
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initNotConfigured() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.configure_biometric)
setAdvancedUnlockedMessageView("")
openBiometricSetting()
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initKeyManagerNotAvailable() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
openBiometricSetting()
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initWaitData() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
setAdvancedUnlockedMessageView("")
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
requireContext().getString(R.string.credential_before_click_advanced_unlock_button))
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
requireActivity().runOnUiThread {
if (allowOpenBiometricPrompt) {
if (cryptoPrompt.isDeviceCredentialOperation)
keepConnection = true
try {
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt)
} catch (e: Exception) {
Log.e(TAG, "Unable to open advanced unlock prompt", e)
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
}
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initEncryptData() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_store_credential)
setAdvancedUnlockedMessageView("")
advancedUnlockManager?.initEncryptData { cryptoPrompt ->
// Set listener to open the biometric dialog and save credential
mAdvancedUnlockInfoView?.setIconViewClickListener { _ ->
openAdvancedUnlockPrompt(cryptoPrompt)
}
} ?: throw Exception("AdvancedUnlockHelper not initialized")
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initDecryptData() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_unlock_database)
setAdvancedUnlockedMessageView("")
advancedUnlockManager?.let { unlockHelper ->
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
cipherDatabase?.let {
unlockHelper.initDecryptData(it.specParameters) { cryptoPrompt ->
// Set listener to open the biometric dialog and check credential
mAdvancedUnlockInfoView?.setIconViewClickListener { _ ->
openAdvancedUnlockPrompt(cryptoPrompt)
}
// Auto open the biometric prompt
if (mAutoOpenPrompt) {
mAutoOpenPrompt = false
openAdvancedUnlockPrompt(cryptoPrompt)
}
}
} ?: deleteEncryptedDatabaseKey()
}
} ?: throw IODatabaseException()
} ?: throw Exception("AdvancedUnlockHelper not initialized")
}
@Synchronized
fun initAdvancedUnlockMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mAllowAdvancedUnlockMenu = false
when (biometricMode) {
Mode.BIOMETRIC_UNAVAILABLE -> initNotAvailable()
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired()
Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
Mode.WAIT_CREDENTIAL -> initWaitData()
Mode.STORE_CREDENTIAL -> initEncryptData()
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
}
invalidateBiometricMenu()
}
}
private fun invalidateBiometricMenu() {
// Show fingerprint key deletion
if (!mAddBiometricMenuInProgress) {
mAddBiometricMenuInProgress = true
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
mAllowAdvancedUnlockMenu = containsCipher
&& (biometricMode != Mode.BIOMETRIC_UNAVAILABLE
&& biometricMode != Mode.KEY_MANAGER_UNAVAILABLE)
mAddBiometricMenuInProgress = false
requireActivity().invalidateOptionsMenu()
}
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun connect(databaseUri: Uri) {
showViews(true)
this.databaseFileUri = databaseUri
cipherDatabaseListener = object: CipherDatabaseAction.DatabaseListener {
override fun onDatabaseCleared() {
deleteEncryptedDatabaseKey()
}
}
cipherDatabaseAction.apply {
reloadPreferences()
cipherDatabaseListener?.let {
registerDatabaseListener(it)
}
}
checkUnlockAvailability()
}
@RequiresApi(Build.VERSION_CODES.M)
fun disconnect(hideViews: Boolean = true,
closePrompt: Boolean = true) {
this.databaseFileUri = null
// Close the biometric prompt
allowOpenBiometricPrompt = false
if (closePrompt)
advancedUnlockManager?.closeBiometricPrompt()
cipherDatabaseListener?.let {
cipherDatabaseAction.unregisterDatabaseListener(it)
}
biometricMode = Mode.BIOMETRIC_UNAVAILABLE
if (hideViews) {
showViews(false)
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun deleteEncryptedDatabaseKey() {
allowOpenBiometricPrompt = false
mAdvancedUnlockInfoView?.setIconViewClickListener(false, null)
advancedUnlockManager?.closeBiometricPrompt()
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
checkUnlockAvailability()
}
}
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
requireActivity().runOnUiThread {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
setAdvancedUnlockedMessageView(errString.toString())
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationFailed() {
requireActivity().runOnUiThread {
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationSucceeded() {
requireActivity().runOnUiThread {
when (biometricMode) {
Mode.BIOMETRIC_UNAVAILABLE -> {
}
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> {
}
Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> {
}
Mode.KEY_MANAGER_UNAVAILABLE -> {
}
Mode.WAIT_CREDENTIAL -> {
}
Mode.STORE_CREDENTIAL -> {
// newly store the entered password in encrypted way
mBuilderListener?.retrieveCredentialForEncryption()?.let { credential ->
advancedUnlockManager?.encryptData(credential)
}
AdvancedUnlockNotificationService.startServiceForTimeout(requireContext())
}
Mode.EXTRACT_CREDENTIAL -> {
// retrieve the encrypted value from preferences
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
cipherDatabase?.encryptedValue?.let { value ->
advancedUnlockManager?.decryptData(value)
} ?: deleteEncryptedDatabaseKey()
}
} ?: throw IODatabaseException()
}
}
}
}
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
databaseFileUri?.let { databaseUri ->
mBuilderListener?.onCredentialEncrypted(databaseUri, encryptedValue, ivSpec)
}
}
override fun handleDecryptedResult(decryptedValue: String) {
// Load database directly with password retrieve
databaseFileUri?.let {
mBuilderListener?.onCredentialDecrypted(it, decryptedValue)
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onInvalidKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
}
override fun onGenericException(e: Exception) {
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
setAdvancedUnlockedMessageView(errorMessage)
}
private fun showViews(show: Boolean) {
requireActivity().runOnUiThread {
mAdvancedUnlockInfoView?.visibility = if (show)
View.VISIBLE
else {
View.GONE
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedTitleView(textId: Int) {
requireActivity().runOnUiThread {
mAdvancedUnlockInfoView?.setTitle(textId)
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedMessageView(textId: Int) {
requireActivity().runOnUiThread {
mAdvancedUnlockInfoView?.setMessage(textId)
}
}
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
requireActivity().runOnUiThread {
mAdvancedUnlockInfoView?.message = text
}
}
fun performEducation(passwordActivityEducation: PasswordActivityEducation,
readOnlyEducationPerformed: Boolean,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null) {
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)
}
}
enum class Mode {
BIOMETRIC_UNAVAILABLE,
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED,
KEY_MANAGER_UNAVAILABLE,
WAIT_CREDENTIAL,
STORE_CREDENTIAL,
EXTRACT_CREDENTIAL
}
interface BuilderListener {
fun retrieveCredentialForEncryption(): String
fun conditionToStoreCredential(): Boolean
fun onCredentialEncrypted(databaseUri: Uri, encryptedCredential: String, ivSpec: String)
fun onCredentialDecrypted(databaseUri: Uri, decryptedCredential: String)
}
override fun onPause() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!keepConnection) {
// If close prompt, bug "user not authenticated in Android R"
disconnect(false)
advancedUnlockManager = null
}
}
super.onPause()
}
override fun onDestroyView() {
mAdvancedUnlockInfoView = null
super.onDestroyView()
}
override fun onDestroy() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
disconnect()
advancedUnlockManager = null
mBuilderListener = null
}
super.onDestroy()
}
override fun onDetach() {
mBuilderListener = null
super.onDetach()
}
companion object {
private val TAG = AdvancedUnlockFragment::class.java.name
}
}

View File

@@ -0,0 +1,465 @@
/*
* 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.biometric
import android.app.Activity
import android.app.KeyguardManager
import android.content.Context
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties
import android.util.Base64
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.*
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.security.KeyStore
import java.security.UnrecoverableKeyException
import java.util.concurrent.Executors
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
@RequiresApi(api = Build.VERSION_CODES.M)
class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) {
private var keyStore: KeyStore? = null
private var keyGenerator: KeyGenerator? = null
private var cipher: Cipher? = null
private var biometricPrompt: BiometricPrompt? = null
private var authenticationCallback = object: BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
advancedUnlockCallback?.onAuthenticationSucceeded()
}
override fun onAuthenticationFailed() {
advancedUnlockCallback?.onAuthenticationFailed()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
advancedUnlockCallback?.onAuthenticationError(errorCode, errString)
}
}
var advancedUnlockCallback: AdvancedUnlockCallback? = null
private var isKeyManagerInit = false
private val biometricUnlockEnable = PreferencesUtil.isBiometricUnlockEnable(retrieveContext())
private val deviceCredentialUnlockEnable = PreferencesUtil.isDeviceCredentialUnlockEnable(retrieveContext())
val isKeyManagerInitialized: Boolean
get() {
if (!isKeyManagerInit) {
advancedUnlockCallback?.onGenericException(Exception("Biometric not initialized"))
}
return isKeyManagerInit
}
private fun isBiometricOperation(): Boolean {
return biometricUnlockEnable || isDeviceCredentialBiometricOperation()
}
// Since Android 30, device credential is also a biometric operation
private fun isDeviceCredentialOperation(): Boolean {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.R
&& deviceCredentialUnlockEnable
}
private fun isDeviceCredentialBiometricOperation(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& deviceCredentialUnlockEnable
}
init {
if (isDeviceSecure(retrieveContext())
&& (biometricUnlockEnable || deviceCredentialUnlockEnable)) {
try {
this.keyStore = KeyStore.getInstance(ADVANCED_UNLOCK_KEYSTORE)
this.keyGenerator = KeyGenerator.getInstance(ADVANCED_UNLOCK_KEY_ALGORITHM, ADVANCED_UNLOCK_KEYSTORE)
this.cipher = Cipher.getInstance(
ADVANCED_UNLOCK_KEY_ALGORITHM + "/"
+ ADVANCED_UNLOCK_BLOCKS_MODES + "/"
+ ADVANCED_UNLOCK_ENCRYPTION_PADDING)
isKeyManagerInit = (keyStore != null
&& keyGenerator != null
&& cipher != null)
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize the keystore", e)
isKeyManagerInit = false
advancedUnlockCallback?.onGenericException(e)
}
} else {
// really not much to do when no fingerprint support found
isKeyManagerInit = false
}
}
private fun getSecretKey(): SecretKey? {
if (!isKeyManagerInitialized) {
return null
}
try {
// Create new key if needed
keyStore?.let { keyStore ->
keyStore.load(null)
try {
if (!keyStore.containsAlias(ADVANCED_UNLOCK_KEYSTORE_KEY)) {
// Set the alias of the entry in Android KeyStore where the key will appear
// and the constrains (purposes) in the constructor of the Builder
keyGenerator?.init(
KeyGenParameterSpec.Builder(
ADVANCED_UNLOCK_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
// Require the user to authenticate with a fingerprint to authorize every use
// of the key, don't use it for device credential because it's the user authentication
.apply {
if (biometricUnlockEnable) {
setUserAuthenticationRequired(true)
}
}
.build())
keyGenerator?.generateKey()
}
} catch (e: Exception) {
Log.e(TAG, "Unable to create a key in keystore", e)
advancedUnlockCallback?.onGenericException(e)
}
return keyStore.getKey(ADVANCED_UNLOCK_KEYSTORE_KEY, null) as SecretKey?
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve the key in keystore", e)
advancedUnlockCallback?.onGenericException(e)
}
return null
}
fun initEncryptData(actionIfCypherInit
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
if (!isKeyManagerInitialized) {
return
}
try {
getSecretKey()?.let { secretKey ->
cipher?.let { cipher ->
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
actionIfCypherInit.invoke(
AdvancedUnlockCryptoPrompt(
cipher,
R.string.advanced_unlock_prompt_store_credential_title,
R.string.advanced_unlock_prompt_store_credential_message,
isDeviceCredentialOperation(), isBiometricOperation())
)
}
}
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
advancedUnlockCallback?.onInvalidKeyException(unrecoverableKeyException)
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize encrypt data", e)
advancedUnlockCallback?.onGenericException(e)
}
}
fun encryptData(value: String) {
if (!isKeyManagerInitialized) {
return
}
try {
val encrypted = cipher?.doFinal(value.toByteArray())
val encryptedBase64 = Base64.encodeToString(encrypted, Base64.NO_WRAP)
// passes updated iv spec on to callback so this can be stored for decryption
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
val ivSpecValue = Base64.encodeToString(spec.iv, Base64.NO_WRAP)
advancedUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to encrypt data", e)
advancedUnlockCallback?.onGenericException(e)
}
}
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
if (!isKeyManagerInitialized) {
return
}
try {
// important to restore spec here that was used for decryption
val iv = Base64.decode(ivSpecValue, Base64.NO_WRAP)
val spec = IvParameterSpec(iv)
getSecretKey()?.let { secretKey ->
cipher?.let { cipher ->
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
actionIfCypherInit.invoke(
AdvancedUnlockCryptoPrompt(
cipher,
R.string.advanced_unlock_prompt_extract_credential_title,
null,
isDeviceCredentialOperation(), isBiometricOperation())
)
}
}
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
deleteKeystoreKey()
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize decrypt data", e)
advancedUnlockCallback?.onGenericException(e)
}
}
fun decryptData(encryptedValue: String) {
if (!isKeyManagerInitialized) {
return
}
try {
// actual decryption here
val encrypted = Base64.decode(encryptedValue, Base64.NO_WRAP)
cipher?.doFinal(encrypted)?.let { decrypted ->
advancedUnlockCallback?.handleDecryptedResult(String(decrypted))
}
} catch (badPaddingException: BadPaddingException) {
Log.e(TAG, "Unable to decrypt data", badPaddingException)
advancedUnlockCallback?.onInvalidKeyException(badPaddingException)
} catch (e: Exception) {
Log.e(TAG, "Unable to decrypt data", e)
advancedUnlockCallback?.onGenericException(e)
}
}
fun deleteKeystoreKey() {
try {
keyStore?.load(null)
keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
} catch (e: Exception) {
Log.e(TAG, "Unable to delete entry key in keystore", e)
advancedUnlockCallback?.onGenericException(e)
}
}
@Suppress("DEPRECATION")
@Synchronized
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
// Init advanced unlock prompt
if (biometricPrompt == null) {
biometricPrompt = BiometricPrompt(retrieveContext(),
Executors.newSingleThreadExecutor(),
authenticationCallback)
}
val promptTitle = retrieveContext().getString(cryptoPrompt.promptTitleId)
val promptDescription = cryptoPrompt.promptDescriptionId?.let { descriptionId ->
retrieveContext().getString(descriptionId)
} ?: ""
if (cryptoPrompt.isBiometricOperation) {
val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
setTitle(promptTitle)
if (promptDescription.isNotEmpty())
setDescription(promptDescription)
setConfirmationRequired(false)
if (isDeviceCredentialBiometricOperation()) {
setAllowedAuthenticators(DEVICE_CREDENTIAL)
} else {
setNegativeButtonText(retrieveContext().getString(android.R.string.cancel))
}
}.build()
biometricPrompt?.authenticate(
promptInfoExtractCredential,
BiometricPrompt.CryptoObject(cryptoPrompt.cipher))
}
else if (cryptoPrompt.isDeviceCredentialOperation) {
val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java)
retrieveContext().startActivityForResult(
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription),
REQUEST_DEVICE_CREDENTIAL)
}
}
@Synchronized
fun onActivityResult(requestCode: Int, resultCode: Int) {
if (requestCode == REQUEST_DEVICE_CREDENTIAL) {
if (resultCode == Activity.RESULT_OK) {
advancedUnlockCallback?.onAuthenticationSucceeded()
} else {
advancedUnlockCallback?.onAuthenticationFailed()
}
}
}
fun closeBiometricPrompt() {
biometricPrompt?.cancelAuthentication()
}
interface AdvancedUnlockErrorCallback {
fun onInvalidKeyException(e: Exception)
fun onGenericException(e: Exception)
}
interface AdvancedUnlockCallback : AdvancedUnlockErrorCallback {
fun onAuthenticationSucceeded()
fun onAuthenticationFailed()
fun onAuthenticationError(errorCode: Int, errString: CharSequence)
fun handleEncryptedResult(encryptedValue: String, ivSpec: String)
fun handleDecryptedResult(decryptedValue: String)
}
companion object {
private val TAG = AdvancedUnlockManager::class.java.name
private const val ADVANCED_UNLOCK_KEYSTORE = "AndroidKeyStore"
private const val ADVANCED_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
private const val ADVANCED_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
private const val REQUEST_DEVICE_CREDENTIAL = 556
@RequiresApi(api = Build.VERSION_CODES.M)
fun canAuthenticate(context: Context): Int {
return try {
BiometricManager.from(context).canAuthenticate(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
BIOMETRIC_STRONG or DEVICE_CREDENTIAL
} else {
BIOMETRIC_STRONG
}
)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
try {
BiometricManager.from(context).canAuthenticate(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
BIOMETRIC_WEAK or DEVICE_CREDENTIAL
} else {
BIOMETRIC_WEAK
}
)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
}
}
}
@RequiresApi(api = Build.VERSION_CODES.M)
fun isDeviceSecure(context: Context): Boolean {
val keyguardManager = ContextCompat.getSystemService(context, KeyguardManager::class.java)
return keyguardManager?.isDeviceSecure ?: false
}
@RequiresApi(api = Build.VERSION_CODES.M)
fun biometricUnlockSupported(context: Context): Boolean {
val biometricCanAuthenticate = try {
BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
try {
BiometricManager.from(context).canAuthenticate(BIOMETRIC_WEAK)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
}
}
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
)
}
@RequiresApi(api = Build.VERSION_CODES.M)
fun deviceCredentialUnlockSupported(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ContextCompat.getSystemService(context, KeyguardManager::class.java)?.apply {
return isDeviceSecure
}
}
return false
}
/**
* Remove entry key in keystore
*/
@RequiresApi(api = Build.VERSION_CODES.M)
fun deleteEntryKeyInKeystoreForBiometric(fragmentActivity: FragmentActivity,
advancedCallback: AdvancedUnlockErrorCallback) {
AdvancedUnlockManager{ fragmentActivity }.apply {
advancedUnlockCallback = object : AdvancedUnlockCallback {
override fun onAuthenticationSucceeded() {}
override fun onAuthenticationFailed() {}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {}
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {}
override fun handleDecryptedResult(decryptedValue: String) {}
override fun onInvalidKeyException(e: Exception) {
advancedCallback.onInvalidKeyException(e)
}
override fun onGenericException(e: Exception) {
advancedCallback.onGenericException(e)
}
}
deleteKeystoreKey()
}
}
}
}

View File

@@ -1,369 +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.biometric
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import android.widget.CompoundButton
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricConstants
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
@RequiresApi(api = Build.VERSION_CODES.M)
class AdvancedUnlockedManager(var context: FragmentActivity,
var databaseFileUri: Uri,
private var advancedUnlockInfoView: AdvancedUnlockInfoView?,
private var checkboxPasswordView: CompoundButton?,
private var onCheckedPasswordChangeListener: CompoundButton.OnCheckedChangeListener? = null,
var passwordView: TextView?,
private var loadDatabaseAfterRegisterCredentials: (encryptedPassword: String?, ivSpec: String?) -> Unit,
private var loadDatabaseAfterRetrieveCredentials: (decryptedPassword: String?) -> Unit)
: BiometricUnlockDatabaseHelper.BiometricUnlockCallback {
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
private var biometricMode: Mode = Mode.UNAVAILABLE
/**
* Manage setting to auto open biometric prompt
*/
private var biometricPromptAutoOpenPreference = PreferencesUtil.isBiometricPromptAutoOpenEnable(context)
var isBiometricPromptAutoOpenEnable: Boolean = true
get() {
return field && biometricPromptAutoOpenPreference
}
// Variable to check if the prompt can be open (if the right activity is currently shown)
// checkBiometricAvailability() allows open biometric prompt and onDestroy() removes the authorization
private var allowOpenBiometricPrompt = false
private var cipherDatabaseAction = CipherDatabaseAction.getInstance(context.applicationContext)
init {
// Add a check listener to change fingerprint mode
checkboxPasswordView?.setOnCheckedChangeListener { compoundButton, checked ->
checkBiometricAvailability()
// Add old listener to enable the button, only be call here because of onCheckedChange bug
onCheckedPasswordChangeListener?.onCheckedChanged(compoundButton, checked)
}
}
/**
* Check biometric availability and change the current mode depending of device's state
*/
fun checkBiometricAvailability() {
// biometric not supported (by API level or hardware) so keep option hidden
// or manually disable
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate()
allowOpenBiometricPrompt = true
if (!PreferencesUtil.isBiometricUnlockEnable(context)
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
toggleMode(Mode.UNAVAILABLE)
} else {
// biometric is available but not configured, show icon but in disabled state with some information
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
toggleMode(Mode.BIOMETRIC_NOT_CONFIGURED)
} else {
// Check if fingerprint well init (be called the first time the fingerprint is configured
// and the activity still active)
if (biometricUnlockDatabaseHelper?.isKeyManagerInitialized != true) {
biometricUnlockDatabaseHelper = BiometricUnlockDatabaseHelper(context)
// callback for fingerprint findings
biometricUnlockDatabaseHelper?.biometricUnlockCallback = this
biometricUnlockDatabaseHelper?.authenticationCallback = biometricAuthenticationCallback
}
// Recheck to change the mode
if (biometricUnlockDatabaseHelper?.isKeyManagerInitialized != true) {
toggleMode(Mode.KEY_MANAGER_UNAVAILABLE)
} else {
if (checkboxPasswordView?.isChecked == true) {
// listen for encryption
toggleMode(Mode.STORE_CREDENTIAL)
} else {
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
// biometric available but no stored password found yet for this DB so show info don't listen
toggleMode( if (containsCipher) {
// listen for decryption
Mode.EXTRACT_CREDENTIAL
} else {
// wait for typing
Mode.WAIT_CREDENTIAL
})
}
}
}
}
}
}
private fun toggleMode(newBiometricMode: Mode) {
if (newBiometricMode != biometricMode) {
biometricMode = newBiometricMode
initBiometricMode()
}
}
private val biometricAuthenticationCallback = object : BiometricPrompt.AuthenticationCallback () {
override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence) {
context.runOnUiThread {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
setAdvancedUnlockedMessageView(errString.toString())
}
}
override fun onAuthenticationFailed() {
context.runOnUiThread {
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
setAdvancedUnlockedMessageView(R.string.biometric_not_recognized)
}
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
context.runOnUiThread {
when (biometricMode) {
Mode.UNAVAILABLE -> {}
Mode.BIOMETRIC_NOT_CONFIGURED -> {}
Mode.KEY_MANAGER_UNAVAILABLE -> {}
Mode.WAIT_CREDENTIAL -> {}
Mode.STORE_CREDENTIAL -> {
// newly store the entered password in encrypted way
biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString())
}
Mode.EXTRACT_CREDENTIAL -> {
// retrieve the encrypted value from preferences
cipherDatabaseAction.getCipherDatabase(databaseFileUri) {
it?.encryptedValue?.let { value ->
biometricUnlockDatabaseHelper?.decryptData(value)
}
}
}
}
}
}
}
private fun initNotAvailable() {
showFingerPrintViews(false)
advancedUnlockInfoView?.setIconViewClickListener(false, null)
}
private fun initNotConfigured() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.configure_biometric)
setAdvancedUnlockedMessageView("")
advancedUnlockInfoView?.setIconViewClickListener(false) {
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
}
}
private fun initKeyManagerNotAvailable() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
setAdvancedUnlockedMessageView("")
advancedUnlockInfoView?.setIconViewClickListener(false) {
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
}
}
private fun initWaitData() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
setAdvancedUnlockedMessageView("")
advancedUnlockInfoView?.setIconViewClickListener(false) {
biometricAuthenticationCallback.onAuthenticationError(
BiometricConstants.ERROR_UNABLE_TO_PROCESS
, context.getString(R.string.credential_before_click_biometric_button))
}
}
private fun openBiometricPrompt(biometricPrompt: BiometricPrompt?,
cryptoObject: BiometricPrompt.CryptoObject,
promptInfo: BiometricPrompt.PromptInfo) {
context.runOnUiThread {
if (allowOpenBiometricPrompt)
biometricPrompt?.authenticate(promptInfo, cryptoObject)
}
}
private fun initEncryptData() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.open_biometric_prompt_store_credential)
setAdvancedUnlockedMessageView("")
biometricUnlockDatabaseHelper?.initEncryptData { biometricPrompt, cryptoObject, promptInfo ->
cryptoObject?.let { crypto ->
// Set listener to open the biometric dialog and save credential
advancedUnlockInfoView?.setIconViewClickListener { _ ->
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
}
}
}
}
private fun initDecryptData() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.open_biometric_prompt_unlock_database)
setAdvancedUnlockedMessageView("")
if (biometricUnlockDatabaseHelper != null) {
cipherDatabaseAction.getCipherDatabase(databaseFileUri) {
it?.specParameters?.let { specs ->
biometricUnlockDatabaseHelper?.initDecryptData(specs) { biometricPrompt, cryptoObject, promptInfo ->
cryptoObject?.let { crypto ->
// Set listener to open the biometric dialog and check credential
advancedUnlockInfoView?.setIconViewClickListener { _ ->
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
}
// Auto open the biometric prompt
if (isBiometricPromptAutoOpenEnable) {
isBiometricPromptAutoOpenEnable = false
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
}
}
}
}
}
}
}
@Synchronized
fun initBiometricMode() {
when (biometricMode) {
Mode.UNAVAILABLE -> initNotAvailable()
Mode.BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
Mode.WAIT_CREDENTIAL -> initWaitData()
Mode.STORE_CREDENTIAL -> initEncryptData()
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
}
// Show fingerprint key deletion
context.invalidateOptionsMenu()
}
fun destroy() {
// Close the biometric prompt
allowOpenBiometricPrompt = false
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
// Restore the checked listener
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
}
// Only to fix multiple fingerprint menu #332
private var addBiometricMenuInProgress = false
fun inflateOptionsMenu(menuInflater: MenuInflater, menu: Menu) {
if (!addBiometricMenuInProgress) {
addBiometricMenuInProgress = true
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) {
if ((biometricMode != Mode.UNAVAILABLE && biometricMode != Mode.BIOMETRIC_NOT_CONFIGURED)
&& it) {
menuInflater.inflate(R.menu.advanced_unlock, menu)
addBiometricMenuInProgress = false
}
}
}
}
fun deleteEntryKey() {
biometricUnlockDatabaseHelper?.deleteEntryKey()
cipherDatabaseAction.deleteByDatabaseUri(databaseFileUri)
biometricMode = Mode.BIOMETRIC_NOT_CONFIGURED
checkBiometricAvailability()
}
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
loadDatabaseAfterRegisterCredentials.invoke(encryptedValue, ivSpec)
}
override fun handleDecryptedResult(decryptedValue: String) {
// Load database directly with password retrieve
loadDatabaseAfterRetrieveCredentials.invoke(decryptedValue)
}
override fun onInvalidKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.biometric_invalid_key)
}
override fun onBiometricException(e: Exception) {
e.localizedMessage?.let {
setAdvancedUnlockedMessageView(it)
}
}
private fun showFingerPrintViews(show: Boolean) {
context.runOnUiThread {
advancedUnlockInfoView?.visibility = if (show) View.VISIBLE else View.GONE
}
}
private fun setAdvancedUnlockedTitleView(textId: Int) {
context.runOnUiThread {
advancedUnlockInfoView?.setTitle(textId)
}
}
private fun setAdvancedUnlockedMessageView(textId: Int) {
context.runOnUiThread {
advancedUnlockInfoView?.setMessage(textId)
}
}
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
context.runOnUiThread {
advancedUnlockInfoView?.message = text
}
}
enum class Mode {
UNAVAILABLE, BIOMETRIC_NOT_CONFIGURED, KEY_MANAGER_UNAVAILABLE, WAIT_CREDENTIAL, STORE_CREDENTIAL, EXTRACT_CREDENTIAL
}
companion object {
private val TAG = AdvancedUnlockedManager::class.java.name
}
}

View File

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

View File

@@ -42,7 +42,7 @@ class AesKdf : KdfEngine() {
}
}
override val defaultKeyRounds: Long = 6000L
override val defaultKeyRounds: Long = 500000L
override fun getName(resources: Resources): String {
return resources.getString(R.string.kdf_AES)

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.crypto.keyDerivation
import android.content.res.Resources
import androidx.annotation.StringRes
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.stream.bytes16ToUuid
import com.kunzisoft.keepass.utils.UnsignedInt
@@ -27,7 +28,11 @@ import java.io.IOException
import java.security.SecureRandom
import java.util.*
class Argon2Kdf internal constructor() : KdfEngine() {
class Argon2Kdf(private val type: Type) : KdfEngine() {
init {
uuid = type.CIPHER_UUID
}
override val defaultParameters: KdfParameters
get() {
@@ -45,12 +50,8 @@ class Argon2Kdf internal constructor() : KdfEngine() {
override val defaultKeyRounds: Long
get() = DEFAULT_ITERATIONS
init {
uuid = CIPHER_UUID
}
override fun getName(resources: Resources): String {
return resources.getString(R.string.kdf_Argon2)
return resources.getString(type.nameId)
}
@Throws(IOException::class)
@@ -72,7 +73,9 @@ class Argon2Kdf internal constructor() : KdfEngine() {
val secretKey = kdfParameters.getByteArray(PARAM_SECRET_KEY)
val assocData = kdfParameters.getByteArray(PARAM_ASSOC_DATA)
return Argon2Native.transformKey(masterKey,
return Argon2Native.transformKey(
type,
masterKey,
salt,
parallelism,
memory,
@@ -141,9 +144,8 @@ class Argon2Kdf internal constructor() : KdfEngine() {
override val maxParallelism: Long
get() = MAX_PARALLELISM
companion object {
val CIPHER_UUID: UUID = bytes16ToUuid(
enum class Type(val CIPHER_UUID: UUID, @StringRes val nameId: Int) {
ARGON2_D(bytes16ToUuid(
byteArrayOf(0xEF.toByte(),
0x63.toByte(),
0x6D.toByte(),
@@ -159,7 +161,27 @@ class Argon2Kdf internal constructor() : KdfEngine() {
0x03.toByte(),
0xE3.toByte(),
0x0A.toByte(),
0x0C.toByte()))
0x0C.toByte())), R.string.kdf_Argon2d),
ARGON2_ID(bytes16ToUuid(
byteArrayOf(0x9E.toByte(),
0x29.toByte(),
0x8B.toByte(),
0x19.toByte(),
0x56.toByte(),
0xDB.toByte(),
0x47.toByte(),
0x73.toByte(),
0xB2.toByte(),
0x3D.toByte(),
0xFC.toByte(),
0x3E.toByte(),
0xC6.toByte(),
0xF0.toByte(),
0xA1.toByte(),
0xE6.toByte())), R.string.kdf_Argon2id);
}
companion object {
private const val PARAM_SALT = "S" // byte[]
private const val PARAM_PARALLELISM = "P" // UInt32

View File

@@ -26,12 +26,29 @@ import java.io.IOException;
public class Argon2Native {
public static byte[] transformKey(byte[] password, byte[] salt, UnsignedInt parallelism,
enum CType {
ARGON2_D(0),
ARGON2_I(1),
ARGON2_ID(2);
int cValue = 0;
CType(int i) {
cValue = i;
}
}
public static byte[] transformKey(Argon2Kdf.Type type, byte[] password, byte[] salt, UnsignedInt parallelism,
UnsignedInt memory, UnsignedInt iterations, byte[] secretKey,
byte[] associatedData, UnsignedInt version) throws IOException {
NativeLib.INSTANCE.init();
CType cType = CType.ARGON2_D;
if (type.equals(Argon2Kdf.Type.ARGON2_ID))
cType = CType.ARGON2_ID;
return nTransformMasterKey(
cType.cValue,
password,
salt,
parallelism.toKotlinInt(),
@@ -42,7 +59,7 @@ public class Argon2Native {
version.toKotlinInt());
}
private static native byte[] nTransformMasterKey(byte[] password, byte[] salt, int parallelism,
private static native byte[] nTransformMasterKey(int type, byte[] password, byte[] salt, int parallelism,
int memory, int iterations, byte[] secretKey,
byte[] associatedData, int version) throws IOException;
}

View File

@@ -21,5 +21,6 @@ package com.kunzisoft.keepass.crypto.keyDerivation
object KdfFactory {
var aesKdf = AesKdf()
var argon2Kdf = Argon2Kdf()
var argon2dKdf = Argon2Kdf(Argon2Kdf.Type.ARGON2_D)
var argon2idKdf = Argon2Kdf(Argon2Kdf.Type.ARGON2_ID)
}

View File

@@ -169,6 +169,10 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
serviceConnection = null
}
fun isBinded(): Boolean {
return mBinder != null
}
fun registerProgressTask() {
stopDialog()

View File

@@ -431,6 +431,7 @@ class Database {
searchInPasswords = false
searchInUrls = true
searchInNotes = true
searchInOTP = false
searchInOther = true
searchInUUIDs = false
searchInTags = false

View File

@@ -411,8 +411,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
*/
/**
* Retrieve generated entry info,
* Remove parameter fields and add auto generated elements in auto custom fields
* Retrieve generated entry info.
* If are not [raw] data, remove parameter fields and add auto generated elements in auto custom fields
*/
fun getEntryInfo(database: Database?, raw: Boolean = false): EntryInfo {
val entryInfo = EntryInfo()
@@ -464,6 +464,9 @@ class Entry : Node, EntryVersionedInterface<Group> {
database?.binaryPool?.let { binaryPool ->
addAttachments(binaryPool, newEntryInfo.attachments)
}
// Update date time
lastAccessTime = DateInstant()
lastModificationTime = DateInstant()
database?.stopManageEntry(this)
}
@@ -497,5 +500,12 @@ class Entry : Node, EntryVersionedInterface<Group> {
}
const val PMS_TAN_ENTRY = "<TAN>"
/**
* True if [field] name is not a standard field name
*/
fun newExtraFieldNameAllowed(field: Field): Boolean {
return EntryKDBX.newCustomNameAllowed(field.name)
}
}
}

View File

@@ -186,6 +186,10 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
override fun isInRecycleBin(group: GroupKDB): Boolean {
var currentGroup: GroupKDB? = group
// Init backup group variable
if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
findBackupGroupId()
if (backupGroup == null)
return false
@@ -203,17 +207,21 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return false
}
/**
* Ensure that the backup tree exists if enabled, and create it
* if it doesn't exist
*/
fun ensureBackupExists() {
private fun findBackupGroupId() {
rootGroups.forEach { currentGroup ->
if (currentGroup.level == 0
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
backupGroupId = currentGroup.id
}
}
}
/**
* Ensure that the backup tree exists if enabled, and create it
* if it doesn't exist
*/
fun ensureBackupExists() {
findBackupGroupId()
if (backupGroup == null) {
// Create recycle bin

View File

@@ -113,7 +113,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
init {
kdfList.add(KdfFactory.aesKdf)
kdfList.add(KdfFactory.argon2Kdf)
kdfList.add(KdfFactory.argon2dKdf)
kdfList.add(KdfFactory.argon2idKdf)
}
constructor()

View File

@@ -369,6 +369,14 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
const val STR_URL = "URL"
const val STR_NOTES = "Notes"
fun newCustomNameAllowed(name: String): Boolean {
return !(name.equals(STR_TITLE, true)
|| name.equals(STR_USERNAME, true)
|| name.equals(STR_PASSWORD, true)
|| name.equals(STR_URL, true)
|| name.equals(STR_NOTES, true))
}
@JvmField
val CREATOR: Parcelable.Creator<EntryKDBX> = object : Parcelable.Creator<EntryKDBX> {
override fun createFromParcel(parcel: Parcel): EntryKDBX {

View File

@@ -28,14 +28,13 @@ import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorK
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDBX
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.model.getSearchString
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
class SearchHelper {
companion object {
const val MAX_SEARCH_ENTRY = 6
const val MAX_SEARCH_ENTRY = 10
/**
* Utility method to perform actions if item is found or not after an auto search in [database]
@@ -53,7 +52,7 @@ class SearchHelper {
&& !searchInfo.containsOnlyNullValues()) {
// If search provide results
database.createVirtualGroupFromSearchInfo(
searchInfo.getSearchString(context),
searchInfo.toString(),
PreferencesUtil.omitBackup(context),
MAX_SEARCH_ENTRY
)?.let { searchGroup ->

View File

@@ -33,6 +33,7 @@ class SearchParameters {
var searchInUrls = true
var searchInGroupNames = false
var searchInNotes = true
var searchInOTP = false
var searchInOther = true
var searchInUUIDs = false
var searchInTags = true
@@ -54,6 +55,7 @@ class SearchParameters {
this.searchInUrls = source.searchInUrls
this.searchInGroupNames = source.searchInGroupNames
this.searchInNotes = source.searchInNotes
this.searchInOTP = source.searchInOTP
this.searchInOther = source.searchInOther
this.searchInUUIDs = source.searchInUUIDs
this.searchInTags = source.searchInTags
@@ -69,6 +71,7 @@ class SearchParameters {
searchInUrls = false
searchInGroupNames = false
searchInNotes = false
searchInOTP = false
searchInOther = false
searchInUUIDs = false
searchInTags = false

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.search.iterator
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.otp.OtpEntryFields
import java.util.*
import kotlin.collections.Map.Entry
@@ -75,6 +76,7 @@ class EntrySearchStringIteratorKDBX(
EntryKDBX.STR_PASSWORD -> mSearchParameters.searchInPasswords
EntryKDBX.STR_URL -> mSearchParameters.searchInUrls
EntryKDBX.STR_NOTES -> mSearchParameters.searchInNotes
OtpEntryFields.OTP_FIELD -> mSearchParameters.searchInOTP
else -> mSearchParameters.searchInOther
}
}

View File

@@ -86,8 +86,8 @@ class PasswordActivityEducation(activity: Activity)
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationBiometricPerformed(activity),
TapTarget.forView(educationView,
activity.getString(R.string.education_biometric_title),
activity.getString(R.string.education_biometric_summary))
activity.getString(R.string.education_advanced_unlock_title),
activity.getString(R.string.education_advanced_unlock_summary))
.textColorInt(Color.WHITE)
.tintTarget(false)
.cancelable(true),

View File

@@ -42,6 +42,7 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.*
@@ -184,7 +185,6 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
}
}
@Suppress("DEPRECATION")
private fun switchToPreviousKeyboard() {
var imeManager: InputMethodManager? = null
try {
@@ -244,6 +244,14 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
if (entryInfoKey != null) {
currentInputConnection.commitText(entryInfoKey!!.password, 1)
}
val otpFieldExists = entryInfoKey?.containsCustomField(OTP_TOKEN_FIELD) ?: false
actionGoAutomatically(!otpFieldExists)
}
KEY_OTP -> {
if (entryInfoKey != null) {
currentInputConnection.commitText(
entryInfoKey!!.getGeneratedFieldValue(OTP_TOKEN_FIELD), 1)
}
actionGoAutomatically()
}
KEY_URL -> {
@@ -255,7 +263,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
KEY_FIELDS -> {
if (entryInfoKey != null) {
fieldsAdapter?.apply {
setFields(entryInfoKey!!.customFields)
setFields(entryInfoKey!!.customFields.filter { it.name != OTP_TOKEN_FIELD})
notifyDataSetChanged()
}
}
@@ -273,10 +281,11 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
currentInputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB))
}
private fun actionGoAutomatically() {
private fun actionGoAutomatically(switchToPreviousKeyboardIfAllowed: Boolean = true) {
if (PreferencesUtil.isAutoGoActionEnable(this)) {
currentInputConnection.performEditorAction(EditorInfo.IME_ACTION_GO)
if (PreferencesUtil.isKeyboardPreviousFillInEnable(this)) {
if (switchToPreviousKeyboardIfAllowed
&& PreferencesUtil.isKeyboardPreviousFillInEnable(this)) {
switchToPreviousKeyboard()
}
}
@@ -327,6 +336,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
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

View File

@@ -22,12 +22,15 @@ package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.Parcelable
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.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import java.util.*
import kotlin.collections.ArrayList
class EntryInfo : Parcelable {
@@ -88,21 +91,92 @@ class EntryInfo : Parcelable {
return customFields.any { !it.protectedValue.isProtected }
}
fun containsCustomField(label: String): Boolean {
return customFields.lastOrNull { it.name == label } != null
}
fun isAutoGeneratedField(field: Field): Boolean {
return field.name == OTP_TOKEN_FIELD
}
fun getGeneratedFieldValue(label: String): String {
otpModel?.let {
if (label == OTP_TOKEN_FIELD) {
if (label == OTP_TOKEN_FIELD) {
otpModel?.let {
return OtpElement(it).token
}
}
return customFields.lastOrNull { it.name == label }?.protectedValue?.toString() ?: ""
}
private fun addUniqueField(field: Field, number: Int = 0) {
var sameName = false
var sameValue = false
val suffix = if (number > 0) "_$number" else ""
customFields.forEach { currentField ->
// Not write the same data again
if (currentField.protectedValue.stringValue == field.protectedValue.stringValue) {
sameValue = true
return
}
// Same name but new value, create a new suffix
if (currentField.name == field.name + suffix) {
sameName = true
addUniqueField(field, number + 1)
return
}
}
if (!sameName && !sameValue)
(customFields as ArrayList<Field>).add(Field(field.name + suffix, field.protectedValue))
}
fun saveSearchInfo(database: Database?, searchInfo: SearchInfo) {
searchInfo.otpString?.let { otpString ->
// Replace the OTP field
OtpEntryFields.parseOTPUri(otpString)?.let { otpElement ->
if (title.isEmpty())
title = otpElement.issuer
if (username.isEmpty())
username = otpElement.name
// Add OTP field
val mutableCustomFields = customFields as ArrayList<Field>
val otpField = OtpEntryFields.buildOtpField(otpElement, null, null)
if (mutableCustomFields.contains(otpField)) {
mutableCustomFields.remove(otpField)
}
mutableCustomFields.add(otpField)
}
} ?: searchInfo.webDomain?.let { webDomain ->
// If unable to save web domain in custom field or URL not populated, save in URL
val scheme = searchInfo.webScheme
val webScheme = if (scheme.isNullOrEmpty()) "http" else scheme
val webDomainToStore = "$webScheme://$webDomain"
if (database?.allowEntryCustomFields() != true || url.isEmpty()) {
url = webDomainToStore
}
else if (url != webDomainToStore){
// Save web domain in custom field
addUniqueField(Field(WEB_DOMAIN_FIELD_NAME,
ProtectedString(false, webDomainToStore)),
1 // Start to one because URL is a standard field name
)
}
} ?: run {
// Save application id in custom field
if (database?.allowEntryCustomFields() == true) {
searchInfo.applicationId?.let { applicationId ->
addUniqueField(Field(APPLICATION_ID_FIELD_NAME,
ProtectedString(false, applicationId))
)
}
}
}
}
companion object {
const val WEB_DOMAIN_FIELD_NAME = "URL"
const val APPLICATION_ID_FIELD_NAME = "AndroidApp"
@JvmField
val CREATOR: Parcelable.Creator<EntryInfo> = object : Parcelable.Creator<EntryInfo> {
override fun createFromParcel(parcel: Parcel): EntryInfo {

View File

@@ -0,0 +1,35 @@
package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.Parcelable
data class RegisterInfo(val searchInfo: SearchInfo,
val username: String?,
val password: String?): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readParcelable(SearchInfo::class.java.classLoader) ?: SearchInfo(),
parcel.readString() ?: "",
parcel.readString() ?: "") {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(searchInfo, flags)
parcel.writeString(username)
parcel.writeString(password)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<RegisterInfo> {
override fun createFromParcel(parcel: Parcel): RegisterInfo {
return RegisterInfo(parcel)
}
override fun newArray(size: Int): Array<RegisterInfo?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -2,11 +2,16 @@ package com.kunzisoft.keepass.model
import android.content.Context
import android.content.res.Resources
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.ObjectNameResource
import com.kunzisoft.keepass.utils.UriUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
class SearchInfo : ObjectNameResource, Parcelable {
@@ -18,22 +23,40 @@ class SearchInfo : ObjectNameResource, Parcelable {
else -> null
}
}
// A web domain can also containing an IP
var webDomain: String? = null
set(value) {
field = when {
value == null -> null
Regex(WEB_DOMAIN_REGEX).matches(value) -> value
Regex(WEB_IP_REGEX).matches(value) -> value
else -> null
}
}
var webScheme: String? = null
get() {
return if (webDomain == null) null else field
}
var otpString: String? = null
constructor()
constructor(toCopy: SearchInfo?) {
applicationId = toCopy?.applicationId
webDomain = toCopy?.webDomain
webScheme = toCopy?.webScheme
otpString = toCopy?.otpString
}
private constructor(parcel: Parcel) {
val readAppId = parcel.readString()
applicationId = if (readAppId.isNullOrEmpty()) null else readAppId
val readDomain = parcel.readString()
webDomain = if (readDomain.isNullOrEmpty()) null else readDomain
val readScheme = parcel.readString()
webScheme = if (readScheme.isNullOrEmpty()) null else readScheme
val readOtp = parcel.readString()
otpString = if (readOtp.isNullOrEmpty()) null else readOtp
}
override fun describeContents(): Int {
@@ -43,14 +66,24 @@ class SearchInfo : ObjectNameResource, Parcelable {
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(applicationId ?: "")
parcel.writeString(webDomain ?: "")
parcel.writeString(webScheme ?: "")
parcel.writeString(otpString ?: "")
}
override fun getName(resources: Resources): String {
otpString?.let { otpString ->
OtpEntryFields.parseOTPUri(otpString)?.let { otpElement ->
return "${otpElement.type} (${Uri.decode(otpElement.issuer)}:${Uri.decode(otpElement.name)})"
}
}
return toString()
}
fun containsOnlyNullValues(): Boolean {
return applicationId == null && webDomain == null
return applicationId == null
&& webDomain == null
&& webScheme == null
&& otpString == null
}
override fun equals(other: Any?): Boolean {
@@ -61,6 +94,8 @@ class SearchInfo : ObjectNameResource, Parcelable {
if (applicationId != other.applicationId) return false
if (webDomain != other.webDomain) return false
if (webScheme != other.webScheme) return false
if (otpString != other.otpString) return false
return true
}
@@ -68,17 +103,20 @@ class SearchInfo : ObjectNameResource, Parcelable {
override fun hashCode(): Int {
var result = applicationId?.hashCode() ?: 0
result = 31 * result + (webDomain?.hashCode() ?: 0)
result = 31 * result + (webScheme?.hashCode() ?: 0)
result = 31 * result + (otpString?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return webDomain ?: applicationId ?: ""
return otpString ?: webDomain ?: applicationId ?: ""
}
companion object {
// https://gist.github.com/rishabhmhjn/8663966
const val APPLICATION_ID_REGEX = "^(?:[a-zA-Z]+(?:\\d*[a-zA-Z_]*)*)(?:\\.[a-zA-Z]+(?:\\d*[a-zA-Z_]*)*)+\$"
const val WEB_DOMAIN_REGEX = "^(?!://)([a-zA-Z0-9-_]+\\.)*[a-zA-Z0-9][a-zA-Z0-9-_]+\\.[a-zA-Z]{2,11}?\$"
const val WEB_IP_REGEX = "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\$"
@JvmField
val CREATOR: Parcelable.Creator<SearchInfo> = object : Parcelable.Creator<SearchInfo> {
@@ -90,16 +128,28 @@ class SearchInfo : ObjectNameResource, Parcelable {
return arrayOfNulls(size)
}
}
/**
* Get the concrete web domain AKA without sub domain if needed
*/
fun getConcreteWebDomain(context: Context,
webDomain: String?,
concreteWebDomain: (String?) -> Unit) {
CoroutineScope(Dispatchers.Main).launch {
if (webDomain != null) {
// Warning, web domain can contains IP, don't crop in this case
if (PreferencesUtil.searchSubdomains(context)
|| Regex(WEB_IP_REGEX).matches(webDomain)) {
concreteWebDomain.invoke(webDomain)
} else {
val publicSuffixList = PublicSuffixList(context)
concreteWebDomain.invoke(publicSuffixList
.getPublicSuffixPlusOne(webDomain).await())
}
} else {
concreteWebDomain.invoke(null)
}
}
}
}
}
fun SearchInfo.getSearchString(context: Context): String {
return run {
if (!PreferencesUtil.searchSubdomains(context))
UriUtil.getWebDomainWithoutSubDomain(webDomain)
else
webDomain
}
?: applicationId
?: ""
}

View File

@@ -0,0 +1,143 @@
package com.kunzisoft.keepass.notifications
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Binder
import android.os.IBinder
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import kotlinx.coroutines.*
class AdvancedUnlockNotificationService : NotificationService() {
private lateinit var mTempCipherDao: ArrayList<CipherDatabaseEntity>
private var mActionTaskBinder = AdvancedUnlockBinder()
private var notificationTimeoutMilliSecs: Long = 0
private var mTimerJob: Job? = null
inner class AdvancedUnlockBinder: Binder() {
fun getCipherDatabase(databaseUri: Uri): CipherDatabaseEntity? {
return mTempCipherDao.firstOrNull { it.databaseUri == databaseUri.toString()}
}
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity) {
val cipherDatabaseRetrieve = mTempCipherDao.firstOrNull { it.databaseUri == cipherDatabaseEntity.databaseUri }
cipherDatabaseRetrieve?.replaceContent(cipherDatabaseEntity)
?: mTempCipherDao.add(cipherDatabaseEntity)
}
fun deleteByDatabaseUri(databaseUri: Uri) {
mTempCipherDao.firstOrNull { it.databaseUri == databaseUri.toString() }?.let {
mTempCipherDao.remove(it)
}
}
fun deleteAll() {
mTempCipherDao.clear()
}
}
override val notificationId: Int = 593
override fun retrieveChannelId(): String {
return CHANNEL_ADVANCED_UNLOCK_ID
}
override fun retrieveChannelName(): String {
return getString(R.string.advanced_unlock)
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return mActionTaskBinder
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val deleteIntent = Intent(this, AdvancedUnlockNotificationService::class.java).apply {
action = ACTION_REMOVE_KEYS
}
val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val biometricUnlockEnabled = PreferencesUtil.isBiometricUnlockEnable(this)
val notificationBuilder = buildNewNotification().apply {
setSmallIcon(if (biometricUnlockEnabled) {
R.drawable.notification_ic_fingerprint_unlock_24dp
} else {
R.drawable.notification_ic_device_unlock_24dp
})
intent?.let {
setContentTitle(getString(R.string.advanced_unlock))
}
setContentText(getString(R.string.advanced_unlock_tap_delete))
setContentIntent(pendingDeleteIntent)
// Unfortunately swipe is disabled in lollipop+
setDeleteIntent(pendingDeleteIntent)
}
when (intent?.action) {
ACTION_TIMEOUT -> {
notificationTimeoutMilliSecs = PreferencesUtil.getAdvancedUnlockTimeout(this)
// Not necessarily a foreground service
if (mTimerJob == null && notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
mTimerJob = CoroutineScope(Dispatchers.Main).launch {
val maxPos = 100
val posDurationMills = notificationTimeoutMilliSecs / maxPos
for (pos in maxPos downTo 0) {
notificationBuilder.setProgress(maxPos, pos, false)
startForeground(notificationId, notificationBuilder.build())
delay(posDurationMills)
if (pos <= 0) {
stopSelf()
}
}
notificationManager?.cancel(notificationId)
mTimerJob = null
cancel()
}
} else {
startForeground(notificationId, notificationBuilder.build())
}
}
ACTION_REMOVE_KEYS -> {
stopSelf()
}
else -> {}
}
return START_STICKY
}
override fun onCreate() {
super.onCreate()
mTempCipherDao = ArrayList()
}
override fun onDestroy() {
mTempCipherDao.clear()
mTimerJob?.cancel()
super.onDestroy()
}
companion object {
private const val CHANNEL_ADVANCED_UNLOCK_ID = "com.kunzisoft.keepass.notification.channel.unlock"
private const val ACTION_TIMEOUT = "ACTION_TIMEOUT"
private const val ACTION_REMOVE_KEYS = "ACTION_REMOVE_KEYS"
fun startServiceForTimeout(context: Context) {
if (PreferencesUtil.isTempAdvancedUnlockEnable(context)) {
context.startService(Intent(context, AdvancedUnlockNotificationService::class.java).apply {
action = ACTION_TIMEOUT
})
}
}
fun stopService(context: Context) {
context.stopService(Intent(context, AdvancedUnlockNotificationService::class.java))
}
}
}

View File

@@ -52,6 +52,14 @@ class AttachmentFileNotificationService: LockNotificationService() {
private val mainScope = CoroutineScope(Dispatchers.Main)
override fun retrieveChannelId(): String {
return CHANNEL_ATTACHMENT_ID
}
override fun retrieveChannelName(): String {
return getString(R.string.entry_attachments)
}
inner class ActionTaskBinder: Binder() {
fun getService(): AttachmentFileNotificationService = this@AttachmentFileNotificationService
@@ -430,6 +438,8 @@ class AttachmentFileNotificationService: LockNotificationService() {
companion object {
private val TAG = AttachmentFileNotificationService::javaClass.name
private const val CHANNEL_ATTACHMENT_ID = "com.kunzisoft.keepass.notification.channel.attachment"
const val ACTION_ATTACHMENT_FILE_START_UPLOAD = "ACTION_ATTACHMENT_FILE_START_UPLOAD"
const val ACTION_ATTACHMENT_FILE_START_DOWNLOAD = "ACTION_ATTACHMENT_FILE_START_DOWNLOAD"
const val ACTION_ATTACHMENT_REMOVE = "ACTION_ATTACHMENT_REMOVE"

View File

@@ -39,6 +39,14 @@ class ClipboardEntryNotificationService : LockNotificationService() {
private var notificationTimeoutMilliSecs: Long = 0
private var cleanCopyNotificationTimerTask: Thread? = null
override fun retrieveChannelId(): String {
return CHANNEL_CLIPBOARD_ID
}
override fun retrieveChannelName(): String {
return getString(R.string.clipboard)
}
override fun onCreate() {
super.onCreate()
@@ -230,6 +238,8 @@ class ClipboardEntryNotificationService : LockNotificationService() {
private val TAG = ClipboardEntryNotificationService::class.java.name
private const val CHANNEL_CLIPBOARD_ID = "com.kunzisoft.keepass.notification.channel.clipboard"
const val ACTION_NEW_NOTIFICATION = "ACTION_NEW_NOTIFICATION"
const val EXTRA_ENTRY_INFO = "EXTRA_ENTRY_INFO"
const val EXTRA_CLIPBOARD_FIELDS = "EXTRA_CLIPBOARD_FIELDS"

View File

@@ -23,11 +23,11 @@ import android.app.PendingIntent
import android.content.Intent
import android.net.Uri
import android.os.Binder
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.database.action.*
import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseRunnable
@@ -70,6 +70,14 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
private var mMessageId: Int? = null
private var mWarningId: Int? = null
override fun retrieveChannelId(): String {
return CHANNEL_DATABASE_ID
}
override fun retrieveChannelName(): String {
return getString(R.string.database)
}
inner class ActionTaskBinder: Binder() {
fun getService(): DatabaseTaskNotificationService = this@DatabaseTaskNotificationService
@@ -274,14 +282,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
// Database is normally open
if (mDatabase.loaded) {
// Build Intents for notification action
var pendingDatabaseFlag = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
pendingDatabaseFlag = PendingIntent.FLAG_IMMUTABLE
}
val pendingDatabaseIntent = PendingIntent.getActivity(this,
0,
Intent(this, GroupActivity::class.java),
pendingDatabaseFlag)
Intent(this, GroupActivity::class.java).apply {
ReadOnlyHelper.putReadOnlyInIntent(this, mDatabase.isReadOnly)
},
PendingIntent.FLAG_UPDATE_CURRENT)
val deleteIntent = Intent(this, DatabaseTaskNotificationService::class.java).apply {
action = ACTION_DATABASE_CLOSE
}
@@ -760,6 +766,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
private val TAG = DatabaseTaskNotificationService::class.java.name
private const val CHANNEL_DATABASE_ID = "com.kunzisoft.keepass.notification.channel.database"
const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK"
const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK"
const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_TASK"

View File

@@ -40,6 +40,14 @@ class KeyboardEntryNotificationService : LockNotificationService() {
private var pendingDeleteIntent: PendingIntent? = null
override fun retrieveChannelId(): String {
return CHANNEL_MAGIKEYBOARD_ID
}
override fun retrieveChannelName(): String {
return getString(R.string.magic_keyboard_title)
}
private fun stopNotificationAndSendLockIfNeeded() {
// Clear the entry if define in preferences
if (PreferencesUtil.isClearKeyboardNotificationEnable(this)) {
@@ -145,8 +153,9 @@ class KeyboardEntryNotificationService : LockNotificationService() {
private const val TAG = "KeyboardEntryNotifSrv"
const val ENTRY_INFO_KEY = "ENTRY_INFO_KEY"
private const val CHANNEL_MAGIKEYBOARD_ID = "com.kunzisoft.keepass.notification.channel.magikeyboard"
const val ENTRY_INFO_KEY = "ENTRY_INFO_KEY"
const val ACTION_CLEAN_KEYBOARD_ENTRY = "ACTION_CLEAN_KEYBOARD_ENTRY"
fun launchNotificationIfAllowed(context: Context, entry: EntryInfo, toast: Boolean) {

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.notifications
import android.content.Intent
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.LockReceiver
import com.kunzisoft.keepass.utils.registerLockReceiver
import com.kunzisoft.keepass.utils.unregisterLockReceiver

View File

@@ -24,6 +24,14 @@ abstract class NotificationService : Service() {
return null
}
open fun retrieveChannelId(): String {
return CHANNEL_ID
}
open fun retrieveChannelName(): String {
return CHANNEL_NAME
}
override fun onCreate() {
super.onCreate()
@@ -31,9 +39,9 @@ abstract class NotificationService : Service() {
// Create notification channel for Oreo+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (notificationManager?.getNotificationChannel(CHANNEL_ID_KEEPASS) == null) {
val channel = NotificationChannel(CHANNEL_ID_KEEPASS,
CHANNEL_NAME_KEEPASS,
if (notificationManager?.getNotificationChannel(retrieveChannelId()) == null) {
val channel = NotificationChannel(retrieveChannelId(),
retrieveChannelName(),
NotificationManager.IMPORTANCE_DEFAULT).apply {
enableVibration(false)
setSound(null, null)
@@ -51,7 +59,7 @@ abstract class NotificationService : Service() {
}
protected fun buildNewNotification(): NotificationCompat.Builder {
return NotificationCompat.Builder(this, CHANNEL_ID_KEEPASS)
return NotificationCompat.Builder(this, retrieveChannelId())
.setColor(colorNotificationAccent)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setVisibility(NotificationCompat.VISIBILITY_SECRET)
@@ -70,7 +78,7 @@ abstract class NotificationService : Service() {
}
companion object {
const val CHANNEL_ID_KEEPASS = "com.kunzisoft.keepass.notification.channel"
const val CHANNEL_NAME_KEEPASS = "KeePassDX notification"
private const val CHANNEL_ID = "com.kunzisoft.keepass.notification.channel"
private const val CHANNEL_NAME = "KeePassDX notification"
}
}

View File

@@ -216,13 +216,17 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
return secret.isNotEmpty() && checkBase64Secret(secret)
}
fun replaceSpaceChars(parameter: String): String {
fun removeLineChars(parameter: String): String {
return parameter.replace("[\\r|\\n|\\t|\\u00A0]+".toRegex(), "")
}
fun removeSpaceChars(parameter: String): String {
return parameter.replace("[\\r|\\n|\\t|\\s|\\u00A0]+".toRegex(), "")
}
fun replaceBase32Chars(parameter: String): String {
// Add 'A' at end if not Base32 length
var parameterNewSize = replaceSpaceChars(parameter.toUpperCase(Locale.ENGLISH))
var parameterNewSize = removeSpaceChars(parameter.toUpperCase(Locale.ENGLISH))
while (parameterNewSize.length % 8 != 0) {
parameterNewSize += 'A'
}

View File

@@ -24,9 +24,9 @@ import android.net.Uri
import android.util.Log
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.otp.OtpElement.Companion.replaceSpaceChars
import com.kunzisoft.keepass.otp.OtpElement.Companion.removeLineChars
import com.kunzisoft.keepass.otp.OtpElement.Companion.removeSpaceChars
import com.kunzisoft.keepass.otp.TokenCalculator.*
import java.net.URLEncoder
import java.util.*
import java.util.regex.Pattern
@@ -35,7 +35,7 @@ object OtpEntryFields {
private val TAG = OtpEntryFields::class.java.name
// Field from KeePassXC
private const val OTP_FIELD = "otp"
const val OTP_FIELD = "otp"
// URL parameters (https://github.com/google/google-authenticator/wiki/Key-Uri-Format)
private const val OTP_SCHEME = "otpauth"
@@ -49,6 +49,9 @@ object OtpEntryFields {
private const val ENCODER_URL_PARAM = "encoder"
private const val COUNTER_URL_PARAM = "counter"
// OTPauth URI
private const val REGEX_OTP_AUTH = "^(?:otpauth://([ht]otp)/)(?:(?:([^:?#]*): *)?([^:?#]*))(?:\\?([^#]+))$"
// Key-values (maybe from plugin or old KeePassXC)
private const val SEED_KEY = "key"
private const val DIGITS_KEY = "size"
@@ -91,7 +94,25 @@ object OtpEntryFields {
// HOTP fields from KeePass 2
if (parseHOTPFromField(getField, otpElement))
return otpElement
return null
}
/**
* Tell if [otpUri] is a valid Otp URI
*/
fun isOTPUri(otpUri: String): Boolean {
if (Pattern.matches(REGEX_OTP_AUTH, otpUri))
return true
return false
}
/**
* Get OtpElement from [otpUri]
*/
fun parseOTPUri(otpUri: String): OtpElement? {
val otpElement = OtpElement()
if (parseOTPUri({ key -> if (key == OTP_FIELD) otpUri else null }, otpElement))
return otpElement
return null
}
@@ -104,8 +125,8 @@ object OtpEntryFields {
*/
private fun parseOTPUri(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
val otpPlainText = getField(OTP_FIELD)
if (otpPlainText != null && otpPlainText.isNotEmpty()) {
val uri = Uri.parse(replaceSpaceChars(otpPlainText))
if (otpPlainText != null && otpPlainText.isNotEmpty() && isOTPUri(otpPlainText)) {
val uri = Uri.parse(removeSpaceChars(otpPlainText))
if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase(Locale.ENGLISH)) {
Log.e(TAG, "Invalid or missing scheme in uri")
@@ -135,12 +156,19 @@ object OtpEntryFields {
}
val nameParam = validateAndGetNameInPath(uri.path)
if (nameParam != null && nameParam.isNotEmpty())
otpElement.name = nameParam
if (nameParam != null && nameParam.isNotEmpty()) {
val userIdArray = nameParam.split(":", "%3A")
if (userIdArray.size > 1) {
otpElement.issuer = removeLineChars(userIdArray[0])
otpElement.name = removeLineChars(userIdArray[1])
} else {
otpElement.name = removeLineChars(nameParam)
}
}
val issuerParam = uri.getQueryParameter(ISSUER_URL_PARAM)
if (issuerParam != null && issuerParam.isNotEmpty())
otpElement.issuer = issuerParam
otpElement.issuer = removeLineChars(issuerParam)
val secretParam = uri.getQueryParameter(SECRET_URL_PARAM)
if (secretParam != null && secretParam.isNotEmpty()) {
@@ -211,15 +239,15 @@ object OtpEntryFields {
}
val issuer =
if (title != null && title.isNotEmpty())
replaceCharsForUrl(title)
encodeParameter(title)
else
replaceCharsForUrl(otpElement.issuer)
encodeParameter(otpElement.issuer)
val accountName =
if (username != null && username.isNotEmpty())
replaceCharsForUrl(username)
encodeParameter(username)
else
replaceCharsForUrl(otpElement.name)
val uriString = StringBuilder("otpauth://$otpAuthority/$issuer:$accountName" +
encodeParameter(otpElement.name)
val uriString = StringBuilder("otpauth://$otpAuthority/$issuer%3A$accountName" +
"?$SECRET_URL_PARAM=${otpElement.getBase32Secret()}" +
"&$counterOrPeriodLabel=$counterOrPeriodValue" +
"&$DIGITS_URL_PARAM=${otpElement.digits}" +
@@ -233,8 +261,8 @@ object OtpEntryFields {
return Uri.parse(uriString.toString())
}
private fun replaceCharsForUrl(parameter: String): String {
return URLEncoder.encode(replaceSpaceChars(parameter), "UTF-8")
private fun encodeParameter(parameter: String): String {
return Uri.encode(OtpElement.removeLineChars(parameter))
}
private fun parseTOTPKeyValues(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
@@ -321,7 +349,7 @@ object OtpEntryFields {
// path is "/name", so remove leading "/", and trailing white spaces
val name = path.substring(1).trim { it <= ' ' }
return if (name.isEmpty()) {
null // only white spaces.
null
} else name
}
@@ -338,7 +366,7 @@ object OtpEntryFields {
/**
* Build Otp field from an OtpElement
*/
fun buildOtpField(otpElement: OtpElement, title: String?, username: String?): Field {
fun buildOtpField(otpElement: OtpElement, title: String? = null, username: String? = null): Field {
return Field(OTP_FIELD, ProtectedString(true,
buildOtpUri(otpElement, title, username).toString()))
}

View File

@@ -30,7 +30,7 @@ import android.view.autofill.AutofillManager
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.biometric.BiometricManager
import androidx.fragment.app.FragmentActivity
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.SwitchPreference
@@ -41,14 +41,18 @@ import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment
import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.icons.IconPackChooser
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.preference.IconPackListPreference
import com.kunzisoft.keepass.utils.UriUtil
class NestedAppSettingsFragment : NestedSettingsFragment() {
private var deleteKeysAlertDialog: AlertDialog? = null
override fun onCreateScreenPreference(screen: Screen, savedInstanceState: Bundle?, rootKey: String?) {
// Load the preferences from an XML resource
@@ -207,17 +211,18 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
setPreferencesFromResource(R.xml.preferences_advanced_unlock, rootKey)
activity?.let { activity ->
val biometricUnlockEnablePreference: SwitchPreference? = findPreference(getString(R.string.biometric_unlock_enable_key))
// < M solve verifyError exception
var biometricUnlockSupported = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val biometricCanAuthenticate = BiometricManager.from(activity).canAuthenticate()
biometricUnlockSupported = biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
}
if (!biometricUnlockSupported) {
val deviceCredentialUnlockEnablePreference: SwitchPreference? = findPreference(getString(R.string.device_credential_unlock_enable_key))
val autoOpenPromptPreference: SwitchPreference? = findPreference(getString(R.string.biometric_auto_open_prompt_key))
val tempAdvancedUnlockPreference: SwitchPreference? = findPreference(getString(R.string.temp_advanced_unlock_enable_key))
val biometricUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
AdvancedUnlockManager.biometricUnlockSupported(activity)
} else false
biometricUnlockEnablePreference?.apply {
// False if under Marshmallow
biometricUnlockEnablePreference?.apply {
if (!biometricUnlockSupported) {
isChecked = false
setOnPreferenceClickListener { preference ->
(preference as SwitchPreference).isChecked = false
@@ -225,46 +230,98 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
.show(parentFragmentManager, "unavailableFeatureDialog")
false
}
} else {
setOnPreferenceClickListener {
val biometricChecked = biometricUnlockEnablePreference.isChecked
val deviceCredentialChecked = deviceCredentialUnlockEnablePreference?.isChecked ?: false
if (!biometricChecked) {
biometricUnlockEnablePreference.isChecked = true
deleteKeysMessage(activity) {
biometricUnlockEnablePreference.isChecked = false
autoOpenPromptPreference?.isEnabled = deviceCredentialChecked
tempAdvancedUnlockPreference?.isEnabled = deviceCredentialChecked
}
} else {
if (deviceCredentialChecked) {
biometricUnlockEnablePreference.isChecked = false
deleteKeysMessage(activity) {
biometricUnlockEnablePreference.isChecked = true
deviceCredentialUnlockEnablePreference?.isChecked = false
}
} else {
autoOpenPromptPreference?.isEnabled = true
tempAdvancedUnlockPreference?.isEnabled = true
}
}
true
}
}
}
val deleteKeysFingerprints: Preference? = findPreference(getString(R.string.biometric_delete_all_key_key))
if (!biometricUnlockSupported) {
deleteKeysFingerprints?.isEnabled = false
} else {
deleteKeysFingerprints?.setOnPreferenceClickListener {
context?.let { context ->
AlertDialog.Builder(context)
.setMessage(resources.getString(R.string.biometric_delete_all_key_warning))
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(resources.getString(android.R.string.ok)
) { _, _ ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
BiometricUnlockDatabaseHelper.deleteEntryKeyInKeystoreForBiometric(
activity,
object : BiometricUnlockDatabaseHelper.BiometricUnlockErrorCallback {
fun showException(e: Exception) {
Toast.makeText(context,
getString(R.string.biometric_scanning_error, e.localizedMessage),
Toast.LENGTH_SHORT).show()
}
override fun onInvalidKeyException(e: Exception) {
showException(e)
}
override fun onBiometricException(e: Exception) {
showException(e)
}
})
}
CipherDatabaseAction.getInstance(context.applicationContext).deleteAll()
}
.setNegativeButton(resources.getString(android.R.string.cancel))
{ _, _ -> }.show()
val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
AdvancedUnlockManager.deviceCredentialUnlockSupported(activity)
} else false
deviceCredentialUnlockEnablePreference?.apply {
// Biometric unlock already checked
if (biometricUnlockEnablePreference?.isChecked == true)
isChecked = false
if (!deviceCredentialUnlockSupported) {
isChecked = false
setOnPreferenceClickListener { preference ->
(preference as SwitchPreference).isChecked = false
UnavailableFeatureDialogFragment.getInstance(Build.VERSION_CODES.M)
.show(parentFragmentManager, "unavailableFeatureDialog")
false
}
} else {
setOnPreferenceClickListener {
val deviceCredentialChecked = deviceCredentialUnlockEnablePreference.isChecked
val biometricChecked = biometricUnlockEnablePreference?.isChecked ?: false
if (!deviceCredentialChecked) {
deviceCredentialUnlockEnablePreference.isChecked = true
deleteKeysMessage(activity) {
deviceCredentialUnlockEnablePreference.isChecked = false
autoOpenPromptPreference?.isEnabled = biometricChecked
tempAdvancedUnlockPreference?.isEnabled = biometricChecked
}
} else {
if (biometricChecked) {
deviceCredentialUnlockEnablePreference.isChecked = false
deleteKeysMessage(activity) {
deviceCredentialUnlockEnablePreference.isChecked = true
biometricUnlockEnablePreference?.isChecked = false
}
} else {
autoOpenPromptPreference?.isEnabled = true
tempAdvancedUnlockPreference?.isEnabled = true
}
}
true
}
}
}
autoOpenPromptPreference?.isEnabled = biometricUnlockEnablePreference?.isChecked == true
|| deviceCredentialUnlockEnablePreference?.isChecked == true
tempAdvancedUnlockPreference?.isEnabled = biometricUnlockEnablePreference?.isChecked == true
|| deviceCredentialUnlockEnablePreference?.isChecked == true
tempAdvancedUnlockPreference?.setOnPreferenceClickListener {
tempAdvancedUnlockPreference.isChecked = !tempAdvancedUnlockPreference.isChecked
deleteKeysMessage(activity) {
tempAdvancedUnlockPreference.isChecked = !tempAdvancedUnlockPreference.isChecked
}
true
}
val deleteKeysFingerprints: Preference? = findPreference(getString(R.string.biometric_delete_all_key_key))
if (biometricUnlockSupported || deviceCredentialUnlockSupported) {
deleteKeysFingerprints?.setOnPreferenceClickListener {
deleteKeysMessage(activity)
false
}
} else {
deleteKeysFingerprints?.isEnabled = false
}
}
@@ -274,6 +331,42 @@ 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.deleteEntryKeyInKeystoreForBiometric(
activity,
object : AdvancedUnlockManager.AdvancedUnlockErrorCallback {
fun showException(e: Exception) {
Toast.makeText(context,
getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
Toast.LENGTH_SHORT).show()
}
override fun onInvalidKeyException(e: Exception) {
showException(e)
}
override fun onGenericException(e: Exception) {
showException(e)
}
})
}
AdvancedUnlockNotificationService.stopService(activity.applicationContext)
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
}
.setNegativeButton(resources.getString(android.R.string.cancel)
) { _, _ ->}
.create()
deleteKeysAlertDialog?.show()
}
private fun onCreateAppearancePreferences(rootKey: String?) {
setPreferencesFromResource(R.xml.preferences_appearance, rootKey)
@@ -333,11 +426,9 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
override fun onResume() {
super.onResume()
activity?.let { activity ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
findPreference<SwitchPreference?>(getString(R.string.settings_autofill_enable_key))?.let {
autoFillEnablePreference ->
findPreference<SwitchPreference?>(getString(R.string.settings_autofill_enable_key))?.let { autoFillEnablePreference ->
val autofillManager = activity.getSystemService(AutofillManager::class.java)
autoFillEnablePreference.isChecked = autofillManager != null
&& autofillManager.hasEnabledAutofillServices()
@@ -346,6 +437,11 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
}
override fun onPause() {
deleteKeysAlertDialog?.dismiss()
super.onPause()
}
private var mCount = 0
override fun onStop() {
super.onStop()

View File

@@ -19,12 +19,14 @@
*/
package com.kunzisoft.keepass.settings
import android.app.backup.BackupManager
import android.content.Context
import android.content.res.Resources
import android.net.Uri
import androidx.preference.PreferenceManager
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.timeout.TimeoutHelper
import java.util.*
@@ -43,6 +45,7 @@ object PreferencesUtil {
}
apply()
}
BackupManager(context).dataChanged()
}
fun getDefaultDatabasePath(context: Context): String? {
@@ -123,6 +126,12 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.hide_expired_entries_default))
}
fun showUUID(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.show_uuid_key),
context.resources.getBoolean(R.bool.show_uuid_default))
}
/**
* Retrieve the text size in % (1 for 100%)
*/
@@ -195,6 +204,13 @@ object PreferencesUtil {
?: TimeoutHelper.DEFAULT_TIMEOUT
}
fun getAdvancedUnlockTimeout(context: Context): Long {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getString(context.getString(R.string.temp_advanced_unlock_timeout_key),
context.getString(R.string.temp_advanced_unlock_timeout_default))?.toLong()
?: TimeoutHelper.DEFAULT_TIMEOUT
}
fun isLockDatabaseWhenScreenShutOffEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.lock_database_screen_off_key),
@@ -219,13 +235,38 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.enable_auto_save_database_default))
}
fun isBiometricUnlockEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
context.resources.getBoolean(R.bool.biometric_unlock_enable_default))
fun isAdvancedUnlockEnable(context: Context): Boolean {
return isBiometricUnlockEnable(context) || isDeviceCredentialUnlockEnable(context)
}
fun isBiometricPromptAutoOpenEnable(context: Context): Boolean {
fun isBiometricUnlockEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val biometricSupported = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
AdvancedUnlockManager.biometricUnlockSupported(context)
} else {
false
}
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
context.resources.getBoolean(R.bool.biometric_unlock_enable_default))
&& biometricSupported
}
fun isDeviceCredentialUnlockEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
// Priority to biometric unlock
val biometricAlreadySupported = isBiometricUnlockEnable(context)
return prefs.getBoolean(context.getString(R.string.device_credential_unlock_enable_key),
context.resources.getBoolean(R.bool.device_credential_unlock_enable_default))
&& !biometricAlreadySupported
}
fun isTempAdvancedUnlockEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.temp_advanced_unlock_enable_key),
context.resources.getBoolean(R.bool.temp_advanced_unlock_enable_default))
}
fun isAdvancedUnlockPromptAutoOpenEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.biometric_auto_open_prompt_key),
context.resources.getBoolean(R.bool.biometric_auto_open_prompt_default))
@@ -327,24 +368,32 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.delete_entered_password_default))
}
fun isKeyboardEntrySelectionEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.keyboard_selection_entry_key),
context.resources.getBoolean(R.bool.keyboard_selection_entry_default))
}
fun isKeyboardNotificationEntryEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.keyboard_notification_entry_key),
context.resources.getBoolean(R.bool.keyboard_notification_entry_default))
}
fun isKeyboardEntrySelectionEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.keyboard_selection_entry_key),
context.resources.getBoolean(R.bool.keyboard_selection_entry_default))
}
fun isKeyboardSearchShareEnable(context: Context): Boolean {
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))
}
fun isKeyboardSaveSearchInfoEnable(context: Context): Boolean {
if (!isKeyboardSearchShareEnable(context))
return false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.keyboard_save_search_info_key),
context.resources.getBoolean(R.bool.keyboard_save_search_info_default))
}
fun isAutoGoActionEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.keyboard_auto_go_action_key),
@@ -375,12 +424,37 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.keyboard_previous_fill_in_default))
}
fun isKeyboardPreviousLockEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.keyboard_previous_lock_key),
context.resources.getBoolean(R.bool.keyboard_previous_lock_default))
}
fun isAutofillCloseDatabaseEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.autofill_close_database_key),
context.resources.getBoolean(R.bool.autofill_close_database_default))
}
fun isAutofillAutoSearchEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.autofill_auto_search_key),
context.resources.getBoolean(R.bool.autofill_auto_search_default))
}
fun isAutofillSaveSearchInfoEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.autofill_save_search_info_key),
context.resources.getBoolean(R.bool.autofill_save_search_info_default))
}
fun askToSaveAutofillData(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.autofill_ask_to_save_data_key),
context.resources.getBoolean(R.bool.autofill_ask_to_save_data_default))
}
/**
* Retrieve the default Blocklist for application ID, including the current app
*/

View File

@@ -35,6 +35,7 @@ import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.view.showActionError
@@ -81,7 +82,7 @@ open class SettingsActivity
}
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()

View File

@@ -54,7 +54,6 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
val fragmentManager = childFragmentManager
chromaColorFragment = fragmentManager.findFragmentByTag(TAG_FRAGMENT_COLORS) as ChromaColorFragment?
val fragmentTransaction = fragmentManager.beginTransaction()
database?.let { database ->
val initColor = try {
@@ -69,7 +68,10 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
if (chromaColorFragment == null) {
chromaColorFragment = newInstance(arguments)
fragmentTransaction.add(com.kunzisoft.androidclearchroma.R.id.color_dialog_container, chromaColorFragment!!, TAG_FRAGMENT_COLORS).commit()
fragmentManager.beginTransaction().apply {
add(com.kunzisoft.androidclearchroma.R.id.color_dialog_container, chromaColorFragment!!, TAG_FRAGMENT_COLORS)
commit()
}
}
alertDialogBuilder.setPositiveButton(android.R.string.ok) { _, _ ->

View File

@@ -77,7 +77,12 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
cancelLockPendingIntent(context)
}
}
LOCK_ACTION,
LOCK_ACTION -> {
lockAction.invoke()
if (PreferencesUtil.isKeyboardPreviousLockEnable(context)) {
backToPreviousKeyboardAction?.invoke()
} else {}
}
REMOVE_ENTRY_MAGIKEYBOARD_ACTION -> {
lockAction.invoke()
}

View File

@@ -53,23 +53,19 @@ object MenuUtil {
fun onDefaultMenuOptionsItemSelected(activity: Activity,
item: MenuItem,
readOnly: Boolean = READ_ONLY_DEFAULT,
timeoutEnable: Boolean = false): Boolean {
timeoutEnable: Boolean = false) {
when (item.itemId) {
R.id.menu_contribute -> {
onContributionItemSelected(activity)
return true
}
R.id.menu_app_settings -> {
// To avoid flickering when launch settings in a LockingActivity
SettingsActivity.launch(activity, readOnly, timeoutEnable)
return true
}
R.id.menu_about -> {
val intent = Intent(activity, AboutActivity::class.java)
activity.startActivity(intent)
return true
}
else -> return true
}
}
}

View File

@@ -22,8 +22,10 @@ package com.kunzisoft.keepass.utils
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.R
@@ -94,19 +96,6 @@ object UriUtil {
null
}
fun getWebDomainWithoutSubDomain(webDomain: String?): String? {
webDomain?.split(".")?.let { domainArray ->
if (domainArray.isEmpty()) {
return ""
}
if (domainArray.size == 1) {
return domainArray[0];
}
return domainArray[domainArray.size - 2] + "." + domainArray[domainArray.size - 1]
}
return null
}
fun decode(uri: String?): String {
return Uri.decode(uri) ?: ""
}
@@ -146,6 +135,24 @@ object UriUtil {
gotoUrl(context, context.getString(resId))
}
fun isExternalAppInstalled(context: Context, packageName: String): Boolean {
try {
context.applicationContext.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
return true
} catch (e: Exception) {
Log.e(javaClass.simpleName, "App not accessible", e)
}
return false
}
fun openExternalApp(context: Context, packageName: String) {
try {
context.startActivity(context.applicationContext.packageManager.getLaunchIntentForPackage(packageName))
} catch (e: Exception) {
Log.e(javaClass.simpleName, "App cannot be open", e)
}
}
fun getBinaryDir(context: Context): File {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
context.applicationContext.noBackupFilesDir

View File

@@ -27,10 +27,12 @@ import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.biometric.FingerPrintAnimatedVector
@RequiresApi(api = Build.VERSION_CODES.M)
class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
@@ -48,25 +50,25 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
inflater?.inflate(R.layout.view_advanced_unlock, this)
unlockContainerView = findViewById(R.id.fingerprint_container)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
unlockTitleTextView = findViewById(R.id.biometric_title)
unlockMessageTextView = findViewById(R.id.biometric_message)
unlockIconImageView = findViewById(R.id.biometric_image)
// Init the fingerprint animation
unlockAnimatedVector = FingerPrintAnimatedVector(context, unlockIconImageView!!)
}
unlockTitleTextView = findViewById(R.id.biometric_title)
unlockMessageTextView = findViewById(R.id.biometric_message)
unlockIconImageView = findViewById(R.id.biometric_image)
}
fun startIconViewAnimation() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
unlockAnimatedVector?.startScan()
}
private fun startIconViewAnimation() {
unlockAnimatedVector?.startScan()
}
fun stopIconViewAnimation() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
unlockAnimatedVector?.stopScan()
private fun stopIconViewAnimation() {
unlockAnimatedVector?.stopScan()
}
fun setIconResource(iconId: Int) {
unlockIconImageView?.setImageResource(iconId)
// Init the fingerprint animation
unlockAnimatedVector = when (iconId) {
R.drawable.fingerprint -> FingerPrintAnimatedVector(context, unlockIconImageView!!)
else -> null
}
}

View File

@@ -43,6 +43,7 @@ import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.*
@@ -78,6 +79,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val historyListView: RecyclerView
private val historyAdapter = EntryHistoryAdapter(context)
private val uuidContainerView: View
private val uuidView: TextView
private val uuidReferenceView: TextView
@@ -126,6 +128,10 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
adapter = historyAdapter
}
uuidContainerView = findViewById(R.id.entry_UUID_container)
uuidContainerView?.apply {
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
}
uuidView = findViewById(R.id.entry_UUID)
uuidReferenceView = findViewById(R.id.entry_UUID_reference)
}

View File

@@ -30,7 +30,8 @@ import android.widget.TextView
import androidx.annotation.StringRes
import androidx.core.text.util.LinkifyCompat
import com.kunzisoft.keepass.R
import java.util.regex.Pattern
import com.kunzisoft.keepass.model.EntryInfo.Companion.APPLICATION_ID_FIELD_NAME
import com.kunzisoft.keepass.utils.UriUtil
class EntryField @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
@@ -101,9 +102,7 @@ class EntryField @JvmOverloads constructor(context: Context,
setTextIsSelectable(true)
}
applyHiddenStyle(isProtected && !showButtonView.isSelected)
if (valueView.autoLinkMask == LINKIFY_MASKS) {
linkify()
}
if (!isProtected) linkify()
}
}
@@ -113,13 +112,23 @@ class EntryField @JvmOverloads constructor(context: Context,
}
private fun linkify() {
valueView.autoLinkMask = LINKIFY_MASKS
LinkifyCompat.addLinks(valueView, LINKIFY_MASKS)
when {
labelView.text.contains(APPLICATION_ID_FIELD_NAME) -> {
val packageName = valueView.text.toString()
if (UriUtil.isExternalAppInstalled(context, packageName)) {
valueView.customLink {
UriUtil.openExternalApp(context, packageName)
}
}
}
else -> {
LinkifyCompat.addLinks(valueView, Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES)
}
}
}
fun setLinkAll() {
valueView.autoLinkMask = LINKIFY_ALL
LinkifyCompat.addLinks(valueView, LINKIFY_ALL)
LinkifyCompat.addLinks(valueView, Linkify.ALL)
}
fun activateCopyButton(enable: Boolean) {
@@ -131,8 +140,4 @@ class EntryField @JvmOverloads constructor(context: Context,
copyButtonView.setOnClickListener(onClickActionListener)
copyButtonView.visibility = if (onClickActionListener == null) GONE else VISIBLE
}
companion object {
private const val LINKIFY_MASKS = Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES
private const val LINKIFY_ALL = Linkify.ALL
}
}

View File

@@ -25,7 +25,13 @@ import android.animation.ValueAnimator
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.text.Selection
import android.text.Spannable
import android.text.SpannableString
import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.text.method.PasswordTransformationMethod
import android.text.style.ClickableSpan
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.TextView
@@ -57,7 +63,7 @@ fun TextView.applyHiddenStyle(hide: Boolean, changeMaxLines: Boolean = true) {
}
fun TextView.setTextSize(unit: Int, defaultSize: Float, multiplier: Float) {
if (multiplier > 0.0F && multiplier != 1.0F)
if (multiplier > 0.0F)
setTextSize(unit, defaultSize * multiplier)
}
@@ -68,6 +74,21 @@ fun TextView.strikeOut(strikeOut: Boolean) {
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
fun TextView.customLink(listener: (View) -> Unit) {
val spannableString = SpannableString(this.text)
val clickableSpan = object : ClickableSpan() {
override fun onClick(view: View) {
Selection.setSelection((view as TextView).text as Spannable, 0)
view.invalidate()
listener.invoke(view)
}
}
spannableString.setSpan(clickableSpan, 0, text.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
this.movementMethod = LinkMovementMethod.getInstance() // without LinkMovementMethod, link can not click
this.setText(spannableString, TextView.BufferType.SPANNABLE)
}
fun Snackbar.asError(): Snackbar {
this.view.apply {
setBackgroundColor(Color.RED)

View File

@@ -4,8 +4,12 @@ import android.app.Application
import android.net.Uri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.app.database.IOActionTask
import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
class DatabaseFileViewModel(application: Application) : AndroidViewModel(application) {
@@ -15,6 +19,33 @@ class DatabaseFileViewModel(application: Application) : AndroidViewModel(applica
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(application.applicationContext)
}
val isDefaultDatabase: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>()
}
fun checkIfIsDefaultDatabase(databaseUri: Uri) {
IOActionTask(
{
(UriUtil.parse(PreferencesUtil.getDefaultDatabasePath(getApplication<App>().applicationContext))
== databaseUri)
},
{
isDefaultDatabase.value = it
}
).execute()
}
fun removeDefaultDatabase() {
IOActionTask(
{
PreferencesUtil.saveDefaultDatabasePath(getApplication<App>().applicationContext,
null)
},
{
}
).execute()
}
val databaseFileLoaded: MutableLiveData<DatabaseFile> by lazy {
MutableLiveData<DatabaseFile>()
}

View File

@@ -1,7 +1,6 @@
package com.kunzisoft.keepass.viewmodels
import android.app.Application
import android.app.backup.BackupManager
import android.net.Uri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
@@ -42,11 +41,8 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
fun setDefaultDatabase(databaseFile: DatabaseFile?) {
IOActionTask(
{
val context = getApplication<App>().applicationContext
UriUtil.parse(PreferencesUtil.getDefaultDatabasePath(context))
PreferencesUtil.saveDefaultDatabasePath(context, databaseFile?.databaseUri)
val backupManager = BackupManager(context)
backupManager.dataChanged()
PreferencesUtil.saveDefaultDatabasePath(getApplication<App>().applicationContext,
databaseFile?.databaseUri)
},
{
checkDefaultDatabase()

View File

@@ -0,0 +1,135 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.lib.publicsuffixlist
import android.content.Context
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
/**
* API for reading and accessing the public suffix list.
*
* > A "public suffix" is one under which Internet users can (or historically could) directly register names. Some
* > examples of public suffixes are .com, .co.uk and pvt.k12.ma.us. The Public Suffix List is a list of all known
* > public suffixes.
*
* Note that this implementation applies the rules of the public suffix list only and does not validate domains.
*
* https://publicsuffix.org/
* https://github.com/publicsuffix/list
*/
class PublicSuffixList(
context: Context,
dispatcher: CoroutineDispatcher = Dispatchers.IO,
private val scope: CoroutineScope = CoroutineScope(dispatcher)
) {
private val data: PublicSuffixListData by lazy { PublicSuffixListLoader.load(context) }
/**
* Prefetch the public suffix list from disk so that it is available in memory.
*/
fun prefetch(): Deferred<Unit> = scope.async {
data.run { Unit }
}
/**
* Returns true if the given [domain] is a public suffix; false otherwise.
*
* E.g.:
* ```
* co.uk -> true
* com -> true
* mozilla.org -> false
* org -> true
* ```
*
* Note that this method ignores the default "prevailing rule" described in the formal public suffix list algorithm:
* If no rule matches then the passed [domain] is assumed to *not* be a public suffix.
*
* @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values
* are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result.
*/
fun isPublicSuffix(domain: String): Deferred<Boolean> = scope.async {
when (data.getPublicSuffixOffset(domain)) {
is PublicSuffixOffset.PublicSuffix -> true
else -> false
}
}
/**
* Returns the public suffix and one more level; known as the registrable domain. Returns `null` if
* [domain] is a public suffix itself.
*
* E.g.:
* ```
* wwww.mozilla.org -> mozilla.org
* www.bcc.co.uk -> bbc.co.uk
* a.b.ide.kyoto.jp -> b.ide.kyoto.jp
* ```
*
* @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values
* are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result.
*/
fun getPublicSuffixPlusOne(domain: String): Deferred<String?> = scope.async {
when (val offset = data.getPublicSuffixOffset(domain)) {
is PublicSuffixOffset.Offset -> domain
.split('.')
.drop(offset.value)
.joinToString(separator = ".")
else -> null
}
}
/**
* Returns the public suffix of the given [domain]; known as the effective top-level domain (eTLD). Returns `null`
* if the [domain] is a public suffix itself.
*
* E.g.:
* ```
* wwww.mozilla.org -> org
* www.bcc.co.uk -> co.uk
* a.b.ide.kyoto.jp -> ide.kyoto.jp
* ```
*
* @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values
* are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result.
*/
fun getPublicSuffix(domain: String) = scope.async {
when (val offset = data.getPublicSuffixOffset(domain)) {
is PublicSuffixOffset.Offset -> domain
.split('.')
.drop(offset.value + 1)
.joinToString(separator = ".")
else -> null
}
}
/**
* Strips the public suffix from the given [domain]. Returns the original domain if no public suffix could be
* stripped.
*
* E.g.:
* ```
* wwww.mozilla.org -> www.mozilla
* www.bcc.co.uk -> www.bbc
* a.b.ide.kyoto.jp -> a.b
* ```
*
* @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values
* are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result.
*/
fun stripPublicSuffix(domain: String) = scope.async {
when (val offset = data.getPublicSuffixOffset(domain)) {
is PublicSuffixOffset.Offset -> domain
.split('.')
.joinToString(separator = ".", limit = offset.value + 1, truncated = "")
.dropLast(1)
else -> domain
}
}
}

View File

@@ -0,0 +1,158 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.lib.publicsuffixlist
import mozilla.components.lib.publicsuffixlist.ext.binarySearch
import java.net.IDN
/**
* Class wrapping the public suffix list data and offering methods for accessing rules in it.
*/
internal class PublicSuffixListData(
private val rules: ByteArray,
private val exceptions: ByteArray
) {
private fun binarySearchRules(labels: List<ByteArray>, labelIndex: Int): String? {
return rules.binarySearch(labels, labelIndex)
}
private fun binarySearchExceptions(labels: List<ByteArray>, labelIndex: Int): String? {
return exceptions.binarySearch(labels, labelIndex)
}
@Suppress("ReturnCount")
fun getPublicSuffixOffset(domain: String): PublicSuffixOffset? {
if (domain.isEmpty()) {
return null
}
val domainLabels = IDN.toUnicode(domain).split('.')
if (domainLabels.find { it.isEmpty() } != null) {
// At least one of the labels is empty: Bail out.
return null
}
val rule = findMatchingRule(domainLabels)
if (domainLabels.size == rule.size && rule[0][0] != PublicSuffixListData.EXCEPTION_MARKER) {
// The domain is a public suffix.
return if (rule == PublicSuffixListData.PREVAILING_RULE) {
PublicSuffixOffset.PrevailingRule
} else {
PublicSuffixOffset.PublicSuffix
}
}
return if (rule[0][0] == PublicSuffixListData.EXCEPTION_MARKER) {
// Exception rules hold the effective TLD plus one.
PublicSuffixOffset.Offset(domainLabels.size - rule.size)
} else {
// Otherwise the rule is for a public suffix, so we must take one more label.
PublicSuffixOffset.Offset(domainLabels.size - (rule.size + 1))
}
}
/**
* Find a matching rule for the given domain labels.
*
* This algorithm is based on OkHttp's PublicSuffixDatabase class:
* https://github.com/square/okhttp/blob/master/okhttp/src/main/java/okhttp3/internal/publicsuffix/PublicSuffixDatabase.java
*/
private fun findMatchingRule(domainLabels: List<String>): List<String> {
// Break apart the domain into UTF-8 labels, i.e. foo.bar.com turns into [foo, bar, com].
val domainLabelsBytes = domainLabels.map { it.toByteArray(Charsets.UTF_8) }
val exactMatch = findExactMatch(domainLabelsBytes)
val wildcardMatch = findWildcardMatch(domainLabelsBytes)
val exceptionMatch = findExceptionMatch(domainLabelsBytes, wildcardMatch)
if (exceptionMatch != null) {
return ("${PublicSuffixListData.EXCEPTION_MARKER}$exceptionMatch").split('.')
}
if (exactMatch == null && wildcardMatch == null) {
return PublicSuffixListData.PREVAILING_RULE
}
val exactRuleLabels = exactMatch?.split('.') ?: PublicSuffixListData.EMPTY_RULE
val wildcardRuleLabels = wildcardMatch?.split('.') ?: PublicSuffixListData.EMPTY_RULE
return if (exactRuleLabels.size > wildcardRuleLabels.size) {
exactRuleLabels
} else {
wildcardRuleLabels
}
}
/**
* Returns an exact match or null.
*/
private fun findExactMatch(labels: List<ByteArray>): String? {
// Start by looking for exact matches. We start at the leftmost label. For example, foo.bar.com
// will look like: [foo, bar, com], [bar, com], [com]. The longest matching rule wins.
for (i in 0 until labels.size) {
val rule = binarySearchRules(labels, i)
if (rule != null) {
return rule
}
}
return null
}
/**
* Returns a wildcard match or null.
*/
private fun findWildcardMatch(labels: List<ByteArray>): String? {
// In theory, wildcard rules are not restricted to having the wildcard in the leftmost position.
// In practice, wildcards are always in the leftmost position. For now, this implementation
// cheats and does not attempt every possible permutation. Instead, it only considers wildcards
// in the leftmost position. We assert this fact when we generate the public suffix file. If
// this assertion ever fails we'll need to refactor this implementation.
if (labels.size > 1) {
val labelsWithWildcard = labels.toMutableList()
for (labelIndex in 0 until labelsWithWildcard.size) {
labelsWithWildcard[labelIndex] = PublicSuffixListData.WILDCARD_LABEL
val rule = binarySearchRules(labelsWithWildcard, labelIndex)
if (rule != null) {
return rule
}
}
}
return null
}
private fun findExceptionMatch(labels: List<ByteArray>, wildcardMatch: String?): String? {
// Exception rules only apply to wildcard rules, so only try it if we matched a wildcard.
if (wildcardMatch == null) {
return null
}
for (labelIndex in 0 until labels.size) {
val rule = binarySearchExceptions(labels, labelIndex)
if (rule != null) {
return rule
}
}
return null
}
companion object {
val WILDCARD_LABEL = byteArrayOf('*'.toByte())
val PREVAILING_RULE = listOf("*")
val EMPTY_RULE = listOf<String>()
const val EXCEPTION_MARKER = '!'
}
}
internal sealed class PublicSuffixOffset {
data class Offset(val value: Int) : PublicSuffixOffset()
object PublicSuffix : PublicSuffixOffset()
object PrevailingRule : PublicSuffixOffset()
}

View File

@@ -0,0 +1,48 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.lib.publicsuffixlist
import android.content.Context
import java.io.BufferedInputStream
import java.io.IOException
private const val PUBLIC_SUFFIX_LIST_FILE = "publicsuffixes"
internal object PublicSuffixListLoader {
fun load(context: Context): PublicSuffixListData = context.assets.open(
PUBLIC_SUFFIX_LIST_FILE
).buffered().use { stream ->
val publicSuffixSize = stream.readInt()
val publicSuffixBytes = stream.readFully(publicSuffixSize)
val exceptionSize = stream.readInt()
val exceptionBytes = stream.readFully(exceptionSize)
PublicSuffixListData(publicSuffixBytes, exceptionBytes)
}
}
@Suppress("MagicNumber")
private fun BufferedInputStream.readInt(): Int {
return (read() and 0xff shl 24
or (read() and 0xff shl 16)
or (read() and 0xff shl 8)
or (read() and 0xff))
}
private fun BufferedInputStream.readFully(size: Int): ByteArray {
val bytes = ByteArray(size)
var offset = 0
while (offset < size) {
val read = read(bytes, offset, size - offset)
if (read == -1) {
throw IOException("Unexpected end of stream")
}
offset += read
}
return bytes
}

View File

@@ -0,0 +1,122 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.lib.publicsuffixlist.ext
import kotlin.experimental.and
private const val BITMASK = 0xff.toByte()
/**
* Performs a binary search for the provided [labels] on the [ByteArray]'s data.
*
* This algorithm is based on OkHttp's PublicSuffixDatabase class:
* https://github.com/square/okhttp/blob/master/okhttp/src/main/java/okhttp3/internal/publicsuffix/PublicSuffixDatabase.java
*/
@Suppress("ComplexMethod", "NestedBlockDepth")
internal fun ByteArray.binarySearch(labels: List<ByteArray>, labelIndex: Int): String? {
var low = 0
var high = size
var match: String? = null
while (low < high) {
val mid = (low + high) / 2
val start = findStartOfLineFromIndex(mid)
val end = findEndOfLineFromIndex(start)
val publicSuffixLength = start + end - start
var compareResult: Int
var currentLabelIndex = labelIndex
var currentLabelByteIndex = 0
var publicSuffixByteIndex = 0
var expectDot = false
while (true) {
val byte0 = if (expectDot) {
expectDot = false
'.'.toByte()
} else {
labels[currentLabelIndex][currentLabelByteIndex] and BITMASK
}
val byte1 = this[start + publicSuffixByteIndex] and BITMASK
// Compare the bytes. Note that the file stores UTF-8 encoded bytes, so we must compare the
// unsigned bytes.
@Suppress("EXPERIMENTAL_API_USAGE")
compareResult = (byte0.toUByte() - byte1.toUByte()).toInt()
if (compareResult != 0) {
break
}
publicSuffixByteIndex++
currentLabelByteIndex++
if (publicSuffixByteIndex == publicSuffixLength) {
break
}
if (labels[currentLabelIndex].size == currentLabelByteIndex) {
// We've exhausted our current label. Either there are more labels to compare, in which
// case we expect a dot as the next character. Otherwise, we've checked all our labels.
if (currentLabelIndex == labels.size - 1) {
break
} else {
currentLabelIndex++
currentLabelByteIndex = -1
expectDot = true
}
}
}
if (compareResult < 0) {
high = start - 1
} else if (compareResult > 0) {
low = start + end + 1
} else {
// We found a match, but are the lengths equal?
val publicSuffixBytesLeft = publicSuffixLength - publicSuffixByteIndex
var labelBytesLeft = labels[currentLabelIndex].size - currentLabelByteIndex
for (i in currentLabelIndex + 1 until labels.size) {
labelBytesLeft += labels[i].size
}
if (labelBytesLeft < publicSuffixBytesLeft) {
high = start - 1
} else if (labelBytesLeft > publicSuffixBytesLeft) {
low = start + end + 1
} else {
// Found a match.
match = String(this, start, publicSuffixLength, Charsets.UTF_8)
break
}
}
}
return match
}
/**
* Search for a '\n' that marks the start of a value. Don't go back past the start of the array.
*/
private fun ByteArray.findStartOfLineFromIndex(start: Int): Int {
var index = start
while (index > -1 && this[index] != '\n'.toByte()) {
index--
}
index++
return index
}
/**
* Search for a '\n' that marks the end of a value.
*/
private fun ByteArray.findEndOfLineFromIndex(start: Int): Int {
var end = 1
while (this[start + end] != '\n'.toByte()) {
end++
}
return end
}

View File

@@ -129,7 +129,7 @@ void throwExceptionF(JNIEnv *env, jclass exception, const char *format, ...) {
JNIEXPORT jbyteArray
JNICALL Java_com_kunzisoft_keepass_crypto_keyDerivation_Argon2Native_nTransformMasterKey(JNIEnv *env,
jobject this, jbyteArray password, jbyteArray salt, jint parallelism, jint memory,
jobject this, jint type, jbyteArray password, jbyteArray salt, jint parallelism, jint memory,
jint iterations, jbyteArray secretKey, jbyteArray associatedData, jint version) {
argon2_context context;
@@ -169,7 +169,7 @@ JNICALL Java_com_kunzisoft_keepass_crypto_keyDerivation_Argon2Native_nTransformM
context.flags = ARGON2_DEFAULT_FLAGS;
context.version = (uint32_t) version;
int argonResult = argon2_ctx(&context, Argon2_d);
int argonResult = argon2_ctx(&context, (argon2_type) type);
jbyteArray result;
if (argonResult != ARGON2_OK) {

View File

@@ -25,6 +25,7 @@
const char *argon2_type2string(argon2_type type, int uppercase) {
switch (type) {
default:
case Argon2_d:
return uppercase ? "Argon2d" : "argon2d";
case Argon2_i:

View File

@@ -473,7 +473,7 @@ JNIEXPORT jbyteArray JNICALL Java_com_kunzisoft_keepass_crypto_finalkey_NativeAE
(*env)->GetByteArrayRegion(env, seed, 0, MASTER_KEY_SIZE, (jbyte *)mk.c_seed);
(*env)->GetByteArrayRegion(env, key, 0, MASTER_KEY_SIZE, (jbyte *)mk.key1);
// step 2: encrypt the hash "rounds" (default: 6000) times
// step 2: encrypt the hash "rounds"
iret = pthread_create( &t1, NULL, (void*)generate_key_material, (void*)&mk );
if( iret != 0 ) {
(*env)->ThrowNew(env, bad_arg, "TransformMasterKey: failed to launch thread 1"); // FIXME: get a better exception class for this...

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<gradient xmlns:android="http://schemas.android.com/apk/res/android"
android:endColor="#0000"
android:endX="80"
android:endY="80"
android:startColor="#4e000000"
android:startX="0"
android:startY="0"
android:type="linear"/>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="@dimen/advanced_unlock_size"
android:height="@dimen/advanced_unlock_size"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<path
android:fillColor="#fffbfb"
android:strokeWidth="1"
android:pathData="M 13 4 L 8 13 L 11.777344 13 L 11 20 L 16 11 L 12.222656 11 L 13 4 z" />
</group>
</vector>

View File

@@ -15,8 +15,8 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="@dimen/fingerprint_width"
android:height="@dimen/fingerprint_height"
android:width="@dimen/advanced_unlock_size"
android:height="@dimen/advanced_unlock_size"
android:viewportWidth="@integer/fingerprint_viewport_width"
android:viewportHeight="@integer/fingerprint_viewport_height">

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