Compare commits

...

480 Commits

Author SHA1 Message Date
J-Jamet
ada8f74e2c Merge branch 'release/4.1.0' 2024-11-18 11:54:27 +01:00
J-Jamet
1ddfeaf950 fix: update Gemfile 2024-11-18 11:07:14 +01:00
J-Jamet
2ed2cc1499 fix: Replace strong tag 2024-11-18 11:00:46 +01:00
J-Jamet
be0f90f12a Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2024-11-18 10:58:28 +01:00
J-Jamet
4b362df23b Merge branch 'master' into develop 2024-11-17 20:32:20 +01:00
J-Jamet
b953a1c2f6 fix: Package authenticity 2024-11-17 20:29:45 +01:00
J-Jamet
4c30fa43d3 fix: Screenshot protection #1870 2024-11-17 19:32:23 +01:00
J-Jamet
6866b1a3bb fix: change expires icon 2024-11-17 14:59:07 +01:00
J-Jamet
328030f152 fix: timer precision #1467 2024-11-17 14:46:16 +01:00
J-Jamet
fd195bd926 fix: reset timer only if in DAO 2024-11-17 13:52:58 +01:00
J-Jamet
87e07366cd Resets the advanced unlock expiration #1600 2024-11-17 13:31:47 +01:00
J-Jamet
8133977e09 feat: Add shield icon as password strength indicator #1355 2024-11-16 12:56:46 +01:00
J-Jamet
11199b996c fix: Password color and entropy view 2024-11-15 19:25:00 +01:00
J-Jamet
eee61db189 Add entropy 2024-11-15 17:45:09 +01:00
J-Jamet
c7c5130030 fix: passkeyview to passwordview 2024-11-14 19:57:35 +01:00
J-Jamet
6de14a3840 fix: TimeZone 2024-11-13 21:13:50 +01:00
J-Jamet
55206b3dde fix: All past dates and times are crossed out #1710 2024-11-13 20:54:31 +01:00
Priit Jõerüüt
e1a44477af Translated using Weblate (Estonian)
Currently translated at 32.9% (217 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-11-12 18:00:27 +01:00
Aitor Elorza
23afee453e Translated using Weblate (Basque)
Currently translated at 96.8% (638 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2024-11-12 18:00:26 +01:00
J-Jamet
8e05309021 feat: Support otpauth://steam/Steam link #1289 2024-11-11 16:06:48 +01:00
J-Jamet
26fdf87070 fix: distinct domain names #1820 2024-11-11 15:34:48 +01:00
J-Jamet
1c95a0edc4 fix: distinct domain names #1105 2024-11-08 21:11:26 +01:00
J-Jamet
4723fb39e9 fix: Sort 2024-11-08 13:27:26 +01:00
Jamil Farajov
13d667d81c Translated using Weblate (Azerbaijani)
Currently translated at 98.6% (650 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-11-07 12:00:40 +00:00
Jamil Farajov
dce255dc58 Translated using Weblate (Azerbaijani)
Currently translated at 91.9% (606 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-11-06 12:00:23 +01:00
Random
f72c9704d9 Translated using Weblate (Italian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2024-11-06 12:00:21 +01:00
Sylvain Pichon
e623010e91 Translated using Weblate (French)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2024-11-05 07:00:22 +00:00
J-Jamet
587bfdc162 fix: Month save 2024-11-04 20:02:19 +01:00
J-Jamet
7c1c299282 fix: Off-by-one when selecting date fields #1695 2024-11-04 19:43:30 +01:00
Priit Jõerüüt
f2a5c0b04b Translated using Weblate (Estonian)
Currently translated at 32.6% (215 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-11-03 23:00:16 +01:00
J-Jamet
4ba77b76ec feat: Instant in copy #1889 2024-11-02 13:37:33 +01:00
J-Jamet
0bfce44317 fix: unlocking procedure #1760 2024-11-02 11:04:15 +01:00
solokot
13b6d6384c Translated using Weblate (Russian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-10-29 00:12:52 +01:00
ginger-co
80838bbef0 Translated using Weblate (Hebrew)
Currently translated at 92.5% (610 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2024-10-27 15:00:19 +01:00
Avi Parshan
8a557ff2fb Translated using Weblate (Hebrew)
Currently translated at 81.0% (534 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2024-10-26 15:45:55 +02:00
ginger-co
16e394087d Translated using Weblate (Hebrew)
Currently translated at 81.0% (534 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2024-10-26 15:45:55 +02:00
Avi Parshan
ee1b67b36e Translated using Weblate (Hebrew)
Currently translated at 70.4% (464 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2024-10-26 15:04:50 +02:00
ginger-co
36b9fa2387 Translated using Weblate (Hebrew)
Currently translated at 70.4% (464 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2024-10-26 15:04:50 +02:00
Jérémy JAMET
378169e939 Update README.md, remove the keytool section 2024-10-24 11:50:33 +02:00
Jamil Farajov
70a01e559d Translated using Weblate (Azerbaijani)
Currently translated at 36.8% (243 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-10-22 17:16:33 +02:00
shinebrillant
c09e8196f3 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2024-10-22 17:16:32 +02:00
J-Jamet
0382c05152 fix: Filters 2024-10-21 19:54:47 +02:00
Celeste Liu
75cf6e2a56 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2024-10-21 00:20:33 +02:00
J-Jamet
9fb4754430 feat: Number of children #421 2024-10-20 21:15:21 +02:00
J-Jamet
0312b504a9 fix: upgrade CHANGELOG 2024-10-20 18:51:28 +02:00
J-Jamet
1d6a9651bf fix: upgrade to API34 #1894 2024-10-20 18:44:06 +02:00
Jamil Farajov
e36b18e85e Translated using Weblate (Azerbaijani)
Currently translated at 26.5% (175 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-10-18 00:23:21 +02:00
Roko Magdalenić
4ae4951e0d Translated using Weblate (Serbian)
Currently translated at 51.1% (337 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sr/
2024-10-16 23:15:48 +02:00
Roko Magdalenić
a65f52ffba Translated using Weblate (Serbian (Latin script))
Currently translated at 59.7% (394 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sr_Latn/
2024-10-16 23:15:47 +02:00
Linerly
6de25ffa65 Translated using Weblate (Indonesian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2024-10-15 04:15:44 +02:00
Francisco Serrador
d8429bdd99 Translated using Weblate (Spanish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-10-13 22:16:18 +00:00
Jamil Farajov
26976ae6cf Translated using Weblate (Azerbaijani)
Currently translated at 22.7% (150 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-10-09 20:12:00 +02:00
Jamil Farajov
53beaca563 Translated using Weblate (Azerbaijani)
Currently translated at 18.6% (123 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-10-06 23:53:53 +02:00
Jamil Farajov
77628e2fb9 Translated using Weblate (Azerbaijani)
Currently translated at 18.2% (120 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-10-06 16:35:40 +02:00
Jamil Farajov
40d2f2de96 Translated using Weblate (Azerbaijani)
Currently translated at 18.0% (119 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-10-06 05:40:45 +02:00
Jamil Farajov
84cdb2483f Translated using Weblate (Azerbaijani)
Currently translated at 15.9% (105 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-10-06 05:04:26 +02:00
Jamil Farajov
c7866bfbbf Translated using Weblate (Azerbaijani)
Currently translated at 12.8% (85 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-10-05 23:50:54 +02:00
Jamil Farajov
5de1d6b343 Translated using Weblate (Azerbaijani)
Currently translated at 10.7% (71 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-10-05 20:04:36 +02:00
Jamil Farajov
7bde363704 Added translation using Weblate (Azerbaijani) 2024-10-05 17:03:03 +02:00
Priit Jõerüüt
1f9b2ce7b9 Translated using Weblate (Estonian)
Currently translated at 25.3% (167 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-10-04 00:41:37 +02:00
Francisco Serrador
ac2e47776a Translated using Weblate (Spanish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-09-25 01:15:42 +02:00
Ghost of Sparta
e7de5ca263 Translated using Weblate (Hungarian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2024-09-24 00:12:32 +02:00
Francisco Serrador
f7f079e653 Translated using Weblate (Spanish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-09-24 00:12:32 +02:00
Ghost of Sparta
f7cccb33de Translated using Weblate (Hungarian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2024-09-22 19:40:47 +02:00
J-Jamet
8177c9c34b fix: textColorHighlight #1711 2024-09-06 18:13:12 +02:00
J-Jamet
850c46f881 feat: Generate keyfile #1290 2024-09-06 17:13:24 +02:00
J-Jamet
ffcfe966d2 fix: Add warning for KeyFile length #1780 2024-09-06 15:55:36 +02:00
J-Jamet
800badd2a4 Merge branch 'shuvashish76-patch-1' into develop 2024-09-06 11:08:16 +02:00
J-Jamet
0a7ffbcc8f fix: Make the apk verification even clearer #1831 2024-09-06 10:31:41 +02:00
J-Jamet
019ec4de9a fix: Avoid DEPENDENCY_INFO_BLOCK 2024-09-06 09:54:35 +02:00
Priit Jõerüüt
78d1e4a12a Translated using Weblate (Estonian)
Currently translated at 24.8% (164 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-09-02 17:09:18 +02:00
SC
6c99fefad0 Translated using Weblate (Portuguese)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2024-08-23 19:09:13 +00:00
SC
24bc1424b9 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2024-08-23 19:09:12 +00:00
Random
94a0e17cfc Translated using Weblate (Italian)
Currently translated at 99.8% (658 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2024-08-23 19:09:12 +00:00
獅童乱(しどらん)
2f012d8cf2 Translated using Weblate (Japanese)
Currently translated at 98.0% (646 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2024-08-22 11:09:12 +00:00
gallegonovato
75b800054f Translated using Weblate (Spanish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-08-16 15:09:18 +02:00
Renko
d7c7733315 Translated using Weblate (Romanian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-08-15 13:09:11 +02:00
Ihor Hordiichuk
99600ad8d8 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2024-08-15 13:09:11 +02:00
Milo Ivir
4a4c7b8b6b Translated using Weblate (Croatian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2024-08-14 12:09:21 +02:00
jonnysemon
8c3267b345 Translated using Weblate (Arabic)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2024-08-14 12:09:21 +02:00
ΣΤΑΥΡΟΣ ΔΑΛΙΑΚΟΠΟΥΛΟΣ
c0240b047b Translated using Weblate (Greek)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2024-08-14 12:09:20 +02:00
Fjuro
c52957ccfe Translated using Weblate (Czech)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2024-08-14 12:09:18 +02:00
Besnik Bleta
fb3f057adf Translated using Weblate (Albanian)
Currently translated at 54.9% (362 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2024-08-12 17:09:28 +02:00
109247019824
e333bd08a4 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-08-12 17:09:28 +02:00
Oğuz Ersen
1844a269cb Translated using Weblate (Turkish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2024-08-12 17:09:27 +02:00
大王叫我来巡山
859882d24f Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2024-08-12 17:09:26 +02:00
solokot
c95543b8b0 Translated using Weblate (Russian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-08-12 17:09:26 +02:00
Wellington Terumi Uemura
d874125dc1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2024-08-12 17:09:25 +02:00
Matthaiks
795cd099f4 Translated using Weblate (Polish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2024-08-12 17:09:24 +02:00
Alex Bruinsma
66ef6fd9d8 Translated using Weblate (Dutch)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2024-08-12 17:09:23 +02:00
gallegonovato
3b0655354d Translated using Weblate (Spanish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-08-12 17:09:23 +02:00
Keterion
76acec93fd Translated using Weblate (German)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-08-12 17:09:22 +02:00
VfBFan
3b60068369 Translated using Weblate (German)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-08-12 17:09:21 +02:00
Keterion
a0fd0a71a2 Translated using Weblate (Catalan)
Currently translated at 96.0% (633 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2024-08-11 16:57:29 +02:00
Keterion
907accbcc9 Translated using Weblate (English)
Currently translated at 99.8% (658 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2024-08-11 16:57:29 +02:00
Alex Bruinsma
ee284abf8d Translated using Weblate (Dutch)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2024-08-07 16:09:12 +00:00
Hierax Swiftwing
8239275770 Translated using Weblate (Serbian)
Currently translated at 9.1% (60 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sr/
2024-08-01 03:09:13 +02:00
Hierax Swiftwing
029485bace Translated using Weblate (Serbian)
Currently translated at 8.6% (57 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sr/
2024-07-31 02:09:18 +02:00
Thom
cef9f6ae3b Translated using Weblate (Danish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2024-07-31 02:09:17 +02:00
Hierax Swiftwing
8d88c94956 Added translation using Weblate (Serbian) 2024-07-30 02:03:40 +02:00
Patricio Carrau
692e155117 Translated using Weblate (French)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2024-07-30 02:03:40 +02:00
The One
9a9410de2b Translated using Weblate (Swedish)
Currently translated at 63.2% (417 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2024-07-29 23:09:12 +02:00
ssantos
a27f1181ea Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2024-07-15 14:09:14 +02:00
searinminecraft
ac65cadb1b Translated using Weblate (Filipino)
Currently translated at 47.0% (310 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fil/
2024-07-13 06:09:13 +00:00
Renko
4345e75b20 Translated using Weblate (Romanian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-07-10 16:09:29 +02:00
j
f64d085e7b Translated using Weblate (Lithuanian)
Currently translated at 61.0% (402 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2024-07-10 16:09:28 +02:00
Kemal Karagöz
325b878f0a Translated using Weblate (Turkish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2024-07-09 10:09:19 +02:00
Salih ARSLAN
330f375a30 Translated using Weblate (Turkish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2024-07-06 11:49:51 +02:00
Fqwe1
7d6c211de1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2024-07-04 16:09:10 +02:00
Ben Towali
af5b36752c Translated using Weblate (Estonian)
Currently translated at 19.8% (131 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-07-01 22:09:24 +02:00
WebTrans
e0f563befb Translated using Weblate (Hebrew)
Currently translated at 44.3% (292 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2024-07-01 22:09:23 +02:00
Ben Towali
bf1b84dfea Translated using Weblate (Estonian)
Currently translated at 9.2% (61 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-06-30 21:20:00 +02:00
searinminecraft
b3f8ce9c16 Translated using Weblate (Filipino)
Currently translated at 46.4% (306 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fil/
2024-06-27 07:14:57 +02:00
Mr-Update
499152d066 Translated using Weblate (German)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-06-25 11:09:13 +02:00
VfBFan
c47e7edc9e Translated using Weblate (German)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-06-25 11:09:12 +02:00
shuvashish76
afc034b495 Improved IzzyOnDroid shield 2024-06-24 19:00:43 +05:30
J-Jamet
e1d19741af Merge branch 'feature/API34' into develop 2024-06-24 13:26:36 +02:00
J-Jamet
8d2de40df6 fix: Upgrade all modules to API 34 #1730 2024-06-24 13:14:52 +02:00
J-Jamet
f4b7db667f fix: Broadcast Receiver #1730 2024-06-24 12:59:33 +02:00
J-Jamet
4032e52317 fix: Upgrade Foreground service and version to 4.1.0 2024-06-24 12:32:05 +02:00
J-Jamet
f9db4325d8 Merge branch 'master' into develop 2024-06-24 10:37:39 +02:00
Jérémy JAMET
815824f76d Merge pull request #1862 from shuvashish76/patch-1
Add download sources table
2024-06-24 10:23:01 +02:00
Masowick
8534672c33 Translated using Weblate (German)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-06-24 10:20:54 +02:00
shuvashish76
b9bb9a166a Update README.md 2024-06-24 13:39:11 +05:30
shuvashish76
185c886472 Add download sources table 2024-06-24 04:52:01 +05:30
Mr-Update
acc565d021 Translated using Weblate (German)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-06-23 15:57:04 +02:00
solokot
64db137c6c Translated using Weblate (Russian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-06-21 20:09:59 +02:00
Kirill Isakov
4f2bdeb2c9 Translated using Weblate (Russian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-06-20 19:09:58 +02:00
ngocanhtve
527994084b Translated using Weblate (Vietnamese)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2024-06-19 02:09:25 +00:00
J-Jamet
ffaf5c9475 Merge tag '4.0.8' into develop
4.0.8
2024-06-18 20:19:52 +02:00
J-Jamet
d1e24bfcd8 Merge branch 'release/4.0.8' 2024-06-18 20:19:46 +02:00
J-Jamet
aab8612c63 fix: update fastlane 2024-06-18 20:19:40 +02:00
J-Jamet
79c1e2d21c fix: #1848 #1850 2024-06-18 20:08:54 +02:00
J-Jamet
7ff44f4839 Merge tag '4.0.7' into develop
4.0.7
2024-06-17 23:20:57 +02:00
J-Jamet
2de0272e3e Merge branch 'release/4.0.7' 2024-06-17 23:20:48 +02:00
J-Jamet
1d22fe72d0 fix: ru fastlane 2024-06-17 23:20:30 +02:00
J-Jamet
7e9821551c fix: tags 2024-06-17 22:53:21 +02:00
J-Jamet
8fb3cd71b9 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2024-06-17 22:50:28 +02:00
J-Jamet
b0f6f1f2ba fix: Update CHANGELOG 2024-06-17 19:07:43 +02:00
Jérémy JAMET
df56dc4a4a Merge pull request #1840 from lgjint/fix-inline-suggestion
fix: inline suggestions in keyboard only show ''Select entry''
2024-06-17 19:05:29 +02:00
J-Jamet
b572ec4901 fix: Update CHANGELOG 2024-06-17 12:46:43 +02:00
J-Jamet
75759171c1 fix: Upgrade to 4.0.7 2024-06-17 12:39:47 +02:00
J-Jamet
322fe185dd fix: Description 2024-06-17 12:38:39 +02:00
J-Jamet
effe514ecb fix: Show broken link by default 2024-06-17 11:45:42 +02:00
jonnysemon
4ff9f69548 Translated using Weblate (Arabic)
Currently translated at 99.5% (656 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2024-06-11 21:02:14 +02:00
Priit Jõerüüt
e1c7cb17da Translated using Weblate (Estonian)
Currently translated at 2.7% (18 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-06-10 20:09:43 +02:00
jonnysemon
e6defd7770 Translated using Weblate (Arabic)
Currently translated at 99.2% (654 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2024-06-10 20:09:42 +02:00
J-Jamet
bba9f45075 fix: Group height resizable #1770 2024-06-10 18:45:22 +02:00
Priit Jõerüüt
de1a2d1557 Added translation using Weblate (Estonian) 2024-06-10 12:10:53 +02:00
ngocanhtve
e995d06a26 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2024-06-08 06:09:18 +02:00
NicolaeFericitu
63f54a1318 Translated using Weblate (Romanian)
Currently translated at 98.4% (649 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-06-05 19:09:11 +00:00
Milo Ivir
0828c9f901 Translated using Weblate (Croatian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2024-06-05 19:09:10 +00:00
lgjint
51cd38450a fix: inline suggestions in keyboard only show ''Select entry'' Kunzisoft/KeePassDX#1708 2024-06-03 22:42:53 +08:00
Knut-Helge Eikrem
0a4a720fea Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2024-06-02 19:30:56 +02:00
Knut-Helge Eikrem
7f7ce171e0 Translated using Weblate (English)
Currently translated at 99.6% (657 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2024-06-02 19:30:56 +02:00
ΣΤΑΥΡΟΣ ΔΑΛΙΑΚΟΠΟΥΛΟΣ
87622cc6ec Translated using Weblate (Greek)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2024-06-02 00:09:19 +02:00
solokot
aaf2e64fdb Translated using Weblate (Russian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-05-29 20:09:24 +02:00
Patricio Carrau
c6f22e7ce5 Translated using Weblate (French)
Currently translated at 99.8% (658 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2024-05-28 00:09:11 +02:00
Random
749a2d19aa Translated using Weblate (Italian)
Currently translated at 99.8% (658 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2024-05-25 10:09:13 +02:00
gallegonovato
ed3491fbba Translated using Weblate (Spanish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-05-21 18:13:40 +02:00
109247019824
f5e4b7cd6a Translated using Weblate (Bulgarian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-05-21 08:01:49 +00:00
大王叫我来巡山
0b614e81ee Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2024-05-21 08:01:48 +00:00
Wellington Terumi Uemura
0a93b660cf Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2024-05-21 08:01:47 +00:00
Oğuz Ersen
71e104e4bd Translated using Weblate (Turkish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2024-05-20 09:01:58 +02:00
Matthaiks
189c5de7ea Translated using Weblate (Polish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2024-05-20 09:01:58 +02:00
Stephan Paternotte
a5e141c361 Translated using Weblate (Dutch)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2024-05-20 09:01:57 +02:00
Masowick
e76d8d4df7 Translated using Weblate (German)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-05-20 09:01:56 +02:00
Fjuro
d54c093985 Translated using Weblate (Czech)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2024-05-20 09:01:55 +02:00
Hosted Weblate
086709e40f Merge branch 'origin/develop' into Weblate. 2024-05-19 07:55:21 +02:00
SC
9477e32ec8 Translated using Weblate (Portuguese)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2024-05-18 10:01:56 +02:00
SC
a66a0628ad Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2024-05-18 10:01:55 +02:00
Random
ea018bd4c1 Translated using Weblate (Italian)
Currently translated at 99.8% (657 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2024-05-18 10:01:54 +02:00
searinminecraft
8e81ee3b75 Translated using Weblate (Filipino)
Currently translated at 42.2% (278 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fil/
2024-05-16 08:02:04 +00:00
NicolaeFericitu
9c326f9be9 Translated using Weblate (Romanian)
Currently translated at 98.6% (649 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-05-16 08:02:02 +00:00
J-Jamet
7d2a0aa793 fix: Bad link for device unlocking #1826 2024-05-15 22:25:04 +02:00
J-Jamet
22d1442033 fix: Screenshot banner #1770 2024-05-15 22:05:31 +02:00
J-Jamet
ffa32a5501 fix: Entry validation button not accessible with the keyboard open #1770 2024-05-15 21:24:01 +02:00
J-Jamet
f50a6a8416 fix: Remove the "merge, reload" wording in read only mode #1709 2024-05-15 19:19:18 +02:00
bowornsin
7a82b75ee2 Translated using Weblate (Thai)
Currently translated at 90.1% (593 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2024-05-15 07:01:51 +00:00
NicolaeFericitu
350c0585b8 Translated using Weblate (Romanian)
Currently translated at 98.1% (646 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-05-15 07:01:51 +00:00
J-Jamet
5c4454d3ed fix: Nav header margin 2024-05-14 16:32:13 +02:00
J-Jamet
dfd1ae7a50 fix:
Removing a file from recents list doesn't remove star #1755
2024-05-14 15:16:53 +02:00
J-Jamet
d16fdd062f fix: F-Droid logotype #1814 2024-05-14 14:22:50 +02:00
J-Jamet
d6432698fc Merge branch 'yurtpage-i18n' into develop 2024-05-13 13:17:38 +02:00
J-Jamet
1a9c1e8bc1 Merge branch 'i18n' of github.com:yurtpage/KeePassDX into yurtpage-i18n 2024-05-13 13:17:20 +02:00
J-Jamet
2d05c6cb13 fix:
Coordinator layout to show the Snackbar error
2024-05-13 12:40:02 +02:00
J-Jamet
05f689913f fix:
Database is 0 Byte if Yubikey save is canceled #1680
2024-05-13 12:16:42 +02:00
J-Jamet
9e714c4192 fix:
Database is 0 Byte if Yubikey save is canceled #1680
2024-05-13 11:38:16 +02:00
J-Jamet
e1ad1e31f8 Gemfile 2024-05-13 09:50:02 +02:00
109247019824
b473bf1da8 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-05-09 13:33:28 +02:00
Milo Ivir
dfdc6eecb6 Translated using Weblate (Croatian)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2024-05-09 13:33:28 +02:00
ngocanhtve
e9145df77a Translated using Weblate (Vietnamese)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2024-05-07 14:07:14 +02:00
Pavel
4e824990d1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2024-05-07 14:07:13 +02:00
Wellington Terumi Uemura
5a053378e1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2024-05-07 14:07:12 +02:00
J-Jamet
e09306b6f3 Merge remote-tracking branch 'origin/develop' into develop 2024-05-06 16:15:44 +02:00
J-Jamet
21e577c1d3 fix: Change version to 4.0.6 2024-05-06 16:15:30 +02:00
searinminecraft
9d85d0979c Translated using Weblate (Filipino)
Currently translated at 14.4% (95 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fil/
2024-05-05 16:07:16 +02:00
Linerly
e4fec7ea1f Translated using Weblate (Indonesian)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2024-05-05 16:07:12 +02:00
Fjuro
c998e513fc Translated using Weblate (Czech)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2024-05-05 16:07:11 +02:00
searinminecraft
855b06c4b2 Added translation using Weblate (Filipino) 2024-05-05 05:34:40 +02:00
Oğuz Ersen
776012f02f Translated using Weblate (Turkish)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2024-05-04 15:07:23 +02:00
何意挽秋風
9bff531618 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2024-05-04 15:07:22 +02:00
DVXG
4d64818da1 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2024-05-04 15:07:21 +02:00
qx100
837c8773a8 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2024-05-04 15:07:20 +02:00
solokot
97a199b504 Translated using Weblate (Russian)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-05-04 15:07:19 +02:00
Matthaiks
9cd37be5b1 Translated using Weblate (Polish)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2024-05-04 15:07:18 +02:00
Stephan Paternotte
90895fed52 Translated using Weblate (Dutch)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2024-05-04 15:07:17 +02:00
gallegonovato
df4b73abbb Translated using Weblate (Spanish)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-05-04 15:07:15 +02:00
ΣΤΑΥΡΟΣ ΔΑΛΙΑΚΟΠΟΥΛΟΣ
d2bed08ae0 Translated using Weblate (Greek)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2024-05-04 15:07:14 +02:00
Masowick
fd5e1472e1 Translated using Weblate (German)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-05-04 15:07:13 +02:00
Anonymous
6a22126006 Translated using Weblate (Romanian)
Currently translated at 97.8% (644 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-05-03 14:38:37 +02:00
Anonymous
ff5e9049a0 Translated using Weblate (German)
Currently translated at 99.5% (655 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-05-03 14:38:36 +02:00
J-Jamet
7570aa9f49 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2024-05-03 14:32:56 +02:00
J-Jamet
4fcdd3da23 Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2024-05-03 14:07:35 +02:00
qx100
f8a65b4d13 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2024-05-02 09:45:46 +02:00
Yurt Page
5bb70a5043 fastlane i18n ru
Signed-off-by: Yurt Page <yurtpage@gmail.com>
2024-04-23 19:18:09 +03:00
J-Jamet
c4788159e4 Merge branch 'thekyber-patch-1' into develop 2024-04-22 17:13:16 +02:00
J-Jamet
403b0aedca Merge branch 'ngocanhtve-master' into develop 2024-04-22 17:06:56 +02:00
TheKyber
cae12b84df better way to verify github apk files
Verifying the signing certificate hash is way better than verifying the hash of the apk files, because it does not change at all, and it will be very noticeable if it was changed from the README.md file than just GitHub release notes.

And you will not have to calculate the hash and publish it in the future, so less work for you.
2024-04-19 13:52:26 +01:00
J-Jamet
40dbbbf0a0 fix: Recognize Brave forms 2024-04-19 13:41:58 +02:00
Champ0999
9e0da27484 Translated using Weblate (English (United Kingdom))
Currently translated at 35.2% (228 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en_GB/
2024-04-18 18:03:25 +02:00
Gia Huy Lữ
7e6c2463d6 Translated using Weblate (Vietnamese)
Currently translated at 43.2% (280 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2024-04-18 18:03:24 +02:00
Thomas
69a76db166 Translated using Weblate (Chinese (Traditional))
Currently translated at 93.1% (603 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2024-04-18 18:03:24 +02:00
devchung
efd282e326 Translated using Weblate (Yue (Traditional))
Currently translated at 20.7% (134 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/yue_Hant/
2024-04-17 14:33:37 +02:00
devchung
6471fef28c Translated using Weblate (Chinese (Traditional))
Currently translated at 92.5% (599 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2024-04-17 14:33:36 +02:00
devchung
5a0691b70c Added translation using Weblate (Yue (Traditional)) 2024-04-17 08:21:39 +02:00
J-Jamet
27ea357348 fix: Fix username recognition with firefox #1665 2024-04-14 20:44:51 +02:00
bowornsin
e12de29cce Translated using Weblate (Thai)
Currently translated at 91.8% (594 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2024-04-12 06:01:55 +02:00
Rasmus
a236be4e38 Translated using Weblate (Danish)
Currently translated at 99.0% (641 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2024-04-05 14:01:53 +02:00
SpaceFrrog
5300643f62 Translated using Weblate (English (United Kingdom))
Currently translated at 25.9% (168 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en_GB/
2024-04-04 09:43:58 +02:00
SpaceFrrog
469b837daf Translated using Weblate (German)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-04-04 09:43:57 +02:00
Ghost of Sparta
84ca3e8982 Translated using Weblate (Hungarian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2024-03-28 12:02:02 +01:00
Tim Trek
bfb8be159a Translated using Weblate (German)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-03-19 07:09:23 +01:00
bowornsin
13d7af9c76 Translated using Weblate (Thai)
Currently translated at 91.6% (593 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2024-03-17 08:43:10 +01:00
David C
66d78497b9 Translated using Weblate (Slovenian)
Currently translated at 10.2% (66 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sl/
2024-03-08 14:01:54 +01:00
109247019824
32743f7e19 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-03-06 22:01:44 +01:00
109247019824
c2cc4451ff Translated using Weblate (Bulgarian)
Currently translated at 95.0% (615 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-03-05 15:02:38 +01:00
109247019824
67370bbc3d Translated using Weblate (Bulgarian)
Currently translated at 84.8% (549 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-03-04 08:01:48 +01:00
Lenni
1201ae1956 Translated using Weblate (English (United Kingdom))
Currently translated at 25.1% (163 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en_GB/
2024-03-03 07:01:51 +01:00
109247019824
0eb2786ce5 Translated using Weblate (Bulgarian)
Currently translated at 82.3% (533 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-03-03 07:01:50 +01:00
109247019824
bbdeb66f8e Translated using Weblate (Bulgarian)
Currently translated at 78.3% (507 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-03-02 06:02:00 +01:00
J-Jamet
641698a652 Merge branch 'MDP43140-master' into develop 2024-03-01 17:24:16 +01:00
J-Jamet
15880e995a Merge branch 'master' of github.com:MDP43140/KeePassDX into MDP43140-master 2024-03-01 17:23:54 +01:00
J-Jamet
1e849b106b Merge branch 'Rilele-master' into develop 2024-03-01 16:36:42 +01:00
109247019824
31bc405e86 Translated using Weblate (Bulgarian)
Currently translated at 73.1% (473 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-02-29 23:02:09 +01:00
109247019824
6e69eee2a3 Translated using Weblate (Bulgarian)
Currently translated at 69.2% (448 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-02-27 20:49:46 +01:00
109247019824
279b095f30 Translated using Weblate (Bulgarian)
Currently translated at 62.1% (402 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-02-27 12:02:02 +01:00
109247019824
9821499cdb Translated using Weblate (Bulgarian)
Currently translated at 36.9% (239 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-02-26 08:02:03 +01:00
--//--
b76c39862a Translated using Weblate (Burmese)
Currently translated at 6.8% (44 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/my/
2024-02-23 16:01:59 +01:00
adfc
7948d234f5 Translated using Weblate (English (United Kingdom))
Currently translated at 13.9% (90 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en_GB/
2024-02-23 16:01:58 +01:00
109247019824
50fc3aa9fc Translated using Weblate (Bulgarian)
Currently translated at 20.2% (131 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-02-23 16:01:57 +01:00
adfc
79a7adb05b Translated using Weblate (Hindi)
Currently translated at 22.7% (147 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hi/
2024-02-23 16:01:56 +01:00
Besnik Bleta
c02770ec2a Translated using Weblate (Albanian)
Currently translated at 55.9% (362 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2024-02-22 13:02:04 +01:00
Elvis
bf24033513 Translated using Weblate (Uzbek)
Currently translated at 7.5% (49 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uz/
2024-02-18 20:02:04 +01:00
solokot
d97c87e90c Translated using Weblate (Russian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-02-18 20:02:02 +01:00
Elvis
6d29fdc448 Translated using Weblate (Uzbek)
Currently translated at 4.3% (28 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uz/
2024-02-17 19:39:44 +01:00
Elvis
d16f0dd3f6 Translated using Weblate (Russian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-02-17 19:39:43 +01:00
Elvis
d023c97049 Added translation using Weblate (Uzbek) 2024-02-17 19:08:49 +01:00
jonnysemon
73c8d63f0e Translated using Weblate (Arabic)
Currently translated at 99.3% (643 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2024-02-07 11:52:29 +01:00
Salif Mehmed
30709de7e2 Translated using Weblate (Bulgarian)
Currently translated at 20.0% (130 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-02-06 23:01:53 +01:00
Salif Mehmed
c3e560766e Translated using Weblate (Bulgarian)
Currently translated at 17.9% (116 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-02-05 13:01:51 +01:00
Aleksandar Sazdanović
7c7889e87e Translated using Weblate (Serbian (latin))
Currently translated at 37.0% (240 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sr_Latn/
2024-02-04 12:01:59 +01:00
Pere O
6547ccecdd Translated using Weblate (Catalan)
Currently translated at 97.9% (634 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2024-02-03 09:35:48 +01:00
Pere O
d467a591b0 Translated using Weblate (Catalan)
Currently translated at 65.3% (423 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2024-02-01 23:01:52 +01:00
solokot
42613ff480 Translated using Weblate (Russian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-01-21 10:01:50 +01:00
NicolaeFericitu
3a3d4b9e54 Translated using Weblate (Romanian)
Currently translated at 99.6% (645 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-01-15 17:06:12 +01:00
NicolaeFericitu
0b5c0bcf74 Translated using Weblate (Romanian)
Currently translated at 88.4% (572 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-01-09 23:06:34 +00:00
Deleted User
5937f009e1 Translated using Weblate (Danish)
Currently translated at 98.4% (637 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2024-01-09 23:06:13 +00:00
SC
c7f3d2a64e Translated using Weblate (Portuguese)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2024-01-06 17:06:11 +01:00
NicolaeFericitu
41263307d7 Translated using Weblate (Romanian)
Currently translated at 78.0% (505 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-01-06 17:06:11 +01:00
SC
f2ff52477e Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2024-01-06 17:06:11 +01:00
Mike
6baa5a0730 Translated using Weblate (Slovak)
Currently translated at 95.0% (615 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2023-12-31 18:36:23 +01:00
ERYpTION
5a5cb8c866 Translated using Weblate (Danish)
Currently translated at 96.9% (627 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2023-12-08 20:03:49 +00:00
Kenny
a3a78366f7 Translated using Weblate (Romanian)
Currently translated at 76.3% (494 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2023-12-06 15:11:11 +01:00
ngocanhtve
d936292ed9 Updated vi\strings.xml
Complete the Vietnamese translation.
2023-12-01 21:35:47 +07:00
ngocanhtve
a3ded18f59 Translated using Weblate (Vietnamese)
Currently translated at 38.3% (248 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2023-12-01 14:14:44 +01:00
Avi Parshan
0969135f7f Translated using Weblate (Hebrew)
Currently translated at 44.9% (291 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2023-11-27 18:02:20 +00:00
Rilele
7e1c1a6324 Fix German translation 2023-11-25 13:02:36 +01:00
J-Jamet
aa43dd2a49 fix: Upgrade to 4.1.0 2023-11-10 23:00:34 +01:00
J-Jamet
81fe9b6fc2 fix: phone field recognition 2023-11-10 22:56:23 +01:00
J-Jamet
6807fdc1cc Merge tag '4.0.5' into develop
4.0.5
2023-11-09 08:16:36 +01:00
J-Jamet
ac1590810f Merge branch 'release/4.0.5' 2023-11-09 08:16:29 +01:00
J-Jamet
9dd4d77535 fix: upgrade version 2023-11-09 08:06:30 +01:00
J-Jamet
c7aa6a9d96 fix: revert #1490 2023-11-09 08:02:16 +01:00
J-Jamet
fc56db2f83 fix: Autofill recognition 2023-11-08 20:43:50 +01:00
MDP43140
c58e56257d Fix Indonesia language
language code for Indonesia is apparently `in` instead of `id` in Java, thus makes the app displays default English language for users with Indonesian language. a simple folder rename should hopefully fix this
2023-11-07 23:27:49 +07:00
solokot
b27eb280a8 Translated using Weblate (Russian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2023-11-07 10:11:49 +01:00
J-Jamet
78c7f4078c Merge tag '4.0.4' into develop
4.0.4
2023-11-07 00:33:28 +01:00
J-Jamet
28ccaffcf5 Merge branch 'release/4.0.4' 2023-11-07 00:33:15 +01:00
J-Jamet
78739558d6 fix: password color #1490 2023-11-07 00:26:24 +01:00
J-Jamet
cd195d30de fix: update CHANGELOG 2023-11-06 23:49:58 +01:00
J-Jamet
03bf752284 fix: update CHANGELOG 2023-11-06 23:48:39 +01:00
J-Jamet
238fab3e1d fix: device unlock #1682 2023-11-06 23:47:31 +01:00
J-Jamet
fcd9af8f84 Revert "Revert "fix: Remove Lock in Autofill""
This reverts commit b44491ebbe.
2023-11-06 23:37:28 +01:00
J-Jamet
b44491ebbe Revert "fix: Remove Lock in Autofill"
This reverts commit 544f7003f6.
2023-11-06 23:35:14 +01:00
J-Jamet
1f8018fd5b fix: autofill 2023-11-06 23:23:42 +01:00
J-Jamet
1d67656fa0 fix: autofill 2023-11-06 22:40:48 +01:00
J-Jamet
64b8023d1a fix: upgrade version and CHANGELOG 2023-11-06 21:40:01 +01:00
J-Jamet
cc1697e7ec fix: last form field recognition #1572 2023-11-06 21:37:27 +01:00
J-Jamet
28943e77e8 Merge tag '4.0.3' into develop
4.0.3
2023-11-06 12:29:02 +01:00
J-Jamet
575109da9f Merge branch 'release/4.0.3' 2023-11-06 12:28:53 +01:00
J-Jamet
a99667d471 feat: New fastfile to build Libre in github 2023-11-06 12:28:28 +01:00
J-Jamet
6a7420bd3a fix: replace tags 2023-11-06 10:45:20 +01:00
J-Jamet
e8dbe05615 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2023-11-06 10:35:53 +01:00
J-Jamet
6b6566cd29 fix: small change #1674 2023-11-06 08:58:36 +01:00
J-Jamet
0001d31c2c fix: small change #1674 2023-11-06 08:57:48 +01:00
J-Jamet
974686e698 fix: small changes 2023-11-04 18:56:36 +01:00
J-Jamet
7b7063b9be fix: check biometric unlock availability before build the fragment #1400 2023-11-04 18:14:24 +01:00
J-Jamet
55061a9469 fix: runtime exception #1649 2023-11-04 18:01:23 +01:00
J-Jamet
c433fb643c fix: change password color dynamically #1490 2023-11-04 17:33:40 +01:00
J-Jamet
02306385b6 fix: #1641 #1656 2023-11-04 16:09:10 +01:00
J-Jamet
432ac1bcec Merge branch 'JohnVeness-biometric' into develop 2023-11-04 16:07:41 +01:00
J-Jamet
d9480e0c9a Merge branch 'MkQtS-f-droid-link' into develop 2023-11-04 16:05:24 +01:00
J-Jamet
815fb911d6 fix: regex OTP recognition #1596 2023-11-04 16:03:20 +01:00
J-Jamet
68cbdae8e0 fix: update CHANGELOG 2023-11-04 15:07:15 +01:00
J-Jamet
2d8f8aeef3 fix: Compatibility mode to retrieve username #1508 2023-11-04 13:02:57 +01:00
J-Jamet
479bc7be71 Upgrade to 4.0.3 2023-11-04 11:36:44 +01:00
/dev/urandom
cbc6df2e62 Translated using Weblate (Esperanto)
Currently translated at 21.9% (142 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eo/
2023-11-03 06:10:15 +01:00
Urystem
d16b4cfadb Added translation using Weblate (Kazakh) 2023-10-31 12:48:01 +01:00
ngocanhtve
a97bad1f86 Translated using Weblate (Vietnamese)
Currently translated at 34.4% (223 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2023-10-30 07:09:19 +01:00
J-Jamet
7198ffff43 fix: Save as 2023-10-29 20:45:35 +01:00
ngocanhtve
e173159d13 Translated using Weblate (Vietnamese)
Currently translated at 33.3% (216 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2023-10-28 17:33:10 +02:00
Jean Mareilles
1b54b79e88 Translated using Weblate (French)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2023-10-27 11:26:36 +02:00
P.O
bb3436615e Translated using Weblate (Swedish)
Currently translated at 60.1% (389 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2023-10-16 04:19:17 +00:00
Stasky745
809db61c35 Translated using Weblate (Catalan)
Currently translated at 62.4% (404 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2023-10-16 04:19:16 +00:00
elgratea
88e53fcba8 Translated using Weblate (Bulgarian)
Currently translated at 13.1% (85 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2023-10-13 21:02:05 +02:00
Balázs Meskó
fe68b7e294 Translated using Weblate (Hungarian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2023-10-08 16:01:48 +02:00
Åzze
b7b76d6da7 Translated using Weblate (Finnish)
Currently translated at 39.7% (257 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fi/
2023-10-08 16:01:48 +02:00
Fjuro
e2eae43fc9 Translated using Weblate (Czech)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2023-10-08 16:01:47 +02:00
bowornsin
f41ecec09c Translated using Weblate (Thai)
Currently translated at 91.4% (592 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2023-10-02 02:01:14 +02:00
John Veness
15ad4d11ef Fix paragraph break in Biometric warning text
Added an extra new line between the last two paragraphs, for consistency with the paragraphs above.
2023-10-01 21:07:54 +01:00
P.O
0cf9d98f14 Translated using Weblate (Swedish)
Currently translated at 59.3% (384 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2023-09-29 18:35:22 +02:00
elgratea
612e642523 Translated using Weblate (Bulgarian)
Currently translated at 9.8% (64 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2023-09-19 22:59:13 +02:00
Milo Ivir
0ff77eb157 Translated using Weblate (Croatian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2023-09-18 17:01:16 +00:00
Alexthegib
2b8935a5d7 Translated using Weblate (Portuguese)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2023-09-16 02:53:56 +02:00
Mesut Akcan
afdc5c8460 Translated using Weblate (Turkish)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2023-09-13 15:48:51 +02:00
alejandracios
91ba2dff2d Translated using Weblate (Spanish)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2023-09-13 15:48:50 +02:00
MkQtS
2d26079c49 readme: don't specify language in F-droid links
then F-droid will follow the browser locale
2023-09-13 19:16:43 +08:00
J-Jamet
f13d99e0d1 Merge tag '4.0.2' into develop
4.0.2
2023-09-11 21:55:56 +02:00
J-Jamet
798c95d8a8 Merge branch 'release/4.0.2' 2023-09-11 21:55:48 +02:00
J-Jamet
ef77c2acfb fix: Add buggy method comment #1638 2023-09-11 21:38:12 +02:00
J-Jamet
11a98267a2 fix: Upgrade to 4.0.2 2023-09-11 21:17:18 +02:00
J-Jamet
b2aa1155d0 fix: Autofill authentication 2023-09-11 21:13:26 +02:00
Linerly
d3182b8d2a Translated using Weblate (Indonesian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2023-09-11 19:51:53 +02:00
jonnysemon
f52d139acc Translated using Weblate (Arabic)
Currently translated at 97.5% (631 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2023-09-11 19:51:53 +02:00
Eric
87e9a38548 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2023-09-11 19:51:52 +02:00
Ihor Hordiichuk
faa70c57b3 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2023-09-11 19:51:52 +02:00
Alexthegib
5172c07c18 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2023-09-11 19:51:51 +02:00
Wellington Terumi Uemura
a80fa03db4 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2023-09-11 19:51:51 +02:00
Matthaiks
d73e02948e Translated using Weblate (Polish)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2023-09-11 19:51:51 +02:00
Stephan Paternotte
283657e1b7 Translated using Weblate (Dutch)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2023-09-11 19:51:50 +02:00
Random
d3c4a3a17e Translated using Weblate (Italian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2023-09-11 19:51:50 +02:00
Kunzisoft
9184bc40e5 Translated using Weblate (French)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2023-09-11 19:51:50 +02:00
gallegonovato
84bd98ebf4 Translated using Weblate (Spanish)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2023-09-11 19:51:49 +02:00
Retrial
4ef2cbcaeb Translated using Weblate (Greek)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2023-09-11 19:51:49 +02:00
Fjuro
35f8b45bf4 Translated using Weblate (Czech)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2023-09-11 19:51:48 +02:00
J-Jamet
5d6aac2d1b Merge tag '4.0.1' into develop
4.0.1
2023-09-10 11:07:13 +02:00
J-Jamet
c6723ecd4e Merge branch 'release/4.0.1' 2023-09-10 11:07:06 +02:00
J-Jamet
838c8f48d3 fix: Translation for themes #1631 2023-09-10 10:53:43 +02:00
J-Jamet
65229fae1f fix: Lock button in Autofill settings and Magikeyboard settings #1630 2023-09-10 10:26:01 +02:00
J-Jamet
f25ea89160 fix: Add CHANGELOG link 2023-09-10 09:57:56 +02:00
J-Jamet
18401d5d1e fix: Upgrade to 4.0.1 2023-09-10 09:50:26 +02:00
J-Jamet
1f2cf08108 fix: back lock 2023-09-10 09:43:08 +02:00
J-Jamet
74e86badba Merge tag '4.0.0' into develop
4.0.0
2023-09-09 21:34:37 +02:00
J-Jamet
31444a823e Merge branch 'release/4.0.0' 2023-09-09 21:34:28 +02:00
J-Jamet
068933f0fb fix: fastlane changelog version 2023-09-09 21:30:51 +02:00
J-Jamet
f496711280 Replace art screen 2023-09-09 21:24:40 +02:00
J-Jamet
657d2420d6 Revert "fix: Small bug"
This reverts commit 2467721265.
2023-09-09 21:21:04 +02:00
J-Jamet
2467721265 fix: Small bug 2023-09-09 21:11:39 +02:00
J-Jamet
d37fbb9992 fix: Change screenshot 2023-09-09 21:06:33 +02:00
J-Jamet
19b8b54dae fix: Strong tag 2023-09-08 21:22:55 +02:00
J-Jamet
84328caf3c fix: Add browsers to compatibility package 2023-09-08 21:22:36 +02:00
J-Jamet
a0bdfc973a Merge branch 'translations' into develop 2023-09-08 21:16:04 +02:00
J-Jamet
1f91854490 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2023-09-08 21:15:42 +02:00
J-Jamet
1380325b66 Upgrade to 4.0.0 2023-09-08 21:14:21 +02:00
J-Jamet
d244eef62e feat: Add recursive number of entries #874 #1327 2023-09-08 20:32:19 +02:00
Hosted Weblate
8fb8d9ed37 Merge branch 'origin/develop' into Weblate. 2023-09-08 19:35:10 +02:00
J-Jamet
8ce63cb5c5 Merge branch 'JohnVeness-master' into develop 2023-09-08 19:31:49 +02:00
J-Jamet
9ecf2ae942 Merge branch 'master' of github.com:JohnVeness/KeePassDX into JohnVeness-master 2023-09-08 19:31:07 +02:00
J-Jamet
544f7003f6 fix: Remove Lock in Autofill 2023-09-04 20:45:32 +02:00
J-Jamet
6d633c9986 fix: Autofill implementation 2023-09-04 19:16:16 +02:00
J-Jamet
1e77a42c93 fix: Remove Autofill deprecation 2023-09-03 18:41:32 +02:00
J-Jamet
d1f2641e40 fix: On back pressed deprecation 2023-09-03 12:29:45 +02:00
jonnysemon
4b8ae154cc Translated using Weblate (Arabic)
Currently translated at 97.6% (632 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2023-08-29 18:57:00 +02:00
J-Jamet
0c1aacdf83 fix: Navigation bar margin 2023-08-28 21:52:51 +02:00
J-Jamet
5f34df3549 fix: Ellipsis 2023-08-28 17:20:09 +02:00
Alexandru
f2e6aa1abb Translated using Weblate (Romanian)
Currently translated at 72.9% (472 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2023-08-28 10:53:32 +02:00
John Veness
866731df81 Correct "im/exportation" wording 2023-08-20 19:39:09 +01:00
John Veness
5d931e09d5 Changed "app properties" wording to "app settings" 2023-08-20 19:08:51 +01:00
Kunzisoft
fe17c21c01 Translated using Weblate (French)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2023-08-20 11:52:19 +02:00
J-Jamet
085941019e fix: Small bugs 2023-08-19 19:58:26 +02:00
J-Jamet
24b3758545 feat: Entry Edit Activity Fit Window 2023-08-19 19:16:55 +02:00
J-Jamet
9083f99325 feat: Entry Activity Fit Window 2023-08-19 18:15:21 +02:00
J-Jamet
2189be9267 fix: Screenshot mode 2023-08-19 17:47:49 +02:00
J-Jamet
43218eede1 feat: Group screen as fit window 2023-08-19 17:00:26 +02:00
J-Jamet
d1a176d27d fix: Small UI 2023-08-19 12:25:38 +02:00
J-Jamet
cf51af91bf feat: Logo color 2023-08-19 12:20:41 +02:00
Darin Avdeyeva
02ff1188b2 Translated using Weblate (Russian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2023-08-18 15:54:40 +02:00
jonnysemon
0fac9b6864 Translated using Weblate (Arabic)
Currently translated at 97.5% (631 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2023-08-17 01:55:03 +02:00
VfBFan
b550830c30 Translated using Weblate (German)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-08-15 11:47:13 +02:00
gallegonovato
6f485dd298 Translated using Weblate (Spanish)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2023-08-12 10:53:07 +02:00
solokot
b0dfde62c7 Translated using Weblate (Russian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2023-08-09 17:50:55 +02:00
109247019824
686dae0af6 Translated using Weblate (Bulgarian)
Currently translated at 2.9% (19 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2023-08-08 17:21:18 +02:00
Darin Avdeyeva
ee3eabe8c8 Translated using Weblate (Russian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2023-08-08 17:21:18 +02:00
C. Rüdinger
521c8aa6a9 Translated using Weblate (German)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-08-08 17:21:17 +02:00
solokot
66207d599f Translated using Weblate (Russian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2023-08-08 08:51:37 +02:00
Masowick
e0029e0c3f Translated using Weblate (German)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-08-08 08:51:37 +02:00
C. Rüdinger
3683b64721 Translated using Weblate (German)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-08-08 08:51:37 +02:00
J-Jamet
8d4a0971f9 Merge tag '4.0.0beta02' into develop
4.0.0beta02
2023-08-07 16:00:38 +02:00
J-Jamet
e4c3baa344 Merge branch 'release/4.0.0beta02' 2023-08-07 16:00:15 +02:00
J-Jamet
1e60d7e637 fix: Crash 2023-08-07 15:47:20 +02:00
J-Jamet
262b0227c1 fix: Selection switch 2023-08-07 15:27:38 +02:00
J-Jamet
226e461324 fix: Item entry alignment 2023-08-07 13:57:47 +02:00
J-Jamet
151eb26d56 fix: Tab in entry 2023-08-07 13:52:48 +02:00
J-Jamet
335e767426 fix: Small bugs 2023-08-07 13:36:19 +02:00
Hosted Weblate
d212fa180b Merge branch 'origin/develop' into Weblate. 2023-08-06 23:00:27 +02:00
Salif Mehmed
bc4ea2ec2a Translated using Weblate (Bulgarian)
Currently translated at 2.7% (18 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2023-08-06 23:00:27 +02:00
Linerly
5d8c80fc1e Translated using Weblate (Indonesian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2023-08-06 23:00:27 +02:00
Alexandru
a02714ff6e Translated using Weblate (Romanian)
Currently translated at 63.5% (411 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2023-08-06 23:00:26 +02:00
Fjuro
4bd952e223 Translated using Weblate (Czech)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2023-08-06 23:00:25 +02:00
J-Jamet
91bbc6d84e fix: tags 2023-08-06 22:59:48 +02:00
J-Jamet
6dbd16c5f6 Merge branch 'translations' into develop 2023-08-06 22:57:18 +02:00
J-Jamet
76e040c585 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2023-08-06 22:55:11 +02:00
J-Jamet
8de6382a64 fix: update CHANGELOG 2023-08-06 22:54:06 +02:00
J-Jamet
53532ead9f fix: Dialog buttons color 2023-08-06 22:47:33 +02:00
J-Jamet
f4e6baeac2 fix: Entry path style 2023-08-06 21:46:36 +02:00
J-Jamet
5c46fdf41a fix: Replace boolean parcelable 2023-08-06 20:57:39 +02:00
J-Jamet
22073e4bbd fix: Deactivated color 2023-08-06 20:29:01 +02:00
J-Jamet
41e7376b7b feat: Cut and Copy from search CHANGELOG 2023-08-05 17:37:20 +02:00
J-Jamet
3fc26c8c4e feat: Cut and Copy from search CHANGELOG 2023-08-05 17:37:15 +02:00
J-Jamet
14f070a942 feat: Cut and Copy from search CHANGELOG 2023-08-05 17:36:13 +02:00
J-Jamet
c078bd05e2 feat: Cut and Copy from search 2023-08-05 17:29:32 +02:00
J-Jamet
8ce9757b7c fix: Error with coordinator 2023-08-05 17:06:07 +02:00
bowornsin
e028738dc2 Translated using Weblate (Thai)
Currently translated at 83.9% (543 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2023-08-03 11:08:42 +02:00
Masowick
9f4a302b72 Translated using Weblate (German)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-08-03 11:08:42 +02:00
C. Rüdinger
2ef17e0c7a Translated using Weblate (German)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-08-03 11:08:42 +02:00
VfBFan
b86a8c8633 Translated using Weblate (German)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-08-03 11:08:41 +02:00
Masowick
5a3be0853e Translated using Weblate (German)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-08-01 21:36:43 +02:00
Reza Almanda
99568db10c Translated using Weblate (Indonesian)
Currently translated at 96.1% (622 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2023-08-01 07:36:19 +02:00
Milo Ivir
bf892f5b6a Translated using Weblate (Croatian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2023-08-01 07:36:19 +02:00
Eric
8e2c7ba1f0 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2023-08-01 07:36:18 +02:00
solokot
fd3bb4b243 Translated using Weblate (Russian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2023-08-01 07:36:18 +02:00
André Marcelo Alvarenga
7f4a1d6896 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2023-08-01 07:36:17 +02:00
Retrial
d62734e8ac Translated using Weblate (Greek)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2023-08-01 07:36:17 +02:00
VfBFan
fbebc12a38 Translated using Weblate (German)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-08-01 07:36:16 +02:00
Masowick
3c65be2a72 Translated using Weblate (German)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-08-01 07:36:16 +02:00
Fjuro
a29a9f28ef Translated using Weblate (Czech)
Currently translated at 93.9% (608 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2023-08-01 07:36:15 +02:00
J-Jamet
eb14dadb3c fix: Scroll and color flickering 2023-07-31 22:28:53 +02:00
J-Jamet
8d926a306b fix: Shadow on logo 2023-07-31 22:19:55 +02:00
J-Jamet
5699359099 fix: Toolbar flickering 2023-07-31 22:07:41 +02:00
J-Jamet
e3176033dc fix: Margin bug 2023-07-31 22:03:29 +02:00
J-Jamet
9df6215c02 Merge branch 'feature/delete_search_entry_1308' into develop 2023-07-31 21:49:34 +02:00
J-Jamet
93a0e4c0a6 fix: Upgrade CHANGELOG 2023-07-31 21:49:20 +02:00
J-Jamet
f55a824cdc fix: Search in special mode 2023-07-31 21:45:35 +02:00
J-Jamet
766026d3be Merge branch 'develop' into feature/delete_search_entry_1308 2023-07-31 21:24:41 +02:00
J-Jamet
c64fc56496 Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2023-07-31 21:19:46 +02:00
J-Jamet
6e2fb21431 fix: Better theme colors 2023-07-31 21:19:16 +02:00
Jérémy JAMET
2bb70abc39 Merge pull request #1582 from MarijnS95/icon-pack-JavaVersion
Add `JavaVersion` compatible to `icon-pack`
2023-07-31 16:08:35 +02:00
Darin Avdeyeva
a6cb1dbe5c Translated using Weblate (Russian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2023-07-30 15:18:59 +02:00
Alexthegib
5222a72cc6 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2023-07-30 15:18:59 +02:00
Stephan Paternotte
5d3aa44545 Translated using Weblate (Dutch)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2023-07-30 15:18:58 +02:00
random r
61cfda93a5 Translated using Weblate (Italian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2023-07-30 15:18:58 +02:00
Htet Oo Hlaing
b490295b90 Translated using Weblate (Burmese)
Currently translated at 4.6% (30 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/my/
2023-07-30 11:56:12 +02:00
Alexthegib
61035ca47b Translated using Weblate (Portuguese)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2023-07-30 11:56:11 +02:00
Eric
1dc08bbfef Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2023-07-30 11:56:10 +02:00
Ihor Hordiichuk
9ea7c86da7 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2023-07-30 11:56:09 +02:00
solokot
4fa3fb86cb Translated using Weblate (Russian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2023-07-30 11:56:08 +02:00
marfS2
df089f4415 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2023-07-30 11:56:07 +02:00
Matthaiks
6be12eb440 Translated using Weblate (Polish)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2023-07-30 11:56:06 +02:00
Stephan Paternotte
84efd1c497 Translated using Weblate (Dutch)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2023-07-30 11:56:05 +02:00
Kunzisoft
4817654d58 Translated using Weblate (French)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2023-07-30 11:56:04 +02:00
Deleted User
70d45e0bba Translated using Weblate (German)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-07-30 11:56:02 +02:00
C. Rüdinger
a4c7e3860b Translated using Weblate (German)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-07-30 11:56:01 +02:00
VfBFan
2a890091d7 Translated using Weblate (German)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-07-30 11:56:01 +02:00
Htet Oo Hlaing
cc593e6e1f Added translation using Weblate (Burmese) 2023-07-30 04:40:47 +02:00
J-Jamet
552684fd90 feat: Delete entry from search 2023-07-29 21:40:00 +02:00
J-Jamet
a260e1d4e3 fix: Encapsulate Keyboard code 2023-07-29 18:59:16 +02:00
J-Jamet
07bbf232b6 Merge tag '4.0.0beta01' into develop
4.0.0beta01
2023-07-29 12:45:50 +02:00
Marijn Suijten
b6d32999b9 Add JavaVersion compatible to icon-pack
This now also contains Kotlin code.
2023-06-22 01:11:06 +02:00
J-Jamet
0474e5b1fe Merge branch 'Sandelinos-themed-icons' into develop 2022-09-29 16:44:43 +02:00
253 changed files with 10958 additions and 3936 deletions

View File

@@ -1,10 +1,61 @@
KeePassDX(4.1.0)
* Generate keyfile #1290
* Hide template group #1894
* Group count sum recursively #421
* Fix date fields #1695 #1710
* Fix distinct domain names #1105 #1820
* Resets the advanced unlock expiration #1600
* Password entropy #1490 #1355
* Upgrade to API 34 (Android 14) #1730
* Small fixes #1711 #1831 #1780 #1821 #1863 #1889 #1289 #1600 #1467 #1870
KeePassDX(4.0.8)
* Fix graphical bug that prevented databases from being opened on some versions of Android #1848 #1850
KeePassDX(4.0.7)
* Prevent 0 Byte file with cache during a save exception #1620 #1594 #1680
* Fix inline suggestions in keyboard #1840
* Fix broken links by default #1755
* Fix UX by allowing validation in entry edition #1770
* Fix small bugs #1709
KeePassDX(4.0.6)
* Fix form filled recognition #1508 #1735 #1508 #1790 #1783 #1797 #1801 #1802 #1804 #1665
* Fix translations #1707 #1683 #1712
* Update APK verifier #1810
KeePassDX(4.0.5)
* Fix form filled recognition #1572 #1508
* Rollback password color #1686 #1490
KeePassDX(4.0.4)
* Fix form filled recognition #1572 #1677
* Fix device unlock #1682
* Fix password color #1490
KeePassDX(4.0.3)
* Fix "Save as" in Read Only mode #1666
* Fix username autofill #1665 #530 #1572 #1426 #1523 #1556 #1653 #1658 #1508 #1667
* Fix regex OTP recognition #1596
* Change password color dynamically #1490
* Small fixes #1641 #1656 #1649 #1400 #1674
KeePassDX(4.0.2)
* Fix Autofill with API 33
KeePassDX(4.0.1)
* Fix back lock #1635 #1629 #1634
* Fix lock button in settings #1630
* Improve theme translation #1631
KeePassDX(4.0.0)
* New UX/UI with Material 3 #1183 #1529 #1428 #1441
* New UX/UI with Material 3 #1183 #1529 #1428 #1441 #1607
* Material You theme (follow system colors) #1469
* Refactoring inner code #1371
* Migration to API 33
* Fix behaviors #1351
* Fix bugs #1589 #1584 #1545 #1563 #1371
* Cut, copy and delete from search #891 #1308 #1263
* Fix behaviors #1351 #874 #1327
* Fix bugs #1589 #1584 #1545 #1563 #1371 #1609
KeePassDX(3.5.1)
* Fix action dialog with YubiKey challenge-response #1506

View File

@@ -1,29 +1,32 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.6)
CFPropertyList (3.0.7)
base64
nkf
rexml
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.15)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.794.0)
aws-sdk-core (3.180.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
aws-eventstream (1.3.0)
aws-partitions (1.1009.0)
aws-sdk-core (3.213.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.71.0)
aws-sdk-core (~> 3, >= 3.177.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.132.0)
aws-sdk-core (~> 3, >= 3.179.0)
aws-sdk-kms (1.95.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.171.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.6)
aws-sigv4 (1.6.0)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.10.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
@@ -32,12 +35,11 @@ GEM
declarative (0.0.20)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.100.0)
faraday (1.10.3)
excon (0.112.0)
faraday (1.10.4)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -58,22 +60,22 @@ GEM
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.2.7)
fastlane (2.214.0)
fastimage (2.3.1)
fastlane (2.225.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
@@ -82,34 +84,39 @@ GEM
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (~> 0.1.1)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-versioning_android (0.1.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-plugin-versioning_android (0.1.1)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.46.0)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.1)
google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@@ -117,64 +124,63 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.19.0)
google-apis-core (>= 0.9.0, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.7.1)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.1)
google-cloud-storage (1.44.0)
google-cloud-errors (1.4.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.19.0)
google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.7.0)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.5)
http-cookie (1.0.7)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.2)
json (2.6.3)
jwt (2.7.1)
memoist (0.16.2)
mini_magick (4.12.0)
mini_mime (1.1.2)
json (2.8.2)
jwt (2.9.3)
base64
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.3.0)
nanaimo (0.3.0)
multipart-post (2.4.1)
nanaimo (0.4.0)
naturally (2.2.1)
optparse (0.1.1)
nkf (0.2.0)
optparse (0.6.0)
os (1.1.4)
plist (3.7.0)
public_suffix (5.0.3)
rake (13.0.6)
plist (3.7.1)
public_suffix (6.0.1)
rake (13.2.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.6)
rexml (3.3.9)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.17.0)
security (0.1.5)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
@@ -182,28 +188,25 @@ GEM
simctl (1.6.10)
CFPropertyList
naturally
sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.8.1)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.22.0)
xcodeproj (1.27.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
@@ -217,4 +220,4 @@ DEPENDENCIES
fastlane-plugin-versioning_android
BUNDLED WITH
2.1.4
2.5.10

View File

@@ -1,8 +1,8 @@
# Android KeePassDX
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> **Lightweight password manager for Android**, KeePassDX allows editing encrypted data in a single file in KeePass format and fill in the forms in a secure way.
<img alt="KeePassDX Icon" src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> **Lightweight password safe and manager for Android**, KeePassDX allows editing encrypted data in a single file in KeePass format and fill in the forms in a secure way.
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
<img alt="KeePassDX Screenshot" src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
### Features
@@ -48,17 +48,39 @@ Optional visual styles are accessible after a contribution (and a congratulatory
## Download
*[F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) is the recommended way of installing, a libre software project that verifies that all the libraries and app code is libre software.*
*[F-Droid](https://f-droid.org/packages/com.kunzisoft.keepass.libre/) is the recommended way of installing, a libre software project that verifies all the libraries and app code is libre software.*
[<img src="https://f-droid.org/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
[<img src="https://raw.githubusercontent.com/Kunzisoft/Github-badge/main/get-it-on-github.png"
alt="Get it on Github"
height="80">](https://github.com/Kunzisoft/KeePassDX/releases)
| Source | Status | [Version](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ#why-a-libre-and-free-version) |
|--------|--------|---------|
| [Google Play](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free) | ![Google Play Release](https://img.shields.io/endpoint?color=blue&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dcom.kunzisoft.keepass.free%26gl%3DUS%26hl%3Den%26l%3DGoogle%2520Play%26m%3D%24version) | Free + [Pro](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro) |
| [F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) | ![F-Droid Version](https://img.shields.io/f-droid/v/com.kunzisoft.keepass.libre?logo=F-Droid&label=F-Droid) | Libre |
| [IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/com.kunzisoft.keepass.free) | ![IzzyOnDroid Version](https://img.shields.io/endpoint?&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAADAFBMVEUA0////wAA0v8A0v8A0////wD//wAFz/QA0/8A0/8A0/8A0/8A0v///wAA0/8A0/8A0/8A0/8A0//8/gEA0/8A0/8B0/4A0/8A0/8A0/+j5QGAwwIA0//C9yEA0/8A0/8A0/8A0/8A0/8A0/+n4SAA0/8A0/8A0/+o6gCw3lKt7QCv5SC+422b3wC19AC36zAA0/+d1yMA0/8A0/+W2gEA0/+w8ACz8gCKzgG7+QC+9CFLfwkA0/8A0////wAA0/8A0/8A0/8A0/+f2xym3iuHxCGq5BoA1P+m2joI0vONyiCz3mLO7oYA0/8M1Piq3Ei78CbB8EPe8LLj9Ly751G77zWQ1AC96UYC0fi37CL//wAA0/8A0////wD//wCp3jcA0/+j3SGj2i/I72Sx4zHE8FLB8zak1kYeycDI6nRl3qEA0/7V7psA0v6WzTa95mGi2RvB5XkPy9zH5YJ3uwGV1yxVihRLiwdxtQ1ZkAf//wD//wD//wD//wD//wCn5gf//wD//wD//wD//wD//wAA0/+h4A3R6p8A0/+X1w565OD6/ARg237n9csz2vPz+gNt37V/vifO8HW68B/L6ZOCwxXY8KRQsWRzhExAtG/E612a1Rd/pTBpmR9qjysduKVhmxF9mTY51aUozK+CsDSA52T//wD//wAA0////wD//wBJ1JRRxFWjzlxDyXRc0pGT1wCG0CWB3VGUzSTh8h6c0TSr5CCJ5FFxvl6s4H3m8xML0/DA5CvK51EX1N+Y2gSt4Dag3ChE3fax2ki68yO57NF10FRZnUPl88eJxhuCxgCz5EOLwEGf1DFutmahzGW98x0W1PGk3R154MHE6bOn69qv3gy92oG90o+Hn07B7rhCmiyMwECv1nO+0pQfwrCo57xF2daXsVhKrEdenQAduaee1Bsjr42z5D9RoCXy+QNovXpy2Z5MtWDO/TiSukaF3UtE1K6j3B4YwLc5wXlzpyIK0u5zy3uJqg4pu5RTpkZmpVKyAP8A0wBHcExHcEyBUSeEAAABAHRSTlP///9F9wjAAxD7FCEGzBjd08QyEL39abMd6///8P/ZWAnipIv/cC6B//7////////L/1Dz/0D///////86/vYnquY3/v///5T//v///17///////////////84S3QNB/8L/////////////7r/////NP////9l/////wPD4yis/x7Ym2lWSP+em////0n////////v///////////////////7//7pdGN3Urr6/+v/6aT////+//H/o2P/1v+7r7jp4PM/3p4g////g///K///481LxO///v////9w////8v/////9/p3J///a+P9v/5KR/+n///+p/xf//8P//wAAe7FyaAAABCZJREFUSMdj+E8iYKBUgwIHnwQ3N7cEHxcH+///VayoAE0Dh41qR7aBnCIQ8MsJKHH9/99czYYMWlA0cIkJGjMgAKfq//9RNYzIgLcBWYOTiCgDMhDn+B9bh6LebiWyH6L5UZQzONoAHWSHoqEpDkkDsyKqelv1//9rG1HUN9YihZK9AKp6BkG+/6xNqA5ajhSsCkrIipmYGGRa//9vQXVQXSySBnkWJOUMfn5Myuz/G3hR1NdEIUUchwiy+bkTsg4dbW/fu6W/e1c3XMMy5JiOZkFxUFZo74mgKTqaKXu0+2HqVwkja3BH9kFu361JwcHTfPJD4mdfe8ULAdVRyGlJAcVFfg+CQOozZ4XrJ85+JgwBsVXIGriQw5Tp4ZScezd8JiWnBupru30qwJZa+ZAjmWlC8fUZM4qB6kPnLNSPLMWqQQ5ZQ5aOzs1HmamBaQHzFs6y+qAmJCTE8f9/QgKSBg4DJPWc6zVDQkIC09JkZSPD38kukpExFpT4z67uYI/QwCOOCCK/izvu5CWl6AcEWMnKWml7LWbKZfH9/99UkknQHhGsynDz+65eWXv3/JmJrq5eXienVlRUfH/z8VvCf45soKQIH1yDEQsszrp6gwq9C73T87xcXadKl5TkFev4A/2tygmSBqYXqAYJmK+ZuoJydDR1vP09DA0NOy2kpdML81+U/heCpH1JU3jig7lJ5nKOT4i/t6ZHkqGzs4lJmIVHfrj+JR4HqLQSD0yDkCNEpGNn5ix9D03/eJdElTZdKV2TpNOhkwt8YUlNUgimgV0dLMBvf1gz1MolPd5FRcVNSkpDQ8owJeBCDyIhrIDnOD5QcuIU+3/2QKSs9laQ+noNLS0zLWdtqyP7mBAFAw88TwsJgMuJYweBGjYngtWbmeuZOW+bvNQToUFOAlFqOBk4Ov3/L7Z60/aN0p1tUhpa5nqWlub7C3p2I9QzyAghlUvczOz/1fhzPT3XSIfpSmmYAdVbmm1gV0dSz8DSilpUQsqCddIWIA3meuZaJqdMJZEzl6gRqgZIWZAxUdoizERXN8yi5MltcZTChzMaRQM3JNUWHS8rL/+yaPGvMmvr5ywoGoxtkDWwQ+Pb89ycBeWfGSJeL/la+RS1eOPnRtbQKgMRjZg+t8x6PkP273nWQAoFOPAgaeAThKXAmXMrK39Kmr5fsuBlBqoXfJGLe3VbmHjG9Mczi9T//3h7vygXtcDlQtJg44iQiIjIBRbGPO7gghPJy0ZIxT2HOLIUgwxQzsgYrUR350HSIMaJLidhgKY+mw+pflBDrX8E7OGBjPCAPc76gQFSTqAIiYrb/8dRP4CyosJ/rmwU5XIxHMilt4QBJwsSkBMClxOQULBlkRRwEONmR2kJcDGjADX2/+xO8r5iqjExqmLyrWpcPFRta1BfAwCtyN3XpuJ4RgAAAABJRU5ErkJggg==&url=https://apt.izzysoft.de/fdroid/api/v1/shield/com.kunzisoft.keepass.free&label=IzzyOnDroid) | Free |
| [GitHub](https://github.com/Kunzisoft/KeePassDX/releases) / [Obtainium](https://github.com/ImranR98/Obtainium) | ![GitHub Release](https://img.shields.io/github/v/release/Kunzisoft/KeePassDX?include_prereleases&logo=GitHub&label=GitHub) | Free & Libre |
## Package authenticity from GitHub
- Download the app from [GitHub releases](https://github.com/Kunzisoft/KeePassDX/releases/latest)
- Install [`apksigner`](https://developer.android.com/tools/apksigner) from [Android Studio](https://developer.android.com/studio)
- Open the directory where you saved the downloaded file in the Terminal
- Make sure that you have `apksigner` installed by running:
```shell
apksigner --version
```
- Depending on the APK file you downloaded, run:
```shell
apksigner verify --verbose --print-certs -min-sdk-version 24 KeePassDX-*.apk
```
You should get this output :
```shell
Verified using v2 scheme (APK Signature Scheme v2): true
...
Number of signers: 1
Signer #1 certificate SHA-256 digest: 7d55b8af210381aabf960f07e17cf7857b6d2a642ca2da6bf0bdf1b200362f04
...
Signer #1 public key SHA-256 digest: 5d261d3176db1e077b80112824d9390167f3be0561827e42112ed6b71192db81
```
If it's the case, this means that the APK was well built by the author of KeePassDX.
## Frequently Asked Questions
@@ -74,7 +96,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
## License
Copyright © 2023 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
Copyright © 2024 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
This file is part of KeePassDX.

View File

@@ -5,15 +5,14 @@ apply plugin: 'kotlin-kapt'
android {
namespace 'com.kunzisoft.keepass'
compileSdkVersion 33
buildToolsVersion "33.0.2"
compileSdkVersion 34
defaultConfig {
applicationId "com.kunzisoft.keepass"
minSdkVersion 15
targetSdkVersion 33
versionCode = 121
versionName = "4.0.0_beta01"
targetSdkVersion 34
versionCode = 132
versionName = "4.1.0"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
@@ -36,6 +35,13 @@ android {
}
}
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
flavorDimensions "version"
productFlavors {
libre {
@@ -120,7 +126,7 @@ dependencies {
// Autofill
implementation "androidx.autofill:autofill:1.1.0"
// Time
implementation 'joda-time:joda-time:2.10.13'
implementation 'joda-time:joda-time:2.13.0'
// Color
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6'
// Education

View File

@@ -7,10 +7,6 @@
<group
android:translateX="-12"
android:translateY="-12">
<path
android:fillColor="#ffa726"
android:strokeWidth="1.99999297"
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
<path
android:fillColor="#ffffff"
android:strokeWidth="1.99999297"

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="84"
android:viewportHeight="84">
<group
android:translateX="-12"
android:translateY="-12">
<path
android:fillColor="#ffffff"
android:strokeWidth="1.99999297"
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
</group>
</vector>

View File

@@ -7,10 +7,6 @@
<group
android:translateX="-12"
android:translateY="-12">
<path
android:fillColor="#ffa726"
android:strokeWidth="1.99999297"
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
<path
android:fillColor="#ffffff"
android:strokeWidth="1.99999297"

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="84"
android:viewportHeight="84">
<group
android:translateX="-12"
android:translateY="-12">
<path
android:fillColor="#ffffff"
android:strokeWidth="1.99999297"
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
</group>
</vector>

View File

@@ -9,6 +9,10 @@
android:anyDensity="true" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-permission
android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
@@ -118,7 +122,7 @@
android:name="com.kunzisoft.keepass.activities.GroupActivity"
android:exported="false"
android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustPan">
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="android.app.default_searchable"
android:value="com.kunzisoft.keepass.search.SearchResults"
@@ -197,18 +201,27 @@
<service
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
android:foregroundServiceType="dataSync"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.services.AttachmentFileNotificationService"
android:foregroundServiceType="dataSync"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.services.ClipboardEntryNotificationService"
android:foregroundServiceType="specialUse"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
android:foregroundServiceType="specialUse"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
android:foregroundServiceType="specialUse"
android:enabled="true"
android:exported="false" />
<!-- Receiver for Autofill -->
@@ -235,10 +248,6 @@
<action android:name="android.view.InputMethod" />
</intent-filter>
</service>
<service
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
android:enabled="true"
android:exported="false" />
<receiver
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
android:exported="true">

View File

@@ -38,7 +38,11 @@ import android.graphics.RectF
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.util.TypedValue
import android.view.*
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.DecelerateInterpolator
import android.view.animation.Interpolator
@@ -202,7 +206,7 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
override fun onDown(e: MotionEvent): Boolean = true
override fun onScroll(
e1: MotionEvent,
e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
@@ -220,7 +224,7 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
}
override fun onFling(
e1: MotionEvent,
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float

View File

@@ -25,6 +25,7 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi
@@ -44,6 +45,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.utils.WebDomain
import java.lang.RuntimeException
@RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : DatabaseModeActivity() {
@@ -216,6 +218,8 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
companion object {
private val TAG = AutofillLauncherActivity::class.java.name
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
@@ -224,37 +228,51 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
fun getPendingIntentForSelection(context: Context,
searchInfo: SearchInfo? = null,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent {
return PendingIntent.getActivity(context, 0,
// Doesn't work with direct extra Parcelable (don't know why?)
// Wrap into a bundle to bypass the problem
Intent(context, AutofillLauncherActivity::class.java).apply {
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
putParcelable(KEY_SEARCH_INFO, searchInfo)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
}
})
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
})
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent? {
try {
return PendingIntent.getActivity(
context, 0,
// Doesn't work with direct extra Parcelable (don't know why?)
// Wrap into a bundle to bypass the problem
Intent(context, AutofillLauncherActivity::class.java).apply {
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
putParcelable(KEY_SEARCH_INFO, searchInfo)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
}
})
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
}
)
} catch (e: RuntimeException) {
Log.e(TAG, "Unable to create pending intent for selection", e)
return null
}
}
fun getPendingIntentForRegistration(context: Context,
registerInfo: RegisterInfo): PendingIntent {
return PendingIntent.getActivity(context, 0,
Intent(context, AutofillLauncherActivity::class.java).apply {
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
putExtra(KEY_REGISTER_INFO, registerInfo)
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
})
registerInfo: RegisterInfo): PendingIntent? {
try {
return PendingIntent.getActivity(
context, 0,
Intent(context, AutofillLauncherActivity::class.java).apply {
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
putExtra(KEY_REGISTER_INFO, registerInfo)
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
}
)
} catch (e: RuntimeException) {
Log.e(TAG, "Unable to create pending intent for registration", e)
return null
}
}
fun launchForRegistration(context: Context,

View File

@@ -23,6 +23,7 @@ import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
@@ -30,15 +31,20 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout
@@ -69,15 +75,19 @@ import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.view.WindowInsetPosition
import com.kunzisoft.keepass.view.applyWindowInsets
import com.kunzisoft.keepass.view.changeControlColor
import com.kunzisoft.keepass.view.changeTitleColor
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.setTransparentNavigationBar
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.EntryViewModel
import java.util.UUID
class EntryActivity : DatabaseLockActivity() {
private var footer: ViewGroup? = null
private var coordinatorLayout: CoordinatorLayout? = null
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var appBarLayout: AppBarLayout? = null
@@ -128,6 +138,7 @@ class EntryActivity : DatabaseLockActivity() {
supportActionBar?.setDisplayShowHomeEnabled(true)
// Get views
footer = findViewById(R.id.activity_entry_footer)
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
appBarLayout = findViewById(R.id.app_bar)
@@ -139,6 +150,14 @@ class EntryActivity : DatabaseLockActivity() {
lockView = findViewById(R.id.lock_button)
loadingView = findViewById(R.id.loading)
// To apply fit window with transparency
setTransparentNavigationBar {
// To fix margin with API 27
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
}
// Empty title
collapsingToolbarLayout?.title = " "
toolbar?.title = " "

View File

@@ -72,6 +72,7 @@ import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
@@ -86,11 +87,15 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.TimeUtil.datePickerToDataDate
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.view.ToolbarAction
import com.kunzisoft.keepass.view.WindowInsetPosition
import com.kunzisoft.keepass.view.applyWindowInsets
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.setTransparentNavigationBar
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.updateLockPaddingLeft
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
@@ -104,6 +109,8 @@ class EntryEditActivity : DatabaseLockActivity(),
ReplaceFileDialogFragment.ActionChooseListener {
// Views
private var footer: View? = null
private var container: View? = null
private var coordinatorLayout: CoordinatorLayout? = null
private var scrollView: NestedScrollView? = null
private var templateSelectorSpinner: Spinner? = null
@@ -156,10 +163,8 @@ class EntryEditActivity : DatabaseLockActivity(),
// Bottom Bar
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
setSupportActionBar(entryEditAddToolBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.setDisplayShowTitleEnabled(false)
footer = findViewById(R.id.activity_entry_edit_footer)
container = findViewById(R.id.activity_entry_edit_container)
coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout)
scrollView = findViewById(R.id.entry_edit_scroll)
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
@@ -168,6 +173,17 @@ class EntryEditActivity : DatabaseLockActivity(),
validateButton = findViewById(R.id.entry_edit_validate)
loadingView = findViewById(R.id.loading)
setSupportActionBar(entryEditAddToolBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.setDisplayShowTitleEnabled(false)
// To apply fit window with transparency
setTransparentNavigationBar(applyToStatusBar = true) {
container?.applyWindowInsets(WindowInsetPosition.TOP_BOTTOM_IME)
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM_IME)
}
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
@@ -286,7 +302,7 @@ class EntryEditActivity : DatabaseLockActivity(),
// Launch the time picker
MaterialTimePicker.Builder().build().apply {
addOnPositiveButtonClickListener {
mEntryEditViewModel.selectTime(this.hour, this.minute)
mEntryEditViewModel.selectTime(DataTime(this.hour, this.minute))
}
show(supportFragmentManager, "TimePickerFragment")
}
@@ -294,7 +310,7 @@ class EntryEditActivity : DatabaseLockActivity(),
// Launch the date picker
MaterialDatePicker.Builder.datePicker().build().apply {
addOnPositiveButtonClickListener {
mEntryEditViewModel.selectDate(it)
mEntryEditViewModel.selectDate(datePickerToDataDate(it))
}
show(supportFragmentManager, "DatePickerFragment")
}
@@ -691,16 +707,16 @@ class EntryEditActivity : DatabaseLockActivity(),
return true
}
android.R.id.home -> {
onBackPressed()
onDatabaseBackPressed()
}
}
return super.onOptionsItemSelected(item)
}
override fun onBackPressed() {
override fun onDatabaseBackPressed() {
onApprovedBackPressed {
super@EntryEditActivity.onBackPressed()
super@EntryEditActivity.onDatabaseBackPressed()
}
}

View File

@@ -31,6 +31,7 @@ import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.WebDomain
@@ -116,7 +117,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
searchInfo: SearchInfo) {
// Setting to integrate Magikeyboard
val searchShareForMagikeyboard = MagikeyboardService.activatedInSettings(this)
val searchShareForMagikeyboard = isKeyboardActivatedInSettings()
// If database is open
val readOnly = database?.isReadOnly != false

View File

@@ -68,11 +68,10 @@ import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.DexUtil
import com.kunzisoft.keepass.utils.MagikeyboardUtil
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.parseUri
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
import com.kunzisoft.keepass.utils.UriUtil.openUrl
import com.kunzisoft.keepass.utils.allowCreateDocumentByStorageAccessFramework
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
@@ -180,17 +179,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
// Load default database if not an orientation change
if (!(savedInstanceState != null
&& savedInstanceState.containsKey(EXTRA_STAY)
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
val databasePath = PreferencesUtil.getDefaultDatabasePath(this)
databasePath?.parseUri()?.let { databaseFileUri ->
launchPasswordActivityWithPath(databaseFileUri)
} ?: run {
Log.i(TAG, "No default database to prepare")
}
// Load default database the first time
databaseFilesViewModel.doForDefaultDatabase { databaseFileUri ->
launchPasswordActivityWithPath(databaseFileUri)
}
// Retrieve the database URI provided by file manager after an orientation change
@@ -366,8 +357,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// only to keep the current activity
outState.putBoolean(EXTRA_STAY, true)
// to retrieve the URI of a created database after an orientation change
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
}
@@ -442,7 +431,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
companion object {
private const val TAG = "FileDbSelectActivity"
private const val EXTRA_STAY = "EXTRA_STAY"
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
/*

View File

@@ -35,7 +35,7 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
@@ -49,7 +49,6 @@ import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
@@ -85,11 +84,11 @@ import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
import com.kunzisoft.keepass.settings.PreferencesUtil
@@ -97,6 +96,8 @@ import com.kunzisoft.keepass.settings.SettingsActivity
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.KeyboardUtil.showKeyboard
import com.kunzisoft.keepass.utils.TimeUtil.datePickerToDataDate
import com.kunzisoft.keepass.utils.UriUtil.openUrl
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
@@ -107,11 +108,15 @@ import com.kunzisoft.keepass.view.AddNodeButtonView
import com.kunzisoft.keepass.view.NavigationDatabaseView
import com.kunzisoft.keepass.view.SearchFiltersView
import com.kunzisoft.keepass.view.ToolbarAction
import com.kunzisoft.keepass.view.WindowInsetPosition
import com.kunzisoft.keepass.view.applyWindowInsets
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.setTransparentNavigationBar
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.updateLockPaddingLeft
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
import com.kunzisoft.keepass.viewmodels.GroupViewModel
import org.joda.time.Instant
class GroupActivity : DatabaseLockActivity(),
@@ -123,9 +128,12 @@ class GroupActivity : DatabaseLockActivity(),
MainCredentialDialogFragment.AskMainCredentialDialogListener {
// Views
private var header: ViewGroup? = null
private var footer: ViewGroup? = null
private var drawerLayout: DrawerLayout? = null
private var databaseNavView: NavigationDatabaseView? = null
private var coordinatorLayout: CoordinatorLayout? = null
private var coordinatorError: CoordinatorLayout? = null
private var lockView: View? = null
private var toolbar: Toolbar? = null
private var databaseModifiedView: ImageView? = null
@@ -223,11 +231,17 @@ class GroupActivity : DatabaseLockActivity(),
&& PreferencesUtil.isKeyboardPreviousSearchEnable(this@GroupActivity)) {
// Change to the previous keyboard and show it
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
ContextCompat.getSystemService(this, InputMethodManager::class.java)
?.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
view.showKeyboard()
}
}
private val mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) { entryId ->
entryId?.let {
// Simply refresh the list when entry is updated
loadGroup()
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
}
private fun addSearch() {
finishNodeAction()
if (mSearchState == null) {
@@ -237,7 +251,6 @@ class GroupActivity : DatabaseLockActivity(),
}
private fun removeSearch() {
finishNodeAction()
mSearchState = null
intent.removeExtra(AUTO_SEARCH_KEY)
if (Intent.ACTION_SEARCH == intent.action) {
@@ -263,9 +276,12 @@ class GroupActivity : DatabaseLockActivity(),
setContentView(layoutInflater.inflate(R.layout.activity_group, null))
// Initialize views
header = findViewById(R.id.activity_group_header)
footer = findViewById(R.id.activity_group_footer)
drawerLayout = findViewById(R.id.drawer_layout)
databaseNavView = findViewById(R.id.database_nav_view)
coordinatorLayout = findViewById(R.id.group_coordinator)
coordinatorError = findViewById(R.id.error_coordinator)
numberChildrenView = findViewById(R.id.group_numbers)
addNodeButtonView = findViewById(R.id.add_node_button)
toolbar = findViewById(R.id.toolbar)
@@ -278,6 +294,12 @@ class GroupActivity : DatabaseLockActivity(),
lockView = findViewById(R.id.lock_button)
loadingView = findViewById(R.id.loading)
// To apply fit window with transparency
setTransparentNavigationBar(applyToStatusBar = true) {
drawerLayout?.applyWindowInsets(WindowInsetPosition.TOP_BOTTOM_IME)
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM_IME)
}
lockView?.setOnClickListener {
lockAndExit()
}
@@ -320,8 +342,9 @@ class GroupActivity : DatabaseLockActivity(),
R.id.menu_save_copy_to -> {
mExternalFileHelper?.createDocument(
getString(R.string.database_file_name_default) +
getString(R.string.database_file_name_copy) +
mDatabase?.defaultFileExtension)
"_" +
Instant.now().toString() +
mDatabase?.defaultFileExtension)
}
R.id.menu_lock_all -> {
lockAndExit()
@@ -339,43 +362,6 @@ class GroupActivity : DatabaseLockActivity(),
searchFiltersView?.closeAdvancedFilters()
mBreadcrumbAdapter = BreadcrumbAdapter(this).apply {
// Open group on breadcrumb click
onItemClickListener = { node, _ ->
// If last item & not a virtual root group
val currentGroup = mMainGroup
if (currentGroup != null && node == currentGroup
&& (currentGroup != mDatabase?.rootGroup
|| mDatabase?.rootGroupIsVirtual == false)
) {
finishNodeAction()
launchDialogToShowGroupInfo(currentGroup)
} else {
if (mGroupFragment?.nodeActionSelectionMode == true) {
finishNodeAction()
}
mDatabase?.let { database ->
onNodeClick(database, node)
}
}
}
onLongItemClickListener = { node, position ->
val currentGroup = mMainGroup
if (currentGroup != null && node == currentGroup
&& (currentGroup != mDatabase?.rootGroup
|| mDatabase?.rootGroupIsVirtual == false)
) {
finishNodeAction()
launchDialogForGroupUpdate(currentGroup)
} else {
onItemClickListener?.invoke(node, position)
}
}
}
breadcrumbListView?.apply {
adapter = mBreadcrumbAdapter
}
// Retrieve group if defined at launch
manageIntent(intent)
@@ -456,7 +442,7 @@ class GroupActivity : DatabaseLockActivity(),
// Launch the time picker
MaterialTimePicker.Builder().build().apply {
addOnPositiveButtonClickListener {
mGroupEditViewModel.selectTime(this.hour, this.minute)
mGroupEditViewModel.selectTime(DataTime(this.hour, this.minute))
}
show(supportFragmentManager, "TimePickerFragment")
}
@@ -464,7 +450,7 @@ class GroupActivity : DatabaseLockActivity(),
// Launch the date picker
MaterialDatePicker.Builder.datePicker().build().apply {
addOnPositiveButtonClickListener {
mGroupEditViewModel.selectDate(it)
mGroupEditViewModel.selectDate(datePickerToDataDate(it))
}
show(supportFragmentManager, "DatePickerFragment")
}
@@ -501,14 +487,12 @@ class GroupActivity : DatabaseLockActivity(),
EntrySelectionHelper.doSpecialAction(intent,
{
mMainGroup?.nodeId?.let { currentParentGroupId ->
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
EntryEditActivity.launchToCreate(
this@GroupActivity,
database,
currentParentGroupId,
resultLauncher
)
}
EntryEditActivity.launchToCreate(
this@GroupActivity,
database,
currentParentGroupId,
mEntryActivityResultLauncher
)
}
},
{
@@ -599,6 +583,43 @@ class GroupActivity : DatabaseLockActivity(),
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
mBreadcrumbAdapter = BreadcrumbAdapter(this, database).apply {
// Open group on breadcrumb click
onItemClickListener = { node, _ ->
// If last item & not a virtual root group
val currentGroup = mMainGroup
if (currentGroup != null && node == currentGroup
&& (currentGroup != mDatabase?.rootGroup
|| mDatabase?.rootGroupIsVirtual == false)
) {
finishNodeAction()
launchDialogToShowGroupInfo(currentGroup)
} else {
if (mGroupFragment?.nodeActionSelectionMode == true) {
finishNodeAction()
}
mDatabase?.let { database ->
onNodeClick(database, node)
}
}
}
onLongItemClickListener = { node, position ->
val currentGroup = mMainGroup
if (currentGroup != null && node == currentGroup
&& (currentGroup != mDatabase?.rootGroup
|| mDatabase?.rootGroupIsVirtual == false)
) {
finishNodeAction()
launchDialogForGroupUpdate(currentGroup)
} else {
onItemClickListener?.invoke(node, position)
}
}
}
breadcrumbListView?.apply {
adapter = mBreadcrumbAdapter
}
mGroupEditViewModel.setGroupNamesNotAllowed(database?.groupNamesNotAllowed)
mRecyclingBinEnabled = !mDatabaseReadOnly
@@ -646,9 +667,13 @@ class GroupActivity : DatabaseLockActivity(),
) {
super.onDatabaseActionFinished(database, actionTask, result)
var newNodes: List<Node> = ArrayList()
result.data?.getBundle(NEW_NODES_KEY)?.let { newNodesBundle ->
newNodes = getListNodesFromBundle(database, newNodesBundle)
var entry: Entry? = null
try {
result.data?.getBundle(NEW_NODES_KEY)?.let { newNodesBundle ->
entry = getListNodesFromBundle(database, newNodesBundle)[0] as Entry
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve entry action for selection", e)
}
when (actionTask) {
@@ -665,27 +690,15 @@ class GroupActivity : DatabaseLockActivity(),
// Save not used
},
{
try {
val entry = newNodes[0] as Entry
entrySelectedForKeyboardSelection(database, entry)
} catch (e: Exception) {
Log.e(
TAG,
"Unable to perform action for keyboard selection after entry update",
e
)
// Keyboard selection
entry?.let {
entrySelectedForKeyboardSelection(database, it)
}
},
{ _, _ ->
try {
val entry = newNodes[0] as Entry
entrySelectedForAutofillSelection(database, entry)
} catch (e: Exception) {
Log.e(
TAG,
"Unable to perform action for autofill selection after entry update",
e
)
// Autofill selection
entry?.let {
entrySelectedForAutofillSelection(database, it)
}
},
{
@@ -694,26 +707,12 @@ class GroupActivity : DatabaseLockActivity(),
)
}
}
ACTION_DATABASE_UPDATE_GROUP_TASK -> {
if (result.isSuccess) {
try {
if (mMainGroup == newNodes[0] as Group)
reloadCurrentGroup()
} catch (e: Exception) {
Log.e(
TAG,
"Unable to perform action after group update",
e
)
}
}
}
}
coordinatorLayout?.showActionErrorIfNeeded(result)
if (!result.isSuccess) {
reloadCurrentGroup()
}
coordinatorError?.showActionErrorIfNeeded(result)
// Reload the group
loadGroup()
finishNodeAction()
}
@@ -841,7 +840,6 @@ class GroupActivity : DatabaseLockActivity(),
}
// Open child group
loadMainGroup(GroupState(group.nodeId, 0))
} catch (e: ClassCastException) {
Log.e(TAG, "Node can't be cast in Group")
}
@@ -850,22 +848,22 @@ class GroupActivity : DatabaseLockActivity(),
val entryVersioned = node as Entry
EntrySelectionHelper.doSpecialAction(intent,
{
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
EntryActivity.launch(
this@GroupActivity,
database,
entryVersioned.nodeId,
resultLauncher
)
}
EntryActivity.launch(
this@GroupActivity,
database,
entryVersioned.nodeId,
mEntryActivityResultLauncher
)
// Do not reload group here
},
{
// Nothing here, a search is simply performed
},
{ searchInfo ->
if (!database.isReadOnly)
if (!database.isReadOnly) {
entrySelectedForSave(database, entryVersioned, searchInfo)
else
loadGroup()
} else
finish()
},
{ searchInfo ->
@@ -876,6 +874,7 @@ class GroupActivity : DatabaseLockActivity(),
updateEntryWithSearchInfo(database, entryVersioned, searchInfo)
}
entrySelectedForKeyboardSelection(database, entryVersioned)
loadGroup()
},
{ searchInfo, _ ->
if (!database.isReadOnly
@@ -885,23 +884,23 @@ class GroupActivity : DatabaseLockActivity(),
updateEntryWithSearchInfo(database, entryVersioned, searchInfo)
}
entrySelectedForAutofillSelection(database, entryVersioned)
loadGroup()
},
{ registerInfo ->
if (!database.isReadOnly)
if (!database.isReadOnly) {
entrySelectedForRegistration(database, entryVersioned, registerInfo)
else
loadGroup()
} else
finish()
})
} catch (e: ClassCastException) {
Log.e(TAG, "Node can't be cast in Entry")
}
}
reloadGroupIfSearch()
}
private fun entrySelectedForSave(database: ContextualDatabase, entry: Entry, searchInfo: SearchInfo) {
reloadCurrentGroup()
removeSearch()
// Save to update the entry
EntryEditActivity.launchToUpdateForSave(
this@GroupActivity,
@@ -913,7 +912,7 @@ class GroupActivity : DatabaseLockActivity(),
}
private fun entrySelectedForKeyboardSelection(database: ContextualDatabase, entry: Entry) {
reloadCurrentGroup()
removeSearch()
// Populate Magikeyboard with entry
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
this,
@@ -923,6 +922,7 @@ class GroupActivity : DatabaseLockActivity(),
}
private fun entrySelectedForAutofillSelection(database: ContextualDatabase, entry: Entry) {
removeSearch()
// Build response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.buildResponseAndSetResult(
@@ -939,7 +939,7 @@ class GroupActivity : DatabaseLockActivity(),
entry: Entry,
registerInfo: RegisterInfo?
) {
reloadCurrentGroup()
removeSearch()
// Registration to update the entry
EntryEditActivity.launchToUpdateForRegistration(
this@GroupActivity,
@@ -972,12 +972,6 @@ class GroupActivity : DatabaseLockActivity(),
actionNodeMode?.finish()
}
private fun reloadGroupIfSearch() {
if (Intent.ACTION_SEARCH == intent.action) {
reloadCurrentGroup()
}
}
override fun onNodeSelected(
database: ContextualDatabase,
nodes: List<Node>
@@ -1023,17 +1017,14 @@ class GroupActivity : DatabaseLockActivity(),
launchDialogForGroupUpdate(node as Group)
}
Type.ENTRY -> {
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
EntryEditActivity.launchToUpdate(
this@GroupActivity,
database,
(node as Entry).nodeId,
resultLauncher
)
}
EntryEditActivity.launchToUpdate(
this@GroupActivity,
database,
(node as Entry).nodeId,
mEntryActivityResultLauncher
)
}
}
reloadGroupIfSearch()
return true
}
@@ -1066,8 +1057,8 @@ class GroupActivity : DatabaseLockActivity(),
nodes: List<Node>
): Boolean {
actionNodeMode?.invalidate()
// Nothing here fragment calls onPasteMenuClick internally
removeSearch()
loadGroup()
return true
}
@@ -1076,8 +1067,8 @@ class GroupActivity : DatabaseLockActivity(),
nodes: List<Node>
): Boolean {
actionNodeMode?.invalidate()
// Nothing here fragment calls onPasteMenuClick internally
removeSearch()
loadGroup()
return true
}
@@ -1112,7 +1103,6 @@ class GroupActivity : DatabaseLockActivity(),
): Boolean {
deleteNodes(nodes)
finishNodeAction()
reloadGroupIfSearch()
return true
}
@@ -1141,6 +1131,8 @@ class GroupActivity : DatabaseLockActivity(),
}
// Padding if lock button visible
toolbarAction?.updateLockPaddingLeft()
loadGroup()
}
override fun onPause() {
@@ -1343,6 +1335,12 @@ class GroupActivity : DatabaseLockActivity(),
mGroupFragment?.onSortSelected(sortNodeEnum, sortNodeParameters)
}
override fun onCancelSpecialMode() {
super.onCancelSpecialMode()
removeSearch()
loadGroup()
}
override fun startActivity(intent: Intent) {
// Get the intent, verify the action and get the query
if (Intent.ACTION_SEARCH == intent.action) {
@@ -1359,12 +1357,7 @@ class GroupActivity : DatabaseLockActivity(),
}
}
private fun reloadCurrentGroup() {
removeSearch()
loadGroup()
}
override fun onBackPressed() {
override fun onDatabaseBackPressed() {
if (mGroupFragment?.nodeActionSelectionMode == true) {
finishNodeAction()
} else {
@@ -1372,8 +1365,8 @@ class GroupActivity : DatabaseLockActivity(),
if (mRootGroup != null && mRootGroup != mCurrentGroup) {
when {
Intent.ACTION_SEARCH == intent.action -> {
// Remove the search
reloadCurrentGroup()
removeSearch()
loadGroup()
}
mPreviousGroupsIds.isEmpty() -> {
super.onRegularBackPressed()

View File

@@ -239,7 +239,7 @@ class IconPickerActivity : DatabaseLockActivity() {
if (mCustomIconsSelectionMode) {
iconPickerViewModel.deselectAllCustomIcons()
} else {
onBackPressed()
onDatabaseBackPressed()
}
}
R.id.menu_edit -> {
@@ -329,9 +329,9 @@ class IconPickerActivity : DatabaseLockActivity() {
})
}
override fun onBackPressed() {
override fun onDatabaseBackPressed() {
setResult()
super.onBackPressed()
super.onDatabaseBackPressed()
}
companion object {

View File

@@ -96,7 +96,7 @@ class KeyGeneratorActivity : DatabaseLockActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
onDatabaseBackPressed()
}
R.id.menu_generate -> {
keyGeneratorViewModel.requireKeyGeneration()
@@ -106,9 +106,9 @@ class KeyGeneratorActivity : DatabaseLockActivity() {
return super.onOptionsItemSelected(item)
}
override fun onBackPressed() {
override fun onDatabaseBackPressed() {
setResult(Activity.RESULT_CANCELED, Intent())
super.onBackPressed()
super.onDatabaseBackPressed()
}
companion object {

View File

@@ -165,18 +165,6 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
startActivity(Intent(this, AppearanceSettingsActivity::class.java))
}
// 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
mainCredentialView?.onPasswordChecked =
CompoundButton.OnCheckedChangeListener { _, _ ->
@@ -245,6 +233,23 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
override fun onResume() {
super.onResume()
// Init Biometric elements only if allowed
if (PreferencesUtil.isAdvancedUnlockEnable(this)) {
advancedUnlockFragment = supportFragmentManager
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
if (advancedUnlockFragment == null) {
advancedUnlockFragment = AdvancedUnlockFragment().also {
supportFragmentManager.commit {
replace(
R.id.fragment_advanced_unlock_container_view,
it,
UNLOCK_FRAGMENT_TAG
)
}
}
}
}
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this@MainCredentialActivity)

View File

@@ -43,6 +43,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(OLD_FILE_DATABASE_INFO)
val newSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(NEW_FILE_DATABASE_INFO)
val readOnlyDatabase: Boolean = arguments?.getBoolean(READ_ONLY_DATABASE) ?: true
if (oldSnapFileDatabaseInfo != null && newSnapFileDatabaseInfo != null) {
// Use the Builder class for convenient dialog construction
@@ -54,7 +55,13 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
stringBuilder.append("\n\n" +oldSnapFileDatabaseInfo.toString(activity)
+ "\n\n" +
newSnapFileDatabaseInfo.toString(activity) + "\n\n")
stringBuilder.append(getString(R.string.warning_database_info_changed_options))
stringBuilder.append(getString(
if (readOnlyDatabase) {
R.string.warning_database_info_changed_options_read_only
} else {
R.string.warning_database_info_changed_options
}
))
} else {
stringBuilder.append(getString(R.string.warning_database_revoked))
}
@@ -77,15 +84,18 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
const val DATABASE_CHANGED_DIALOG_TAG = "databaseChangedDialogFragment"
private const val OLD_FILE_DATABASE_INFO = "OLD_FILE_DATABASE_INFO"
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE"
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
newSnapFileDatabaseInfo: SnapFileDatabaseInfo
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
readOnly: Boolean
)
: DatabaseChangedDialogFragment {
val fragment = DatabaseChangedDialogFragment()
fragment.arguments = Bundle().apply {
putParcelable(OLD_FILE_DATABASE_INFO, oldSnapFileDatabaseInfo)
putParcelable(NEW_FILE_DATABASE_INFO, newSnapFileDatabaseInfo)
putBoolean(READ_ONLY_DATABASE, readOnly)
}
return fragment
}

View File

@@ -1,11 +1,14 @@
package com.kunzisoft.keepass.activities.dialogs
import android.os.Bundle
import android.view.View
import android.view.WindowManager.LayoutParams.FLAG_SECURE
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
@@ -29,6 +32,18 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Screenshot mode or hide views
context?.let {
if (PreferencesUtil.isScreenshotModeEnabled(it)) {
dialog?.window?.clearFlags(FLAG_SECURE)
} else {
dialog?.window?.setFlags(FLAG_SECURE, FLAG_SECURE)
}
}
}
@Suppress("DEPRECATION")
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)

View File

@@ -45,7 +45,6 @@ import com.kunzisoft.keepass.view.InheritedCompletionView
import com.kunzisoft.keepass.view.TagsCompletionView
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
import com.tokenautocomplete.FilteredArrayAdapter
import org.joda.time.DateTime
class GroupEditDialogFragment : DatabaseDialogFragment() {
@@ -90,27 +89,21 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
}
mGroupEditViewModel.onDateSelected.observe(this) { dateMilliseconds ->
mGroupEditViewModel.onDateSelected.observe(this) { date ->
// Save the date
mGroupInfo.expiryTime = DateInstant(
DateTime(mGroupInfo.expiryTime.date)
.withMillis(dateMilliseconds)
.toDate())
mGroupInfo.expiryTime.setDate(date.year, date.month, date.day)
expirationView.dateTime = mGroupInfo.expiryTime
if (expirationView.dateTime.type == DateInstant.Type.DATE_TIME) {
val instantTime = DateInstant(mGroupInfo.expiryTime.date, DateInstant.Type.TIME)
// Trick to recall selection with time
mGroupEditViewModel.requestDateTimeSelection(instantTime)
mGroupEditViewModel.requestDateTimeSelection(
DateInstant(mGroupInfo.expiryTime.instant, DateInstant.Type.TIME)
)
}
}
mGroupEditViewModel.onTimeSelected.observe(this) { viewModelTime ->
// Save the time
mGroupInfo.expiryTime = DateInstant(
DateTime(mGroupInfo.expiryTime.date)
.withHourOfDay(viewModelTime.hours)
.withMinuteOfHour(viewModelTime.minutes)
.toDate(), mGroupInfo.expiryTime.type)
mGroupInfo.expiryTime.setTime(viewModelTime.hour, viewModelTime.minute)
expirationView.dateTime = mGroupInfo.expiryTime
}

View File

@@ -43,8 +43,13 @@ import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.UriUtil.openUrl
import com.kunzisoft.keepass.view.HardwareKeySelectionView
import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.PassKeyView
import com.kunzisoft.keepass.view.PasswordEditView
import com.kunzisoft.keepass.view.applyFontVisibility
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.security.SecureRandom
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
@@ -55,11 +60,12 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private lateinit var rootView: View
private lateinit var passwordCheckBox: CompoundButton
private lateinit var passwordView: PassKeyView
private lateinit var passwordEditView: PasswordEditView
private lateinit var passwordRepeatTextInputLayout: TextInputLayout
private lateinit var passwordRepeatView: TextView
private lateinit var keyFileCheckBox: CompoundButton
private lateinit var keyFileGenerateButton: View
private lateinit var keyFileSelectionView: KeyFileSelectionView
private lateinit var hardwareKeyCheckBox: CompoundButton
@@ -141,30 +147,40 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
}
passwordCheckBox = rootView.findViewById(R.id.password_checkbox)
passwordView = rootView.findViewById(R.id.password_view)
passwordEditView = rootView.findViewById(R.id.password_view)
passwordRepeatTextInputLayout = rootView.findViewById(R.id.password_repeat_input_layout)
passwordRepeatView = rootView.findViewById(R.id.password_confirmation)
passwordRepeatView.applyFontVisibility()
keyFileCheckBox = rootView.findViewById(R.id.keyfile_checkbox)
keyFileGenerateButton = rootView.findViewById(R.id.keyfile_generate)
keyFileSelectionView = rootView.findViewById(R.id.keyfile_selection)
hardwareKeyCheckBox = rootView.findViewById(R.id.hardware_key_checkbox)
hardwareKeySelectionView = rootView.findViewById(R.id.hardware_key_selection)
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
createdFileUri?.let { uri ->
createKeyFile(uri)
keyFileSelectionView.error = null
keyFileCheckBox.isChecked = true
keyFileSelectionView.uri = uri
}
}
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let { pathUri ->
pathUri.getDocumentFile(requireContext())?.length()?.let { lengthFile ->
keyFileSelectionView.error = null
keyFileCheckBox.isChecked = true
keyFileSelectionView.uri = pathUri
if (lengthFile <= 0L) {
showEmptyKeyFileConfirmationDialog()
}
showLengthKeyFileConfirmationDialog(lengthFile)
}
}
}
keyFileGenerateButton.setOnClickListener {
mExternalFileHelper?.createDocument(DEFAULT_KEYFILE_NAME)
}
keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper)
hardwareKeySelectionView.selectionListener = { hardwareKey ->
@@ -202,6 +218,16 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
return super.onCreateDialog(savedInstanceState)
}
private fun createKeyFile(uri: Uri) {
CoroutineScope(Dispatchers.IO).launch {
activity?.contentResolver?.openOutputStream(uri)?.use { outputStream ->
val randomBytes = ByteArray(DEFAULT_KEYFILE_SIZE)
SecureRandom().nextBytes(randomBytes)
outputStream.write(randomBytes)
}
}
}
private fun approveMainCredential() {
val errorPassword = verifyPassword()
val errorKeyFile = verifyKeyFile()
@@ -250,7 +276,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
var error = false
passwordRepeatTextInputLayout.error = null
if (passwordCheckBox.isChecked) {
mMasterPassword = passwordView.passwordString
mMasterPassword = passwordEditView.passwordString
val confPassword = passwordRepeatView.text.toString()
// Verify that passwords match
@@ -302,13 +328,13 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
super.onResume()
// To check checkboxes if a text is present
passwordView.addTextChangedListener(passwordTextWatcher)
passwordEditView.addTextChangedListener(passwordTextWatcher)
}
override fun onPause() {
super.onPause()
passwordView.removeTextChangedListener(passwordTextWatcher)
passwordEditView.removeTextChangedListener(passwordTextWatcher)
}
private fun showEmptyPasswordConfirmationDialog() {
@@ -339,21 +365,31 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
}
}
private fun showEmptyKeyFileConfirmationDialog() {
private fun showLengthKeyFileConfirmationDialog(length: Long) {
activity?.let {
val builder = AlertDialog.Builder(it)
builder.setMessage(SpannableStringBuilder().apply {
append(getString(R.string.warning_empty_keyfile))
append("\n\n")
append(getString(R.string.warning_empty_keyfile_explanation))
append("\n\n")
append(getString(R.string.warning_sure_add_file))
})
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ ->
keyFileCheckBox.isChecked = false
keyFileSelectionView.uri = null
}
var warning = false
if (length <= 0L) {
warning = true
append("\n\n")
append(getString(R.string.warning_empty_keyfile))
} else if (length > 10485760L) {
warning = true
append("\n\n")
append(getString(R.string.warning_large_keyfile))
}
if (warning) {
append("\n\n")
append(getString(R.string.warning_sure_add_file))
}
})
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ ->
keyFileCheckBox.isChecked = false
keyFileSelectionView.uri = null
}
mEmptyKeyFileConfirmationDialog = builder.create()
mEmptyKeyFileConfirmationDialog?.show()
}
@@ -362,6 +398,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
companion object {
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
private const val DEFAULT_KEYFILE_NAME = "keyfile.bin"
private const val DEFAULT_KEYFILE_SIZE = 128
fun getInstance(allowNoMasterKey: Boolean): SetMainCredentialDialogFragment {
val fragment = SetMainCredentialDialogFragment()

View File

@@ -35,7 +35,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.EntryEditActivity
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
@@ -47,6 +46,7 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.KeyboardUtil.hideKeyboard
import com.kunzisoft.keepass.viewmodels.GroupViewModel
import java.util.LinkedList
@@ -79,19 +79,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
private var mRecycleBinEnable: Boolean = false
private var mRecycleBin: Group? = null
var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) { entryId ->
entryId?.let {
// Simply refresh the list
rebuildList()
// Scroll to the new entry
mDatabase?.getEntryById(it)?.let { entry ->
mAdapter?.indexOf(entry)?.let { position ->
mNodesRecyclerView?.scrollToPosition(position)
}
}
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
}
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
@@ -186,8 +173,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
mAdapter = NodesAdapter(context, database).apply {
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
override fun onNodeClick(database: ContextualDatabase, node: Node) {
if (mCurrentGroup?.isVirtual == false
&& nodeActionSelectionMode) {
if (nodeActionSelectionMode) {
if (listActionNodes.contains(node)) {
// Remove selected item if already selected
listActionNodes.remove(node)
@@ -204,8 +190,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
}
override fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean {
if (mCurrentGroup?.isVirtual == false
&& nodeActionPasteMode == PasteMode.UNDEFINED) {
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
// Select the first item after a long click
if (!listActionNodes.contains(node))
listActionNodes.add(node)
@@ -214,6 +199,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
setActionNodes(listActionNodes)
notifyNodeChanged(node)
activity?.hideKeyboard()
}
return true
}
@@ -277,8 +263,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
activity?.intent?.let {
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
}
rebuildList()
}
override fun onPause() {
@@ -362,14 +346,12 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
}
// Move
if (database.isReadOnly
|| isASearchResult) {
if (database.isReadOnly) {
menu?.removeItem(R.id.menu_move)
}
// Copy (not allowed for group)
if (database.isReadOnly
|| isASearchResult
|| nodes.any { it.type == Type.GROUP }) {
menu?.removeItem(R.id.menu_copy)
}

View File

@@ -34,12 +34,12 @@ import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.password.PassphraseGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.PassKeyView
import com.kunzisoft.keepass.view.PasswordEditView
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
class PassphraseGeneratorFragment : DatabaseFragment() {
private lateinit var passKeyView: PassKeyView
private lateinit var passwordEditView: PasswordEditView
private lateinit var sliderWordCount: Slider
private lateinit var wordCountText: EditText
@@ -62,7 +62,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
passKeyView = view.findViewById(R.id.passphrase_view)
passwordEditView = view.findViewById(R.id.passphrase_view)
val passphraseCopyView: ImageView? = view.findViewById(R.id.passphrase_copy_button)
sliderWordCount = view.findViewById(R.id.slider_word_count)
wordCountText = view.findViewById(R.id.word_count)
@@ -80,7 +80,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
passphraseCopyView?.setOnClickListener {
clipboardHelper.timeoutCopyToClipboard(
getString(R.string.passphrase),
passKeyView.passwordString,
passwordEditView.passwordString,
true
)
}
@@ -146,7 +146,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
generatePassphrase()
mKeyGeneratorViewModel.passphraseGeneratedValidated.observe(viewLifecycleOwner) {
mKeyGeneratorViewModel.setKeyGenerated(passKeyView.passwordString)
mKeyGeneratorViewModel.setKeyGenerated(passwordEditView.passwordString)
}
mKeyGeneratorViewModel.requirePassphraseGeneration.observe(viewLifecycleOwner) {
@@ -219,7 +219,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
} catch (e: Exception) {
Log.e(TAG, "Unable to generate a passphrase", e)
}
passKeyView.passwordString = passphrase
passwordEditView.passwordString = passphrase
charactersCountText.text = getString(R.string.character_count, passphrase.length)
}

View File

@@ -36,12 +36,12 @@ import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.PassKeyView
import com.kunzisoft.keepass.view.PasswordEditView
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
class PasswordGeneratorFragment : DatabaseFragment() {
private lateinit var passKeyView: PassKeyView
private lateinit var passwordEditView: PasswordEditView
private lateinit var sliderLength: Slider
private lateinit var lengthEditView: EditText
@@ -74,7 +74,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
passKeyView = view.findViewById(R.id.password_view)
passwordEditView = view.findViewById(R.id.password_view)
val passwordCopyView: ImageView? = view.findViewById(R.id.password_copy_button)
sliderLength = view.findViewById(R.id.slider_length)
@@ -101,7 +101,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
passwordCopyView?.setOnClickListener {
clipboardHelper.timeoutCopyToClipboard(
getString(R.string.password),
passKeyView.passwordString,
passwordEditView.passwordString,
true
)
}
@@ -195,7 +195,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
generatePassword()
mKeyGeneratorViewModel.passwordGeneratedValidated.observe(viewLifecycleOwner) {
mKeyGeneratorViewModel.setKeyGenerated(passKeyView.passwordString)
mKeyGeneratorViewModel.setKeyGenerated(passwordEditView.passwordString)
}
mKeyGeneratorViewModel.requirePasswordGeneration.observe(viewLifecycleOwner) {
@@ -310,7 +310,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
} catch (e: Exception) {
Log.e(TAG, "Unable to generate a password", e)
}
passKeyView.passwordString = password
passwordEditView.passwordString = password
}
override fun onDestroy() {

View File

@@ -209,6 +209,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
}
override fun finish() {
// To fix weird crash
try {
super.finish()
} catch (e: Exception) {
Log.e(TAG, "Unable to finish the activity", e)
}
}
abstract fun viewToInvalidateTimeout(): View?
override fun onDatabaseActionFinished(
@@ -454,14 +463,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabase?.loaded ?: false)
}
override fun onBackPressed() {
override fun onDatabaseBackPressed() {
if (mTimeoutEnable) {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
mDatabase?.loaded == true) {
super.onBackPressed()
super.onDatabaseBackPressed()
}
} else {
super.onBackPressed()
super.onDatabaseBackPressed()
}
}

View File

@@ -3,6 +3,7 @@ package com.kunzisoft.keepass.activities.legacy
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
@@ -22,18 +23,20 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
private var mToolbarSpecial: ToolbarSpecial? = null
override fun onBackPressed() {
open fun onDatabaseBackPressed() {
if (mSpecialMode != SpecialMode.DEFAULT)
onCancelSpecialMode()
else
super.onBackPressed()
onRegularBackPressed()
}
/**
* To call the regular onBackPressed() method in special mode
*/
protected fun onRegularBackPressed() {
super.onBackPressed()
// Do not call onBackPressedDispatcher.onBackPressed() to avoid loop
// Calling onBackPressed() is now deprecated, directly finish the activity
finish()
}
/**
@@ -72,7 +75,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
open fun onCancelSpecialMode() {
if (isIntentSender()) {
// To get the app caller, only for IntentSender
super.onBackPressed()
onRegularBackPressed()
} else {
EntrySelectionHelper.removeModesFromIntent(intent)
EntrySelectionHelper.removeInfoFromIntent(intent)
@@ -85,7 +88,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
protected fun backToTheAppCaller() {
if (isIntentSender()) {
// To get the app caller, only for IntentSender
super.onBackPressed()
onRegularBackPressed()
} else {
backToTheMainAppAndFinish()
}
@@ -100,6 +103,12 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
onDatabaseBackPressed()
}
})
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
}

View File

@@ -10,6 +10,7 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type
@@ -17,7 +18,7 @@ import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.strikeOut
class BreadcrumbAdapter(val context: Context)
class BreadcrumbAdapter(val context: Context, val database: Database?)
: RecyclerView.Adapter<BreadcrumbAdapter.BreadcrumbGroupViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
@@ -31,6 +32,8 @@ class BreadcrumbAdapter(val context: Context)
var onItemClickListener: ((item: Node, position: Int)->Unit)? = null
var onLongItemClickListener: ((item: Node, position: Int)->Unit)? = null
private var mNodeFilter: NodeFilter = NodeFilter(context, database)
private var mShowNumberEntries = false
private var mShowUUID = false
private var mIconColor: Int = 0
@@ -112,12 +115,10 @@ class BreadcrumbAdapter(val context: Context)
holder.groupNumbersView?.apply {
if (mShowNumberEntries) {
group.refreshNumberOfChildEntries(
Group.ChildFilter.getDefaults(
PreferencesUtil.showExpiredEntries(context)
)
)
text = group.numberOfChildEntries.toString()
text = group.getNumberOfChildEntries(
mNodeFilter.recursiveNumberOfEntries,
mNodeFilter.filter
).toString()
visibility = View.VISIBLE
} else {
visibility = View.GONE

View File

@@ -0,0 +1,30 @@
package com.kunzisoft.keepass.adapters
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.settings.PreferencesUtil
class NodeFilter(
context: Context,
var database: Database? = null
) {
var recursiveNumberOfEntries = PreferencesUtil.recursiveNumberEntries(context)
private set
private var showExpired = PreferencesUtil.showExpiredEntries(context)
private var showTemplate = PreferencesUtil.showTemplates(context)
val filter: (Node) -> Boolean = { node ->
when (node) {
is Entry -> {
node.entryKDB?.isMetaStream() != true
}
is Group -> {
showTemplate || database?.templatesGroup != node
}
else -> true
} && (showExpired || !node.isCurrentlyExpires)
}
}

View File

@@ -66,6 +66,7 @@ class NodesAdapter (
private val mNodeSortedListCallback: NodeSortedListCallback
private val mNodeSortedList: SortedList<Node>
private val mInflater: LayoutInflater = LayoutInflater.from(context)
private val mNodeFilter: NodeFilter = NodeFilter(context, database)
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
@@ -82,7 +83,7 @@ class NodesAdapter (
private var mShowNumberEntries: Boolean = true
private var mShowOTP: Boolean = false
private var mShowUUID: Boolean = false
private var mEntryFilters = arrayOf<Group.ChildFilter>()
private var mNodeFilters: NodeFilter? = null
private var mOldVirtualGroup = false
private var mVirtualGroup = false
@@ -161,9 +162,7 @@ class NodesAdapter (
this.mShowOTP = PreferencesUtil.showOTPToken(context)
this.mShowUUID = PreferencesUtil.showUUID(context)
this.mEntryFilters = Group.ChildFilter.getDefaults(
PreferencesUtil.showExpiredEntries(context)
)
this.mNodeFilters = NodeFilter(context, database)
// Reinit textSize for all view type
mCalculateViewTypeTextSize.forEachIndexed { index, _ -> mCalculateViewTypeTextSize[index] = true }
@@ -176,7 +175,7 @@ class NodesAdapter (
mOldVirtualGroup = mVirtualGroup
mVirtualGroup = group.isVirtual
assignPreferences()
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
mNodeSortedList.replaceAll(group.getChildren(mNodeFilter.filter))
}
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
@@ -197,6 +196,7 @@ class NodesAdapter (
&& oldItem.containsAttachment() == newItem.containsAttachment()
} else if (oldItem is Group && newItem is Group) {
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
&& oldItem.recursiveNumberOfChildEntries == newItem.recursiveNumberOfChildEntries
&& oldItem.notes == newItem.notes
}
return typeContentTheSame
@@ -204,6 +204,11 @@ class NodesAdapter (
&& oldItem.type == newItem.type
&& oldItem.title == newItem.title
&& oldItem.icon == newItem.icon
&& oldItem.creationTime == newItem.creationTime
&& oldItem.lastModificationTime == newItem.lastModificationTime
&& oldItem.lastAccessTime == newItem.lastAccessTime
&& oldItem.expiryTime == newItem.expiryTime
&& oldItem.expires == newItem.expires
&& oldItem.isCurrentlyExpires == newItem.isCurrentlyExpires
}
@@ -472,7 +477,10 @@ class NodesAdapter (
if (mShowNumberEntries) {
holder.numberChildren?.apply {
text = (subNode as Group)
.numberOfChildEntries
.getNumberOfChildEntries(
mNodeFilter.recursiveNumberOfEntries,
mNodeFilter.filter
)
.toString()
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
visibility = View.VISIBLE

View File

@@ -27,6 +27,7 @@ import android.net.Uri
import android.os.IBinder
import android.util.Base64
import android.util.Log
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.database.element.binary.BinaryData.Companion.BASE64_FLAG
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
@@ -69,9 +70,11 @@ class CipherDatabaseAction(context: Context) {
@Synchronized
private fun attachService(performedAction: () -> Unit) {
applicationContext.registerReceiver(mAdvancedUnlockBroadcastReceiver, IntentFilter().apply {
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION)
})
ContextCompat.registerReceiver(applicationContext, mAdvancedUnlockBroadcastReceiver,
IntentFilter().apply {
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION)
}, ContextCompat.RECEIVER_EXPORTED
)
mServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
@@ -97,7 +100,7 @@ class CipherDatabaseAction(context: Context) {
private fun detachService() {
try {
applicationContext.unregisterReceiver(mAdvancedUnlockBroadcastReceiver)
} catch (e: Exception) {}
} catch (_: Exception) {}
mServiceConnection?.let {
AdvancedUnlockNotificationService.unbindService(applicationContext, it)
@@ -181,6 +184,14 @@ class CipherDatabaseAction(context: Context) {
}
}
fun resetCipherParameters(databaseUri: Uri) {
containsCipherDatabase(databaseUri) { contains ->
if (contains) {
mBinder?.resetTimer()
}
}
}
fun addOrUpdateCipherDatabase(cipherEncryptDatabase: CipherEncryptDatabase,
cipherDatabaseResultListener: (() -> Unit)? = null) {
cipherEncryptDatabase.databaseUri?.let { databaseUri ->

View File

@@ -1,7 +1,6 @@
package com.kunzisoft.keepass.autofill
import android.app.assist.AssistStructure
import android.view.inputmethod.InlineSuggestionsRequest
data class AutofillComponent(val assistStructure: AssistStructure,
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)

View File

@@ -29,9 +29,12 @@ import android.graphics.BlendMode
import android.graphics.drawable.Icon
import android.os.Build
import android.service.autofill.Dataset
import android.service.autofill.Field
import android.service.autofill.FillResponse
import android.service.autofill.InlinePresentation
import android.service.autofill.Presentations
import android.util.Log
import android.view.autofill.AutofillId
import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue
import android.widget.RemoteViews
@@ -57,6 +60,7 @@ import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import kotlin.math.min
@RequiresApi(api = Build.VERSION_CODES.O)
@@ -93,41 +97,87 @@ object AutofillHelper {
database: ContextualDatabase,
remoteViewsText: String,
remoteViewsIcon: IconImage? = null): RemoteViews {
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
val remoteViews = RemoteViews(context.packageName, R.layout.item_autofill_entry)
remoteViews.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
if (remoteViewsIcon != null) {
try {
database.iconDrawableFactory.getBitmapFromIcon(context,
remoteViewsIcon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
presentation.setImageViewBitmap(R.id.autofill_entry_icon, bitmap)
remoteViews.setImageViewBitmap(R.id.autofill_entry_icon, bitmap)
}
} catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
}
}
return presentation
return remoteViews
}
private fun buildDataset(context: Context,
database: ContextualDatabase,
entryInfo: EntryInfo,
struct: StructureParser.Result,
additionalBuild: ((build: Dataset.Builder) -> Unit)? = null): Dataset? {
val title = makeEntryTitle(entryInfo)
val views = newRemoteViews(context, database, title, entryInfo.icon)
val builder = Dataset.Builder(views)
builder.setId(entryInfo.id.toString())
private fun Dataset.Builder.addValueToDatasetBuilder(
id: AutofillId,
autofillValue: AutofillValue?
): Dataset.Builder {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
setField(
id, autofillValue?.let {
Field.Builder()
.setValue(it)
.build()
}
)
} else {
@Suppress("DEPRECATION")
setValue(id, autofillValue)
}
Log.d(TAG, "Set Autofill value $autofillValue for id $id")
return this
}
private fun buildDatasetForEntry(context: Context,
database: ContextualDatabase,
entryInfo: EntryInfo,
struct: StructureParser.Result,
inlinePresentation: InlinePresentation?): Dataset {
val remoteViews: RemoteViews = newRemoteViews(context, database, makeEntryTitle(entryInfo), entryInfo.icon)
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Dataset.Builder(Presentations.Builder()
.apply {
inlinePresentation?.let {
setInlinePresentation(inlinePresentation)
}
}
.setDialogPresentation(remoteViews)
.setMenuPresentation(remoteViews)
.build())
} else {
@Suppress("DEPRECATION")
Dataset.Builder(remoteViews).apply {
inlinePresentation?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
setInlinePresentation(inlinePresentation)
}
}
}
}
datasetBuilder.setId(entryInfo.id.toString())
struct.usernameId?.let { usernameId ->
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
datasetBuilder.addValueToDatasetBuilder(
usernameId,
AutofillValue.forText(entryInfo.username)
)
}
struct.passwordId?.let { passwordId ->
builder.setValue(passwordId, AutofillValue.forText(entryInfo.password))
datasetBuilder.addValueToDatasetBuilder(
passwordId,
AutofillValue.forText(entryInfo.password)
)
}
if (entryInfo.expires) {
val year = entryInfo.expiryTime.getYearInt()
val month = entryInfo.expiryTime.getMonthInt()
val year = entryInfo.expiryTime.getYear()
val month = entryInfo.expiryTime.getMonth()
val monthString = month.toString().padStart(2, '0')
val day = entryInfo.expiryTime.getDay()
val dayString = day.toString().padStart(2, '0')
@@ -135,9 +185,15 @@ object AutofillHelper {
struct.creditCardExpirationDateId?.let {
if (struct.isWebView) {
// set date string as defined in https://html.spec.whatwg.org
builder.setValue(it, AutofillValue.forText("$year\u002D$monthString"))
datasetBuilder.addValueToDatasetBuilder(
it,
AutofillValue.forText("$year\u002D$monthString")
)
} else {
builder.setValue(it, AutofillValue.forDate(entryInfo.expiryTime.date.time))
datasetBuilder.addValueToDatasetBuilder(
it,
AutofillValue.forDate(entryInfo.expiryTime.toJavaMilliseconds())
)
}
}
struct.creditCardExpirationYearId?.let {
@@ -151,34 +207,58 @@ object AutofillHelper {
}
if (yearIndex != -1) {
autofillValue = AutofillValue.forList(yearIndex)
builder.setValue(it, autofillValue)
datasetBuilder.addValueToDatasetBuilder(
it,
autofillValue
)
}
}
if (autofillValue == null) {
builder.setValue(it, AutofillValue.forText(year.toString()))
datasetBuilder.addValueToDatasetBuilder(
it,
AutofillValue.forText(year.toString())
)
}
}
struct.creditCardExpirationMonthId?.let {
if (struct.isWebView) {
builder.setValue(it, AutofillValue.forText(monthString))
datasetBuilder.addValueToDatasetBuilder(
it,
AutofillValue.forText(monthString)
)
} else {
if (struct.creditCardExpirationMonthOptions != null) {
// index starts at 0
builder.setValue(it, AutofillValue.forList(month - 1))
datasetBuilder.addValueToDatasetBuilder(
it,
AutofillValue.forList(month - 1)
)
} else {
builder.setValue(it, AutofillValue.forText(monthString))
datasetBuilder.addValueToDatasetBuilder(
it,
AutofillValue.forText(monthString)
)
}
}
}
struct.creditCardExpirationDayId?.let {
if (struct.isWebView) {
builder.setValue(it, AutofillValue.forText(dayString))
datasetBuilder.addValueToDatasetBuilder(
it,
AutofillValue.forText(dayString)
)
} else {
if (struct.creditCardExpirationDayOptions != null) {
builder.setValue(it, AutofillValue.forList(day - 1))
datasetBuilder.addValueToDatasetBuilder(
it,
AutofillValue.forList(day - 1)
)
} else {
builder.setValue(it, AutofillValue.forText(dayString))
datasetBuilder.addValueToDatasetBuilder(
it,
AutofillValue.forText(dayString)
)
}
}
}
@@ -186,29 +266,32 @@ object AutofillHelper {
for (field in entryInfo.customFields) {
if (field.name == TemplateField.LABEL_HOLDER) {
struct.creditCardHolderId?.let { ccNameId ->
builder.setValue(ccNameId, AutofillValue.forText(field.protectedValue.stringValue))
datasetBuilder.addValueToDatasetBuilder(
ccNameId,
AutofillValue.forText(field.protectedValue.stringValue)
)
}
}
if (field.name == TemplateField.LABEL_NUMBER) {
struct.creditCardNumberId?.let { ccnId ->
builder.setValue(ccnId, AutofillValue.forText(field.protectedValue.stringValue))
datasetBuilder.addValueToDatasetBuilder(
ccnId,
AutofillValue.forText(field.protectedValue.stringValue)
)
}
}
if (field.name == TemplateField.LABEL_CVV) {
struct.cardVerificationValueId?.let { cvvId ->
builder.setValue(cvvId, AutofillValue.forText(field.protectedValue.stringValue))
datasetBuilder.addValueToDatasetBuilder(
cvvId,
AutofillValue.forText(field.protectedValue.stringValue)
)
}
}
}
additionalBuild?.invoke(builder)
return try {
builder.build()
} catch (e: Exception) {
// at least one value must be set
null
}
val dataset = datasetBuilder.build()
Log.d(TAG, "Autofill Dataset $dataset created")
return dataset
}
/**
@@ -228,8 +311,8 @@ object AutofillHelper {
return null
}
@RequiresApi(Build.VERSION_CODES.R)
@SuppressLint("RestrictedApi")
@RequiresApi(Build.VERSION_CODES.R)
private fun buildInlinePresentationForEntry(context: Context,
database: ContextualDatabase,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
@@ -239,10 +322,11 @@ object AutofillHelper {
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
if (positionItem <= maxSuggestion - 1
&& inlinePresentationSpecs.size > positionItem
) {
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
if (positionItem <= maxSuggestion - 1) {
// If positionItem is larger than the number of specs in the list, then
// the last spec is used for the remainder of the suggestions
val inlinePresentationSpec = inlinePresentationSpecs[min(positionItem, inlinePresentationSpecs.size - 1)]
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
@@ -335,25 +419,33 @@ object AutofillHelper {
}
}
}
}
entriesInfo.forEachIndexed { _, entry ->
if (numberInlineSuggestions > 0
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& compatInlineSuggestionsRequest != null) {
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult) { builder ->
buildInlinePresentationForEntry(context, database,
compatInlineSuggestionsRequest, numberInlineSuggestions--, entry
)?.let { inlinePresentation ->
builder.setInlinePresentation(inlinePresentation)
}
})
} else {
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult))
try {
// Build inline presentation for compatible keyboard
var inlinePresentation: InlinePresentation? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& numberInlineSuggestions > 0
&& compatInlineSuggestionsRequest != null) {
inlinePresentation = buildInlinePresentationForEntry(
context,
database,
compatInlineSuggestionsRequest,
numberInlineSuggestions--,
entry
)
}
// Create dataset for each entry
responseBuilder.addDataset(
buildDatasetForEntry(context, database, entry, parseResult, inlinePresentation)
)
} catch (e: Exception) {
Log.e(TAG, "Unable to add dataset")
}
}
// Add a new dataset for manual selection
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
@@ -362,25 +454,51 @@ object AutofillHelper {
manualSelection = true
}
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
searchInfo, compatInlineSuggestionsRequest)
AutofillLauncherActivity.getPendingIntentForSelection(context,
searchInfo, compatInlineSuggestionsRequest)?.let { pendingIntent ->
parseResult.allAutofillIds().let { autofillIds ->
autofillIds.forEach { id ->
val builder = Dataset.Builder(manualSelectionView)
var inlinePresentation: InlinePresentation? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpec =
inlineSuggestionsRequest.inlinePresentationSpecs[0]
inlinePresentation = buildInlinePresentationForManualSelection(
context,
inlinePresentationSpec,
pendingIntent
)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Dataset.Builder(Presentations.Builder()
.apply {
inlinePresentation?.let {
builder.setInlinePresentation(it)
setInlinePresentation(it)
}
}
.setDialogPresentation(manualSelectionView)
.setMenuPresentation(manualSelectionView)
.build())
} else {
@Suppress("DEPRECATION")
Dataset.Builder(manualSelectionView).apply {
inlinePresentation?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
setInlinePresentation(it)
}
}
}
builder.setValue(id, null)
builder.setAuthentication(pendingIntent.intentSender)
responseBuilder.addDataset(builder.build())
}
parseResult.allAutofillIds().let { autofillIds ->
autofillIds.forEach { id ->
datasetBuilder.addValueToDatasetBuilder(id, null)
datasetBuilder.setAuthentication(pendingIntent.intentSender)
}
val dataset = datasetBuilder.build()
Log.d(TAG, "Autofill Dataset for manual selection $dataset created")
responseBuilder.addDataset(dataset)
}
}
}
@@ -388,6 +506,7 @@ object AutofillHelper {
return try {
responseBuilder.build()
} catch (e: Exception) {
Log.e(TAG, "Unable to create Autofill response", e)
null
}
}
@@ -424,7 +543,7 @@ object AutofillHelper {
buildResponse(activity, database, entriesInfo, result, null)
}
val mReplyIntent = Intent()
Log.d(activity.javaClass.name, "Successed Autofill auth.")
Log.d(activity.javaClass.name, "Success Autofill auth.")
mReplyIntent.putExtra(
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
response)
@@ -479,4 +598,6 @@ object AutofillHelper {
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
activityResultLauncher?.launch(intent)
}
private val TAG = AutofillHelper::class.java.name
}

View File

@@ -45,7 +45,6 @@ import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.WebDomain
import org.joda.time.DateTime
import java.util.concurrent.atomic.AtomicBoolean
@RequiresApi(api = Build.VERSION_CODES.O)
@@ -57,7 +56,6 @@ class KeeAutofillService : AutofillService() {
private var webDomainBlocklist: Set<String>? = null
private var askToSaveData: Boolean = false
private var autofillInlineSuggestionsEnabled: Boolean = false
private var mLock = AtomicBoolean()
override fun onCreate() {
super.onCreate()
@@ -90,35 +88,37 @@ class KeeAutofillService : AutofillService() {
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
// 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 ->
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
Log.d(TAG, "Autofill requested in compatibility mode")
} else {
Log.d(TAG, "Autofill requested in native mode")
}
// 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
}
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
searchInfo.webDomain = webDomainWithoutSubDomain
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
CompatInlineSuggestionsRequest(request)
} else {
null
}
launchSelection(mDatabase,
searchInfo,
parseResult,
inlineSuggestionsRequest,
callback)
// 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 (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
}
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
searchInfo.webDomain = webDomainWithoutSubDomain
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
CompatInlineSuggestionsRequest(request)
} else {
null
}
launchSelection(mDatabase,
searchInfo,
parseResult,
inlineSuggestionsRequest,
callback)
}
}
}
@@ -157,139 +157,197 @@ class KeeAutofillService : AutofillService() {
searchInfo: SearchInfo,
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
callback: FillCallback) {
var success = false
parseResult.allAutofillIds().let { autofillIds ->
if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response.
val intentSender = AutofillLauncherActivity.getPendingIntentForSelection(this,
searchInfo, inlineSuggestionsRequest).intentSender
val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (database == null) {
if (!parseResult.webDomain.isNullOrEmpty()) {
RemoteViews(
packageName,
R.layout.item_autofill_unlock_web_domain
).apply {
setTextViewText(
R.id.autofill_web_domain_text,
parseResult.webDomain
)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
setTextViewText(
R.id.autofill_app_id_text,
parseResult.applicationId
)
AutofillLauncherActivity.getPendingIntentForSelection(this,
searchInfo, inlineSuggestionsRequest)?.intentSender?.let { intentSender ->
val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (database == null) {
if (!parseResult.webDomain.isNullOrEmpty()) {
RemoteViews(
packageName,
R.layout.item_autofill_unlock_web_domain
).apply {
setTextViewText(
R.id.autofill_web_domain_text,
parseResult.webDomain
)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
setTextViewText(
R.id.autofill_app_id_text,
parseResult.applicationId
)
}
} else {
RemoteViews(packageName, R.layout.item_autofill_unlock)
}
} else {
RemoteViews(packageName, R.layout.item_autofill_unlock)
}
} else {
if (!parseResult.webDomain.isNullOrEmpty()) {
RemoteViews(
packageName,
R.layout.item_autofill_select_entry_web_domain
).apply {
setTextViewText(
R.id.autofill_web_domain_text,
parseResult.webDomain
)
if (!parseResult.webDomain.isNullOrEmpty()) {
RemoteViews(
packageName,
R.layout.item_autofill_select_entry_web_domain
).apply {
setTextViewText(
R.id.autofill_web_domain_text,
parseResult.webDomain
)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(
packageName,
R.layout.item_autofill_select_entry_app_id
).apply {
setTextViewText(
R.id.autofill_app_id_text,
parseResult.applicationId
)
}
} else {
RemoteViews(packageName, R.layout.item_autofill_select_entry)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_select_entry_app_id).apply {
setTextViewText(
R.id.autofill_app_id_text,
parseResult.applicationId
)
}
} else {
RemoteViews(packageName, R.layout.item_autofill_select_entry)
}
}
// Tell the autofill framework the interest to save credentials
if (askToSaveData) {
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
val requiredIds = ArrayList<AutofillId>()
val optionalIds = ArrayList<AutofillId>()
// Tell the autofill framework the interest to save credentials
if (askToSaveData) {
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
val requiredIds = ArrayList<AutofillId>()
val optionalIds = ArrayList<AutofillId>()
// Only if at least a password
parseResult.passwordId?.let { passwordInfo ->
parseResult.usernameId?.let { usernameInfo ->
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
requiredIds.add(usernameInfo)
// Only if at least a password
parseResult.passwordId?.let { passwordInfo ->
parseResult.usernameId?.let { usernameInfo ->
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
requiredIds.add(usernameInfo)
}
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
requiredIds.add(passwordInfo)
}
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
requiredIds.add(passwordInfo)
}
// or a credit card form
if (requiredIds.isEmpty()) {
parseResult.creditCardNumberId?.let { numberId ->
types = types or SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD
requiredIds.add(numberId)
Log.d(TAG, "Asking to save credit card number")
// or a credit card form
if (requiredIds.isEmpty()) {
parseResult.creditCardNumberId?.let { numberId ->
types = types or SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD
requiredIds.add(numberId)
Log.d(TAG, "Asking to save credit card number")
}
parseResult.creditCardExpirationDateId?.let { id -> optionalIds.add(id) }
parseResult.creditCardExpirationYearId?.let { id -> optionalIds.add(id) }
parseResult.creditCardExpirationMonthId?.let { id -> optionalIds.add(id) }
parseResult.creditCardHolderId?.let { id -> optionalIds.add(id) }
parseResult.cardVerificationValueId?.let { id -> optionalIds.add(id) }
}
parseResult.creditCardExpirationDateId?.let { id -> optionalIds.add(id) }
parseResult.creditCardExpirationYearId?.let { id -> optionalIds.add(id) }
parseResult.creditCardExpirationMonthId?.let { id -> optionalIds.add(id) }
parseResult.creditCardHolderId?.let { id -> optionalIds.add(id) }
parseResult.cardVerificationValueId?.let { id -> optionalIds.add(id) }
}
if (requiredIds.isNotEmpty()) {
val builder = SaveInfo.Builder(types, requiredIds.toTypedArray())
if (optionalIds.isNotEmpty()) {
builder.setOptionalIds(optionalIds.toTypedArray())
if (requiredIds.isNotEmpty()) {
val builder = SaveInfo.Builder(types, requiredIds.toTypedArray())
if (optionalIds.isNotEmpty()) {
builder.setOptionalIds(optionalIds.toTypedArray())
}
responseBuilder.setSaveInfo(builder.build())
}
responseBuilder.setSaveInfo(builder.build())
}
}
// Build inline presentation
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
var inlinePresentation: InlinePresentation? = null
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
if (inlineSuggestionsRequest.maxSuggestionCount > 0
&& inlinePresentationSpecs.size > 0) {
val inlinePresentationSpec = inlinePresentationSpecs[0]
// Build inline presentation
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled
) {
var inlinePresentation: InlinePresentation? = null
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs =
inlineSuggestionsRequest.inlinePresentationSpecs
if (inlineSuggestionsRequest.maxSuggestionCount > 0
&& inlinePresentationSpecs.size > 0
) {
val inlinePresentationSpec = inlinePresentationSpecs[0]
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1)) {
// Build the content for IME UI
inlinePresentation = InlinePresentation(
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (UiVersions.getVersions(imeStyle)
.contains(UiVersions.INLINE_UI_VERSION_1)
) {
// Build the content for IME UI
inlinePresentation = InlinePresentation(
InlineSuggestionUi.newContentBuilder(
PendingIntent.getActivity(this,
0,
Intent(this, AutofillSettingsActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
})
PendingIntent.getActivity(
this,
0,
Intent(this, AutofillSettingsActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
)
).apply {
setContentDescription(getString(R.string.autofill_sign_in_prompt))
setTitle(getString(R.string.autofill_sign_in_prompt))
setStartIcon(Icon.createWithResource(this@KeeAutofillService, R.mipmap.ic_launcher_round).apply {
setTintBlendMode(BlendMode.DST)
})
}.build().slice, inlinePresentationSpec, false)
setStartIcon(
Icon.createWithResource(
this@KeeAutofillService,
R.mipmap.ic_launcher_round
).apply {
setTintBlendMode(BlendMode.DST)
})
}.build().slice, inlinePresentationSpec, false
)
}
}
}
// Build response
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
try {
// Buggy method on some API 33 devices
responseBuilder.setAuthentication(
autofillIds,
intentSender,
Presentations.Builder().apply {
inlinePresentation?.let {
setInlinePresentation(it)
}
setDialogPresentation(remoteViewsUnlock)
}.build()
)
} catch (e: Exception) {
Log.e(TAG, "Unable to use the new setAuthentication method.", e)
@Suppress("DEPRECATION")
responseBuilder.setAuthentication(
autofillIds,
intentSender,
remoteViewsUnlock,
inlinePresentation
)
}
} else {
@Suppress("DEPRECATION")
responseBuilder.setAuthentication(
autofillIds,
intentSender,
remoteViewsUnlock,
inlinePresentation
)
}
} else {
@Suppress("DEPRECATION")
responseBuilder.setAuthentication(
autofillIds,
intentSender,
remoteViewsUnlock
)
}
// Build response
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
} else {
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
success = true
callback.onSuccess(responseBuilder.build())
}
callback.onSuccess(responseBuilder.build())
}
}
if (!success)
callback.onFailure("Unable to get Autofill ids for UI selection")
}
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
var success = false
if (askToSaveData) {
val latestStructure = request.fillContexts.last().structure
StructureParser(latestStructure).parse(true)?.let { parseResult ->
@@ -332,14 +390,16 @@ class KeeAutofillService : AutofillService() {
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
// registerInfo))
//} else {
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
callback.onSuccess()
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
success = true
callback.onSuccess()
//}
return
}
}
}
callback.onFailure("Saving form values is not allowed")
if (!success) {
callback.onFailure("Saving form values is not allowed")
}
}
override fun onConnected() {
@@ -348,7 +408,6 @@ class KeeAutofillService : AutofillService() {
}
override fun onDisconnected() {
mLock.set(false)
Log.d(TAG, "onDisconnected")
}

View File

@@ -37,7 +37,6 @@ import kotlin.collections.ArrayList
@RequiresApi(api = Build.VERSION_CODES.O)
class StructureParser(private val structure: AssistStructure) {
private var result: Result? = null
private var usernameNeeded = true
private var usernameIdCandidate: AutofillId? = null
private var usernameValueCandidate: AutofillValue? = null
@@ -105,7 +104,7 @@ class StructureParser(private val structure: AssistStructure) {
if (node.autofillId != null) {
// Parse methods
val hints = node.autofillHints
if (hints != null && hints.isNotEmpty()) {
if (!hints.isNullOrEmpty()) {
if (parseNodeByAutofillHint(node))
returnValue = true
} else if (parseNodeByHtmlAttributes(node))
@@ -134,16 +133,37 @@ class StructureParser(private val structure: AssistStructure) {
it.contains(View.AUTOFILL_HINT_USERNAME, true)
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|| 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("login", true) -> {
// Replace username until we have a password
if (result?.passwordId == null) {
result?.usernameId = autofillId
result?.usernameValue = node.autofillValue
Log.d(TAG, "Autofill username hint if no password")
} else {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username hint if password")
}
}
it.contains(View.AUTOFILL_HINT_PHONE, true) -> {
if (usernameIdCandidate == null) {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill phone")
}
}
it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
// Password Id changed if it's the second times we are here,
// So the last username candidate is most appropriate
if (result?.passwordId != null) {
result?.usernameId = usernameIdCandidate
result?.usernameValue = usernameValueCandidate
}
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password hint")
return true
// Comment "return" to check all the tree
// return true
}
it.equals("cc-name", true) -> {
Log.d(TAG, "Autofill credit card name hint")
@@ -279,14 +299,19 @@ class StructureParser(private val structure: AssistStructure) {
"type" -> {
when (pairAttribute.second.lowercase(Locale.ENGLISH)) {
"tel", "email" -> {
result?.usernameId = autofillId
result?.usernameValue = node.autofillValue
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
if (result?.passwordId == null) {
result?.usernameId = autofillId
result?.usernameValue = node.autofillValue
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
}
"text" -> {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
// Assume username is before password
if (result?.passwordId == null) {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
}
"password" -> {
result?.passwordId = autofillId
@@ -315,85 +340,128 @@ class StructureParser(private val structure: AssistStructure) {
return "0x${"%08x".format(inputType)}"
}
private fun manageTypeText(
node: AssistStructure.ViewNode,
autofillId: AutofillId?,
inputType: Int
): Boolean {
when {
inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS) -> {
if (result?.passwordId == null) {
result?.usernameId = autofillId
result?.usernameValue = node.autofillValue
Log.d(TAG, "Autofill username android text type: ${showHexInputType(inputType)}")
}
}
inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_NORMAL,
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
// Assume the username field is before the password field
if (result?.passwordId == null) {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
}
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
}
inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
// Some forms used visible password as username
if (result?.passwordId == null &&
usernameIdCandidate == null && usernameValueCandidate == null) {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill visible password android text type (as username): ${showHexInputType(inputType)}")
} else if (result?.passwordId == null && result?.passwordValue == null) {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill visible password android text type (as password): ${showHexInputType(inputType)}")
}
}
inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_PASSWORD,
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
return true
}
inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT,
InputType.TYPE_TEXT_VARIATION_FILTER,
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE,
InputType.TYPE_TEXT_VARIATION_PHONETIC,
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS,
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE,
InputType.TYPE_TEXT_VARIATION_URI) -> {
// Type not used
Log.d(TAG, "Autofill not used android text type: ${showHexInputType(inputType)}")
}
else -> {
Log.d(TAG, "Autofill unknown android text type: ${showHexInputType(inputType)}")
}
}
return false
}
private fun manageTypeNumber(
node: AssistStructure.ViewNode,
autofillId: AutofillId?,
inputType: Int
): Boolean {
when {
inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
if (usernameIdCandidate == null) {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate android number type: ${showHexInputType(inputType)}")
}
}
inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
return true
}
else -> {
Log.d(TAG, "Autofill unknown android number type: ${showHexInputType(inputType)}")
}
}
return false
}
private fun manageTypeNull(
node: AssistStructure.ViewNode,
autofillId: AutofillId?,
inputType: Int
): Boolean {
if (node.className == "android.widget.EditText") {
Log.d(TAG, "Autofill null android input type class: ${showHexInputType(inputType)}" +
", get the EditText node class name!")
if (result?.passwordId == null) {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
}
}
return false
}
private fun parseNodeByAndroidInput(node: AssistStructure.ViewNode): Boolean {
val autofillId = node.autofillId
val inputType = node.inputType
when (inputType and InputType.TYPE_MASK_CLASS) {
InputType.TYPE_CLASS_TEXT -> {
when {
inputIsVariationType(inputType,
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,
InputType.TYPE_TEXT_VARIATION_NORMAL,
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
}
inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
// Some forms used visible password as username
if (usernameIdCandidate == null && usernameValueCandidate == null) {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill visible password android text type (as username): ${showHexInputType(inputType)}")
} else if (result?.passwordId == null && result?.passwordValue == null) {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill visible password android text type (as password): ${showHexInputType(inputType)}")
usernameNeeded = false
}
}
inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_PASSWORD,
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
return true
}
inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT,
InputType.TYPE_TEXT_VARIATION_FILTER,
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE,
InputType.TYPE_TEXT_VARIATION_PHONETIC,
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS,
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE,
InputType.TYPE_TEXT_VARIATION_URI) -> {
// Type not used
}
else -> {
Log.d(TAG, "Autofill unknown android text type: ${showHexInputType(inputType)}")
}
}
return manageTypeText(node, autofillId, inputType)
}
InputType.TYPE_CLASS_NUMBER -> {
when {
inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
if (usernameIdCandidate == null) {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate android number type: ${showHexInputType(inputType)}")
}
}
inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
return true
}
else -> {
Log.d(TAG, "Autofill unknown android number type: ${showHexInputType(inputType)}")
}
}
return manageTypeNumber(node, autofillId, inputType)
}
InputType.TYPE_NULL -> {
return manageTypeNull(node, autofillId, inputType)
}
}
return false
@@ -422,58 +490,14 @@ class StructureParser(private val structure: AssistStructure) {
var creditCardExpirationDayOptions: Array<CharSequence>? = null
var usernameId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var passwordId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardHolderId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardNumberId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardExpirationDateId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardExpirationYearId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardExpirationMonthId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardExpirationDayId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var cardVerificationValueId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
fun allAutofillIds(): Array<AutofillId> {
val all = ArrayList<AutofillId>()
@@ -500,13 +524,13 @@ class StructureParser(private val structure: AssistStructure) {
var usernameValue: AutofillValue? = null
set(value) {
if (allowSaveValues && field == null)
if (allowSaveValues)
field = value
}
var passwordValue: AutofillValue? = null
set(value) {
if (allowSaveValues && field == null)
if (allowSaveValues)
field = value
}

View File

@@ -566,6 +566,7 @@ class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCa
this.decryptedValue = decryptedValue
}
)
cipherDatabaseAction.resetCipherParameters(databaseUri)
}
}

View File

@@ -40,6 +40,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
@@ -182,8 +183,11 @@ class DatabaseTaskProvider(
private var databaseInfoListener = object:
DatabaseTaskNotificationService.DatabaseInfoListener {
override fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo,
newDatabaseInfo: SnapFileDatabaseInfo) {
override fun onDatabaseInfoChanged(
previousDatabaseInfo: SnapFileDatabaseInfo,
newDatabaseInfo: SnapFileDatabaseInfo,
readOnlyDatabase: Boolean
) {
activity?.let { activity ->
activity.lifecycleScope.launch {
if (databaseChangedDialogFragment == null) {
@@ -195,7 +199,8 @@ class DatabaseTaskProvider(
if (progressTaskDialogFragment == null) {
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
previousDatabaseInfo,
newDatabaseInfo
newDatabaseInfo,
readOnlyDatabase
)
databaseChangedDialogFragment?.actionDatabaseListener =
mActionDatabaseListener
@@ -326,11 +331,11 @@ class DatabaseTaskProvider(
}
}
}
context.registerReceiver(databaseTaskBroadcastReceiver,
ContextCompat.registerReceiver(context, databaseTaskBroadcastReceiver,
IntentFilter().apply {
addAction(DATABASE_START_TASK_ACTION)
addAction(DATABASE_STOP_TASK_ACTION)
}
}, RECEIVER_NOT_EXPORTED
)
// Check if a service is currently running else do nothing

View File

@@ -44,14 +44,17 @@ open class SaveDatabaseRunnable(
override fun onActionRun() {
database.checkVersion()
if (saveDatabase && result.isSuccess) {
// Save database in all cases if it's a copy
if ((databaseCopyUri != null || saveDatabase) && result.isSuccess) {
try {
val contentResolver = context.contentResolver
// Build temp database file to avoid file corruption if error
database.saveData(
cacheFile = File(context.cacheDir, databaseCopyUri.hashCode().toString()),
databaseOutputStream = contentResolver
.getUriOutputStream(databaseCopyUri ?: database.fileUri),
databaseOutputStream = {
contentResolver
.getUriOutputStream(databaseCopyUri ?: database.fileUri)
},
isNewLocation = databaseCopyUri == null,
mainCredential?.toMasterCredential(contentResolver),
challengeResponseRetriever)

View File

@@ -26,7 +26,7 @@ import com.kunzisoft.keepass.database.element.template.TemplateEngine
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.exception.*
fun DatabaseException.getLocalizedMessage(resources: Resources): String = parameters?.let {
fun DatabaseException.getLocalizedMessage(resources: Resources): String? =
when (this) {
is FileNotFoundDatabaseException -> resources.getString(R.string.file_not_found_content)
is CorruptedDatabaseException -> resources.getString(R.string.corrupted_file)
@@ -39,7 +39,7 @@ fun DatabaseException.getLocalizedMessage(resources: Resources): String = parame
is InvalidCredentialsDatabaseException -> resources.getString(R.string.invalid_credentials)
is KDFMemoryDatabaseException -> resources.getString(R.string.error_load_database_KDF_memory)
is NoMemoryDatabaseException -> resources.getString(R.string.error_out_of_memory)
is DuplicateUuidDatabaseException -> resources.getString(R.string.invalid_db_same_uuid)
is DuplicateUuidDatabaseException -> resources.getString(R.string.invalid_db_same_uuid, parameters[0], parameters[1])
is XMLMalformedDatabaseException -> resources.getString(R.string.error_XML_malformed)
is MergeDatabaseKDBException -> resources.getString(R.string.error_unable_merge_database_kdb)
is MoveEntryDatabaseException -> resources.getString(R.string.error_move_entry_here)
@@ -48,9 +48,8 @@ fun DatabaseException.getLocalizedMessage(resources: Resources): String = parame
is CopyGroupDatabaseException -> resources.getString(R.string.error_copy_group_here)
is DatabaseInputException -> resources.getString(R.string.error_load_database)
is DatabaseOutputException -> resources.getString(R.string.error_save_database)
else -> (mThrowable as? DatabaseException)?.getLocalizedMessage(resources)
else -> localizedMessage
}
} ?: resources.getString(R.string.error_load_database)
fun CompressionAlgorithm.getLocalizedName(resources: Resources): String {
return when (this) {

View File

@@ -25,16 +25,17 @@ import android.content.Context
import android.content.Intent
import android.inputmethodservice.InputMethodService
import android.media.AudioManager
import android.os.Build
import android.util.Log
import android.view.*
import android.view.Gravity
import android.view.HapticFeedbackConstants
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.PopupWindow
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.recyclerview.widget.LinearLayoutManager
@@ -53,8 +54,14 @@ import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.*
import java.util.*
import com.kunzisoft.keepass.utils.KeyboardUtil.showKeyboardPicker
import com.kunzisoft.keepass.utils.KeyboardUtil.switchToPreviousKeyboard
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.LockReceiver
import com.kunzisoft.keepass.utils.REMOVE_ENTRY_MAGIKEYBOARD_ACTION
import com.kunzisoft.keepass.utils.registerLockReceiver
import com.kunzisoft.keepass.utils.unregisterLockReceiver
import java.util.UUID
class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionListener {
@@ -239,24 +246,6 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
}
}
private fun switchToPreviousKeyboard() {
var imeManager: InputMethodManager? = null
try {
imeManager = ContextCompat.getSystemService(this, InputMethodManager::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
switchToPreviousInputMethod()
} else {
@Suppress("DEPRECATION")
window.window?.let { window ->
imeManager?.switchToLastInputMethod(window.attributes.token)
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to switch to the previous IME", e)
imeManager?.showInputMethodPicker()
}
}
override fun onKey(primaryCode: Int, keyCodes: IntArray) {
val inputConnection = currentInputConnection
@@ -267,11 +256,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
playClick(primaryCode)
when (primaryCode) {
KEY_BACK_KEYBOARD -> switchToPreviousKeyboard()
KEY_BACK_KEYBOARD -> {
switchToPreviousKeyboard()
}
KEY_CHANGE_KEYBOARD -> {
ContextCompat.getSystemService(this, InputMethodManager::class.java)
?.showInputMethodPicker()
showKeyboardPicker()
}
KEY_ENTRY -> {
var searchInfo: SearchInfo? = null
@@ -471,14 +460,6 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
}
fun activatedInSettings(context: Context): Boolean {
return ContextCompat.getSystemService(context, InputMethodManager::class.java)
?.enabledInputMethodList
?.any {
it.packageName == context.packageName
} ?: false
}
fun performSelection(items: List<EntryInfo>,
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
actionEntrySelection: (autoSearch: Boolean) -> Unit) {

View File

@@ -0,0 +1,3 @@
package com.kunzisoft.keepass.model
data class DataDate(val year: Int, val month: Int, val day: Int)

View File

@@ -0,0 +1,3 @@
package com.kunzisoft.keepass.model
data class DataTime(val hour: Int, val minute: Int)

View File

@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.password
import android.content.res.Resources
import android.graphics.Color
import android.text.Editable
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
@@ -253,51 +254,62 @@ class PasswordGenerator(private val resources: Resources) {
return charSet.toString()
}
fun colorizedPassword(editable: Editable?) {
editable.toString().forEachIndexed { index, char ->
colorFromChar(char)?.let { color ->
editable?.setSpan(
ForegroundColorSpan(color),
index,
index + 1,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
}
fun getColorizedPassword(password: String): Spannable {
val spannableString = SpannableStringBuilder()
if (password.isNotEmpty()) {
password.forEach {
when {
UPPERCASE_CHARS.contains(it)||
LOWERCASE_CHARS.contains(it) -> {
spannableString.append(it)
}
DIGIT_CHARS.contains(it) -> {
// RED
spannableString.append(colorizeChar(it, Color.rgb(246, 79, 62)))
}
SPECIAL_CHARS.contains(it) -> {
// Blue
spannableString.append(colorizeChar(it, Color.rgb(39, 166, 228)))
}
MINUS_CHAR.contains(it)||
UNDERLINE_CHAR.contains(it)||
BRACKET_CHARS.contains(it) -> {
// Purple
spannableString.append(colorizeChar(it, Color.rgb(185, 38, 209)))
}
extendedChars().contains(it) -> {
// Green
spannableString.append(colorizeChar(it, Color.rgb(44, 181, 50)))
}
else -> {
spannableString.append(it)
}
}
password.forEach { char ->
colorFromChar(char)?.let { color ->
val spannableColorChar = SpannableString(char.toString())
spannableColorChar.setSpan(
ForegroundColorSpan(color),
0,
1,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
spannableString.append(spannableColorChar)
} ?: spannableString.append(char)
}
}
return spannableString
}
private fun colorizeChar(char: Char, color: Int): Spannable {
val spannableColorChar = SpannableString(char.toString())
spannableColorChar.setSpan(
ForegroundColorSpan(color),
0,
1,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
return spannableColorChar
private fun colorFromChar(char: Char): Int? {
return when {
DIGIT_CHARS.contains(char) -> {
// RED
Color.rgb(246, 79, 62)
}
SPECIAL_CHARS.contains(char) -> {
// Blue
Color.rgb(39, 166, 228)
}
MINUS_CHAR.contains(char)||
UNDERLINE_CHAR.contains(char)||
BRACKET_CHARS.contains(char) -> {
// Purple
Color.rgb(185, 38, 209)
}
extendedChars().contains(char) -> {
// Green
Color.rgb(44, 181, 50)
}
else -> {
null
}
}
}
}
}

View File

@@ -26,7 +26,9 @@ class AdvancedUnlockNotificationService : NotificationService() {
return mTempCipherDao.firstOrNull { it.databaseUri == databaseUri.toString()}
}
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity) {
val cipherDatabaseRetrieve = mTempCipherDao.firstOrNull { it.databaseUri == cipherDatabaseEntity.databaseUri }
val cipherDatabaseRetrieve = mTempCipherDao.firstOrNull {
it.databaseUri == cipherDatabaseEntity.databaseUri
}
cipherDatabaseRetrieve?.replaceContent(cipherDatabaseEntity)
?: mTempCipherDao.add(cipherDatabaseEntity)
}
@@ -35,6 +37,9 @@ class AdvancedUnlockNotificationService : NotificationService() {
mTempCipherDao.remove(it)
}
}
fun resetTimer() {
resetTimeJob()
}
fun deleteAll() {
mTempCipherDao.clear()
}
@@ -86,11 +91,19 @@ class AdvancedUnlockNotificationService : NotificationService() {
val notificationTimeoutMilliSecs = PreferencesUtil.getAdvancedUnlockTimeout(this)
// Not necessarily a foreground service
if (mTimerJob == null && notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
defineTimerJob(notificationBuilder, notificationTimeoutMilliSecs) {
defineTimerJob(
notificationBuilder,
NotificationServiceType.ADVANCED_UNLOCK,
notificationTimeoutMilliSecs
) {
sendBroadcast(Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION))
}
} else {
startForeground(notificationId, notificationBuilder.build())
startForegroundCompat(
notificationId,
notificationBuilder,
NotificationServiceType.ADVANCED_UNLOCK
)
}
return mActionTaskBinder

View File

@@ -36,13 +36,13 @@ import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
import java.util.LinkedList
import java.util.concurrent.CopyOnWriteArrayList
@@ -282,15 +282,21 @@ class AttachmentFileNotificationService: LockNotificationService() {
AttachmentState.ERROR -> {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
try {
notificationManager?.notify(
attachmentNotification.notificationId,
builder.build()
)
checkNotificationsPermission(this) {
notificationManager?.notify(
attachmentNotification.notificationId,
builder.build()
)
}
} catch (e: SecurityException) {
Log.e(TAG, "Unable to notify the attachment state", e)
}
} else -> {
startForeground(attachmentNotification.notificationId, builder.build())
startForegroundCompat(
attachmentNotification.notificationId,
builder,
NotificationServiceType.ATTACHMENT
)
}
}
}

View File

@@ -19,16 +19,12 @@
*/
package com.kunzisoft.keepass.services
import android.Manifest
import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
@@ -196,23 +192,29 @@ class ClipboardEntryNotificationService : LockNotificationService() {
//Get settings
val notificationTimeoutMilliSecs = PreferencesUtil.getClipboardTimeout(this)
if (notificationTimeoutMilliSecs != NEVER) {
defineTimerJob(builder, notificationTimeoutMilliSecs, {
val newGeneratedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
// New auto generated value
if (generatedValue != newGeneratedValue) {
generatedValue = newGeneratedValue
clipboardHelper?.copyToClipboard(
fieldToCopy.label,
generatedValue,
fieldToCopy.isSensitive
)
defineTimerJob(
builder,
NotificationServiceType.CLIPBOARD,
notificationTimeoutMilliSecs,
{
val newGeneratedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
// New auto generated value
if (generatedValue != newGeneratedValue) {
generatedValue = newGeneratedValue
clipboardHelper?.copyToClipboard(
fieldToCopy.label,
generatedValue,
fieldToCopy.isSensitive
)
}
},
{
stopNotificationAndSendLockIfNeeded()
// Clean password only if no next field
if (nextFields.size <= 0)
clipboardHelper?.cleanClipboard()
}
}) {
stopNotificationAndSendLockIfNeeded()
// Clean password only if no next field
if (nextFields.size <= 0)
clipboardHelper?.cleanClipboard()
}
)
} else {
// No timer
checkNotificationsPermission {
@@ -226,12 +228,11 @@ class ClipboardEntryNotificationService : LockNotificationService() {
}
private fun checkNotificationsPermission(action: () -> Unit) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED) {
action.invoke()
} else {
showPermissionErrorIfNeeded(this)
}
checkNotificationsPermission(
this,
PreferencesUtil.isClipboardNotificationsEnable(this),
action
)
}
override fun onTaskRemoved(rootIntent: Intent?) {
@@ -255,26 +256,14 @@ class ClipboardEntryNotificationService : LockNotificationService() {
const val EXTRA_CLIPBOARD_FIELDS = "EXTRA_CLIPBOARD_FIELDS"
const val ACTION_CLEAN_CLIPBOARD = "ACTION_CLEAN_CLIPBOARD"
private fun showPermissionErrorIfNeeded(context: Context) {
if (PreferencesUtil.isClipboardNotificationsEnable(context)) {
Toast.makeText(context, R.string.warning_copy_permission, Toast.LENGTH_LONG).show()
}
}
fun checkAndLaunchNotification(
activity: Activity,
entry: EntryInfo
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
activity,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED) {
launchNotificationIfAllowed(activity, entry)
} else {
showPermissionErrorIfNeeded(activity)
}
} else {
checkNotificationsPermission(
activity,
PreferencesUtil.isClipboardNotificationsEnable(activity)
) {
launchNotificationIfAllowed(activity, entry)
}
}

View File

@@ -36,10 +36,24 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.ProgressMessage
import com.kunzisoft.keepass.database.action.*
import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable
import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable
import com.kunzisoft.keepass.database.action.MergeDatabaseRunnable
import com.kunzisoft.keepass.database.action.ReloadDatabaseRunnable
import com.kunzisoft.keepass.database.action.RemoveUnlinkedDataDatabaseRunnable
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
import com.kunzisoft.keepass.database.action.UpdateCompressionBinariesDatabaseRunnable
import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseRunnable
import com.kunzisoft.keepass.database.action.history.RestoreEntryHistoryDatabaseRunnable
import com.kunzisoft.keepass.database.action.node.*
import com.kunzisoft.keepass.database.action.node.ActionNodesValues
import com.kunzisoft.keepass.database.action.node.AddEntryRunnable
import com.kunzisoft.keepass.database.action.node.AddGroupRunnable
import com.kunzisoft.keepass.database.action.node.AfterActionNodesFinish
import com.kunzisoft.keepass.database.action.node.CopyNodesRunnable
import com.kunzisoft.keepass.database.action.node.DeleteNodesRunnable
import com.kunzisoft.keepass.database.action.node.MoveNodesRunnable
import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable
import com.kunzisoft.keepass.database.action.node.UpdateGroupRunnable
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
@@ -62,9 +76,17 @@ import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.utils.getParcelableList
import com.kunzisoft.keepass.utils.putParcelableList
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
import kotlinx.coroutines.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel
import java.util.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.util.UUID
open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater {
@@ -139,6 +161,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
fun onDatabaseInfoChanged(
previousDatabaseInfo: SnapFileDatabaseInfo,
newDatabaseInfo: SnapFileDatabaseInfo,
readOnlyDatabase: Boolean
)
}
@@ -197,8 +220,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
// Call listener to indicate a change in database info
if (!mSaveState && previousDatabaseInfo != null) {
mDatabaseInfoListeners.forEach { listener ->
listener.onDatabaseInfoChanged(previousDatabaseInfo,
lastFileDatabaseInfo)
listener.onDatabaseInfoChanged(
previousDatabaseInfo,
lastFileDatabaseInfo,
mDatabase?.isReadOnly ?: true
)
}
}
mSnapFileDatabaseInfo = lastFileDatabaseInfo
@@ -565,7 +591,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
// Create the notification
startForeground(notificationId, notificationBuilder.build())
startForegroundCompat(
notificationId,
notificationBuilder,
NotificationServiceType.DATABASE_TASK
)
}
private fun removeIntentData(intent: Intent?) {
@@ -831,6 +861,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
if (intent.hasExtra(MAIN_CREDENTIAL_KEY)) {
databaseToMergeMainCredential = intent.getParcelableExtraCompat(MAIN_CREDENTIAL_KEY)
}
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
return MergeDatabaseRunnable(
this,
databaseToMergeUri,
@@ -839,7 +870,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
retrieveResponseFromChallenge(hardwareKey, seed)
},
database,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
!database.isReadOnly && saveDatabase,
{ hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
},
@@ -932,12 +963,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
val parentId: NodeId<*>? = intent.getParcelableExtraCompat(PARENT_ID_KEY)
val newGroup: Group? = intent.getParcelableExtraCompat(GROUP_KEY)
if (parentId == null || newGroup == null) return null
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
database.getGroupById(parentId)?.let { parent ->
AddGroupRunnable(this,
database,
newGroup,
parent,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
!database.isReadOnly && saveDatabase,
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
@@ -959,12 +991,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
val groupId: NodeId<*>? = intent.getParcelableExtraCompat(GROUP_ID_KEY)
val newGroup: Group? = intent.getParcelableExtraCompat(GROUP_KEY)
if (groupId == null || newGroup == null) return null
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
database.getGroupById(groupId)?.let { oldGroup ->
UpdateGroupRunnable(this,
database,
oldGroup,
newGroup,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
!database.isReadOnly && saveDatabase,
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
@@ -986,12 +1019,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
val parentId: NodeId<*>? = intent.getParcelableExtraCompat(PARENT_ID_KEY)
val newEntry: Entry? = intent.getParcelableExtraCompat(ENTRY_KEY)
if (parentId == null || newEntry == null) return null
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
database.getGroupById(parentId)?.let { parent ->
AddEntryRunnable(this,
database,
newEntry,
parent,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
!database.isReadOnly && saveDatabase,
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
@@ -1013,12 +1047,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
val entryId: NodeId<UUID>? = intent.getParcelableExtraCompat(ENTRY_ID_KEY)
val newEntry: Entry? = intent.getParcelableExtraCompat(ENTRY_KEY)
if (entryId == null || newEntry == null) return null
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
database.getEntryById(entryId)?.let { oldEntry ->
UpdateEntryRunnable(this,
database,
oldEntry,
newEntry,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
!database.isReadOnly && saveDatabase,
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
@@ -1039,12 +1074,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val parentId: NodeId<*> = intent.getParcelableExtraCompat(PARENT_ID_KEY) ?: return null
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
database.getGroupById(parentId)?.let { newParent ->
CopyNodesRunnable(this,
database,
getListNodesFromBundle(database, intent.extras!!),
newParent,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
!database.isReadOnly && saveDatabase,
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
@@ -1065,12 +1101,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val parentId: NodeId<*> = intent.getParcelableExtraCompat(PARENT_ID_KEY) ?: return null
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
database.getGroupById(parentId)?.let { newParent ->
MoveNodesRunnable(this,
database,
getListNodesFromBundle(database, intent.extras!!),
newParent,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
!database.isReadOnly && saveDatabase,
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
@@ -1089,11 +1126,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
&& intent.hasExtra(ENTRIES_ID_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
DeleteNodesRunnable(this,
database,
getListNodesFromBundle(database, intent.extras!!),
resources.getString(R.string.recycle_bin),
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
!database.isReadOnly && saveDatabase,
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
@@ -1112,12 +1150,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val entryId: NodeId<UUID> = intent.getParcelableExtraCompat(ENTRY_ID_KEY) ?: return null
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
database.getEntryById(entryId)?.let { mainEntry ->
RestoreEntryHistoryDatabaseRunnable(this,
database,
mainEntry,
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
!database.isReadOnly && saveDatabase
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
@@ -1136,12 +1175,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val entryId: NodeId<UUID> = intent.getParcelableExtraCompat(ENTRY_ID_KEY) ?: return null
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
database.getEntryById(entryId)?.let { mainEntry ->
DeleteEntryHistoryDatabaseRunnable(this,
database,
mainEntry,
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
!database.isReadOnly && saveDatabase
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
@@ -1162,11 +1202,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
val oldElement: CompressionAlgorithm? = intent.getParcelableExtraCompat(OLD_ELEMENT_KEY)
val newElement: CompressionAlgorithm? = intent.getParcelableExtraCompat(NEW_ELEMENT_KEY)
if (oldElement == null || newElement == null) return null
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
return UpdateCompressionBinariesDatabaseRunnable(this,
database,
oldElement,
newElement,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
!database.isReadOnly && saveDatabase
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}.apply {
@@ -1184,9 +1225,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
database: ContextualDatabase,
): ActionRunnable? {
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
return RemoveUnlinkedDataDatabaseRunnable(this,
database,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
!database.isReadOnly && saveDatabase
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}.apply {
@@ -1204,9 +1246,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
database: ContextualDatabase,
): ActionRunnable? {
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
return SaveDatabaseRunnable(this,
database,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
!database.isReadOnly && saveDatabase,
null,
{ hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
@@ -1229,13 +1272,14 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
database: ContextualDatabase
): ActionRunnable? {
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
var databaseCopyUri: Uri? = null
if (intent.hasExtra(DATABASE_URI_KEY)) {
databaseCopyUri = intent.getParcelableExtraCompat(DATABASE_URI_KEY)
}
SaveDatabaseRunnable(this,
database,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
!database.isReadOnly && saveDatabase,
null,
{ hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)

View File

@@ -111,13 +111,18 @@ class KeyboardEntryNotificationService : LockNotificationService() {
.setContentIntent(null)
.setDeleteIntent(pendingDeleteIntent)
notificationManager?.cancel(notificationId)
notificationManager?.notify(notificationId, builder.build())
checkNotificationsPermission(this, PreferencesUtil.isKeyboardNotificationEntryEnable(this)) {
notificationManager?.notify(notificationId, builder.build())
}
// Timeout only if notification clear is available
if (PreferencesUtil.isClearKeyboardNotificationEnable(this)) {
if (mNotificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
defineTimerJob(builder, mNotificationTimeoutMilliSecs) {
defineTimerJob(
builder,
NotificationServiceType.KEYBOARD,
mNotificationTimeoutMilliSecs
) {
stopNotificationAndSendLockIfNeeded()
}
}

View File

@@ -1,17 +1,31 @@
package com.kunzisoft.keepass.services
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.os.Build
import android.os.IBinder
import android.util.TypedValue
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.Stylish
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.joda.time.Instant
abstract class NotificationService : Service() {
@@ -20,6 +34,7 @@ abstract class NotificationService : Service() {
private var colorNotificationAccent: Int = 0
protected var mTimerJob: Job? = null
private var mReset: Boolean = false
protected abstract val notificationId: Int
@@ -74,21 +89,55 @@ abstract class NotificationService : Service() {
}
}
protected fun startForegroundCompat(notificationId: Int,
builder: NotificationCompat.Builder,
type: NotificationServiceType
) {
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val foregroundServiceTimer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
FOREGROUND_SERVICE_TYPE_SPECIAL_USE
} else {
FOREGROUND_SERVICE_TYPE_NONE
}
val foregroundType = when (type) {
NotificationServiceType.DATABASE_TASK -> FOREGROUND_SERVICE_TYPE_DATA_SYNC
NotificationServiceType.ATTACHMENT -> FOREGROUND_SERVICE_TYPE_DATA_SYNC
NotificationServiceType.CLIPBOARD -> foregroundServiceTimer
NotificationServiceType.KEYBOARD -> foregroundServiceTimer
NotificationServiceType.ADVANCED_UNLOCK -> foregroundServiceTimer
}
startForeground(notificationId, builder.build(), foregroundType)
} else {
startForeground(notificationId, builder.build())
}
}
protected fun defineTimerJob(builder: NotificationCompat.Builder,
type: NotificationServiceType,
timeoutMilliseconds: Long,
actionAfterASecond: (() -> Unit)? = null,
actionEnd: () -> Unit) {
mTimerJob?.cancel()
mTimerJob = CoroutineScope(Dispatchers.Main).launch {
if (timeoutMilliseconds > 0) {
val timeoutInSeconds = timeoutMilliseconds / 1000L
for (currentTime in timeoutInSeconds downTo 0) {
var startInstant = Instant.now().millis
var currentTime = timeoutMilliseconds
while (currentTime >= 0) {
// Reset the timer if needed
if (mReset) {
mReset = false
startInstant = Instant.now().millis
currentTime = timeoutMilliseconds
}
// Update every second
actionAfterASecond?.invoke()
builder.setProgress(100,
(currentTime * 100 / timeoutInSeconds).toInt(),
false)
startForeground(notificationId, builder.build())
(currentTime * 100 / timeoutMilliseconds).toInt(),
false)
startForegroundCompat(notificationId, builder, type)
delay(1000)
currentTime = timeoutMilliseconds - (Instant.now().millis - startInstant)
if (currentTime <= 0) {
actionEnd()
}
@@ -103,6 +152,10 @@ abstract class NotificationService : Service() {
}
}
protected fun resetTimeJob() {
mReset = true
}
override fun onDestroy() {
mTimerJob?.cancel()
mTimerJob = null
@@ -114,5 +167,25 @@ abstract class NotificationService : Service() {
companion object {
private const val CHANNEL_ID = "com.kunzisoft.keepass.notification.channel"
private const val CHANNEL_NAME = "KeePassDX notification"
fun checkNotificationsPermission(
context: Context,
showError: Boolean = true,
action: () -> Unit
) {
if (ContextCompat.checkSelfPermission(context,
Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED) {
action.invoke()
} else {
if (showError) {
Toast.makeText(
context,
R.string.warning_copy_permission,
Toast.LENGTH_LONG
).show()
}
}
}
}
}

View File

@@ -0,0 +1,9 @@
package com.kunzisoft.keepass.services
enum class NotificationServiceType {
DATABASE_TASK,
ATTACHMENT,
CLIPBOARD,
KEYBOARD,
ADVANCED_UNLOCK
}

View File

@@ -19,35 +19,16 @@
*/
package com.kunzisoft.keepass.settings
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.widget.Toolbar
import androidx.preference.PreferenceFragmentCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
class AutofillSettingsActivity : DatabaseModeActivity() {
class AutofillSettingsActivity : ExternalSettingsActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_toolbar)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.setTitle(R.string.autofill_preference_title)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, AutofillSettingsFragment())
.commit()
}
override fun retrieveTitle(): Int {
return R.string.autofill_preference_title
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> onBackPressed()
}
return super.onOptionsItemSelected(item)
override fun retrievePreferenceFragment(): PreferenceFragmentCompat {
return AutofillSettingsFragment()
}
}

View File

@@ -0,0 +1,47 @@
package com.kunzisoft.keepass.settings
import android.os.Bundle
import android.view.MenuItem
import androidx.annotation.StringRes
import androidx.appcompat.widget.Toolbar
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
abstract class ExternalSettingsActivity : DatabaseModeActivity() {
private var lockView: FloatingActionButton? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_toolbar)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.setTitle(retrieveTitle())
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
lockView = findViewById(R.id.lock_button)
lockView?.hide()
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, retrievePreferenceFragment())
.commit()
}
}
@StringRes
abstract fun retrieveTitle(): Int
abstract fun retrievePreferenceFragment(): PreferenceFragmentCompat
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> onDatabaseBackPressed()
}
return super.onOptionsItemSelected(item)
}
}

View File

@@ -19,37 +19,17 @@
*/
package com.kunzisoft.keepass.settings
import android.os.Bundle
import androidx.appcompat.widget.Toolbar
import android.view.MenuItem
import androidx.preference.PreferenceFragmentCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
class MagikeyboardSettingsActivity : DatabaseModeActivity() {
class MagikeyboardSettingsActivity : ExternalSettingsActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_toolbar)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.setTitle(R.string.keyboard_setting_label)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, MagikeyboardSettingsFragment())
.commit()
}
override fun retrieveTitle(): Int {
return R.string.keyboard_setting_label
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> onBackPressed()
}
return super.onOptionsItemSelected(item)
override fun retrievePreferenceFragment(): PreferenceFragmentCompat {
return MagikeyboardSettingsFragment()
}
}

View File

@@ -120,49 +120,55 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
activity?.let { activity ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val autoFillEnablePreference: TwoStatePreference? = findPreference(getString(R.string.settings_autofill_enable_key))
val autofillManager = activity.getSystemService(AutofillManager::class.java)
if (autofillManager != null && autofillManager.hasEnabledAutofillServices())
autoFillEnablePreference?.isChecked = autofillManager.hasEnabledAutofillServices()
autoFillEnablePreference?.onPreferenceClickListener = object : Preference.OnPreferenceClickListener {
@RequiresApi(api = Build.VERSION_CODES.O)
override fun onPreferenceClick(preference: Preference): Boolean {
if ((preference as TwoStatePreference).isChecked) {
try {
enableService()
} catch (e: ActivityNotFoundException) {
val error = getString(R.string.error_autofill_enable_service)
preference.isChecked = false
Log.d(javaClass.name, error, e)
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
activity.getSystemService(AutofillManager::class.java)?.let { autofillManager ->
if (autofillManager.hasEnabledAutofillServices())
autoFillEnablePreference?.isChecked = autofillManager.hasEnabledAutofillServices()
autoFillEnablePreference?.onPreferenceClickListener =
object : Preference.OnPreferenceClickListener {
@RequiresApi(api = Build.VERSION_CODES.O)
override fun onPreferenceClick(preference: Preference): Boolean {
if ((preference as TwoStatePreference).isChecked) {
try {
enableService()
} catch (e: ActivityNotFoundException) {
val error =
getString(R.string.error_autofill_enable_service)
preference.isChecked = false
Log.d(javaClass.name, error, e)
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
}
} else {
disableService()
}
return false
}
} else {
disableService()
}
return false
}
@RequiresApi(api = Build.VERSION_CODES.O)
private fun disableService() {
if (autofillManager.hasEnabledAutofillServices()) {
autofillManager.disableAutofillServices()
} else {
Log.d(javaClass.name, "Autofill service already disabled.")
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
private fun disableService() {
if (autofillManager != null && autofillManager.hasEnabledAutofillServices()) {
autofillManager.disableAutofillServices()
} else {
Log.d(javaClass.name, "Autofill service already disabled.")
@RequiresApi(api = Build.VERSION_CODES.O)
@Throws(ActivityNotFoundException::class)
private fun enableService() {
if (!autofillManager.hasEnabledAutofillServices()) {
val intent =
Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
intent.data =
Uri.parse("package:com.kunzisoft.keepass.autofill.KeeAutofillService")
Log.d(javaClass.name, "Autofill enable service: intent=$intent")
startActivity(intent)
} else {
Log.d(javaClass.name, "Autofill service already enabled.")
}
}
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
@Throws(ActivityNotFoundException::class)
private fun enableService() {
if (autofillManager != null && !autofillManager.hasEnabledAutofillServices()) {
val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
intent.data = Uri.parse("package:com.kunzisoft.keepass.autofill.KeeAutofillService")
Log.d(javaClass.name, "Autofill enable service: intent=$intent")
startActivity(intent)
} else {
Log.d(javaClass.name, "Autofill service already enabled.")
}
}
}
} else {
findPreference<Preference>(getString(R.string.autofill_key))?.isVisible = false
@@ -477,13 +483,15 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
getString(R.string.setting_style_brightness_key),
getString(R.string.setting_icon_pack_choose_key),
getString(R.string.show_entry_colors_key),
getString(R.string.hide_expired_entries_key),
getString(R.string.hide_templates_key),
getString(R.string.list_entries_show_username_key),
getString(R.string.list_groups_show_number_entries_key),
getString(R.string.recursive_number_entries_key),
getString(R.string.show_otp_token_key),
getString(R.string.show_uuid_key),
getString(R.string.list_size_key),
getString(R.string.monospace_font_fields_enable_key),
getString(R.string.hide_expired_entries_key),
getString(R.string.enable_education_screens_key),
getString(R.string.reset_education_screens_key) -> {
DATABASE_PREFERENCE_CHANGED = true

View File

@@ -33,11 +33,11 @@ import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.password.PassphraseGenerator
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
import java.util.*
import java.util.Properties
object PreferencesUtil {
@@ -120,6 +120,18 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.show_entry_colors_default))
}
fun showExpiredEntries(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return ! prefs.getBoolean(context.getString(R.string.hide_expired_entries_key),
context.resources.getBoolean(R.bool.hide_expired_entries_default))
}
fun showTemplates(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return ! prefs.getBoolean(context.getString(R.string.hide_templates_key),
context.resources.getBoolean(R.bool.hide_templates_default))
}
fun hideProtectedValue(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.hide_password_key),
@@ -144,6 +156,12 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.list_groups_show_number_entries_default))
}
fun recursiveNumberEntries(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.recursive_number_entries_key),
context.resources.getBoolean(R.bool.recursive_number_entries_default))
}
fun showOTPToken(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.show_otp_token_key),
@@ -156,12 +174,6 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.show_uuid_default))
}
fun showExpiredEntries(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return ! prefs.getBoolean(context.getString(R.string.hide_expired_entries_key),
context.resources.getBoolean(R.bool.hide_expired_entries_default))
}
fun getStyle(context: Context): String {
val defaultStyleString = Stylish.defaultStyle(context)
val styleString = PreferenceManager.getDefaultSharedPreferences(context)
@@ -631,7 +643,7 @@ object PreferencesUtil {
}
fun isKeyboardSaveSearchInfoEnable(context: Context): Boolean {
if (!MagikeyboardService.activatedInSettings(context))
if (!context.isKeyboardActivatedInSettings())
return false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.keyboard_save_search_info_key),
@@ -841,15 +853,17 @@ object PreferencesUtil {
context.getString(R.string.setting_style_brightness_key) -> editor.putString(name, value)
context.getString(R.string.setting_icon_pack_choose_key) -> editor.putString(name, value)
context.getString(R.string.show_entry_colors_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.hide_expired_entries_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.hide_templates_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.hide_password_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.colorize_password_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.list_entries_show_username_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.list_groups_show_number_entries_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.recursive_number_entries_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.show_otp_token_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.show_uuid_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.list_size_key) -> editor.putString(name, value)
context.getString(R.string.monospace_font_fields_enable_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.hide_expired_entries_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.enable_education_screens_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.password_generator_length_key) -> editor.putInt(name, value.toInt())

View File

@@ -26,6 +26,7 @@ import android.os.Bundle
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
@@ -54,6 +55,7 @@ open class SettingsActivity
private var coordinatorLayout: CoordinatorLayout? = null
private var toolbar: Toolbar? = null
private var lockView: FloatingActionButton? = null
private var footer: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -62,10 +64,19 @@ open class SettingsActivity
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
toolbar = findViewById(R.id.toolbar)
lockView = findViewById(R.id.lock_button)
footer = findViewById(R.id.screenshot_mode_banner)
// To apply navigation bar with background color
/* TODO Settings nav bar
setTransparentNavigationBar {
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
}*/
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { selectedFileUri ->
// Import app properties result
// Import app settings result
try {
selectedFileUri?.let { uri ->
val appProperties = Properties()
@@ -80,11 +91,11 @@ open class SettingsActivity
}
} catch (e: Exception) {
Toast.makeText(this, R.string.error_import_app_properties, Toast.LENGTH_LONG).show()
Log.e(TAG, "Unable to import app properties", e)
Log.e(TAG, "Unable to import app settings", e)
}
}
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
// Export app properties result
// Export app settings result
try {
createdFileUri?.let { uri ->
contentResolver?.openOutputStream(uri)?.use { outputStream ->
@@ -96,7 +107,7 @@ open class SettingsActivity
}
} catch (e: Exception) {
Toast.makeText(this, R.string.error_export_app_properties, Toast.LENGTH_LONG).show()
Log.e(DatabaseLockActivity.TAG, "Unable to export app properties", e)
Log.e(DatabaseLockActivity.TAG, "Unable to export app settings", e)
}
}
@@ -107,7 +118,6 @@ open class SettingsActivity
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener {
lockAndExit()
}
@@ -166,7 +176,7 @@ open class SettingsActivity
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> onBackPressed()
android.R.id.home -> onDatabaseBackPressed()
}
return super.onOptionsItemSelected(item)
@@ -200,10 +210,10 @@ open class SettingsActivity
}
}
override fun onBackPressed() {
override fun onDatabaseBackPressed() {
// this if statement is necessary to navigate through nested and main fragments
if (supportFragmentManager.backStackEntryCount == 0) {
super.onBackPressed()
super.onDatabaseBackPressed()
} else {
supportFragmentManager.popBackStack()
}

View File

@@ -26,7 +26,7 @@ import com.kunzisoft.keepass.model.SearchInfo
class AutofillBlocklistWebDomainPreferenceDialogFragmentCompat
: AutofillBlocklistPreferenceDialogFragmentCompat() {
override fun buildSearchInfoFromString(searchInfoString: String): SearchInfo? {
override fun buildSearchInfoFromString(searchInfoString: String): SearchInfo {
val newSearchInfo = searchInfoString
// remove prefix https://
.replace(Regex("^.*://"), "")

View File

@@ -21,13 +21,11 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceDialogFragmentCompat
import com.kunzisoft.keepass.R
@@ -157,20 +155,6 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
switchElementView?.visibility = View.GONE
}
protected fun hideKeyboard(): Boolean {
context?.let {
ContextCompat.getSystemService(it, InputMethodManager::class.java)?.let { inputManager ->
activity?.currentFocus?.let { focus ->
val windowToken = focus.windowToken
if (windowToken != null) {
return inputManager.hideSoftInputFromWindow(windowToken, 0)
}
}
}
}
return false
}
fun setSwitchAction(onCheckedChange: ((isChecked: Boolean)-> Unit)?, defaultChecked: Boolean) {
switchElementView?.visibility = if (onCheckedChange == null) View.GONE else View.VISIBLE
switchElementView?.isChecked = defaultChecked

View File

@@ -29,6 +29,7 @@ import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.util.Log
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
@@ -45,9 +46,9 @@ const val LOCK_ACTION = "com.kunzisoft.keepass.LOCK"
const val REMOVE_ENTRY_MAGIKEYBOARD_ACTION = "com.kunzisoft.keepass.REMOVE_ENTRY_MAGIKEYBOARD"
const val BACK_PREVIOUS_KEYBOARD_ACTION = "com.kunzisoft.keepass.BACK_PREVIOUS_KEYBOARD"
class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
class LockReceiver(private var lockAction: () -> Unit) : BroadcastReceiver() {
var mLockPendingIntent: PendingIntent? = null
private var mLockPendingIntent: PendingIntent? = null
var backToPreviousKeyboardAction: (() -> Unit)? = null
override fun onReceive(context: Context, intent: Intent) {
@@ -60,7 +61,7 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
}
Intent.ACTION_SCREEN_OFF -> {
if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(context)) {
mLockPendingIntent = PendingIntent.getBroadcast(context,
val lockPendingIntent = PendingIntent.getBroadcast(context,
4575,
Intent(intent).apply {
action = LOCK_ACTION
@@ -71,6 +72,7 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
0
}
)
this.mLockPendingIntent = lockPendingIntent
// Launch the effective action after a small time
val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong()
(context.getSystemService(ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
@@ -80,20 +82,20 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
alarmManager.set(
AlarmManager.RTC_WAKEUP,
first,
mLockPendingIntent
lockPendingIntent
)
} else {
alarmManager.setExact(
AlarmManager.RTC_WAKEUP,
first,
mLockPendingIntent
lockPendingIntent
)
}
} else {
alarmManager.set(
AlarmManager.RTC_WAKEUP,
first,
mLockPendingIntent
lockPendingIntent
)
}
}
@@ -120,9 +122,9 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
}
private fun cancelLockPendingIntent(context: Context) {
mLockPendingIntent?.let {
mLockPendingIntent?.let { lockPendingIntent ->
val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager?
alarmManager?.cancel(mLockPendingIntent)
alarmManager?.cancel(lockPendingIntent)
mLockPendingIntent = null
}
}
@@ -131,7 +133,7 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
fun Context.registerLockReceiver(lockReceiver: LockReceiver?,
registerKeyboardAction: Boolean = false) {
lockReceiver?.let {
registerReceiver(it, IntentFilter().apply {
ContextCompat.registerReceiver(this, it, IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_SCREEN_ON)
addAction(LOCK_ACTION)
@@ -139,7 +141,7 @@ fun Context.registerLockReceiver(lockReceiver: LockReceiver?,
addAction(REMOVE_ENTRY_MAGIKEYBOARD_ACTION)
addAction(BACK_PREVIOUS_KEYBOARD_ACTION)
}
})
}, ContextCompat.RECEIVER_EXPORTED)
}
}

View File

@@ -0,0 +1,68 @@
package com.kunzisoft.keepass.utils
import android.app.Activity
import android.content.Context
import android.inputmethodservice.InputMethodService
import android.os.Build
import android.util.Log
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.core.content.ContextCompat
object KeyboardUtil {
fun Activity.hideKeyboard(): Boolean {
ContextCompat.getSystemService(this, InputMethodManager::class.java)?.let { inputManager ->
this.currentFocus?.let { focus ->
focus.windowToken?.let {windowToken ->
return inputManager.hideSoftInputFromWindow(
windowToken, 0)
}
}
}
return false
}
fun View.hideKeyboard(): Boolean {
return ContextCompat.getSystemService(context, InputMethodManager::class.java)
?.hideSoftInputFromWindow(windowToken, 0) ?: false
}
fun View.showKeyboard() {
ContextCompat.getSystemService(context, InputMethodManager::class.java)
?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
}
fun InputMethodService.switchToPreviousKeyboard() {
var imeManager: InputMethodManager? = null
try {
imeManager = ContextCompat.getSystemService(this, InputMethodManager::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
switchToPreviousInputMethod()
} else {
@Suppress("DEPRECATION")
window.window?.let { window ->
imeManager?.switchToLastInputMethod(window.attributes.token)
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to switch to the previous IME", e)
imeManager?.showInputMethodPicker()
}
}
fun Context.showKeyboardPicker() {
ContextCompat.getSystemService(this, InputMethodManager::class.java)
?.showInputMethodPicker()
}
fun Context.isKeyboardActivatedInSettings(): Boolean {
return ContextCompat.getSystemService(this, InputMethodManager::class.java)
?.enabledInputMethodList
?.any {
it.packageName == this.packageName
} ?: false
}
private const val TAG = "KeyboardUtil"
}

View File

@@ -3,13 +3,17 @@ package com.kunzisoft.keepass.utils
import android.content.res.Resources
import androidx.core.os.ConfigurationCompat
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.model.DataDate
import java.text.DateFormat
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
object TimeUtil {
fun DateInstant.getDateTimeString(resources: Resources): String {
val locale = ConfigurationCompat.getLocales(resources.configuration)[0] ?: Locale.ROOT
val date = instant.toDate()
return when (type) {
DateInstant.Type.DATE -> DateFormat.getDateInstance(
DateFormat.MEDIUM,
@@ -26,4 +30,22 @@ object TimeUtil {
.format(date)
}
}
// https://github.com/material-components/material-components-android/issues/882#issuecomment-1111374962
// To fix UTC time in date picker
fun datePickerToDataDate(millis: Long): DataDate {
val selectedUtc = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
selectedUtc.timeInMillis = millis
val selectedLocal = Calendar.getInstance()
selectedLocal.clear()
selectedLocal.set(
selectedUtc.get(Calendar.YEAR),
selectedUtc.get(Calendar.MONTH),
selectedUtc.get(Calendar.DAY_OF_MONTH))
return DataDate(
selectedLocal.get(Calendar.YEAR),
selectedLocal.get(Calendar.MONTH) + 1,
selectedLocal.get(Calendar.DAY_OF_MONTH),
)
}
}

View File

@@ -68,6 +68,7 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
init {
inflate(context)
hideButton()
}
private fun inflate(context: Context) {

View File

@@ -1,3 +0,0 @@
package com.kunzisoft.keepass.view
data class DataTime(val hours: Int, val minutes: Int)

View File

@@ -111,7 +111,7 @@ class DateTimeEditFieldView @JvmOverloads constructor(context: Context,
mDefault
}
set(value) {
mDateTime = DateInstant(value.date, mDateTime.type)
mDateTime = DateInstant(value.instant, mDateTime.type)
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
mDateTime.getDateTimeString(resources)
} else {

View File

@@ -57,8 +57,6 @@ class DateTimeFieldView @JvmOverloads constructor(context: Context,
}
private fun assignExpiresDateText() {
val isExpires = mDateTime.isCurrentlyExpire()
// Show or not the warning icon
expiresImage.isVisible = if (mActivated) {
isExpires
@@ -100,6 +98,13 @@ class DateTimeFieldView @JvmOverloads constructor(context: Context,
mDateTime.type = value
}
var isExpirable: Boolean = false
val isExpires: Boolean
get() {
return isExpirable && mDateTime.isCurrentlyExpire()
}
override var activation: Boolean
get() {
return mActivated
@@ -128,7 +133,7 @@ class DateTimeFieldView @JvmOverloads constructor(context: Context,
mDefault
}
set(value) {
mDateTime = DateInstant(value.date, mDateTime.type)
mDateTime = DateInstant(value.instant, mDateTime.type)
assignExpiresDateText()
}

View File

@@ -29,19 +29,17 @@ import android.util.AttributeSet
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.CredentialStorage
import com.kunzisoft.keepass.utils.KeyboardUtil.showKeyboard
class MainCredentialView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
@@ -231,8 +229,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
fun focusPasswordFieldAndOpenKeyboard() {
passwordTextView.postDelayed({
passwordTextView.requestFocusFromTouch()
ContextCompat.getSystemService(context, InputMethodManager::class.java)
?.showSoftInput(passwordTextView, InputMethodManager.SHOW_IMPLICIT)
passwordTextView.showKeyboard()
}, 100)
}

View File

@@ -22,28 +22,30 @@ package com.kunzisoft.keepass.view
import android.content.Context
import android.text.Editable
import android.text.InputType
import android.text.Spannable
import android.text.SpannableString
import android.text.TextWatcher
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.TextView
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
class PassKeyView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
class PasswordEditView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) {
private var mPasswordEntropyCalculator: PasswordEntropy? = null
private val passwordInputLayout: TextInputLayout
private val passwordText: TextView
private val passwordText: EditText
private val passwordStrengthProgress: LinearProgressIndicator
private val passwordEntropy: TextView
@@ -51,38 +53,19 @@ class PassKeyView @JvmOverloads constructor(context: Context,
private var mMaxLines: Int = 3
private var mShowPassword: Boolean = false
private var mPasswordTextWatcher: MutableList<TextWatcher> = mutableListOf()
private val passwordTextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
mPasswordTextWatcher.forEach {
it.beforeTextChanged(charSequence, i, i1, i2)
}
}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
mPasswordTextWatcher.forEach {
it.onTextChanged(charSequence, i, i1, i2)
}
}
override fun afterTextChanged(editable: Editable) {
mPasswordTextWatcher.forEach {
it.afterTextChanged(editable)
}
getEntropyStrength(editable.toString())
}
}
private var mPasswordTextWatchers: MutableList<TextWatcher> = mutableListOf()
private var mPasswordTextWatcher: TextWatcher? = null
init {
context.theme.obtainStyledAttributes(
attrs,
R.styleable.PassKeyView,
R.styleable.PasswordView,
0, 0).apply {
try {
mViewHint = getString(R.styleable.PassKeyView_passKeyHint)
mViewHint = getString(R.styleable.PasswordView_passwordHint)
?: context.getString(R.string.password)
mMaxLines = getInteger(R.styleable.PassKeyView_passKeyMaxLines, mMaxLines)
mShowPassword = getBoolean(R.styleable.PassKeyView_passKeyVisible,
mMaxLines = getInteger(R.styleable.PasswordView_passwordMaxLines, mMaxLines)
mShowPassword = getBoolean(R.styleable.PasswordView_passwordVisible,
!PreferencesUtil.hideProtectedValue(context))
} finally {
recycle()
@@ -90,31 +73,53 @@ class PassKeyView @JvmOverloads constructor(context: Context,
}
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_passkey, this)
inflater?.inflate(R.layout.view_password_edit, this)
passwordInputLayout = findViewById(R.id.password_input_layout)
passwordInputLayout = findViewById(R.id.password_edit_input_layout)
passwordInputLayout?.hint = mViewHint
passwordText = findViewById(R.id.password_text)
passwordText = findViewById(R.id.password_edit_text)
if (mShowPassword) {
passwordText?.inputType = passwordText.inputType or
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
}
passwordText?.maxLines = mMaxLines
passwordText?.applyFontVisibility()
passwordText.addTextChangedListener(passwordTextWatcher)
passwordStrengthProgress = findViewById(R.id.password_strength_progress)
passwordStrengthProgress = findViewById(R.id.password_edit_strength_progress)
passwordStrengthProgress?.apply {
setIndicatorColor(PasswordEntropy.Strength.RISKY.color)
progress = 0
max = 100
}
passwordEntropy = findViewById(R.id.password_entropy)
passwordEntropy = findViewById(R.id.password_edit_entropy)
mPasswordEntropyCalculator = PasswordEntropy {
passwordText?.text?.toString()?.let { firstPassword ->
getEntropyStrength(firstPassword)
}
}
mPasswordTextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
mPasswordTextWatchers.forEach {
it.beforeTextChanged(charSequence, i, i1, i2)
}
}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
mPasswordTextWatchers.forEach {
it.onTextChanged(charSequence, i, i1, i2)
}
}
override fun afterTextChanged(editable: Editable) {
mPasswordTextWatchers.forEach {
it.afterTextChanged(editable)
}
getEntropyStrength(editable.toString())
PasswordGenerator.colorizedPassword(editable)
}
}
passwordText?.addTextChangedListener(mPasswordTextWatcher)
}
private fun getEntropyStrength(passwordText: String) {
@@ -134,11 +139,18 @@ class PassKeyView @JvmOverloads constructor(context: Context,
}
fun addTextChangedListener(textWatcher: TextWatcher) {
mPasswordTextWatcher.add(textWatcher)
mPasswordTextWatchers.add(textWatcher)
}
fun removeTextChangedListener(textWatcher: TextWatcher) {
mPasswordTextWatcher.remove(textWatcher)
mPasswordTextWatchers.remove(textWatcher)
}
private fun spannableValue(value: String): Spannable {
return if (PreferencesUtil.colorizePassword(context))
PasswordGenerator.getColorizedPassword(value)
else
SpannableString(value)
}
var passwordString: String
@@ -146,11 +158,6 @@ class PassKeyView @JvmOverloads constructor(context: Context,
return passwordText.text.toString()
}
set(value) {
val spannableString =
if (PreferencesUtil.colorizePassword(context))
PasswordGenerator.getColorizedPassword(value)
else
SpannableString(value)
passwordText.text = spannableString
passwordText.setText(spannableValue(value))
}
}

View File

@@ -0,0 +1,159 @@
/*
* Copyright 2024 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.view
import android.content.Context
import android.os.Build
import android.text.Spannable
import android.util.AttributeSet
import android.util.TypedValue
import android.widget.TextView
import androidx.core.view.ViewCompat
import androidx.core.view.setPadding
import androidx.core.widget.TextViewCompat
import androidx.core.widget.doAfterTextChanged
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
class PasswordTextEditFieldView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: TextEditFieldView(context, attrs, defStyle) {
private var mPasswordEntropyCalculator: PasswordEntropy = PasswordEntropy {
valueView.text?.toString()?.let { firstPassword ->
getEntropyStrength(firstPassword)
}
}
private var isColorizedPasswordActivated = PreferencesUtil.colorizePassword(context)
private var passwordProgressViewId = ViewCompat.generateViewId()
private var passwordEntropyViewId = ViewCompat.generateViewId()
private var mPasswordProgress = LinearProgressIndicator(context).apply {
layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT
).apply {
addRule(ALIGN_PARENT_BOTTOM)
}
setPadding(
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
1f,
context.resources.displayMetrics
).toInt()
)
setIndicatorColor(PasswordEntropy.Strength.RISKY.color)
progress = 0
max = 100
}
private val mPasswordEntropyView = TextView(context).apply {
layoutParams = LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT
).apply {
addRule(ALIGN_PARENT_BOTTOM)
}
setPadding(
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
4f,
context.resources.displayMetrics
).toInt()
)
TextViewCompat.setTextAppearance(this, R.style.KeepassDXStyle_Text_Indicator)
}
init {
buildViews()
valueView.doAfterTextChanged { editable ->
getEntropyStrength(editable.toString())
PasswordGenerator.colorizedPassword(editable)
}
addView(mPasswordProgress)
addView(mPasswordEntropyView)
}
private fun buildViews() {
mPasswordProgress.apply {
id = passwordProgressViewId
layoutParams = (layoutParams as LayoutParams?)?.also {
it.addRule(LEFT_OF, actionImageButtonId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it.addRule(START_OF, actionImageButtonId)
}
}
}
mPasswordEntropyView.apply {
id = passwordEntropyViewId
layoutParams = (layoutParams as LayoutParams?)?.also {
it.addRule(ALIGN_RIGHT, passwordProgressViewId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it.addRule(ALIGN_END, passwordProgressViewId)
}
}
}
}
private fun getEntropyStrength(passwordText: String) {
mPasswordEntropyCalculator.getEntropyStrength(passwordText) { entropyStrength ->
mPasswordProgress.apply {
post {
setIndicatorColor(entropyStrength.strength.color)
setProgressCompat(entropyStrength.estimationPercent, true)
}
}
mPasswordEntropyView.apply {
post {
text = PasswordEntropy.getStringEntropy(resources, entropyStrength.entropy)
}
}
}
}
override fun spannableValue(value: String?): Spannable? {
if (value == null)
return null
return if (isColorizedPasswordActivated)
PasswordGenerator.getColorizedPassword(value)
else
super.spannableValue(value)
}
override var label: String
get() {
return super.label
}
set(value) {
super.label = value
// Define views Ids with label value
passwordProgressViewId = "passwordProgressViewId $value".hashCode()
passwordEntropyViewId = "passwordEntropyViewId $value".hashCode()
buildViews()
}
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2024 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.view
import android.content.Context
import android.graphics.Color
import android.text.SpannableString
import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
import android.text.style.ImageSpan
import android.util.AttributeSet
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
class PasswordTextFieldView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: TextFieldView(context, attrs, defStyle) {
private var mPasswordEntropyCalculator: PasswordEntropy = PasswordEntropy {
valueView.text?.toString()?.let { firstPassword ->
getEntropyStrength(firstPassword)
}
}
private var indicatorDrawable = ContextCompat.getDrawable(
context,
R.drawable.ic_shield_white_24dp
)?.apply {
val lineHeight = labelView.lineHeight
setBounds(0,0,lineHeight, lineHeight)
DrawableCompat.setTint(this, Color.TRANSPARENT)
}
override var label: String
get() {
return labelView.text.toString().removeSuffix(ICON_STRING_SPACES)
}
set(value) {
indicatorDrawable?.let { drawable ->
val spannableString = SpannableString("$value$ICON_STRING_SPACES")
val startPosition = spannableString.split(ICON_STRING)[0].length
val endPosition = startPosition + ICON_STRING.length
spannableString
.setSpan(
ImageSpan(drawable),
startPosition,
endPosition,
SPAN_EXCLUSIVE_EXCLUSIVE
)
labelView.text = spannableString
} ?: kotlin.run {
labelView.text = value
}
}
override fun setLabel(@StringRes labelId: Int) {
label = resources.getString(labelId)
}
override var value: String
get() {
return valueView.text.toString()
}
set(value) {
val spannableString =
if (PreferencesUtil.colorizePassword(context))
PasswordGenerator.getColorizedPassword(value)
else
SpannableString(value)
valueView.text = spannableString
changeProtectedValueParameters()
}
override fun setValue(@StringRes valueId: Int) {
value = resources.getString(valueId)
}
private fun getEntropyStrength(passwordText: String) {
mPasswordEntropyCalculator.getEntropyStrength(passwordText) { entropyStrength ->
labelView.apply {
post {
val strengthColor = entropyStrength.strength.color
indicatorDrawable?.let { drawable ->
DrawableCompat.setTint(drawable, strengthColor)
}
invalidate()
}
}
}
}
companion object {
private const val ICON_STRING = "[icon]"
private const val ICON_STRING_SPACES = " $ICON_STRING"
}
}

View File

@@ -1,6 +1,5 @@
package com.kunzisoft.keepass.view
import android.app.Activity
import android.content.Context
import android.os.Build
import android.os.Parcel
@@ -11,11 +10,9 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.annotation.IdRes
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Field
@@ -27,6 +24,7 @@ import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.KeyboardUtil.hideKeyboard
import com.kunzisoft.keepass.utils.readParcelableCompat
@@ -102,8 +100,7 @@ abstract class TemplateAbstractView<
}
buildTemplateAndPopulateInfo()
clearFocus()
ContextCompat.getSystemService(context, InputMethodManager::class.java)
?.hideSoftInputFromWindow(windowToken, 0)
hideKeyboard()
}
}

View File

@@ -17,8 +17,10 @@ import com.kunzisoft.keepass.database.element.template.TemplateAttribute
import com.kunzisoft.keepass.database.element.template.TemplateAttributeAction
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
import com.kunzisoft.keepass.model.DataDate
import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.otp.OtpEntryFields
import org.joda.time.DateTime
class TemplateEditView @JvmOverloads constructor(context: Context,
@@ -112,7 +114,9 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
override fun buildLinearTextView(templateAttribute: TemplateAttribute,
field: Field): TextEditFieldView? {
return context?.let {
TextEditFieldView(it).apply {
(if (TemplateField.isStandardPasswordName(context, templateAttribute.label))
PasswordTextEditFieldView(it)
else TextEditFieldView(it)).apply {
// hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout
setProtection(field.protectedValue.isProtected)
default = templateAttribute.default
@@ -211,35 +215,31 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
val dateTimeView = getFieldViewById(viewId)
if (dateTimeView is DateTimeEditFieldView) {
dateTimeView.dateTime = DateInstant(
action.invoke(dateTimeView.dateTime).date,
dateTimeView.dateTime.type)
action.invoke(dateTimeView.dateTime).instant,
dateTimeView.dateTime.type
)
}
}
}
fun setCurrentDateTimeValue(dateMilliseconds: Long) {
fun setCurrentDateTimeValue(date: DataDate) {
// Save the date
setCurrentDateTimeSelection { instant ->
val newDateInstant = DateInstant(
DateTime(instant.date)
.withMillis(dateMilliseconds)
.toDate(), instant.type)
if (instant.type == DateInstant.Type.DATE_TIME) {
val instantTime = DateInstant(instant.date, DateInstant.Type.TIME)
setCurrentDateTimeSelection { dateInstant ->
dateInstant.setDate(date.year, date.month, date.day)
if (dateInstant.type == DateInstant.Type.DATE_TIME) {
// Trick to recall selection with time
mOnDateInstantClickListener?.invoke(instantTime)
mOnDateInstantClickListener?.invoke(
DateInstant(dateInstant.instant, DateInstant.Type.TIME)
)
}
newDateInstant
dateInstant
}
}
fun setCurrentTimeValue(time: DataTime) {
setCurrentDateTimeSelection { instant ->
DateInstant(
DateTime(instant.date)
.withHourOfDay(time.hours)
.withMinuteOfHour(time.minutes)
.toDate(), instant.type)
setCurrentDateTimeSelection { dateInstant ->
dateInstant.setTime(time.hour, time.minute)
dateInstant
}
}

View File

@@ -10,6 +10,7 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.element.template.TemplateAttribute
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
import com.kunzisoft.keepass.model.OtpModel
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
@@ -48,7 +49,9 @@ class TemplateView @JvmOverloads constructor(context: Context,
field: Field): TextFieldView? {
// Add an action icon if needed
return context?.let {
TextFieldView(it).apply {
(if (TemplateField.isStandardPasswordName(context, templateAttribute.label))
PasswordTextFieldView(it)
else TextFieldView(it)).apply {
applyFontVisibility(mFontInVisibility)
setProtection(field.protectedValue.isProtected, mHideProtectedValue)
label = templateAttribute.alias
@@ -100,13 +103,12 @@ class TemplateView @JvmOverloads constructor(context: Context,
return context?.let {
DateTimeFieldView(it).apply {
label = TemplateField.getLocalizedName(context, field.name)
val dateInstantType = templateAttribute.options.getDateFormat()
type = templateAttribute.options.getDateFormat()
isExpirable = templateAttribute.options.getExpirable()
try {
val value = field.protectedValue.toString().trim()
type = dateInstantType
activation = value.isNotEmpty()
} catch (e: Exception) {
type = dateInstantType
activation = false
}
}

View File

@@ -4,15 +4,16 @@ import android.content.Context
import android.os.Build
import android.text.InputFilter
import android.text.InputType
import android.text.Spannable
import android.text.SpannableString
import android.util.AttributeSet
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.annotation.DrawableRes
import androidx.appcompat.view.ContextThemeWrapper
import androidx.appcompat.widget.AppCompatImageButton
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
@@ -20,28 +21,27 @@ import androidx.core.view.isVisible
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
class TextEditFieldView @JvmOverloads constructor(context: Context,
open class TextEditFieldView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: RelativeLayout(context, attrs, defStyle), GenericTextFieldView {
private var labelViewId = ViewCompat.generateViewId()
private var valueViewId = ViewCompat.generateViewId()
private var actionImageButtonId = ViewCompat.generateViewId()
protected var actionImageButtonId = ViewCompat.generateViewId()
private val labelView = TextInputLayout(context).apply {
layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT)
}
private val valueView = TextInputEditText(
ContextThemeWrapper(getContext(),
R.style.KeepassDXStyle_TextInputLayout)
protected val valueView = TextInputEditText(
ContextThemeWrapper(
getContext(),
R.style.KeepassDXStyle_TextInputLayout
)
).apply {
layoutParams = LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT,
@@ -57,7 +57,10 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
maxLines = 1
}
private var actionImageButton = AppCompatImageButton(
ContextThemeWrapper(context, R.style.KeepassDXStyle_ImageButton_Simple), null, 0).apply {
ContextThemeWrapper(
context,
R.style.KeepassDXStyle_ImageButton_Simple
), null, 0).apply {
layoutParams = LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT).also {
@@ -86,10 +89,10 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
private fun buildViews() {
labelView.apply {
id = labelViewId
layoutParams = (layoutParams as LayoutParams?).also {
it?.addRule(LEFT_OF, actionImageButtonId)
layoutParams = (layoutParams as LayoutParams?)?.also {
it.addRule(LEFT_OF, actionImageButtonId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(START_OF, actionImageButtonId)
it.addRule(START_OF, actionImageButtonId)
}
}
}
@@ -123,18 +126,16 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
buildViews()
}
protected open fun spannableValue(value: String?): Spannable? {
return SpannableString(value)
}
override var value: String
get() {
return valueView.text?.toString() ?: ""
}
set(value) {
val spannableString =
if (PreferencesUtil.colorizePassword(context)
&& TemplateField.isStandardPasswordName(context, label))
PasswordGenerator.getColorizedPassword(value)
else
SpannableString(value)
valueView.setText(spannableString)
valueView.setText(spannableValue(value))
}
override var default: String = ""
@@ -145,6 +146,7 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
valueView.filters += InputFilter.LengthFilter(MAX_CHARS_LIMIT)
}
else -> {
@Suppress("KotlinConstantConditions")
val chars = if (numberChars > MAX_CHARS_LIMIT) MAX_CHARS_LIMIT else numberChars
valueView.filters += InputFilter.LengthFilter(chars)
}
@@ -164,6 +166,7 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
valueView.maxLines = MAX_LINES_LIMIT
}
else -> {
@Suppress("KotlinConstantConditions")
val lines = if (numberLines > MAX_LINES_LIMIT) MAX_LINES_LIMIT else numberLines
valueView.inputType = valueView.inputType or
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE

View File

@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.view
import android.content.Context
import android.os.Build
import android.text.InputFilter
import android.text.SpannableString
import android.text.util.Linkify
import android.util.AttributeSet
import android.util.TypedValue
@@ -38,15 +37,11 @@ import androidx.core.text.util.LinkifyCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
import com.kunzisoft.keepass.model.EntryInfo.Companion.APPLICATION_ID_FIELD_NAME
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil.openExternalApp
class TextFieldView @JvmOverloads constructor(context: Context,
open class TextFieldView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: RelativeLayout(context, attrs, defStyle), GenericTextFieldView {
@@ -56,7 +51,7 @@ class TextFieldView @JvmOverloads constructor(context: Context,
private var showButtonId = ViewCompat.generateViewId()
private var copyButtonId = ViewCompat.generateViewId()
private val labelView = AppCompatTextView(context).apply {
protected val labelView = AppCompatTextView(context).apply {
setTextAppearance(context,
R.style.KeepassDXStyle_TextAppearance_LabelTextStyle)
layoutParams = LayoutParams(
@@ -77,7 +72,7 @@ class TextFieldView @JvmOverloads constructor(context: Context,
}
}
}
private val valueView = AppCompatTextView(context).apply {
protected val valueView = AppCompatTextView(context).apply {
setTextAppearance(context,
R.style.KeepassDXStyle_TextAppearance_TextNode)
layoutParams = LayoutParams(
@@ -131,46 +126,46 @@ class TextFieldView @JvmOverloads constructor(context: Context,
private fun buildViews() {
copyButton.apply {
id = copyButtonId
layoutParams = (layoutParams as LayoutParams?).also {
it?.addRule(ALIGN_PARENT_RIGHT)
layoutParams = (layoutParams as LayoutParams?)?.also {
it.addRule(ALIGN_PARENT_RIGHT)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(ALIGN_PARENT_END)
it.addRule(ALIGN_PARENT_END)
}
}
}
showButton.apply {
id = showButtonId
layoutParams = (layoutParams as LayoutParams?).also {
layoutParams = (layoutParams as LayoutParams?)?.also {
if (copyButton.isVisible) {
it?.addRule(LEFT_OF, copyButtonId)
it.addRule(LEFT_OF, copyButtonId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(START_OF, copyButtonId)
it.addRule(START_OF, copyButtonId)
}
} else {
it?.addRule(ALIGN_PARENT_RIGHT)
it.addRule(ALIGN_PARENT_RIGHT)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(ALIGN_PARENT_END)
it.addRule(ALIGN_PARENT_END)
}
}
}
}
labelView.apply {
id = labelViewId
layoutParams = (layoutParams as LayoutParams?).also {
it?.addRule(LEFT_OF, showButtonId)
layoutParams = (layoutParams as LayoutParams?)?.also {
it.addRule(LEFT_OF, showButtonId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(START_OF, showButtonId)
it.addRule(START_OF, showButtonId)
}
}
}
valueView.apply {
id = valueViewId
layoutParams = (layoutParams as LayoutParams?).also {
it?.addRule(LEFT_OF, showButtonId)
layoutParams = (layoutParams as LayoutParams?)?.also {
it.addRule(LEFT_OF, showButtonId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(START_OF, showButtonId)
it.addRule(START_OF, showButtonId)
}
it?.addRule(BELOW, labelViewId)
it.addRule(BELOW, labelViewId)
}
}
}
@@ -188,7 +183,7 @@ class TextFieldView @JvmOverloads constructor(context: Context,
labelView.text = value
}
fun setLabel(@StringRes labelId: Int) {
open fun setLabel(@StringRes labelId: Int) {
labelView.setText(labelId)
}
@@ -197,17 +192,11 @@ class TextFieldView @JvmOverloads constructor(context: Context,
return valueView.text.toString()
}
set(value) {
val spannableString =
if (PreferencesUtil.colorizePassword(context)
&& TemplateField.isStandardPasswordName(context, label))
PasswordGenerator.getColorizedPassword(value)
else
SpannableString(value)
valueView.text = spannableString
valueView.text = value
changeProtectedValueParameters()
}
fun setValue(@StringRes valueId: Int) {
open fun setValue(@StringRes valueId: Int) {
value = resources.getString(valueId)
changeProtectedValueParameters()
}
@@ -237,7 +226,7 @@ class TextFieldView @JvmOverloads constructor(context: Context,
invalidate()
}
private fun changeProtectedValueParameters() {
protected fun changeProtectedValueParameters() {
valueView.apply {
if (showButton.isVisible) {
applyHiddenStyle(showButton.isSelected)

View File

@@ -22,9 +22,16 @@ package com.kunzisoft.keepass.view
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.app.Activity
import android.content.Context
import android.graphics.*
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.os.Build
import android.text.Selection
import android.text.Spannable
import android.text.SpannableString
@@ -43,8 +50,14 @@ import androidx.appcompat.view.menu.ActionMenuItemView
import androidx.appcompat.widget.ActionMenuView
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.google.android.material.snackbar.Snackbar
@@ -287,3 +300,88 @@ fun CollapsingToolbarLayout.changeTitleColor(color: Int) {
setExpandedTitleColor(color)
invalidate()
}
fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, applyWindowInsets: () -> Unit) {
// Only in portrait
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1
&& resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
WindowCompat.setDecorFitsSystemWindows(window, false)
window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector)
if (applyToStatusBar) {
obtainStyledAttributes(intArrayOf(R.attr.colorSurface)).apply {
window.statusBarColor = getColor(0, Color.GRAY)
recycle()
}
}
applyWindowInsets.invoke()
}
}
/**
* Apply a margin to a view to fix the window inset
*/
fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.BOTTOM) {
ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
var consumed = false
// To fix listener in API 27
if (view is ViewGroup) {
view.forEach { child ->
// Dispatch the insets to the child
val childResult = ViewCompat.dispatchApplyWindowInsets(child, windowInsets)
// If the child consumed the insets, record it
if (childResult.isConsumed) {
consumed = true
}
}
}
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
when (position) {
WindowInsetPosition.TOP -> {
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
}
}
}
WindowInsetPosition.LEGIT_TOP -> {
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = 0
}
}
}
WindowInsetPosition.BOTTOM -> {
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = insets.bottom
}
}
}
WindowInsetPosition.BOTTOM_IME -> {
val imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = if (imeHeight > 1) 0 else insets.bottom
}
}
}
WindowInsetPosition.TOP_BOTTOM_IME -> {
val imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
bottomMargin = if (imeHeight > 1) imeHeight else 0
}
}
}
}
// If any of the children consumed the insets, return an appropriate value
if (consumed) WindowInsetsCompat.CONSUMED else windowInsets
}
}
enum class WindowInsetPosition {
TOP, BOTTOM, LEGIT_TOP, BOTTOM_IME, TOP_BOTTOM_IME
}

View File

@@ -3,6 +3,7 @@ package com.kunzisoft.keepass.viewmodels
import android.app.Application
import android.net.Uri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
@@ -19,9 +20,8 @@ class DatabaseFileViewModel(application: Application) : AndroidViewModel(applica
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(application.applicationContext)
}
val isDefaultDatabase: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>()
}
private val mIsDefaultDatabase = MutableLiveData<Boolean>()
val isDefaultDatabase: LiveData<Boolean> = mIsDefaultDatabase
fun checkIfIsDefaultDatabase(databaseUri: Uri) {
IOActionTask(
@@ -30,7 +30,7 @@ class DatabaseFileViewModel(application: Application) : AndroidViewModel(applica
?.parseUri() == databaseUri)
},
{
isDefaultDatabase.value = it
mIsDefaultDatabase.value = it
}
).execute()
}
@@ -46,13 +46,12 @@ class DatabaseFileViewModel(application: Application) : AndroidViewModel(applica
).execute()
}
val databaseFileLoaded: MutableLiveData<DatabaseFile> by lazy {
MutableLiveData<DatabaseFile>()
}
private val mDatabaseFileLoaded = MutableLiveData<DatabaseFile>()
val databaseFileLoaded: LiveData<DatabaseFile> = mDatabaseFileLoaded
fun loadDatabaseFile(databaseUri: Uri) {
mFileDatabaseHistoryAction?.getDatabaseFile(databaseUri) { databaseFileRetrieved ->
databaseFileLoaded.value = databaseFileRetrieved
mDatabaseFileLoaded.value = databaseFileRetrieved
}
}
}

View File

@@ -2,6 +2,7 @@ package com.kunzisoft.keepass.viewmodels
import android.app.Application
import android.net.Uri
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import com.kunzisoft.keepass.app.App
@@ -10,8 +11,8 @@ import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.IOActionTask
import com.kunzisoft.keepass.utils.parseUri
import com.kunzisoft.keepass.utils.UriUtil.releaseUriPermission
import com.kunzisoft.keepass.utils.parseUri
class DatabaseFilesViewModel(application: Application) : AndroidViewModel(application) {
@@ -25,11 +26,29 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
MutableLiveData<DatabaseFileData>()
}
private var mDefaultDatabaseAlreadyChecked : Boolean = false
val defaultDatabase: MutableLiveData<Uri?> by lazy {
MutableLiveData<Uri?>()
}
fun checkDefaultDatabase() {
fun doForDefaultDatabase(action: (defaultDatabaseUri: Uri) -> Unit) {
if (!mDefaultDatabaseAlreadyChecked) {
mDefaultDatabaseAlreadyChecked = true
val context = getApplication<App>().applicationContext
PreferencesUtil.getDefaultDatabasePath(context)?.parseUri()?.let { databaseFileUri ->
if (FileDatabaseInfo(context, databaseFileUri).exists) {
action.invoke(databaseFileUri)
} else {
Log.e(TAG, "Unable to automatically load a non-accessible file")
}
} ?: run {
Log.i(TAG, "No default database to prepare")
}
}
}
private fun checkDefaultDatabase() {
IOActionTask(
{
PreferencesUtil.getDefaultDatabasePath(getApplication<App>().applicationContext)
@@ -149,4 +168,8 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
enum class DatabaseFileAction {
NONE, ADD, UPDATE, DELETE
}
companion object {
private val TAG = DatabaseFilesViewModel::class.java.name
}
}

View File

@@ -43,8 +43,8 @@ class KeyGeneratorViewModel: ViewModel() {
val requirePassphraseGeneration : LiveData<Void?> get() = _requirePassphraseGeneration
private val _requirePassphraseGeneration = SingleLiveEvent<Void?>()
fun setKeyGenerated(passKey: String) {
_keyGenerated.value = passKey
fun setKeyGenerated(value: String) {
_keyGenerated.value = value
}
fun validateKeyGenerated() {

View File

@@ -4,7 +4,8 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.view.DataTime
import com.kunzisoft.keepass.model.DataDate
import com.kunzisoft.keepass.model.DataTime
abstract class NodeEditViewModel : ViewModel() {
@@ -23,8 +24,8 @@ abstract class NodeEditViewModel : ViewModel() {
val requestDateTimeSelection : LiveData<DateInstant> get() = _requestDateTimeSelection
private val _requestDateTimeSelection = SingleLiveEvent<DateInstant>()
val onDateSelected : LiveData<Long> get() = _onDateSelected
private val _onDateSelected = SingleLiveEvent<Long>()
val onDateSelected : LiveData<DataDate> get() = _onDateSelected
private val _onDateSelected = SingleLiveEvent<DataDate>()
val onTimeSelected : LiveData<DataTime> get() = _onTimeSelected
private val _onTimeSelected = SingleLiveEvent<DataTime>()
@@ -57,12 +58,12 @@ abstract class NodeEditViewModel : ViewModel() {
_requestDateTimeSelection.value = dateInstant
}
fun selectDate(dateMilliseconds: Long) {
_onDateSelected.value = dateMilliseconds
fun selectDate(date: DataDate) {
_onDateSelected.value = date
}
fun selectTime(hours: Int, minutes: Int) {
_onTimeSelected.value = DataTime(hours, minutes)
fun selectTime(dataTime: DataTime) {
_onTimeSelected.value = dataTime
}
private enum class ColorRequest {

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:alpha="0.38" android:color="?attr/colorSecondary" android:state_activated="true"/>
<item android:alpha="1.00" android:color="?attr/colorSecondary" android:state_activated="false"/>
</selector>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:alpha="0.60" android:color="?attr/colorSurface"/>
</selector>

View File

@@ -8,7 +8,7 @@
<item android:state_selected="true">
<shape>
<corners android:radius="25dp" />
<solid android:color="?attr/colorSecondaryContainer"/>
<solid android:color="?attr/colorSecondary"/>
</shape>
</item>
<item android:state_selected="false">

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M11 16C11 16.6 10.6 17 10 17S9 16.6 9 16C9 15.4 9.4 15 10 15S11 15.4 11 16M20 8V20C20 21.1 19.1 22 18 22H6C4.9 22 4 21.1 4 20V4C4 2.9 4.9 2 6 2H14M18 15H12.8C12.2 13.4 10.5 12.6 9 13.2C7.4 13.8 6.6 15.5 7.2 17S9.5 19.4 11 18.8C11.9 18.5 12.5 17.8 12.8 17H14V19H16V17H18M18.5 9L13 3.5V9H18.5Z" />
</vector>

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M18,22l-0.01,-6L14,12l3.99,-4.01L18,2H6v6l4,4l-4,3.99V22H18zM8,7.5V4h8v3.5l-4,4L8,7.5z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4z"/>
</vector>

View File

@@ -1,7 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path android:fillColor="#ffffff" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
</vector>

View File

@@ -19,17 +19,15 @@
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:filterTouchesWhenObscured="true"
android:fitsSystemWindows="true">
android:filterTouchesWhenObscured="true">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/toolbar_coordinator"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintTop_toTopOf="parent">
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
@@ -95,7 +93,8 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:paddingBottom="48dp">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/history_container"
@@ -137,13 +136,28 @@
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/activity_entry_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent">
<com.google.android.material.tabs.TabLayout
android:id="@+id/entry_content_tab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="180dp"
android:background="@drawable/background_item_selection"
android:backgroundTint="@color/surface_selector"
android:layout_marginBottom="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_gravity="bottom|center_horizontal"
app:tabMode="fixed">
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:tabMode="fixed"
tools:targetApi="lollipop">
<com.google.android.material.tabs.TabItem
android:id="@+id/entry_content_tab_main"
@@ -159,24 +173,25 @@
</com.google.android.material.tabs.TabLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" />
</FrameLayout>
<include
layout="@layout/view_button_lock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|bottom" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ProgressBar
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -22,34 +22,31 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:importantForAutofill="noExcludeDescendants"
android:id="@+id/activity_entry_edit_container"
tools:targetApi="o"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:filterTouchesWhenObscured="true">
<com.kunzisoft.keepass.view.ToolbarSpecial
android:id="@+id/special_mode_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/entry_edit_coordinator_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/entry_edit_bottom_bar">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.kunzisoft.keepass.view.ToolbarSpecial
android:id="@+id/special_mode_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.appbar.AppBarLayout>
app:layout_constraintTop_toBottomOf="@+id/special_mode_view"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.core.widget.NestedScrollView
android:id="@+id/entry_edit_scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:scrollbarStyle="insideOverlay"
android:scrollbars="none"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
@@ -57,7 +54,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="@dimen/card_view_margin_vertical"
android:paddingBottom="@dimen/card_view_margin_vertical">
android:paddingBottom="128dp">
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/entry_edit_template_selector"
android:layout_width="match_parent"
@@ -77,32 +74,55 @@
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.kunzisoft.keepass.view.ToolbarAction
android:id="@+id/entry_edit_bottom_bar"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/activity_entry_edit_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
app:layout_constraintBottom_toBottomOf="parent">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/entry_edit_validate"
style="@style/KeepassDXStyle.Fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/validate"
android:src="@drawable/ic_check_white_24dp"
app:fabCustomSize="@dimen/button_small_size"
app:layout_constraintTop_toTopOf="@+id/entry_edit_bottom_bar"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/bottom_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner">
<include
layout="@layout/view_button_lock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"/>
<com.kunzisoft.keepass.view.ToolbarAction
android:id="@+id/entry_edit_bottom_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:layout_constraintBottom_toBottomOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/entry_edit_validate"
style="@style/KeepassDXStyle.Fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/validate"
android:src="@drawable/ic_check_white_24dp"
app:fabCustomSize="@dimen/button_small_size"
app:layout_constraintTop_toTopOf="@+id/entry_edit_bottom_bar"
app:layout_constraintBottom_toBottomOf="@+id/entry_edit_bottom_bar"
android:layout_marginBottom="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<include
layout="@layout/view_button_lock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<include
app:layout_constraintTop_toBottomOf="@+id/bottom_toolbar"
app:layout_constraintBottom_toBottomOf="parent"
layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ProgressBar
android:id="@+id/loading"
@@ -110,12 +130,9 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
app:layout_anchor="@+id/entry_scroll"
app:layout_anchorGravity="top|center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -68,12 +68,12 @@
android:layout_height="wrap_content"
android:textSize="32sp"
android:textStyle="bold"
android:shadowColor="#80000000"
android:shadowDx="2"
android:shadowColor="#AD000000"
android:shadowDx="0"
android:shadowDy="2"
android:shadowRadius="4"
android:shadowRadius="2"
android:paddingTop="?attr/actionBarSize"
android:textColor="@color/green_light"
android:textColor="?attr/colorPrimaryContainer"
android:gravity="center"
android:text="@string/app_name_part1"/>
<TextView
@@ -84,12 +84,12 @@
android:layout_marginLeft="2dp"
android:textSize="32sp"
android:textStyle="bold"
android:shadowColor="#80000000"
android:shadowDx="2"
android:shadowColor="#AD000000"
android:shadowDx="0"
android:shadowDy="2"
android:shadowRadius="4"
android:shadowRadius="2"
android:paddingTop="?attr/actionBarSize"
android:textColor="@color/orange"
android:textColor="?attr/colorSecondary"
android:gravity="center"
android:text="@string/app_name_part2"/>
<TextView
@@ -102,12 +102,12 @@
android:textSize="32sp"
android:textStyle="bold"
android:visibility="gone"
android:shadowColor="#80000000"
android:shadowDx="2"
android:shadowColor="#AD000000"
android:shadowDx="0"
android:shadowDy="2"
android:shadowRadius="4"
android:shadowRadius="2"
android:paddingTop="?attr/actionBarSize"
android:textColor="@color/green_lightest"
android:textColor="?attr/colorSecondaryContainer"
android:gravity="center"
android:text="@string/app_name_part3"/>
</LinearLayout>

View File

@@ -31,71 +31,78 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.kunzisoft.keepass.view.ToolbarSpecial
android:id="@+id/special_mode_view"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/activity_group_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:title="@string/app_name"
app:layout_constraintTop_toBottomOf="@+id/special_mode_view">
<TextView
android:id="@+id/database_name"
android:layout_width="wrap_content"
app:layout_constraintTop_toTopOf="parent">
<com.kunzisoft.keepass.view.ToolbarSpecial
android:id="@+id/special_mode_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginLeft="12dp"
android:singleLine="true"
tools:text="Database"
style="@style/KeepassDXStyle.Title.OnSurface" />
</com.google.android.material.appbar.MaterialToolbar>
app:layout_constraintTop_toTopOf="parent" />
<FrameLayout
android:layout_width="48dp"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
android:layout_marginLeft="48dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/special_mode_view"
app:layout_constraintBottom_toTopOf="@+id/group_coordinator">
<ImageView
android:id="@+id/database_color"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="center_vertical|start"
android:visibility="gone"
android:src="@drawable/background_rounded_square"
android:contentDescription="@string/content_description_database_color"/>
<ImageView
android:id="@+id/database_modified"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="center_vertical|start"
android:visibility="gone"
android:src="@drawable/ic_modified_white_12dp"
android:contentDescription="@string/save"/>
</FrameLayout>
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:title="@string/app_name"
app:layout_constraintTop_toBottomOf="@+id/special_mode_view">
<TextView
android:id="@+id/database_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginLeft="12dp"
android:maxLines="2"
android:ellipsize="end"
tools:text="Database"
style="@style/KeepassDXStyle.Title.OnSurface" />
</com.google.android.material.appbar.MaterialToolbar>
<FrameLayout
android:layout_width="48dp"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
android:layout_marginLeft="48dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/special_mode_view"
app:layout_constraintBottom_toBottomOf="parent">
<ImageView
android:id="@+id/database_color"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="center_vertical|start"
android:visibility="gone"
android:src="@drawable/background_rounded_square"
android:contentDescription="@string/content_description_database_color"/>
<ImageView
android:id="@+id/database_modified"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="center_vertical|start"
android:visibility="gone"
android:src="@drawable/ic_modified_white_12dp"
android:contentDescription="@string/save"/>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/group_coordinator"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/toolbar_action"
app:layout_constraintTop_toBottomOf="@+id/toolbar">
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/activity_group_header">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:layout_scrollFlags="scroll|snap|enterAlways">
<androidx.recyclerview.widget.RecyclerView
@@ -114,56 +121,65 @@
</com.google.android.material.appbar.AppBarLayout>
<RelativeLayout
android:id="@+id/node_list_container"
<FrameLayout
android:id="@+id/nodes_list_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_below="@+id/toolbar">
android:background="?android:attr/windowBackground"
android:layout_below="@+id/toolbar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<FrameLayout
android:id="@+id/nodes_list_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="@dimen/selectable_margin_vertical"
android:background="?android:attr/windowBackground" />
</RelativeLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/activity_group_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent">
<com.kunzisoft.keepass.view.ToolbarAction
android:id="@+id/toolbar_action"
android:layout_width="match_parent"
android:layout_height="64dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
<com.kunzisoft.keepass.view.AddNodeButtonView
android:id="@+id/add_node_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_anchor="@+id/node_list_container"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintEnd_toEndOf="parent"
app:layout_anchor="@+id/nodes_list_fragment_container"
app:layout_anchorGravity="end|bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.kunzisoft.keepass.view.ToolbarAction
android:id="@+id/toolbar_action"
android:layout_width="match_parent"
android:layout_height="64dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/loading"
<include
layout="@layout/view_button_lock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" />
</FrameLayout>
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintStart_toStartOf="parent" />
<include
layout="@layout/view_button_lock"
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/error_coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"/>
<ProgressBar
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
<include layout="@layout/view_screenshot_mode_banner" />
android:layout_gravity="center"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.kunzisoft.keepass.view.NavigationDatabaseView
@@ -171,7 +187,6 @@
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
style="@style/Widget.Material3.NavigationView"
android:fitsSystemWindows="true" />
style="@style/Widget.Material3.NavigationView" />
</androidx.drawerlayout.widget.DrawerLayout>

View File

@@ -67,9 +67,19 @@
android:layout_margin="12dp"
android:contentDescription="@string/about"
android:elevation="4dp"
android:src="@drawable/ic_app_white_24dp"
android:src="@drawable/ic_app_lock_white_24dp"
app:tint="?attr/colorSecondary"
android:background="@drawable/background_image"
android:backgroundTint="@color/green"/>
app:backgroundTint="?attr/colorPrimaryContainer"/>
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:layout_margin="12dp"
android:contentDescription="@string/about"
android:elevation="4dp"
android:src="@drawable/ic_app_key_white_24dp"
app:tint="?attr/colorOnPrimaryContainer"/>
</FrameLayout>
<com.google.android.material.appbar.MaterialToolbar
@@ -105,6 +115,11 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:layout_width="match_parent"
android:layout_height="116dp"
app:layout_constraintTop_toTopOf="parent"
android:background="?attr/colorSurface" />
<FrameLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -113,10 +128,6 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<View
android:layout_width="match_parent"
android:layout_height="116dp"
android:background="?attr/colorSurface" />
<com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View File

@@ -18,42 +18,45 @@
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:filterTouchesWhenObscured="true"
android:background="?android:attr/windowBackground"
android:fitsSystemWindows="true">
android:background="?android:attr/windowBackground">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/toolbar_coordinator"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner">
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" >
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/toolbar_default"
app:layout_constraintTop_toTopOf="parent"/>
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/toolbar"
layout="@layout/toolbar_default" />
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<include
layout="@layout/view_button_lock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|bottom" />
layout="@layout/view_button_lock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
app:layout_anchorGravity="bottom|start"
app:layout_dodgeInsetEdges="bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<include layout="@layout/view_screenshot_mode_banner" />
<include layout="@layout/view_screenshot_mode_banner"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -50,14 +50,14 @@
android:layout_height="wrap_content"
android:layout_margin="@dimen/card_view_padding">
<com.kunzisoft.keepass.view.PassKeyView
<com.kunzisoft.keepass.view.PasswordEditView
android:id="@+id/passphrase_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toStartOf="@+id/passphrase_copy_button"
android:layout_toLeftOf="@+id/passphrase_copy_button"
app:passKeyHint="@string/passphrase"
app:passKeyMaxLines="7"/>
app:passwordHint="@string/passphrase"
app:passwordMaxLines="7"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/passphrase_copy_button"

View File

@@ -50,7 +50,7 @@
android:layout_height="wrap_content"
android:layout_margin="@dimen/card_view_padding">
<com.kunzisoft.keepass.view.PassKeyView
<com.kunzisoft.keepass.view.PasswordEditView
android:id="@+id/password_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -207,6 +207,8 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/ignore_chars_filter_layout"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_width="0dp"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText

View File

@@ -29,7 +29,7 @@
android:scrollbarStyle="insideOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="64dp"
android:paddingBottom="120dp"
android:clipToPadding="false" />
<LinearLayout
android:id="@+id/not_found_container"

View File

@@ -72,11 +72,11 @@
android:text="@string/password"/>
<!-- Password Input -->
<com.kunzisoft.keepass.view.PassKeyView
<com.kunzisoft.keepass.view.PasswordEditView
android:id="@+id/password_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:passKeyVisible="false"/>
app:passwordVisible="false"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/password_repeat_input_layout"
android:layout_width="match_parent"
@@ -121,6 +121,15 @@
android:layout_height="wrap_content"
android:text="@string/entry_keyfile"/>
<com.google.android.material.button.MaterialButton
style="@style/KeepassDXStyle.Button.Secondary"
android:id="@+id/keyfile_generate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="end"
app:icon="@drawable/ic_file_key_white_24dp"
android:text="@string/generate_keyfile" />
<com.kunzisoft.keepass.view.KeyFileSelectionView
android:id="@+id/keyfile_selection"
android:layout_width="match_parent"

View File

@@ -23,7 +23,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="@dimen/selectable_margin_vertical"
android:paddingTop="@dimen/selectable_margin_top"
android:paddingHorizontal="@dimen/selectable_margin_horizontal">
<androidx.constraintlayout.widget.ConstraintLayout
@@ -66,9 +66,7 @@
android:layout_marginEnd="12dp"
android:layout_marginRight="12dp"
android:orientation="vertical"
android:paddingTop="4dp"
android:paddingBottom="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/node_path"
app:layout_constraintEnd_toStartOf="@+id/node_options"
app:layout_constraintLeft_toRightOf="@+id/node_icon"
app:layout_constraintRight_toLeftOf="@+id/node_options"
@@ -102,15 +100,6 @@
android:lines="1"
android:singleLine="true"
tools:text="7543A7EAB2EA7CFD1394F1615EBEB08C" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/node_path"
style="@style/KeepassDXStyle.Meta.Entry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:visibility="gone"
tools:text="Database / Group A / Group B" />
</LinearLayout>
<LinearLayout
@@ -123,9 +112,9 @@
android:layout_marginRight="12dp"
android:gravity="end"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="@+id/node_container_info"
app:layout_constraintBottom_toTopOf="@+id/node_path"
app:layout_constraintEnd_toEndOf="parent">
<LinearLayout
android:id="@+id/node_otp_container"
@@ -173,5 +162,21 @@
</LinearLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/node_path"
style="@style/KeepassDXStyle.Meta.Entry.Important"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="2dp"
android:maxLines="2"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/node_container_info"
app:layout_constraintTop_toBottomOf="@+id/node_container_info"
tools:text="Database / Group A / Group B" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -23,7 +23,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="@dimen/selectable_margin_vertical"
android:paddingTop="@dimen/selectable_margin_top"
android:paddingHorizontal="@dimen/selectable_margin_horizontal">
<RelativeLayout

View File

@@ -1,91 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
android:gravity="bottom"
android:orientation="vertical"
android:layout_marginBottom="6dp"
android:paddingTop="36dp"
android:paddingLeft="@dimen/default_margin"
android:paddingRight="@dimen/default_margin"
android:paddingBottom="@dimen/default_margin"
tools:ignore="UnusedAttribute">
<TextView
android:id="@+id/nav_database_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:paddingStart="8dp"
android:paddingLeft="8dp"
android:paddingEnd="8dp"
android:paddingRight="8dp"
style="@style/KeepassDXStyle.Text.Info.OnSurface"
android:textSize="11sp"
tools:text="version"
android:textIsSelectable="true" />
<ImageView
android:id="@+id/nav_database_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/content_description_nav_header"
app:layout_constraintTop_toBottomOf="@+id/nav_database_version"
app:layout_constraintBottom_toTopOf="@+id/nav_database_name"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="6dp"
android:layout_marginLeft="6dp"
android:layout_marginBottom="12dp"
app:srcCompat="@drawable/ic_database_white_36dp"
style="@style/KeepassDXStyle.Icon"
app:tint="?attr/colorSecondary" />
<ImageView
android:id="@+id/nav_database_modified"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/save"
android:src="@drawable/ic_modified_white_12dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/nav_database_icon"
app:layout_constraintStart_toEndOf="@+id/nav_database_icon"
app:layout_constraintTop_toTopOf="@+id/nav_database_icon" />
<ImageView
android:id="@+id/nav_database_color"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_gravity="center"
android:contentDescription="@string/content_description_database_color"
android:src="@drawable/background_icon"
app:layout_constraintBottom_toBottomOf="@+id/nav_database_name"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/nav_database_name"
style="@style/KeepassDXStyle.Title.OnSurface"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:maxLines="2"
android:text="@string/database"
android:textIsSelectable="true"
app:layout_constraintBottom_toTopOf="@+id/nav_database_path"
app:layout_constraintEnd_toStartOf="@+id/nav_database_color"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/nav_database_path"
style="@style/KeepassDXStyle.Text.Info.OnSurface"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
android:textSize="11sp"
android:text="@string/path"
android:textIsSelectable="true"/>
</androidx.constraintlayout.widget.ConstraintLayout>
android:gravity="bottom"
android:layout_margin="@dimen/default_margin">
<TextView
android:id="@+id/nav_database_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:paddingStart="8dp"
android:paddingLeft="8dp"
android:paddingEnd="8dp"
android:paddingRight="8dp"
style="@style/KeepassDXStyle.Text.Info.OnSurface"
android:textSize="11sp"
tools:text="version"
android:textIsSelectable="true" />
<ImageView
android:id="@+id/nav_database_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/content_description_nav_header"
app:layout_constraintTop_toBottomOf="@+id/nav_database_version"
app:layout_constraintBottom_toTopOf="@+id/nav_database_name"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="6dp"
android:layout_marginLeft="6dp"
android:layout_marginBottom="12dp"
app:srcCompat="@drawable/ic_database_white_36dp"
style="@style/KeepassDXStyle.Icon"
app:tint="?attr/colorSecondary" />
<ImageView
android:id="@+id/nav_database_modified"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/save"
android:src="@drawable/ic_modified_white_12dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/nav_database_icon"
app:layout_constraintStart_toEndOf="@+id/nav_database_icon"
app:layout_constraintTop_toTopOf="@+id/nav_database_icon" />
<ImageView
android:id="@+id/nav_database_color"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_gravity="center"
android:contentDescription="@string/content_description_database_color"
android:src="@drawable/background_icon"
app:layout_constraintBottom_toBottomOf="@+id/nav_database_name"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/nav_database_name"
style="@style/KeepassDXStyle.Title.OnSurface"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:maxLines="2"
android:text="@string/database"
android:textIsSelectable="true"
app:layout_constraintBottom_toTopOf="@+id/nav_database_path"
app:layout_constraintEnd_toStartOf="@+id/nav_database_color"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/nav_database_path"
style="@style/KeepassDXStyle.Text.Info.OnSurface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
android:textSize="11sp"
android:text="@string/path"
android:textIsSelectable="true"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

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