Compare commits

...

204 Commits

Author SHA1 Message Date
J-Jamet
e19ad3a8cc Merge branch 'release/4.2.1' 2025-10-24 18:16:16 +02:00
J-Jamet
51fd8a77eb fix: translations tags 2025-10-24 18:05:52 +02:00
J-Jamet
5ee0c2eb13 Merge branch 'translations' into develop 2025-10-24 18:03:58 +02:00
Hosted Weblate
6d0ef8265c Merge branch 'origin/develop' into Weblate. 2025-10-24 17:43:35 +02:00
J-Jamet
ea69d5acb2 fix: Add brave version code for Autofill #2235 2025-10-24 17:40:38 +02:00
J-Jamet
1fb9595ec3 fix: Update CHANGELOG 2025-10-24 17:21:54 +02:00
J-Jamet
88e0bd51dc fix: Dialog database action #2234 2025-10-24 17:20:02 +02:00
J-Jamet
67477cc53b fix: unused import 2025-10-24 14:51:19 +02:00
J-Jamet
d2549d61d6 fix: Autofill pending intent bypass #2238 2025-10-24 14:43:42 +02:00
J-Jamet
d6dc75961b fix: lastmodificationTime equals #2223 2025-10-24 12:47:11 +02:00
Random
f40c83812a Translated using Weblate (Italian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/it/
2025-10-23 23:02:48 +00:00
jonnysemon
b29c638d20 Translated using Weblate (Arabic)
Currently translated at 66.6% (2 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ar/
2025-10-23 23:02:46 +00:00
Priit Jõerüüt
5bb03c2eef Translated using Weblate (Estonian)
Currently translated at 94.7% (666 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-10-23 23:02:43 +00:00
J-Jamet
a76b1195e5 fix: Small bug 2025-10-22 15:36:07 +02:00
Random
64da26f42c Translated using Weblate (Italian)
Currently translated at 66.6% (2 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/it/
2025-10-22 12:03:05 +02:00
Random
ef82552a0f Translated using Weblate (Italian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2025-10-22 12:03:03 +02:00
Masowick
d61b27ccd0 Translated using Weblate (German)
Currently translated at 97.5% (686 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-10-22 12:02:43 +02:00
J-Jamet
910ba99056 fix: Remove unused comment 2025-10-21 12:42:21 +02:00
J-Jamet
3de2a9acfd fix: Update CHANGELOG 2025-10-21 12:37:40 +02:00
J-Jamet
a48dccf27a fix: Entries missing after database merge #2223 2025-10-21 12:36:23 +02:00
Ghost of Sparta
2a561fb37e Translated using Weblate (Hungarian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/hu/
2025-10-21 08:02:57 +00:00
Fjuro
e27a329ac5 Translated using Weblate (Czech)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/cs/
2025-10-21 08:02:51 +00:00
solokot
8e06a2a7cb Translated using Weblate (Russian)
Currently translated at 66.6% (2 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ru/
2025-10-21 08:02:49 +00:00
Ihor Hordiichuk
ace82852af Translated using Weblate (Ukrainian)
Currently translated at 99.8% (702 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2025-10-21 08:02:47 +00:00
Fjuro
73369974b8 Translated using Weblate (Czech)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2025-10-21 08:02:46 +00:00
J-Jamet
332eda8a7a fix: Upgrade to 4.2.1 2025-10-20 19:43:20 +02:00
J-Jamet
e5ea1e35aa fix: Magikeyboard Auto search #2233 2025-10-20 19:41:00 +02:00
Sketch6580
86aae9635a Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/zh_Hans/
2025-10-20 04:55:09 +00:00
Fjuro
db3ccae87d Translated using Weblate (Czech)
Currently translated at 66.6% (2 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/cs/
2025-10-20 04:55:02 +00:00
jonnysemon
4cec26967c Translated using Weblate (Arabic)
Currently translated at 33.3% (1 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ar/
2025-10-20 04:55:00 +00:00
Oğuz Ersen
a0368a4981 Translated using Weblate (Turkish)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/tr/
2025-10-20 04:54:59 +00:00
Priit Jõerüüt
a1c7fe1e99 Translated using Weblate (Estonian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/et/
2025-10-20 04:54:58 +00:00
solokot
bf247ddeb7 Translated using Weblate (Russian)
Currently translated at 33.3% (1 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ru/
2025-10-20 04:54:56 +00:00
Liner Seven
1d2bc0fbfb Translated using Weblate (Japanese)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ja/
2025-10-20 04:54:54 +00:00
Besnik Bleta
85a12fe4ee Translated using Weblate (Albanian)
Currently translated at 91.7% (645 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2025-10-20 04:54:36 +00:00
109247019824
a443ef996b Translated using Weblate (Bulgarian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-10-20 04:54:15 +00:00
Oğuz Ersen
c6995ad403 Translated using Weblate (Turkish)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2025-10-20 04:53:29 +00:00
Telaneo
9018807eb8 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2025-10-20 04:53:15 +00:00
Stephan Paternotte
b463106dd5 Translated using Weblate (Dutch)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-10-20 04:52:13 +00:00
Liner Seven
a23d28e1fa Translated using Weblate (Japanese)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-10-20 04:52:06 +00:00
Ghost of Sparta
a0454f42d0 Translated using Weblate (Hungarian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2025-10-20 04:51:53 +00:00
Masowick
1c2ac88f47 Translated using Weblate (German)
Currently translated at 97.2% (684 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-10-20 04:51:10 +00:00
Fjuro
11eb1bae45 Translated using Weblate (Czech)
Currently translated at 97.5% (686 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2025-10-20 04:50:53 +00:00
Hosted Weblate
089d86165a Merge branch 'origin/develop' into Weblate. 2025-10-18 12:49:16 +02:00
109247019824
6a7362ad35 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-10-18 12:49:16 +02:00
Telaneo
d2c10e2e4e Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.1% (697 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2025-10-18 12:49:15 +02:00
jonnysemon
0c20a14e67 Translated using Weblate (Arabic)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-10-18 12:49:14 +02:00
Sketch6580
acccf290de Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2025-10-18 12:49:13 +02:00
Viktor Varvaruk
6ebe0f78af Translated using Weblate (Ukrainian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2025-10-18 12:49:12 +02:00
Matthaiks
935c09ccd2 Translated using Weblate (Polish)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-10-18 12:48:22 +02:00
Stephan Paternotte
1eb10ad5bd Translated using Weblate (Dutch)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-10-18 12:48:22 +02:00
Liner Seven
ca4283151e Translated using Weblate (Japanese)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-10-18 12:48:22 +02:00
Masowick
8fb98ca4e7 Translated using Weblate (German)
Currently translated at 97.2% (684 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-10-18 12:48:21 +02:00
Fjuro
be74c9710f Translated using Weblate (Czech)
Currently translated at 93.8% (660 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2025-10-18 12:48:21 +02:00
J-Jamet
24fb3c4c30 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-10-18 12:45:46 +02:00
solokot
3bdc5fe600 Translated using Weblate (Russian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-10-17 20:32:30 +02:00
Kunzisoft
c30884d6d0 Translated using Weblate (French)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2025-10-17 20:32:30 +02:00
VfBFan
5d26c3bd09 Translated using Weblate (German)
Currently translated at 97.2% (684 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-10-17 20:32:30 +02:00
J-Jamet
02e35cf5b7 Merge branch 'celenityy-master' into develop 2025-10-17 12:36:07 +02:00
Artyom Rybakov
085aefd2b9 Translated using Weblate (Russian)
Currently translated at 33.3% (1 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ru/
2025-10-17 12:33:01 +02:00
Kunzisoft
1ea5b7a50c Translated using Weblate (French)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/fr/
2025-10-17 12:33:00 +02:00
Kunzisoft
6cba96dd42 Translated using Weblate (French)
Currently translated at 99.7% (701 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2025-10-17 12:32:45 +02:00
Kunzisoft
46238a76bc Translated using Weblate (English)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2025-10-17 12:32:42 +02:00
Wilker Santana da Silva
d5a9b664a1 Translated using Weblate (English)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2025-10-17 12:32:42 +02:00
J-Jamet
6fdc4504d5 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-10-17 12:30:13 +02:00
J-Jamet
8a7c411a35 fix: title and description 2025-10-17 12:25:34 +02:00
Hosted Weblate
5ff9d5fa2f Merge branch 'origin/develop' into Weblate. 2025-10-17 11:35:46 +02:00
109247019824
bb0f3c80d3 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-10-17 11:35:45 +02:00
jonnysemon
597d9c8274 Translated using Weblate (Arabic)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-10-17 11:35:45 +02:00
Viktor Varvaruk
4dd8c06fd2 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2025-10-17 11:35:44 +02:00
Wilker Santana da Silva
72c66b3cd9 Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.1% (676 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2025-10-17 11:35:43 +02:00
Liner Seven
c2223afa6f Translated using Weblate (Japanese)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-10-17 11:35:42 +02:00
J-Jamet
d338d1340f fix: App title and description 2025-10-17 11:33:06 +02:00
celenity
ed4423666b Add support for IronFox Nightly
Signed-off-by: celenity <celenity@celenity.dev>
2025-10-16 21:16:06 -04:00
Sketch6580
d21fe662ff Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/zh_Hans/
2025-10-16 12:52:52 +02:00
solokot
4da1c5bd92 Translated using Weblate (Russian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-10-16 12:52:52 +02:00
Ghost of Sparta
18c18605fb Translated using Weblate (Hungarian)
Currently translated at 96.1% (676 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2025-10-16 12:52:52 +02:00
VfBFan
988cb1a8d0 Translated using Weblate (German)
Currently translated at 97.2% (684 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-10-16 12:52:51 +02:00
Milo Ivir
b6e01767e0 Translated using Weblate (Croatian)
Currently translated at 0.0% (0 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/hr/
2025-10-16 08:35:10 +02:00
Matthaiks
5414854e9c Translated using Weblate (Polish)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/pl/
2025-10-16 08:35:10 +02:00
Martin Milchevski
ae7f0732c6 Translated using Weblate (Macedonian)
Currently translated at 0.0% (0 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/mk/
2025-10-16 08:35:10 +02:00
Stephan Paternotte
d49d33fe3a Translated using Weblate (Dutch)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/nl/
2025-10-16 08:35:09 +02:00
Fjuro
5e7fc2d468 Translated using Weblate (Czech)
Currently translated at 0.0% (0 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/cs/
2025-10-16 08:35:09 +02:00
jonnysemon
0d26e6a870 Translated using Weblate (Arabic)
Currently translated at 0.0% (0 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ar/
2025-10-16 08:35:09 +02:00
Priit Jõerüüt
dd92f9ceb6 Translated using Weblate (Estonian)
Currently translated at 0.0% (0 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/et/
2025-10-16 08:35:08 +02:00
Liner Seven
ff9239b9c4 Translated using Weblate (Japanese)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ja/
2025-10-16 08:35:08 +02:00
Anonymous
319d35e485 Translated using Weblate (Japanese)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ja/
2025-10-16 08:35:08 +02:00
VfBFan
28e65a4601 Translated using Weblate (German)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/de/
2025-10-16 08:35:07 +02:00
Masowick
eb626e5bfe Translated using Weblate (German)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/de/
2025-10-16 08:35:07 +02:00
VfBFan
e1decf9a23 Translated using Weblate (German)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/de/
2025-10-16 08:35:07 +02:00
109247019824
fff0e84b95 Translated using Weblate (Bulgarian)
Currently translated at 96.7% (680 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-10-16 08:35:06 +02:00
Sketch6580
d73a7004b1 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2025-10-16 08:35:04 +02:00
大王叫我来巡山
f71061e835 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2025-10-16 08:35:04 +02:00
Matthaiks
b2d25cc512 Translated using Weblate (Polish)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-10-16 08:35:03 +02:00
Stephan Paternotte
4d54b56c1d Translated using Weblate (Dutch)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-10-16 08:35:02 +02:00
Liner Seven
c764c6afff Translated using Weblate (Japanese)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-10-16 08:35:02 +02:00
Sylvain Pichon
87b97a3849 Translated using Weblate (French)
Currently translated at 95.0% (668 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2025-10-16 08:35:01 +02:00
TKF
5e6db44476 Translated using Weblate (French)
Currently translated at 95.0% (668 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2025-10-16 08:35:01 +02:00
VfBFan
8615fa817f Translated using Weblate (German)
Currently translated at 95.3% (670 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-10-16 08:35:00 +02:00
Masowick
2f891bacd3 Translated using Weblate (German)
Currently translated at 95.3% (670 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-10-16 08:35:00 +02:00
J-Jamet
0d8a426df4 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-10-16 00:08:01 +02:00
J-Jamet
c952eb4415 Merge branch 'master' into develop 2025-10-15 23:59:04 +02:00
J-Jamet
2fd53b9416 fix: Update README.md 2025-10-15 23:58:49 +02:00
J-Jamet
244ca08890 Merge tag '4.2.0' into develop
4.2.0
2025-10-15 23:30:49 +02:00
J-Jamet
208f1e97d5 Merge branch 'release/4.2.0' 2025-10-15 23:30:40 +02:00
J-Jamet
e4e0628e20 fix: Upgrade CHANGELOG 2025-10-15 23:22:41 +02:00
J-Jamet
f60f31771f fix: Upgrade to 4.2.0 2025-10-15 23:16:45 +02:00
J-Jamet
ff6367bac4 fix: Change screens 2025-10-15 23:08:16 +02:00
J-Jamet
540e72812e fix: Passkey browser signature #2213 2025-10-15 21:15:10 +02:00
J-Jamet
5fe4af8e9d fix: Passkey multiple instance #2215 2025-10-15 20:09:27 +02:00
J-Jamet
ae42ab43b7 fix: Passkey multiple instance #2215 2025-10-15 19:37:56 +02:00
J-Jamet
c463055971 fix: Passkey back #2215 2025-10-15 15:46:26 +02:00
J-Jamet
1849dca81d fix: Form filling auto search #2204 2025-10-15 15:14:30 +02:00
J-Jamet
b3dd3dcfb5 fix: Toast "Usage parameter is null"
#2214
2025-10-14 15:36:08 +02:00
J-Jamet
fef88ff270 feat: Add KPEX_PASSKEY_FLAG_BE and KPEX_PASSKEY_FLAG_BS flags #2212 2025-10-14 14:21:52 +02:00
J-Jamet
f1f7dd1e6c fix: Upgrade to 4.2.0beta04 2025-10-14 14:12:01 +02:00
wheremygit
409f290e33 Translated using Weblate (Marathi)
Currently translated at 1.9% (13 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/mr/
2025-10-14 10:07:25 +02:00
J-Jamet
96c3af097a fix: Callback after registration 2025-10-13 14:57:13 +02:00
J-Jamet
4fe6b2e115 fix: Multiple validation 2025-10-13 11:48:12 +02:00
J-Jamet
cc936b9304 fix: Database Info 2025-10-13 11:08:56 +02:00
J-Jamet
e7f2a22583 fix: Hardware key dialog 2025-10-13 10:52:25 +02:00
J-Jamet
4bf905ecda fix: Hardware key #2196 2025-10-13 10:33:04 +02:00
J-Jamet
f8d80525d9 fix: Hardware key #2196 2025-10-13 10:32:43 +02:00
Linerly
7ce6092270 Translated using Weblate (Indonesian)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2025-10-12 07:15:04 +02:00
J-Jamet
65857596a6 fix: Multiple launch instance 2025-10-10 13:52:08 +02:00
J-Jamet
e6253336bd fix: Remove intent data 2025-10-09 20:29:46 +02:00
J-Jamet
e5595a3275 fix: Database workflow 2025-10-09 19:41:36 +02:00
J-Jamet
366e8bf1d7 fix: IconCompat 2025-10-09 18:39:59 +02:00
J-Jamet
fa63265599 fix: null database 2025-10-09 15:13:58 +02:00
J-Jamet
755e0ea9a5 fix: Database State 2025-10-09 14:31:03 +02:00
Random
a819f2f8a8 Translated using Weblate (Italian)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2025-10-09 08:07:29 +00:00
mrkaato0
c92da0a72f Translated using Weblate (Finnish)
Currently translated at 62.5% (415 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fi/
2025-10-09 08:07:27 +00:00
Fjuro
524963dbd8 Translated using Weblate (Czech)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2025-10-09 08:07:25 +00:00
J-Jamet
50b1ac388e fix: Complete refactoring of database action 2025-10-08 15:53:41 +02:00
J-Jamet
51c62034df fix: Refactoring database as flow 2025-10-08 12:11:41 +02:00
J-Jamet
e4d0cd89c6 fix: Small refactoring 2025-10-08 11:40:25 +02:00
Priit Jõerüüt
bfe50fa985 Translated using Weblate (Estonian)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-10-07 18:02:24 +02:00
Besnik Bleta
3d798e6585 Translated using Weblate (Albanian)
Currently translated at 97.7% (649 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2025-10-07 18:02:23 +02:00
109247019824
068c59ac98 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-10-07 18:02:22 +02:00
Oğuz Ersen
34ec94a0c3 Translated using Weblate (Turkish)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2025-10-07 18:02:18 +02:00
Telaneo
576a355342 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2025-10-07 18:02:18 +02:00
jonnysemon
aa19f11699 Translated using Weblate (Arabic)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-10-07 18:02:17 +02:00
大王叫我来巡山
2fb4dff46d Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2025-10-07 18:02:15 +02:00
solokot
e6cf3f12a5 Translated using Weblate (Russian)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-10-07 18:02:14 +02:00
Matthaiks
ca94ce86ba Translated using Weblate (Polish)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-10-07 18:02:12 +02:00
Stephan Paternotte
dea6b25bb4 Translated using Weblate (Dutch)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-10-07 18:02:11 +02:00
Liner Seven
c48f64d331 Translated using Weblate (Japanese)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-10-07 18:02:09 +02:00
Ghost of Sparta
5e3a504c1f Translated using Weblate (Hungarian)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2025-10-07 18:02:07 +02:00
Masowick
b9b7d7b2db Translated using Weblate (German)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-10-07 18:02:03 +02:00
J-Jamet
e085d5d277 fix: Remove unused code 2025-10-07 13:56:42 +02:00
J-Jamet
05336e93a0 fix: Special Characters #2180 2025-10-07 13:56:27 +02:00
J-Jamet
90b3b56893 feat: Warning when overwriting existing passkey #2124 2025-10-07 13:27:46 +02:00
J-Jamet
02c514272e feat: OTP tag #2122 2025-10-07 11:56:14 +02:00
J-Jamet
989e47ed12 Merge branch 'develop' into release/4.2.0 2025-10-06 17:38:54 +02:00
J-Jamet
1caf132558 Merge tag '4.1.9' into develop
4.1.9
2025-10-06 17:27:35 +02:00
J-Jamet
1b98bd740c Merge branch 'release/4.1.9' 2025-10-06 17:27:26 +02:00
J-Jamet
5adeb5cde0 fix: Tags 2025-10-06 17:20:59 +02:00
J-Jamet
b949d5d861 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-10-06 17:18:20 +02:00
J-Jamet
b4264a30a4 fix: Update description 2025-10-06 17:16:57 +02:00
J-Jamet
cf799c0f68 fix: Update to 4.1.9 2025-10-06 17:14:10 +02:00
J-Jamet
97f0ca519b fix: Killed service #2201 2025-10-06 16:59:42 +02:00
J-Jamet
cf4047b701 Merge branch 'chenxiaolong-landscape-insets' into develop 2025-10-06 13:58:39 +02:00
J-Jamet
40608a3eb5 Merge branch 'landscape-insets' of github.com:chenxiaolong/KeePassDX into chenxiaolong-landscape-insets 2025-10-06 13:58:20 +02:00
J-Jamet
99cb50d031 fix: Bug report title 2025-10-06 12:47:30 +02:00
Oğuz Ersen
b0d0c35241 Translated using Weblate (Turkish)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/tr/
2025-10-05 18:02:07 +00:00
Mekyla Credo
6044c93a4a Translated using Weblate (Filipino)
Currently translated at 46.5% (309 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fil/
2025-10-05 18:02:05 +00:00
Oğuz Ersen
b544b5d54d Translated using Weblate (Turkish)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2025-10-05 18:02:02 +00:00
Andrew Gunnerson
852378e484 Simplify inset logic, fix landscape mode, fix cutout overlapping
The commit primarily fixes a few overlapping issues caused by the window
inset handling. Previously, there were two main issues:

* Because setTransparentNavigationBar() checked for portrait mode, the
  inset logic never executed in landscape mode. This caused the app to
  overlap the status bar and navigation bar.

* The inset logic did not have handling for displayCutout insets. In
  landscape mode, this would cause the app to overlap the notch or
  camera hole punch area on phones.

In addition to fixing those issues, this commit simplifies the inset
logic a bit:

* applyWindowInsets() now accepts an EnumSet of WindowInsetPosition to
  avoid needing to duplicate logic for the various position
  combinations.

* Insets are now applied to the main container in the layout instead of
  individual elements where possible. This eliminates the need for the
  previous manual IME height handling logic in BOTTOM_IME vs
  TOP_BOTTOM_IME (for avoiding the insets being applied twice).

* Since insets are now applied to the main layout container,
  applyWindowInsets() now takes systemBars, displayCutout, and ime all
  into consideration. This should avoid all possible overlapping.

* Add support for using padding instead of margins for insets. This is
  used for GroupActivity's navigation drawer, where Material design
  intends for the drawer background to be drawn behind system bars.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-10-04 17:24:24 -04:00
Alonso González Chaves
711a344860 Translated using Weblate (Spanish)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/es/
2025-10-04 14:02:05 +02:00
Alonso González Chaves
72087c7e5c Translated using Weblate (Spanish)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2025-10-04 14:02:01 +02:00
J-Jamet
a337de3679 fix: Small refactoring 2025-10-01 15:41:49 +02:00
J-Jamet
75b37f5a9f fix: Settings 2025-09-30 15:29:10 +02:00
J-Jamet
075f54b286 feat: passkey selection after close database setting #2187 2025-09-30 15:19:05 +02:00
J-Jamet
e07cbc2e14 fix: Default backup state 2025-09-30 13:19:25 +02:00
J-Jamet
ac29b7bac7 fix: Launch immediately 2025-09-30 13:19:07 +02:00
J-Jamet
b9129cb941 fix: Update CHANGELOG 2025-09-29 19:40:28 +02:00
J-Jamet
6957fcd81a fix: Small bugs 2025-09-29 18:39:14 +02:00
J-Jamet
cfe56fc055 fix: Small error 2025-09-29 00:04:19 +02:00
J-Jamet
6f3e065ad1 fix: Back UI 2025-09-28 23:49:08 +02:00
J-Jamet
abfa7a3f47 fix: Registration and webDomain coroutine 2025-09-28 23:24:37 +02:00
J-Jamet
dd0d85e54e fix: Autofill refactoring 2025-09-26 21:42:22 +02:00
J-Jamet
76c20263f7 fix: Refactoring type mode call 2025-09-25 20:50:14 +02:00
J-Jamet
e447388611 fix: Refactoring activity launcher 2025-09-25 16:32:25 +02:00
J-Jamet
1bfec67c02 fix: Empty save parameter 2025-09-25 14:26:15 +02:00
J-Jamet
45041216d6 fix: Autofill recognition and ask to save disabled by default 2025-09-25 13:54:01 +02:00
J-Jamet
e075e9018c fix: Refactoring result launcher 2025-09-25 12:42:40 +02:00
J-Jamet
eed304ec40 fix: Search settings #2112 #2181 2025-09-24 22:36:50 +02:00
J-Jamet
5bcbbac97f fix: Save mode as registration mode 2025-09-24 19:25:24 +02:00
J-Jamet
ea4750fc11 fix: Change store description 2025-09-24 16:43:44 +02:00
J-Jamet
5037821529 Revert "fix: Backup eligibility default #2150"
This reverts commit 1d4e1687cf.
2025-09-24 14:33:43 +02:00
J-Jamet
3a4c88f19a fix: Empty database name #2159 2025-09-24 14:27:57 +02:00
J-Jamet
e960a8e169 fix: Upgrade to 4.2.0beta03 2025-09-24 14:14:29 +02:00
J-Jamet
1d4e1687cf fix: Backup eligibility default #2150 2025-09-24 14:13:49 +02:00
J-Jamet
033fa95285 Merge branch 'develop' into release/4.2.0 2025-09-24 13:21:22 +02:00
J-Jamet
f17d211fbd fix: Change appearance summary #2171 2025-09-24 13:19:57 +02:00
J-Jamet
cae69e7572 fix: Update CHANGELOG 2025-09-18 21:05:26 +02:00
Xo
ae903ad236 Translated using Weblate (Hebrew)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-09-15 09:02:00 +00:00
Milo Ivir
7c3a15ce79 Translated using Weblate (Croatian)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2025-09-12 16:01:59 +00:00
shinebrillant
b609d4e182 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/zh_Hant/
2025-09-11 13:02:01 +00:00
Artyom Rybakov
e8ecf28f7c Translated using Weblate (Russian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ru/
2025-09-10 14:21:47 +02:00
shinebrillant
3d5adbfc01 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2025-09-10 14:21:46 +02:00
Matthaiks
72bfc50703 Translated using Weblate (Polish)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-09-10 14:21:46 +02:00
Matthaiks
a60e2e780d Translated using Weblate (Polish)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-09-09 12:01:58 +00:00
Retrial
9210851765 Translated using Weblate (Greek)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2025-09-09 12:01:56 +00:00
232 changed files with 5884 additions and 4091 deletions

View File

@@ -1,4 +1,4 @@
name: Bug Report
name: Bug report
description: Report a bug.
labels: ["bug"]
body:

View File

@@ -1,5 +1,24 @@
KeePassDX(4.2.1)
* Fix Magikeyboard autosearch #2233
* Fix database merge #2223
* Fix dialog database action #2234
* Fix autofill selection #2238 #2235
* Small fixes
KeePassDX(4.2.0)
* Passkeys management #1421 #2097 (Thx @cali-95)
* Passkeys management #1421 #2097 (@cali-95)
* Confirm usage of passkey #2165 #2124
* Dialog to manage missing signature #2152 #2155 #2161 #2160
* Capture error #2159 #2215
* Change Passkey Backup Eligibility & Backup State #2135 #2150 #2212
* Search settings #2112 #2181 #2187 #2204
* Autofill refactoring #765 #2196
* Small fixes #2157 #2164 #2171 #2122 #2180 #2209 #2214
KeePassDX(4.1.9)
* Fix landscape UI #2198 #2200 (@chenxiaolong)
* Fix start loop and flash screen #2201
* Small fixes
KeePassDX(4.1.8)
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)

View File

@@ -6,19 +6,21 @@
### Features
- Create database files / entries and groups.
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
- **Passkeys** for authentication and **local storage of private keys**.
- **Biometric recognition** for fast unlocking (fingerprint / face unlock / …).
- **One-Time Password** management (HOTP / TOTP) for two-factor authentication (2FA).
- **Autofill** for easy form filling with passwords.
- **Magikeyboard** to efficiently fill in any field.
- Create **encrypted database files**.
- Organisation of credentials by **entry** and in **group** trees.
- Allows opening and **copying URI / URL fields quickly**.
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
- Material design with **themes**.
- **Auto-Fill** and Integration.
- Field filling **keyboard**.
- Dynamic **templates**
- Dynamic **templates** for each type of entry.
- **History** of each entry.
- Precise management of **settings**.
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
- Material design with **themes**.
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
- Code written in **native languages** (Kotlin / Java / JNI / C).
KeePassDX is **open source** and **ad-free**.

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass"
minSdkVersion 19
targetSdkVersion 35
versionCode = 142
versionName = "4.2.0beta02"
versionCode = 146
versionName = "4.2.1"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"

View File

@@ -48,6 +48,18 @@
]
}
},
{
"type": "android",
"info": {
"package_name": "org.ironfoxoss.ironfox.nightly",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C5:E2:91:B5:A5:71:F9:C8:CD:9A:97:99:C2:C9:4E:02:EC:97:03:94:88:93:F2:CA:75:6D:67:B9:42:04:F9:04"
}
]
}
},
{
"type": "android",
"info": {

View File

@@ -178,18 +178,22 @@
<activity
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
<activity
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
android:theme="@style/Theme.Transparent" />
android:name="com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity"
android:theme="@style/Theme.Transparent"
android:exported="false"
android:excludeFromRecents="true" />
<activity
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden"
android:excludeFromRecents="true"/>
android:exported="false"
android:excludeFromRecents="true" />
<activity
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
android:theme="@style/Theme.Transparent"
android:launchMode="singleInstance"
android:exported="true">
android:exported="true"
android:excludeFromRecents="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
@@ -208,8 +212,8 @@
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden"
android:excludeFromRecents="true"
android:exported="false"
android:excludeFromRecents="true"
tools:targetApi="upside_down_cake" />
<service
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"

View File

@@ -79,11 +79,13 @@ 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.EnumSet
import java.util.UUID
class EntryActivity : DatabaseLockActivity() {
private var footer: ViewGroup? = null
private var container: View? = null
private var coordinatorLayout: CoordinatorLayout? = null
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var appBarLayout: AppBarLayout? = null
@@ -135,6 +137,7 @@ class EntryActivity : DatabaseLockActivity() {
// Get views
footer = findViewById(R.id.activity_entry_footer)
container = findViewById(R.id.activity_entry_container)
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
appBarLayout = findViewById(R.id.app_bar)
@@ -150,8 +153,12 @@ class EntryActivity : DatabaseLockActivity() {
setTransparentNavigationBar {
// To fix margin with API 27
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
container?.applyWindowInsets(EnumSet.of(
WindowInsetPosition.TOP_MARGINS,
WindowInsetPosition.BOTTOM_MARGINS,
WindowInsetPosition.START_MARGINS,
WindowInsetPosition.END_MARGINS,
))
}
// Empty title
@@ -305,11 +312,11 @@ class EntryActivity : DatabaseLockActivity() {
mEntryViewModel.historySelected.observe(this) { historySelected ->
mDatabase?.let { database ->
launch(
this,
database,
historySelected.nodeId,
historySelected.historyPosition,
mEntryActivityResultLauncher
activity = this,
database = database,
entryId = historySelected.nodeId,
historyPosition = historySelected.historyPosition,
activityResultLauncher = mEntryActivityResultLauncher
)
}
}
@@ -323,9 +330,8 @@ class EntryActivity : DatabaseLockActivity() {
return coordinatorLayout
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mEntryViewModel.loadDatabase(database)
}
@@ -471,11 +477,12 @@ class EntryActivity : DatabaseLockActivity() {
R.id.menu_edit -> {
mDatabase?.let { database ->
mMainEntryId?.let { entryId ->
EntryEditActivity.launchToUpdate(
this,
database,
entryId,
mEntryActivityResultLauncher
EntryEditActivity.launch(
activity = this,
database = database,
registrationType = EntryEditActivity.RegistrationType.UPDATE,
nodeId = entryId,
activityResultLauncher = mEntryActivityResultLauncher
)
}
}
@@ -513,7 +520,7 @@ class EntryActivity : DatabaseLockActivity() {
// Transit data in previous Activity after an update
Intent().apply {
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
setResult(Activity.RESULT_OK, this)
setResult(RESULT_OK, this)
}
super.finish()
}
@@ -527,34 +534,22 @@ class EntryActivity : DatabaseLockActivity() {
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
/**
* Open standard Entry activity
* Open standard or history Entry activity
*/
fun launch(activity: Activity,
database: ContextualDatabase,
entryId: NodeId<UUID>,
activityResultLauncher: ActivityResultLauncher<Intent>) {
fun launch(
activity: Activity,
database: ContextualDatabase,
entryId: NodeId<UUID>,
historyPosition: Int? = null,
activityResultLauncher: ActivityResultLauncher<Intent>
) {
if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
activityResultLauncher.launch(intent)
}
}
}
/**
* Open history Entry activity
*/
fun launch(activity: Activity,
database: ContextualDatabase,
entryId: NodeId<UUID>,
historyPosition: Int,
activityResultLauncher: ActivityResultLauncher<Intent>) {
if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
historyPosition?.let {
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
}
activityResultLauncher.launch(intent)
}
}

View File

@@ -36,14 +36,14 @@ import android.widget.Spinner
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.timepicker.MaterialTimePicker
@@ -59,10 +59,10 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildSpecialModeResponseAndSetResult
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
@@ -101,6 +101,8 @@ import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
import kotlinx.coroutines.launch
import java.util.EnumSet
import java.util.UUID
class EntryEditActivity : DatabaseLockActivity(),
@@ -155,9 +157,6 @@ class EntryEditActivity : DatabaseLockActivity(),
}
}
// To ask data lost only one time
private var backPressedAlreadyApproved = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_entry_edit)
@@ -181,8 +180,12 @@ class EntryEditActivity : DatabaseLockActivity(),
// To apply fit window with transparency
setTransparentNavigationBar(applyToStatusBar = true) {
container?.applyWindowInsets(WindowInsetPosition.TOP_BOTTOM_IME)
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM_IME)
container?.applyWindowInsets(EnumSet.of(
WindowInsetPosition.TOP_MARGINS,
WindowInsetPosition.BOTTOM_MARGINS,
WindowInsetPosition.START_MARGINS,
WindowInsetPosition.END_MARGINS,
))
}
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
@@ -206,8 +209,8 @@ class EntryEditActivity : DatabaseLockActivity(),
mDatabase,
entryId,
parentId,
EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent),
EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
intent.retrieveRegisterInfo()
?: intent.retrieveSearchInfo()?.toRegisterInfo()
)
// To retrieve attachment
@@ -374,30 +377,30 @@ class EntryEditActivity : DatabaseLockActivity(),
} ?: run {
updateEntry(entrySave.oldEntry, entrySave.newEntry)
}
}
// Don't wait for saving if it's to provide autofill
mDatabase?.let { database ->
EntrySelectionHelper.doSpecialAction(
intent = intent,
defaultAction = {},
searchAction = {},
saveAction = {},
keyboardSelectionAction = {
entryValidatedForKeyboardSelection(database, entrySave.newEntry)
},
autofillSelectionAction = { _, _ ->
entryValidatedForAutofillSelection(database, entrySave.newEntry)
},
autofillRegistrationAction = {
entryValidatedForAutofillRegistration(entrySave.newEntry)
},
passkeySelectionAction = {
entryValidatedForPasskeySelection(database, entrySave.newEntry)
},
passkeyRegistrationAction = {
entryValidatedForPasskeyRegistration(database, entrySave.newEntry)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mEntryEditViewModel.uiState.collect { uiState ->
when (uiState) {
EntryEditViewModel.UIState.Loading -> {}
EntryEditViewModel.UIState.ShowOverwriteMessage -> {
if (mEntryEditViewModel.warningOverwriteDataAlreadyApproved.not()) {
AlertDialog.Builder(this@EntryEditActivity)
.setTitle(R.string.warning_overwrite_data_title)
.setMessage(R.string.warning_overwrite_data_description)
.setNegativeButton(android.R.string.cancel) { _, _ ->
mEntryEditViewModel.backPressedAlreadyApproved = true
onCancelSpecialMode()
}
.setPositiveButton(android.R.string.ok) { _, _ ->
mEntryEditViewModel.warningOverwriteDataAlreadyApproved = true
}
.create().show()
}
}
}
)
}
}
}
}
@@ -410,13 +413,13 @@ class EntryEditActivity : DatabaseLockActivity(),
return true
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mAllowCustomFields = database?.allowEntryCustomFields() == true
mAllowOTP = database?.allowOTP == true
mEntryEditViewModel.loadDatabase(database)
mAllowCustomFields = database.allowEntryCustomFields() == true
mAllowOTP = database.allowOTP == true
mEntryEditViewModel.loadTemplateEntry(database)
mTemplatesSelectorAdapter?.apply {
iconDrawableFactory = mDatabase?.iconDrawableFactory
iconDrawableFactory = database.iconDrawableFactory
notifyDataSetChanged()
}
}
@@ -427,6 +430,7 @@ class EntryEditActivity : DatabaseLockActivity(),
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
mEntryEditViewModel.unlockAction()
when (actionTask) {
ACTION_DATABASE_CREATE_ENTRY_TASK,
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
@@ -442,23 +446,27 @@ class EntryEditActivity : DatabaseLockActivity(),
searchAction = {
// Nothing when search retrieved
},
saveAction = {
entryValidatedForSave(entry)
selectionAction = { intentSender, typeMode, searchInfo ->
when(typeMode) {
TypeMode.DEFAULT -> {}
TypeMode.MAGIKEYBOARD ->
entryValidatedForKeyboardSelection(database, entry)
TypeMode.PASSKEY ->
entryValidatedForPasskey(database, entry)
TypeMode.AUTOFILL ->
entryValidatedForAutofill(database, entry)
}
},
keyboardSelectionAction = {
entryValidatedForKeyboardSelection(database, entry)
},
autofillSelectionAction = { _, _ ->
entryValidatedForAutofillSelection(database, entry)
},
autofillRegistrationAction = {
entryValidatedForAutofillRegistration(entry)
},
passkeySelectionAction = {
entryValidatedForPasskeySelection(database, entry)
},
passkeyRegistrationAction = {
entryValidatedForPasskeyRegistration(database, entry)
registrationAction = { _, typeMode, _ ->
when(typeMode) {
TypeMode.DEFAULT ->
entryValidatedForSave(entry)
TypeMode.MAGIKEYBOARD -> {}
TypeMode.PASSKEY ->
entryValidatedForPasskey(database, entry)
TypeMode.AUTOFILL ->
entryValidatedForAutofill(database, entry)
}
}
)
}
@@ -477,46 +485,26 @@ class EntryEditActivity : DatabaseLockActivity(),
}
private fun entryValidatedForKeyboardSelection(database: ContextualDatabase, entry: Entry) {
// Populate Magikeyboard with entry
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
this,
entry.getEntryInfo(database)
// Build Magikeyboard response with the entry selected
this.buildSpecialModeResponseAndSetResult(
entryInfo = entry.getEntryInfo(database),
extras = buildEntryResult(entry)
)
onValidateSpecialMode()
// Don't keep activity history for entry edition
finishForEntryResult(entry)
}
private fun entryValidatedForAutofillSelection(database: ContextualDatabase, entry: Entry) {
private fun entryValidatedForAutofill(database: ContextualDatabase, entry: Entry) {
// Build Autofill response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity,
database,
entry.getEntryInfo(database))
}
onValidateSpecialMode()
}
private fun entryValidatedForPasskeySelection(database: ContextualDatabase, entry: Entry) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
this.buildPasskeyResponseAndSetResult(
entryInfo = entry.getEntryInfo(database)
this.buildSpecialModeResponseAndSetResult(
entryInfo = entry.getEntryInfo(database),
extras = buildEntryResult(entry)
)
}
onValidateSpecialMode()
}
private fun entryValidatedForAutofillRegistration(entry: Entry) {
//if (isIntentSender()) {
// TODO Autofill Callback #765
//}
onValidateSpecialMode()
if (!isIntentSender()) {
finishForEntryResult(entry)
}
}
private fun entryValidatedForPasskeyRegistration(database: ContextualDatabase, entry: Entry) {
private fun entryValidatedForPasskey(database: ContextualDatabase, entry: Entry) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
this.buildPasskeyResponseAndSetResult(
entryInfo = entry.getEntryInfo(database),
@@ -757,13 +745,13 @@ class EntryEditActivity : DatabaseLockActivity(),
}
private fun onApprovedBackPressed(approved: () -> Unit) {
if (!backPressedAlreadyApproved) {
if (mEntryEditViewModel.backPressedAlreadyApproved.not()) {
AlertDialog.Builder(this)
.setMessage(R.string.discard_changes)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.discard) { _, _ ->
mAttachmentFileBinderManager?.stopUploadAllAttachments()
backPressedAlreadyApproved = true
mEntryEditViewModel.backPressedAlreadyApproved = true
approved.invoke()
}.create().show()
} else {
@@ -783,7 +771,7 @@ class EntryEditActivity : DatabaseLockActivity(),
val bundle = buildEntryResult(entry)
val intentEntry = Intent()
intentEntry.putExtras(bundle)
setResult(Activity.RESULT_OK, intentEntry)
setResult(RESULT_OK, intentEntry)
super.finish()
} catch (e: Exception) {
// Exception when parcelable can't be done
@@ -791,6 +779,10 @@ class EntryEditActivity : DatabaseLockActivity(),
}
}
enum class RegistrationType {
UPDATE, CREATE
}
companion object {
private val TAG = EntryEditActivity::class.java.name
@@ -800,23 +792,12 @@ class EntryEditActivity : DatabaseLockActivity(),
const val KEY_PARENT = "parent"
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
fun registerForEntryResult(fragment: Fragment,
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
entryAddedOrUpdatedListener.invoke(
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
)
} else {
entryAddedOrUpdatedListener.invoke(null)
}
}
}
fun registerForEntryResult(activity: FragmentActivity,
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
fun registerForEntryResult(
activity: FragmentActivity,
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit
): ActivityResultLauncher<Intent> {
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
if (result.resultCode == RESULT_OK) {
entryAddedOrUpdatedListener.invoke(
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
)
@@ -827,176 +808,72 @@ class EntryEditActivity : DatabaseLockActivity(),
}
/**
* Launch EntryEditActivity to update an existing entry by his [entryId]
* Launch EntryEditActivity to update an existing entry or to add a new entry in an existing group
*/
fun launchToUpdate(activity: Activity,
database: ContextualDatabase,
entryId: NodeId<UUID>,
activityResultLauncher: ActivityResultLauncher<Intent>) {
fun launch(
activity: Activity,
database: ContextualDatabase,
registrationType: RegistrationType,
nodeId: NodeId<*>,
activityResultLauncher: ActivityResultLauncher<Intent>
) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
when (registrationType) {
RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
}
activityResultLauncher.launch(intent)
}
}
}
/**
* Launch EntryEditActivity to add a new entry in an existent group
* Launch EntryEditActivity to add a new entry in special selection
*/
fun launchToCreate(activity: Activity,
database: ContextualDatabase,
groupId: NodeId<*>,
activityResultLauncher: ActivityResultLauncher<Intent>) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
activityResultLauncher.launch(intent)
}
}
}
fun launchToUpdateForSave(context: Context,
database: ContextualDatabase,
entryId: NodeId<UUID>,
searchInfo: SearchInfo) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
EntrySelectionHelper.startActivityForSaveModeResult(
context,
intent,
searchInfo
)
}
}
}
fun launchToCreateForSave(context: Context,
database: ContextualDatabase,
groupId: NodeId<*>,
searchInfo: SearchInfo) {
fun launchForSelection(
context: Context,
database: ContextualDatabase,
typeMode: TypeMode,
groupId: NodeId<*>,
searchInfo: SearchInfo? = null,
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
EntrySelectionHelper.startActivityForSaveModeResult(
context,
intent,
searchInfo
EntrySelectionHelper.startActivityForSelectionModeResult(
context = context,
intent = intent,
typeMode = typeMode,
searchInfo = searchInfo,
activityResultLauncher = activityResultLauncher
)
}
}
}
/**
* Launch EntryEditActivity to add a new entry in keyboard selection
* Launch EntryEditActivity to update an updated entry or register a new entry (from autofill)
*/
fun launchForKeyboardSelectionResult(context: Context,
database: ContextualDatabase,
groupId: NodeId<*>,
searchInfo: SearchInfo? = null) {
fun launchForRegistration(
context: Context,
database: ContextualDatabase,
nodeId: NodeId<*>,
registerInfo: RegisterInfo? = null,
typeMode: TypeMode,
registrationType: RegistrationType,
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
context,
intent,
searchInfo
)
}
}
}
/**
* Launch EntryEditActivity to add a new entry in autofill selection
*/
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: AppCompatActivity,
database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
groupId: NodeId<*>,
searchInfo: SearchInfo? = null) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
activity,
intent,
activityResultLauncher,
autofillComponent,
searchInfo
)
}
}
}
/**
* Launch EntryEditActivity to add a new passkey entry
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
fun launchForPasskeySelectionResult(context: Context,
database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>?,
groupId: NodeId<*>,
searchInfo: SearchInfo? = null) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
context,
intent,
activityResultLauncher,
searchInfo
)
}
}
}
/**
* Launch EntryEditActivity to register an updated entry (from autofill)
*/
fun launchToUpdateForRegistration(context: Context,
database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>?,
entryId: NodeId<UUID>,
registerInfo: RegisterInfo?,
typeMode: TypeMode) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
EntrySelectionHelper.startActivityForRegistrationModeResult(
context,
activityResultLauncher,
intent,
registerInfo,
typeMode
)
}
}
}
/**
* Launch EntryEditActivity to register a new entry (from autofill)
*/
fun launchToCreateForRegistration(context: Context,
database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>?,
groupId: NodeId<*>,
registerInfo: RegisterInfo? = null,
typeMode: TypeMode) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
when (registrationType) {
RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
}
EntrySelectionHelper.startActivityForRegistrationModeResult(
context,
activityResultLauncher,

View File

@@ -19,7 +19,6 @@
*/
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
@@ -33,8 +32,6 @@ import android.view.MenuItem
import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
@@ -50,10 +47,8 @@ import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
@@ -99,9 +94,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
private var mExternalFileHelper: ExternalFileHelper? = null
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.buildActivityResultLauncher()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -131,7 +123,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let {
launchPasswordActivityWithPath(uri)
launchMainCredentialActivityWithPath(uri)
}
}
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
@@ -160,7 +152,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
launchPasswordActivity(
launchMainCredentialActivity(
databaseFileUri,
fileDatabaseHistoryEntityToOpen.keyFileUri,
fileDatabaseHistoryEntityToOpen.hardwareKey
@@ -179,7 +171,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
// Load default database the first time
databaseFilesViewModel.doForDefaultDatabase { databaseFileUri ->
launchPasswordActivityWithPath(databaseFileUri)
launchMainCredentialActivityWithPath(databaseFileUri)
}
// Retrieve the database URI provided by file manager after an orientation change
@@ -224,11 +216,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
if (database != null) {
launchGroupActivityIfLoaded(database)
}
override fun onDatabaseRetrieved(database: ContextualDatabase) {
launchGroupActivityIfLoaded(database)
}
override fun onDatabaseActionFinished(
@@ -236,8 +225,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
if (result.isSuccess) {
// Update list
when (actionTask) {
@@ -287,17 +274,58 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
}
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
MainCredentialActivity.launch(this,
databaseUri,
keyFile,
hardwareKey,
{ exception ->
fileNoFoundAction(exception)
private fun launchMainCredentialActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
try {
EntrySelectionHelper.doSpecialAction(
intent = this.intent,
defaultAction = {
MainCredentialActivity.launch(
activity = this,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey
)
},
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() },
mCredentialActivityResultLauncher)
searchAction = { searchInfo ->
MainCredentialActivity.launchForSearchResult(
activity = this,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
selectionAction = { intentSenderMode, typeMode, searchInfo ->
MainCredentialActivity.launchForSelection(
activity = this,
activityResultLauncher = if (intentSenderMode)
mCredentialActivityResultLauncher else null,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
typeMode = typeMode,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
registrationAction = { intentSenderMode, typeMode, registerInfo ->
MainCredentialActivity.launchForRegistration(
activity = this,
activityResultLauncher = if (intentSenderMode)
mCredentialActivityResultLauncher else null,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
typeMode = typeMode,
registerInfo = registerInfo
)
onLaunchActivitySpecialMode()
}
)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
}
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
@@ -307,12 +335,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
{ onValidateSpecialMode() },
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() },
mCredentialActivityResultLauncher)
mCredentialActivityResultLauncher
)
}
}
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
launchPasswordActivity(databaseUri, null, null)
private fun launchMainCredentialActivityWithPath(databaseUri: Uri) {
launchMainCredentialActivity(databaseUri, null, null)
// Delete flickering for kitkat <=
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
@@ -336,10 +365,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
}
mDatabase?.let { database ->
launchGroupActivityIfLoaded(database)
}
// Show recent files if allowed
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
databaseFilesViewModel.loadListOfDatabases()
@@ -358,7 +383,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
try {
mDatabaseFileUri?.let { databaseUri ->
// Create the new database
createDatabase(databaseUri, mainCredential)
mDatabaseViewModel.createDatabase(databaseUri, mainCredential)
}
} catch (e: Exception) {
val error = getString(R.string.error_create_database_file)
@@ -442,71 +467,35 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
* -------------------------
*/
fun launchForSearchResult(context: Context,
searchInfo: SearchInfo) {
EntrySelectionHelper.startActivityForSearchModeResult(context,
Intent(context, FileDatabaseSelectActivity::class.java),
searchInfo)
fun launchForSearch(
context: Context,
searchInfo: SearchInfo
) {
EntrySelectionHelper.startActivityForSearchModeResult(
context = context,
intent = Intent(context, FileDatabaseSelectActivity::class.java),
searchInfo = searchInfo
)
}
/*
* -------------------------
* Save Launch
* Selection Launch
* -------------------------
*/
fun launchForSaveResult(context: Context,
searchInfo: SearchInfo) {
EntrySelectionHelper.startActivityForSaveModeResult(context,
Intent(context, FileDatabaseSelectActivity::class.java),
searchInfo)
}
/*
* -------------------------
* Keyboard Launch
* -------------------------
*/
fun launchForKeyboardSelectionResult(activity: Activity,
searchInfo: SearchInfo? = null) {
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
searchInfo)
}
/*
* -------------------------
* Autofill Launch
* -------------------------
*/
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: AppCompatActivity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null) {
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
activityResultLauncher,
autofillComponent,
searchInfo)
}
/*
* -------------------------
* Passkey Launch
* -------------------------
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
fun launchForPasskeySelectionResult(activity: Activity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
searchInfo: SearchInfo? = null) {
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
activityResultLauncher,
searchInfo
fun launchForSelection(
context: Context,
typeMode: TypeMode,
searchInfo: SearchInfo? = null,
activityResultLauncher: ActivityResultLauncher<Intent>?,
) {
EntrySelectionHelper.startActivityForSelectionModeResult(
context = context,
intent = Intent(context, FileDatabaseSelectActivity::class.java),
searchInfo = searchInfo,
typeMode = typeMode,
activityResultLauncher = activityResultLauncher
)
}
@@ -515,16 +504,18 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
* Registration Launch
* -------------------------
*/
fun launchForRegistration(context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>?,
registerInfo: RegisterInfo? = null,
typeMode: TypeMode) {
fun launchForRegistration(
context: Context,
typeMode: TypeMode,
registerInfo: RegisterInfo? = null,
activityResultLauncher: ActivityResultLauncher<Intent>?,
) {
EntrySelectionHelper.startActivityForRegistrationModeResult(
context,
activityResultLauncher,
Intent(context, FileDatabaseSelectActivity::class.java),
registerInfo,
typeMode
context = context,
intent = Intent(context, FileDatabaseSelectActivity::class.java),
registerInfo = registerInfo,
typeMode = typeMode,
activityResultLauncher = activityResultLauncher
)
}
}

View File

@@ -174,10 +174,10 @@ class IconPickerActivity : DatabaseLockActivity() {
return true
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
if (database?.allowCustomIcons == true) {
if (database.allowCustomIcons) {
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
} else {
uploadButton.visibility = View.GONE

View File

@@ -101,7 +101,7 @@ class ImageViewerActivity : DatabaseLockActivity() {
return true
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
try {
@@ -119,18 +119,16 @@ class ImageViewerActivity : DatabaseLockActivity() {
resources.displayMetrics.heightPixels * 2
)
database?.let { database ->
BinaryDatabaseManager.loadBitmap(
database,
attachment.binaryData,
mImagePreviewMaxWidth
) { bitmapLoaded ->
if (bitmapLoaded == null) {
finish()
} else {
progressView.visibility = View.GONE
imageView.setImageBitmap(bitmapLoaded)
}
BinaryDatabaseManager.loadBitmap(
database,
attachment.binaryData,
mImagePreviewMaxWidth
) { bitmapLoaded ->
if (bitmapLoaded == null) {
finish()
} else {
progressView.visibility = View.GONE
imageView.setImageBitmap(bitmapLoaded)
}
}
} ?: finish()

View File

@@ -36,8 +36,6 @@ import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.biometric.BiometricManager
import androidx.coordinatorlayout.widget.CoordinatorLayout
@@ -56,10 +54,8 @@ import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
import com.kunzisoft.keepass.biometric.deviceUnlockError
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
@@ -128,9 +124,6 @@ class MainCredentialActivity : DatabaseModeActivity() {
private var mReadOnly: Boolean = false
private var mForceReadOnly: Boolean = false
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.buildActivityResultLauncher()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -310,26 +303,20 @@ class MainCredentialActivity : DatabaseModeActivity() {
mDatabaseFileUri?.let { databaseFileUri ->
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
}
mDatabase?.let { database ->
launchGroupActivityIfLoaded(database)
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
if (database != null) {
// Trying to load another database
if (mDatabaseFileUri != null
&& database.fileUri != null
&& mDatabaseFileUri != database.fileUri) {
Toast.makeText(this,
R.string.warning_database_already_opened,
Toast.LENGTH_LONG
).show()
}
launchGroupActivityIfLoaded(database)
// Trying to load another database
if (mDatabaseFileUri != null
&& database.fileUri != null
&& mDatabaseFileUri != database.fileUri) {
Toast.makeText(this,
R.string.warning_database_already_opened,
Toast.LENGTH_LONG
).show()
}
launchGroupActivityIfLoaded(database)
}
override fun onDatabaseActionFinished(
@@ -514,10 +501,11 @@ class MainCredentialActivity : DatabaseModeActivity() {
val password = intent.getStringExtra(KEY_PASSWORD)
// Consume the intent extra password
intent.removeExtra(KEY_PASSWORD)
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
if (password != null) {
mainCredentialView?.populatePasswordTextView(password)
}
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
intent.removeExtra(KEY_LAUNCH_IMMEDIATELY)
if (launchImmediately) {
loadDatabase()
} else {
@@ -572,10 +560,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
clearCredentialsViews()
}
if (mReadOnly && (
mSpecialMode == SpecialMode.SAVE
|| mSpecialMode == SpecialMode.REGISTRATION)
) {
if (mReadOnly && mSpecialMode == SpecialMode.REGISTRATION) {
Log.e(TAG, getString(R.string.error_save_read_only))
Snackbar.make(coordinatorLayout,
R.string.error_save_read_only,
@@ -599,7 +584,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUUID: Boolean) {
loadDatabase(
mDatabaseViewModel.loadDatabase(
databaseUri,
mainCredential,
readOnly,
@@ -752,11 +737,13 @@ class MainCredentialActivity : DatabaseModeActivity() {
private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private fun buildAndLaunchIntent(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
intentBuildLauncher: (Intent) -> Unit) {
private fun buildAndLaunchIntent(
activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
intentBuildLauncher: (Intent) -> Unit
) {
val intent = Intent(activity, MainCredentialActivity::class.java)
intent.putExtra(KEY_FILENAME, databaseFile)
if (keyFile != null)
@@ -773,10 +760,12 @@ class MainCredentialActivity : DatabaseModeActivity() {
*/
@Throws(FileNotFoundException::class)
fun launch(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?) {
fun launch(
activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
activity.startActivity(intent)
}
@@ -789,245 +778,73 @@ class MainCredentialActivity : DatabaseModeActivity() {
*/
@Throws(FileNotFoundException::class)
fun launchForSearchResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo) {
fun launchForSearchResult(
activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSearchModeResult(
activity,
intent,
searchInfo)
}
}
/*
* -------------------------
* Save Launch
* -------------------------
*/
@Throws(FileNotFoundException::class)
fun launchForSaveResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSaveModeResult(
activity,
intent,
searchInfo)
}
}
/*
* -------------------------
* Keyboard Launch
* -------------------------
*/
@Throws(FileNotFoundException::class)
fun launchForKeyboardResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
activity,
intent,
searchInfo)
}
}
/*
* -------------------------
* Autofill Launch
* -------------------------
*/
@RequiresApi(api = Build.VERSION_CODES.O)
@Throws(FileNotFoundException::class)
fun launchForAutofillResult(activity: AppCompatActivity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
activity,
intent,
activityResultLauncher,
autofillComponent,
searchInfo)
}
}
/*
* -------------------------
* Passkey Launch
* -------------------------
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Throws(FileNotFoundException::class)
fun launchForPasskeyResult(activity: Activity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
activity,
intent,
activityResultLauncher,
searchInfo
context = activity,
intent = intent,
searchInfo = searchInfo
)
}
}
/*
* -------------------------
* Registration Launch
* Selection Launch
* -------------------------
*/
fun launchForRegistration(
@Throws(FileNotFoundException::class)
fun launchForSelection(
activity: Activity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
typeMode: TypeMode,
registerInfo: RegisterInfo?
searchInfo: SearchInfo?,
activityResultLauncher: ActivityResultLauncher<Intent>?
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForRegistrationModeResult(
EntrySelectionHelper.startActivityForSelectionModeResult(
context = activity,
activityResultLauncher = activityResultLauncher,
intent = intent,
typeMode = typeMode,
registerInfo = registerInfo
searchInfo = searchInfo,
activityResultLauncher = activityResultLauncher
)
}
}
/*
* -------------------------
* Global Launch
* Registration Launch
* -------------------------
*/
fun launch(activity: AppCompatActivity,
databaseUri: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit,
activityResultLauncher: ActivityResultLauncher<Intent>?) {
try {
EntrySelectionHelper.doSpecialAction(
intent = activity.intent,
defaultAction = {
launch(
activity = activity,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey
)
},
searchAction = { searchInfo ->
launchForSearchResult(
activity = activity,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
saveAction = { searchInfo ->
launchForSaveResult(
activity = activity,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
keyboardSelectionAction = { searchInfo ->
launchForKeyboardResult(
activity = activity,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
autofillSelectionAction = { searchInfo, autofillComponent ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
launchForAutofillResult(
activity = activity,
activityResultLauncher = activityResultLauncher,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
autofillComponent = autofillComponent,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
} else {
onCancelSpecialMode()
}
},
autofillRegistrationAction = { registerInfo ->
launchForRegistration(
activity = activity,
activityResultLauncher = activityResultLauncher,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
typeMode = TypeMode.AUTOFILL,
registerInfo = registerInfo
)
onLaunchActivitySpecialMode()
},
passkeySelectionAction = { searchInfo ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
launchForPasskeyResult(
activity = activity,
activityResultLauncher = activityResultLauncher,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
} else {
onCancelSpecialMode()
}
},
passkeyRegistrationAction = { registerInfo ->
launchForRegistration(
activity = activity,
activityResultLauncher = activityResultLauncher,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
typeMode = TypeMode.PASSKEY,
registerInfo = registerInfo
)
onLaunchActivitySpecialMode()
}
@Throws(FileNotFoundException::class)
fun launchForRegistration(
activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
typeMode: TypeMode,
registerInfo: RegisterInfo?,
activityResultLauncher: ActivityResultLauncher<Intent>?
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForRegistrationModeResult(
context = activity,
intent = intent,
typeMode = typeMode,
registerInfo = registerInfo,
activityResultLauncher = activityResultLauncher,
)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
}
}

View File

@@ -67,7 +67,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
}
builder.setMessage(stringBuilder)
builder.setPositiveButton(android.R.string.ok) { _, _ ->
actionDatabaseListener?.validateDatabaseChanged()
actionDatabaseListener?.onDatabaseChangeValidated()
}
return builder.create()
}
@@ -76,7 +76,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
}
interface ActionDatabaseChangedListener {
fun validateDatabaseChanged()
fun onDatabaseChangeValidated()
}
companion object {
@@ -86,9 +86,10 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
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,
readOnly: Boolean
fun getInstance(
oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
readOnly: Boolean
)
: DatabaseChangedDialogFragment {
val fragment = DatabaseChangedDialogFragment()

View File

@@ -5,6 +5,9 @@ import android.view.View
import android.view.WindowManager.LayoutParams.FLAG_SECURE
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.ContextualDatabase
@@ -12,23 +15,40 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private var mDatabase: ContextualDatabase? = null
private val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDatabaseViewModel.database.observe(this) { database ->
this.mDatabase = database
resetAppTimeoutOnTouchOrFocus()
onDatabaseRetrieved(database)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mDatabaseViewModel.actionState.collect { uiState ->
when (uiState) {
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished(
uiState.database,
uiState.actionTask,
uiState.result
)
}
else -> {}
}
}
}
}
mDatabaseViewModel.actionFinished.observe(this) { result ->
onDatabaseActionFinished(result.database, result.actionTask, result.result)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database ->
database?.let {
onDatabaseRetrieved(database)
}
}
}
}
}
@@ -52,7 +72,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
resetAppTimeoutOnTouchOrFocus()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Can be overridden by a subclass
}

View File

@@ -62,14 +62,14 @@ class GroupDialogFragment : DatabaseDialogFragment() {
private lateinit var uuidContainerView: ViewGroup
private lateinit var uuidReferenceView: TextView
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
}
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
if (database?.allowCustomSearchableGroup() == true) {
if (database.allowCustomSearchableGroup()) {
searchableLabelView.visibility = View.VISIBLE
searchableView.visibility = View.VISIBLE
} else {

View File

@@ -112,32 +112,32 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
}
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
searchableContainerView.visibility = if (database?.allowCustomSearchableGroup() == true) {
searchableContainerView.visibility = if (database.allowCustomSearchableGroup()) {
View.VISIBLE
} else {
View.GONE
}
if (database?.allowAutoType() == true) {
if (database.allowAutoType()) {
autoTypeContainerView.visibility = View.VISIBLE
} else {
autoTypeContainerView.visibility = View.GONE
}
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
tagsCompletionView.apply {
threshold = 1
setAdapter(tagsAdapter)
}
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

View File

@@ -45,10 +45,10 @@ class IconEditDialogFragment : DatabaseDialogFragment() {
private var mCustomIcon: IconImageCustom? = null
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon)
}
mCustomIcon?.let { customIcon ->
populateViewsWithCustomIcon(customIcon)

View File

@@ -35,9 +35,9 @@ import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.UriUtil.openUrl
@@ -258,8 +258,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
showEmptyPasswordConfirmationDialog()
} else if (!error
&& hardwareKey != null
&& !HardwareKeyActivity.isHardwareKeyAvailable(
requireActivity(), hardwareKey, false)
&& !HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)
) {
// show hardware driver dialog if required
error = true

View File

@@ -4,36 +4,59 @@ import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
protected var mDatabase: ContextualDatabase? = null
protected val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
protected val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
if (mDatabase == null || mDatabase != database) {
this.mDatabase = database
onDatabaseRetrieved(database)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mDatabaseViewModel.actionState.collect { uiState ->
when (uiState) {
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished(
uiState.database,
uiState.actionTask,
uiState.result
)
}
else -> {}
}
}
}
}
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result ->
onDatabaseActionFinished(result.database, result.actionTask, result.result)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database ->
database?.let {
onDatabaseRetrieved(database)
}
}
}
}
}
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
context?.let {
view?.resetAppTimeoutWhenViewTouchedOrFocused(it, mDatabase?.loaded)
view?.resetAppTimeoutWhenViewTouchedOrFocused(
context = it,
databaseLoaded = mDatabase?.loaded
)
}
}
@@ -44,8 +67,4 @@ abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
) {
// Can be overridden by a subclass
}
protected fun buildNewBinaryAttachment(): BinaryData? {
return mDatabase?.buildNewBinaryAttachment()
}
}

View File

@@ -230,7 +230,7 @@ class EntryEditFragment: DatabaseFragment() {
val attachmentToUploadUri = it.attachmentToUploadUri
val fileName = it.fileName
buildNewBinaryAttachment()?.let { binaryAttachment ->
mDatabaseViewModel.buildNewAttachment()?.let { binaryAttachment ->
val entryAttachment = Attachment(fileName, binaryAttachment)
// Ask to replace the current attachment
if ((!mAllowMultipleAttachments
@@ -273,13 +273,13 @@ class EntryEditFragment: DatabaseFragment() {
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
templateView.populateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
}
mAllowMultipleAttachments = database?.allowMultipleAttachments == true
mAllowMultipleAttachments = database.allowMultipleAttachments == true
attachmentsAdapter?.database = database
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
@@ -290,12 +290,12 @@ class EntryEditFragment: DatabaseFragment() {
}
}
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
tagsCompletionView.apply {
threshold = 1
setAdapter(tagsAdapter)
}
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
}
private fun assignEntryInfo(entryInfo: EntryInfo?) {

View File

@@ -133,7 +133,7 @@ class EntryFragment: DatabaseFragment() {
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
context?.let { context ->
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
attachmentsAdapter?.database = database

View File

@@ -36,9 +36,9 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.adapters.NodesAdapter
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum
@@ -154,46 +154,44 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
super.onDetach()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
context?.let { context ->
database?.let { database ->
mAdapter = NodesAdapter(context, database).apply {
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
override fun onNodeClick(database: ContextualDatabase, node: Node) {
if (nodeActionSelectionMode) {
if (listActionNodes.contains(node)) {
// Remove selected item if already selected
listActionNodes.remove(node)
} else {
// Add selected item if not already selected
listActionNodes.add(node)
}
nodeClickListener?.onNodeSelected(database, listActionNodes)
setActionNodes(listActionNodes)
notifyNodeChanged(node)
mAdapter = NodesAdapter(context, database).apply {
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
override fun onNodeClick(database: ContextualDatabase, node: Node) {
if (nodeActionSelectionMode) {
if (listActionNodes.contains(node)) {
// Remove selected item if already selected
listActionNodes.remove(node)
} else {
nodeClickListener?.onNodeClick(database, node)
// Add selected item if not already selected
listActionNodes.add(node)
}
nodeClickListener?.onNodeSelected(database, listActionNodes)
setActionNodes(listActionNodes)
notifyNodeChanged(node)
} else {
nodeClickListener?.onNodeClick(database, node)
}
}
override fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean {
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
// Select the first item after a long click
if (!listActionNodes.contains(node))
listActionNodes.add(node)
override fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean {
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
// Select the first item after a long click
if (!listActionNodes.contains(node))
listActionNodes.add(node)
nodeClickListener?.onNodeSelected(database, listActionNodes)
nodeClickListener?.onNodeSelected(database, listActionNodes)
setActionNodes(listActionNodes)
notifyNodeChanged(node)
activity?.hideKeyboard()
}
return true
setActionNodes(listActionNodes)
notifyNodeChanged(node)
activity?.hideKeyboard()
}
})
}
mNodesRecyclerView?.adapter = mAdapter
return true
}
})
}
mNodesRecyclerView?.adapter = mAdapter
}
}
@@ -248,7 +246,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
activity?.intent?.let {
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
specialMode = it.retrieveSpecialMode()
}
}
@@ -299,9 +297,9 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
}
}
private fun containsRecycleBin(nodes: List<Node>): Boolean {
return mDatabase?.isRecycleBinEnabled == true
&& nodes.any { it == mDatabase?.recycleBin }
private fun containsRecycleBin(database: ContextualDatabase?, nodes: List<Node>): Boolean {
return database?.isRecycleBinEnabled == true
&& nodes.any { it == database.recycleBin }
}
fun actionNodesCallback(database: ContextualDatabase,
@@ -328,7 +326,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
// Open and Edit for a single item
if (nodes.size == 1) {
// Edition
if (database.isReadOnly || containsRecycleBin(nodes)) {
if (database.isReadOnly || containsRecycleBin(database, nodes)) {
menu?.removeItem(R.id.menu_edit)
}
} else {
@@ -348,7 +346,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
}
// Deletion
if (database.isReadOnly || containsRecycleBin(nodes)) {
if (database.isReadOnly || containsRecycleBin(database, nodes)) {
menu?.removeItem(R.id.menu_delete)
}
}

View File

@@ -71,8 +71,8 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
resetAppTimeoutWhenViewFocusedOrChanged(view)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
override fun onDatabaseRetrieved(database: ContextualDatabase) {
iconPickerAdapter.iconDrawableFactory = database.iconDrawableFactory
CoroutineScope(Dispatchers.IO).launch {
val populateList = launch {

View File

@@ -48,9 +48,9 @@ class IconPickerFragment : DatabaseFragment() {
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
if (database?.allowCustomIcons == true) 2 else 1)
if (database.allowCustomIcons) 2 else 1)
viewPager.adapter = iconPickerPagerAdapter
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = when (position) {

View File

@@ -107,7 +107,7 @@ class KeyGeneratorFragment : DatabaseFragment() {
super.onDestroyView()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Nothing here
}

View File

@@ -244,7 +244,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Nothing here
}

View File

@@ -293,20 +293,22 @@ class PasswordGeneratorFragment : DatabaseFragment() {
private fun generatePassword() {
var password = ""
try {
password = PasswordGenerator(resources).generatePassword(getPasswordLength(),
uppercaseCompound.isChecked,
lowercaseCompound.isChecked,
digitsCompound.isChecked,
minusCompound.isChecked,
underlineCompound.isChecked,
spaceCompound.isChecked,
specialsCompound.isChecked,
bracketsCompound.isChecked,
extendedCompound.isChecked,
getConsiderChars(),
getIgnoreChars(),
atLeastOneCompound.isChecked,
excludeAmbiguousCompound.isChecked)
password = PasswordGenerator(resources).generatePassword(
length = getPasswordLength(),
upperCase = uppercaseCompound.isChecked,
lowerCase = lowercaseCompound.isChecked,
digits = digitsCompound.isChecked,
minus = minusCompound.isChecked,
underline = underlineCompound.isChecked,
space = spaceCompound.isChecked,
specials = specialsCompound.isChecked,
brackets = bracketsCompound.isChecked,
extended = extendedCompound.isChecked,
considerChars = getConsiderChars(),
ignoreChars = getIgnoreChars(),
atLeastOneFromEach = atLeastOneCompound.isChecked,
excludeAmbiguousChar = excludeAmbiguousCompound.isChecked
)
} catch (e: Exception) {
Log.e(TAG, "Unable to generate a password", e)
}
@@ -318,7 +320,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
super.onDestroy()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Nothing here
}

View File

@@ -1,104 +1,229 @@
package com.kunzisoft.keepass.activities.legacy
import android.net.Uri
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider.Companion.startDatabaseService
import com.kunzisoft.keepass.database.ProgressMessage
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.getBinaryDir
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
protected var mDatabase: ContextualDatabase? = null
protected val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
private val mActionDatabaseListener =
object : DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
override fun onDatabaseChangeValidated() {
mDatabaseViewModel.onDatabaseChangeValidated()
}
}
private val tempServiceParameters = mutableListOf<Pair<Bundle?, String>>()
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { _ ->
// Whether or not the user has accepted, the service can be started,
// There just won't be any notification if it's not allowed.
tempServiceParameters.removeFirstOrNull()?.let {
startDatabaseService(it.first, it.second)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDatabaseTaskProvider = DatabaseTaskProvider(this, showDatabaseDialog())
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
val databaseWasReloaded = database?.wasReloaded == true
if (databaseWasReloaded && finishActivityIfReloadRequested()) {
finish()
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) {
database?.wasReloaded = false
onDatabaseRetrieved(database)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mDatabaseViewModel.actionState.collect { uiState ->
when (uiState) {
is DatabaseViewModel.ActionState.Loading -> {}
is DatabaseViewModel.ActionState.OnDatabaseReloaded -> {
if (finishActivityIfReloadRequested()) {
finish()
}
}
is DatabaseViewModel.ActionState.OnDatabaseInfoChanged -> {
if (manageDatabaseInfo()) {
showDatabaseChangedDialog(
uiState.previousDatabaseInfo,
uiState.newDatabaseInfo,
uiState.readOnlyDatabase
)
}
}
is DatabaseViewModel.ActionState.OnDatabaseActionRequested -> {
startDatabasePermissionService(
uiState.bundle,
uiState.actionTask
)
}
is DatabaseViewModel.ActionState.OnDatabaseActionStarted -> {
showDialog(uiState.progressMessage)
}
is DatabaseViewModel.ActionState.OnDatabaseActionUpdated -> {
showDialog(uiState.progressMessage)
}
is DatabaseViewModel.ActionState.OnDatabaseActionStopped -> {
// nothing here, wait for the action to finish
}
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished(
uiState.database,
uiState.actionTask,
uiState.result
)
stopDialog()
}
}
}
}
}
mDatabaseTaskProvider?.onActionFinish = { database, actionTask, result ->
onDatabaseActionFinished(database, actionTask, result)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database ->
// Nullable function
onUnknownDatabaseRetrieved(database)
database?.let {
onDatabaseRetrieved(database)
}
}
}
}
}
protected open fun showDatabaseDialog(): Boolean {
return true
}
override fun onDestroy() {
mDatabaseTaskProvider?.destroy()
mDatabaseTaskProvider = null
mDatabase = null
super.onDestroy()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
mDatabase = database
mDatabaseViewModel.defineDatabase(database)
/**
* Nullable function to retrieve a database
*/
open fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
// optional method implementation
}
override fun onDatabaseRetrieved(database: ContextualDatabase) {
// optional method implementation
}
open fun manageDatabaseInfo(): Boolean = true
override fun onDatabaseActionFinished(
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
mDatabaseViewModel.onActionFinished(database, actionTask, result)
// optional method implementation
}
fun createDatabase(
databaseUri: Uri,
mainCredential: MainCredential
private fun startDatabasePermissionService(bundle: Bundle?, actionTask: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED
) {
startDatabaseService(bundle, actionTask)
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.POST_NOTIFICATIONS
)
) {
// it's not the first time, so the user deliberately chooses not to display the notification
startDatabaseService(bundle, actionTask)
} else {
AlertDialog.Builder(this)
.setMessage(R.string.warning_database_notification_permission)
.setNegativeButton(R.string.later) { _, _ ->
// Refuses the notification, so start the service
startDatabaseService(bundle, actionTask)
}
.setPositiveButton(R.string.ask) { _, _ ->
// Save the temp parameters to ask the permission
tempServiceParameters.add(Pair(bundle, actionTask))
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}.create().show()
}
} else {
startDatabaseService(bundle, actionTask)
}
}
private fun showDatabaseChangedDialog(
previousDatabaseInfo: SnapFileDatabaseInfo,
newDatabaseInfo: SnapFileDatabaseInfo,
readOnlyDatabase: Boolean
) {
mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential)
lifecycleScope.launch {
if (databaseChangedDialogFragment == null) {
databaseChangedDialogFragment = supportFragmentManager
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
databaseChangedDialogFragment?.actionDatabaseListener =
mActionDatabaseListener
}
if (progressTaskDialogFragment == null) {
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
previousDatabaseInfo,
newDatabaseInfo,
readOnlyDatabase
)
databaseChangedDialogFragment?.actionDatabaseListener =
mActionDatabaseListener
databaseChangedDialogFragment?.show(
supportFragmentManager,
DATABASE_CHANGED_DIALOG_TAG
)
}
}
}
fun loadDatabase(
databaseUri: Uri,
mainCredential: MainCredential,
readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean
) {
mDatabaseTaskProvider?.startDatabaseLoad(
databaseUri,
mainCredential,
readOnly,
cipherEncryptDatabase,
fixDuplicateUuid
)
private fun showDialog(progressMessage: ProgressMessage) {
lifecycleScope.launch {
if (showDatabaseDialog()) {
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = supportFragmentManager
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
}
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = ProgressTaskDialogFragment()
progressTaskDialogFragment?.show(
supportFragmentManager,
PROGRESS_TASK_DIALOG_TAG
)
}
progressTaskDialogFragment?.apply {
updateTitle(progressMessage.titleId)
updateMessage(progressMessage.messageId)
updateWarning(progressMessage.warningId)
setCancellable(progressMessage.cancelable)
}
}
}
}
protected fun closeDatabase() {
mDatabase?.clearAndClose(this.getBinaryDir())
private fun stopDialog() {
progressTaskDialogFragment?.dismissAllowingStateLoss()
progressTaskDialogFragment = null
}
override fun onResume() {
super.onResume()
mDatabaseTaskProvider?.registerProgressTask()
}
override fun onPause() {
mDatabaseTaskProvider?.unregisterProgressTask()
super.onPause()
protected open fun showDatabaseDialog(): Boolean {
return true
}
}

View File

@@ -34,7 +34,7 @@ import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
@@ -87,128 +87,44 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
deleteDatabaseNodes(nodes)
}
mDatabaseViewModel.saveDatabase.observe(this) { save ->
mDatabaseTaskProvider?.startDatabaseSave(save)
}
mDatabaseViewModel.mergeDatabase.observe(this) { save ->
mDatabaseTaskProvider?.startDatabaseMerge(save)
}
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
}
}
mDatabaseViewModel.saveName.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveDescription.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveDescription(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveDefaultUsername.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveColor.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveColor(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveCompression.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveCompression(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.removeUnlinkData.observe(this) {
mDatabaseTaskProvider?.startDatabaseRemoveUnlinkedData(it)
}
mDatabaseViewModel.saveRecycleBin.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveRecycleBin(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveTemplatesGroup.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveTemplatesGroup(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveMaxHistoryItems.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveMaxHistoryItems(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveMaxHistorySize.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveEncryption.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveEncryption(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveKeyDerivation.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveKeyDerivation(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveIterations.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveIterations(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveMemoryUsage.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveParallelism.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveParallelism(it.oldValue, it.newValue, it.save)
}
mExitLock = false
}
open fun finishActivityIfDatabaseNotLoaded(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
override fun onDatabaseRetrieved(database: ContextualDatabase) {
// End activity if database not loaded
if (finishActivityIfDatabaseNotLoaded() && (database == null || !database.loaded)) {
if (database.loaded.not())
finish()
}
// Focus view to reinitialize timeout,
// view is not necessary loaded so retry later in resume
viewToInvalidateTimeout()
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database?.loaded)
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded)
database?.let {
// check timeout
if (mTimeoutEnable) {
if (mLockReceiver == null) {
mLockReceiver = LockReceiver {
mDatabase = null
closeDatabase(database)
mExitLock = true
closeOptionsMenu()
finish()
}
registerLockReceiver(mLockReceiver)
// check timeout
if (mTimeoutEnable) {
if (mLockReceiver == null) {
mLockReceiver = LockReceiver {
closeDatabase(database)
mExitLock = true
closeOptionsMenu()
finish()
}
// After the first creation
// or If simply swipe with another application
// If the time is out -> close the Activity
TimeoutHelper.checkTimeAndLockIfTimeout(this)
// If onCreate already record time
if (!mExitLock)
TimeoutHelper.recordTime(this, database.loaded)
registerLockReceiver(mLockReceiver)
}
mDatabaseReadOnly = database.isReadOnly
mMergeDataAllowed = database.isMergeDataAllowed()
checkRegister()
// After the first creation
// or If simply swipe with another application
// If the time is out -> close the Activity
TimeoutHelper.checkTimeAndLockIfTimeout(this)
// If onCreate already record time
if (!mExitLock)
TimeoutHelper.recordTime(this, database.loaded)
}
mDatabaseReadOnly = database.isReadOnly
mMergeDataAllowed = database.isMergeDataAllowed()
checkRegister()
}
override fun finish() {
@@ -227,7 +143,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
@@ -249,24 +164,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
databaseUri: Uri?,
mainCredential: MainCredential
) {
assignDatabasePassword(databaseUri, mainCredential)
mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
}
private fun assignDatabasePassword(
databaseUri: Uri?,
mainCredential: MainCredential
) {
if (databaseUri != null) {
mDatabaseTaskProvider?.startDatabaseAssignCredential(databaseUri, mainCredential)
}
}
fun assignPassword(mainCredential: MainCredential) {
fun assignMainCredential(mainCredential: MainCredential) {
mDatabase?.let { database ->
database.fileUri?.let { databaseUri ->
// Show the progress dialog now or after dialog confirmation
if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) {
assignDatabasePassword(databaseUri, mainCredential)
mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
} else {
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
.show(supportFragmentManager, "passwordEncodingTag")
@@ -276,45 +182,51 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
fun saveDatabase() {
mDatabaseTaskProvider?.startDatabaseSave(true)
mDatabaseViewModel.saveDatabase(save = true)
}
fun saveDatabaseTo(uri: Uri) {
mDatabaseTaskProvider?.startDatabaseSave(true, uri)
mDatabaseViewModel.saveDatabase(save = true, saveToUri = uri)
}
fun mergeDatabase() {
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable)
mDatabaseViewModel.mergeDatabase(save = mAutoSaveEnable)
}
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable, uri, mainCredential)
mDatabaseViewModel.mergeDatabase(mAutoSaveEnable, uri, mainCredential)
}
fun reloadDatabase() {
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
mDatabaseTaskProvider?.startDatabaseReload(false)
}
mDatabaseViewModel.reloadDatabase(fixDuplicateUuid = false)
}
fun createEntry(newEntry: Entry,
parent: Group) {
mDatabaseTaskProvider?.startDatabaseCreateEntry(newEntry, parent, mAutoSaveEnable)
fun createEntry(
newEntry: Entry,
parent: Group
) {
mDatabaseViewModel.createEntry(newEntry, parent, mAutoSaveEnable)
}
fun updateEntry(oldEntry: Entry,
entryToUpdate: Entry) {
mDatabaseTaskProvider?.startDatabaseUpdateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
fun updateEntry(
oldEntry: Entry,
entryToUpdate: Entry
) {
mDatabaseViewModel.updateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
}
fun copyNodes(nodesToCopy: List<Node>,
newParent: Group) {
mDatabaseTaskProvider?.startDatabaseCopyNodes(nodesToCopy, newParent, mAutoSaveEnable)
fun copyNodes(
nodesToCopy: List<Node>,
newParent: Group
) {
mDatabaseViewModel.copyNodes(nodesToCopy, newParent, mAutoSaveEnable)
}
fun moveNodes(nodesToMove: List<Node>,
newParent: Group) {
mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable)
fun moveNodes(
nodesToMove: List<Node>,
newParent: Group
) {
mDatabaseViewModel.moveNodes(nodesToMove, newParent, mAutoSaveEnable)
}
private fun eachNodeRecyclable(database: ContextualDatabase, nodes: List<Node>): Boolean {
@@ -330,6 +242,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) {
// TODO Move in ViewModel
mDatabase?.let { database ->
// If recycle bin enabled, ensure it exists
if (database.isRecycleBinEnabled) {
@@ -350,11 +263,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
private fun deleteDatabaseNodes(nodes: List<Node>) {
mDatabaseTaskProvider?.startDatabaseDeleteNodes(nodes, mAutoSaveEnable)
mDatabaseViewModel.deleteNodes(nodes, mAutoSaveEnable)
}
fun createGroup(parent: Group,
groupInfo: GroupInfo?) {
fun createGroup(
parent: Group,
groupInfo: GroupInfo?
) {
// TODO Move in ViewModel
// Build the group
mDatabase?.createGroup()?.let { newGroup ->
groupInfo?.let { info ->
@@ -362,12 +278,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
// Not really needed here because added in runnable but safe
newGroup.parent = parent
mDatabaseTaskProvider?.startDatabaseCreateGroup(newGroup, parent, mAutoSaveEnable)
mDatabaseViewModel.createGroup(newGroup, parent, mAutoSaveEnable)
}
}
fun updateGroup(oldGroup: Group,
groupInfo: GroupInfo) {
fun updateGroup(
oldGroup: Group,
groupInfo: GroupInfo
) {
// TODO Move in ViewModel
// If group updated save it in the database
val updateGroup = Group(oldGroup).let { updateGroup ->
updateGroup.apply {
@@ -377,27 +296,28 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
this.setGroupInfo(groupInfo)
}
}
mDatabaseTaskProvider?.startDatabaseUpdateGroup(oldGroup, updateGroup, mAutoSaveEnable)
mDatabaseViewModel.updateGroup(oldGroup, updateGroup, mAutoSaveEnable)
}
fun restoreEntryHistory(mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int) {
mDatabaseTaskProvider
?.startDatabaseRestoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
fun restoreEntryHistory(
mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int
) {
mDatabaseViewModel.restoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
}
fun deleteEntryHistory(mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int) {
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
fun deleteEntryHistory(
mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int
) {
mDatabaseViewModel.deleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
}
private fun checkRegister() {
// If in ave or registration mode, don't allow read only
if ((mSpecialMode == SpecialMode.SAVE
|| mSpecialMode == SpecialMode.REGISTRATION)
&& mDatabaseReadOnly) {
// If in registration mode, don't allow read only
if (mSpecialMode == SpecialMode.REGISTRATION && mDatabaseReadOnly) {
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
EntrySelectionHelper.removeModesFromIntent(intent)
intent.removeModes()
finish()
}
}

View File

@@ -1,12 +1,21 @@
package com.kunzisoft.keepass.activities.legacy
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveTypeMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.model.RegisterInfo
@@ -21,10 +30,25 @@ import com.kunzisoft.keepass.view.ToolbarSpecial
abstract class DatabaseModeActivity : DatabaseActivity() {
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
private var mTypeMode: TypeMode = TypeMode.DEFAULT
protected var mTypeMode: TypeMode = TypeMode.DEFAULT
private var mToolbarSpecial: ToolbarSpecial? = null
/**
* Utility activity result launcher,
* Used recursively, close each activity with return data
*/
protected open var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
setActivityResult(
lockDatabase = false,
resultCode = it.resultCode,
data = it.data
)
}
open fun onDatabaseBackPressed() {
if (mSpecialMode != SpecialMode.DEFAULT)
onCancelSpecialMode()
@@ -50,8 +74,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
fun onLaunchActivitySpecialMode() {
if (!isIntentSender()) {
EntrySelectionHelper.removeModesFromIntent(intent)
EntrySelectionHelper.removeInfoFromIntent(intent)
intent.removeModes()
intent.removeInfo()
finish()
}
}
@@ -60,8 +84,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
if (isIntentSender()) {
super.finish()
} else {
EntrySelectionHelper.removeModesFromIntent(intent)
EntrySelectionHelper.removeInfoFromIntent(intent)
intent.removeModes()
intent.removeInfo()
if (mSpecialMode != SpecialMode.DEFAULT) {
backToTheMainAppAndFinish()
}
@@ -73,8 +97,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
// To get the app caller, only for IntentSender
onRegularBackPressed()
} else {
EntrySelectionHelper.removeModesFromIntent(intent)
EntrySelectionHelper.removeInfoFromIntent(intent)
intent.removeModes()
intent.removeInfo()
if (mSpecialMode != SpecialMode.DEFAULT) {
backToTheMainAppAndFinish()
}
@@ -105,18 +129,18 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
}
})
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
mSpecialMode = intent.retrieveSpecialMode()
mTypeMode = intent.retrieveTypeMode()
}
override fun onResume() {
super.onResume()
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
val registerInfo: RegisterInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)
mSpecialMode = intent.retrieveSpecialMode()
mTypeMode = intent.retrieveTypeMode()
val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
val searchInfo: SearchInfo? = registerInfo?.searchInfo
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
?: intent.retrieveSearchInfo()
// To show the selection mode
mToolbarSpecial = findViewById(R.id.special_mode_view)
@@ -125,9 +149,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
val selectionModeStringId = when (mSpecialMode) {
SpecialMode.DEFAULT, // Not important because hidden
SpecialMode.SEARCH -> R.string.search_mode
SpecialMode.SAVE -> R.string.save_mode
SpecialMode.SELECTION -> R.string.selection_mode
SpecialMode.REGISTRATION -> R.string.registration_mode
SpecialMode.REGISTRATION -> R.string.save_mode // Save is registration mode
}
val typeModeStringId = when (mTypeMode) {
TypeMode.DEFAULT, // Not important because hidden
@@ -145,7 +168,6 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
visible = when (mSpecialMode) {
SpecialMode.DEFAULT -> false
SpecialMode.SEARCH -> true
SpecialMode.SAVE -> true
SpecialMode.SELECTION -> true
SpecialMode.REGISTRATION -> true
}

View File

@@ -4,8 +4,11 @@ import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.tasks.ActionRunnable
interface DatabaseRetrieval {
fun onDatabaseRetrieved(database: ContextualDatabase?)
fun onDatabaseActionFinished(database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result)
fun onDatabaseRetrieved(database: ContextualDatabase)
fun onDatabaseActionFinished(
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
)
}

View File

@@ -24,26 +24,31 @@ import android.content.Context
import android.content.Intent
import android.graphics.drawable.Icon
import android.os.Build
import android.os.Bundle
import android.os.ParcelUuid
import android.util.Log
import android.widget.RemoteViews
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.getEnum
import com.kunzisoft.keepass.utils.getEnumExtra
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.utils.getParcelableList
import com.kunzisoft.keepass.utils.putEnum
import com.kunzisoft.keepass.utils.putEnumExtra
import com.kunzisoft.keepass.utils.putParcelableList
import java.io.IOException
import java.util.UUID
object EntrySelectionHelper {
@@ -51,6 +56,8 @@ object EntrySelectionHelper {
private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
private const val EXTRA_NODES_IDS = "com.kunzisoft.keepass.extra.NODES_IDS"
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.NODE_ID"
/**
* Finish the activity by passing the result code and by locking the database if necessary
@@ -58,7 +65,7 @@ object EntrySelectionHelper {
fun Activity.setActivityResult(
lockDatabase: Boolean = false,
resultCode: Int,
data: Intent? = null,
data: Intent? = null
) {
when (resultCode) {
Activity.RESULT_OK ->
@@ -68,170 +75,212 @@ object EntrySelectionHelper {
}
this.finish()
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
if (lockDatabase) {
// Close the database
this.sendBroadcast(Intent(LOCK_ACTION))
}
}
/**
* Utility method to build a registerForActivityResult,
* Used recursively, close each activity with return data
*/
fun AppCompatActivity.buildActivityResultLauncher(
lockDatabase: Boolean = false,
dataTransformation: (data: Intent?) -> Intent? = { it },
): ActivityResultLauncher<Intent> {
return this.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
setActivityResult(
lockDatabase,
it.resultCode,
dataTransformation(it.data)
)
fun startActivityForSearchModeResult(
context: Context,
intent: Intent,
searchInfo: SearchInfo
) {
intent.addSpecialMode(SpecialMode.SEARCH)
intent.addSearchInfo(searchInfo)
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
fun startActivityForSelectionModeResult(
context: Context,
intent: Intent,
typeMode: TypeMode,
searchInfo: SearchInfo?,
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
) {
intent.addSpecialMode(SpecialMode.SELECTION)
intent.addTypeMode(typeMode)
intent.addSearchInfo(searchInfo)
if (activityResultLauncher == null) {
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
}
fun startActivityForSearchModeResult(context: Context,
intent: Intent,
searchInfo: SearchInfo) {
addSpecialModeInIntent(intent, SpecialMode.SEARCH)
addSearchInfoInIntent(intent, searchInfo)
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
fun startActivityForSaveModeResult(context: Context,
intent: Intent,
searchInfo: SearchInfo) {
addSpecialModeInIntent(intent, SpecialMode.SAVE)
addTypeModeInIntent(intent, TypeMode.DEFAULT)
addSearchInfoInIntent(intent, searchInfo)
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
fun startActivityForKeyboardSelectionModeResult(context: Context,
intent: Intent,
searchInfo: SearchInfo?) {
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
addTypeModeInIntent(intent, TypeMode.MAGIKEYBOARD)
addSearchInfoInIntent(intent, searchInfo)
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
/**
* Utility method to start an activity with an Autofill for result
*/
@RequiresApi(Build.VERSION_CODES.O)
fun startActivityForAutofillSelectionModeResult(
context: Context,
intent: Intent,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo?
) {
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
intent.addAutofillComponent(context, autofillComponent)
addSearchInfoInIntent(intent, searchInfo)
activityResultLauncher?.launch(intent)
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
fun startActivityForPasskeySelectionModeResult(
context: Context,
intent: Intent,
activityResultLauncher: ActivityResultLauncher<Intent>?,
searchInfo: SearchInfo?
) {
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
addTypeModeInIntent(intent, TypeMode.PASSKEY)
addSearchInfoInIntent(intent, searchInfo)
activityResultLauncher?.launch(intent)
activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
}
fun startActivityForRegistrationModeResult(
context: Context?,
context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>?,
intent: Intent,
registerInfo: RegisterInfo?,
typeMode: TypeMode
) {
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
addTypeModeInIntent(intent, typeMode)
addRegisterInfoInIntent(intent, registerInfo)
intent.addSpecialMode(SpecialMode.REGISTRATION)
intent.addTypeMode(typeMode)
intent.addRegisterInfo(registerInfo)
if (activityResultLauncher == null) {
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
activityResultLauncher?.launch(intent) ?: context?.startActivity(intent) ?:
throw IllegalStateException("At least Context or ActivityResultLauncher must not be null")
activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
}
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
/**
* Build the special mode response for internal entry selection for one entry
*/
fun Activity.buildSpecialModeResponseAndSetResult(
entryInfo: EntryInfo,
extras: Bundle? = null
) {
this.buildSpecialModeResponseAndSetResult(listOf(entryInfo), extras)
}
/**
* Build the special mode response for internal entry selection for multiple entries
*/
fun Activity.buildSpecialModeResponseAndSetResult(
entriesInfo: List<EntryInfo>,
extras: Bundle? = null
) {
try {
val mReplyIntent = Intent()
Log.d(javaClass.name, "Success special mode manual selection")
mReplyIntent.addNodesIds(entriesInfo.map { it.id })
extras?.let {
mReplyIntent.putExtras(it)
}
setResult(Activity.RESULT_OK, mReplyIntent)
} catch (e: Exception) {
Log.e(javaClass.name, "Unable to add the result", e)
setResult(Activity.RESULT_CANCELED)
}
}
fun Intent.addSearchInfo(searchInfo: SearchInfo?): Intent {
searchInfo?.let {
intent.putExtra(KEY_SEARCH_INFO, it)
putExtra(KEY_SEARCH_INFO, it)
}
return this
}
fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? {
return intent.getParcelableExtraCompat(KEY_SEARCH_INFO)
fun Bundle.addSearchInfo(searchInfo: SearchInfo?): Bundle {
searchInfo?.let {
putParcelable(KEY_SEARCH_INFO, it)
}
return this
}
private fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
fun Intent.retrieveSearchInfo(): SearchInfo? {
return getParcelableExtraCompat(KEY_SEARCH_INFO)
}
fun Bundle.getSearchInfo(): SearchInfo? {
return getParcelableCompat(KEY_SEARCH_INFO)
}
fun Intent.addRegisterInfo(registerInfo: RegisterInfo?): Intent {
registerInfo?.let {
intent.putExtra(KEY_REGISTER_INFO, it)
putExtra(KEY_REGISTER_INFO, it)
}
return this
}
fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? {
return intent.getParcelableExtraCompat(KEY_REGISTER_INFO)
fun Bundle.addRegisterInfo(registerInfo: RegisterInfo?): Bundle {
registerInfo?.let {
putParcelable(KEY_REGISTER_INFO, it)
}
return this
}
fun removeInfoFromIntent(intent: Intent) {
intent.removeExtra(KEY_SEARCH_INFO)
intent.removeExtra(KEY_REGISTER_INFO)
fun Intent.retrieveRegisterInfo(): RegisterInfo? {
return getParcelableExtraCompat(KEY_REGISTER_INFO)
}
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
// TODO Replace by Intent.addSpecialMode
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
fun Bundle.getRegisterInfo(): RegisterInfo? {
return getParcelableCompat(KEY_REGISTER_INFO)
}
fun Intent.removeInfo() {
removeExtra(KEY_SEARCH_INFO)
removeExtra(KEY_REGISTER_INFO)
}
fun Intent.addSpecialMode(specialMode: SpecialMode): Intent {
this.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
return this
}
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
return SpecialMode.SELECTION
}
return intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
fun Bundle.addSpecialMode(specialMode: SpecialMode): Bundle {
this.putEnum(KEY_SPECIAL_MODE, specialMode)
return this
}
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
// TODO Replace by Intent.addTypeMode
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
fun Intent.retrieveSpecialMode(): SpecialMode {
return this.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
}
fun Bundle.getSpecialMode(): SpecialMode {
return this.getEnum<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
}
fun Intent.addTypeMode(typeMode: TypeMode): Intent {
this.putEnumExtra(KEY_TYPE_MODE, typeMode)
return this
}
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
return TypeMode.AUTOFILL
}
return intent.getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
fun Intent.retrieveTypeMode(): TypeMode {
return getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
}
fun removeModesFromIntent(intent: Intent) {
intent.removeExtra(KEY_SPECIAL_MODE)
intent.removeExtra(KEY_TYPE_MODE)
fun Intent.removeModes() {
removeExtra(KEY_SPECIAL_MODE)
removeExtra(KEY_TYPE_MODE)
}
fun Intent.addNodesIds(nodesIds: List<UUID>): Intent {
this.putParcelableList(EXTRA_NODES_IDS, nodesIds.map { ParcelUuid(it) })
return this
}
fun Intent.retrieveNodesIds(): List<UUID>? {
return getParcelableList<ParcelUuid>(EXTRA_NODES_IDS)?.map { it.uuid }
}
fun Intent.removeNodesIds() {
removeExtra(EXTRA_NODES_IDS)
}
/**
* Add the node id to the intent
*/
fun Intent.addNodeId(nodeId: UUID?) {
nodeId?.let {
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId))
}
}
/**
* Retrieve the node id from the intent
*/
fun Intent.retrieveNodeId(): UUID? {
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
}
fun Intent.removeNodeId() {
removeExtra(EXTRA_NODE_ID)
}
/**
* Retrieve nodes ids from intent and get the corresponding entry info list in [database]
*/
fun Intent.retrieveAndRemoveEntries(database: ContextualDatabase): List<EntryInfo> {
val nodesIds = retrieveNodesIds()
?: throw IOException("NodesIds is null")
removeNodesIds()
return nodesIds.mapNotNull { nodeId ->
database
.getEntryById(NodeIdUUID(nodeId))
?.getEntryInfo(database)
}
}
/**
@@ -239,103 +288,101 @@ object EntrySelectionHelper {
*/
fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
return (specialMode == SpecialMode.SELECTION
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
// TODO Autofill Registration callback #765 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
&& (typeMode == TypeMode.MAGIKEYBOARD || typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|| (specialMode == SpecialMode.REGISTRATION
&& typeMode == TypeMode.PASSKEY)
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
}
fun doSpecialAction(intent: Intent,
defaultAction: () -> Unit,
searchAction: (searchInfo: SearchInfo) -> Unit,
saveAction: (searchInfo: SearchInfo) -> Unit,
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
autofillSelectionAction: (searchInfo: SearchInfo?,
autofillComponent: AutofillComponent) -> Unit,
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit,
passkeySelectionAction: (searchInfo: SearchInfo?) -> Unit,
passkeyRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
when (retrieveSpecialModeFromIntent(intent)) {
fun doSpecialAction(
intent: Intent,
defaultAction: () -> Unit,
searchAction: (searchInfo: SearchInfo) -> Unit,
selectionAction: (
intentSenderMode: Boolean,
typeMode: TypeMode,
searchInfo: SearchInfo?
) -> Unit,
registrationAction: (
intentSenderMode: Boolean,
typeMode: TypeMode,
registerInfo: RegisterInfo?
) -> Unit
) {
when (val specialMode = intent.retrieveSpecialMode()) {
SpecialMode.DEFAULT -> {
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
intent.removeModes()
intent.removeInfo()
defaultAction.invoke()
}
SpecialMode.SEARCH -> {
val searchInfo = retrieveSearchInfoFromIntent(intent)
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
val searchInfo = intent.retrieveSearchInfo()
intent.removeModes()
intent.removeInfo()
if (searchInfo != null)
searchAction.invoke(searchInfo)
else {
defaultAction.invoke()
}
}
SpecialMode.SAVE -> {
val searchInfo = retrieveSearchInfoFromIntent(intent)
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
if (searchInfo != null)
saveAction.invoke(searchInfo)
else {
defaultAction.invoke()
}
}
SpecialMode.SELECTION -> {
val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent)
var autofillComponentInit = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.retrieveAutofillComponent(intent)?.let { autofillComponent ->
autofillSelectionAction.invoke(searchInfo, autofillComponent)
autofillComponentInit = true
}
}
if (!autofillComponentInit) {
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
when (retrieveTypeModeFromIntent(intent)) {
TypeMode.DEFAULT -> {
removeModesFromIntent(intent)
if (searchInfo != null)
searchAction.invoke(searchInfo)
else
defaultAction.invoke()
}
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
TypeMode.PASSKEY -> passkeySelectionAction.invoke(searchInfo)
else -> {
// In this case, error
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
}
val searchInfo: SearchInfo? = intent.retrieveSearchInfo()
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
when (val typeMode = intent.retrieveTypeMode()) {
TypeMode.DEFAULT -> {
intent.removeModes()
if (searchInfo != null)
searchAction.invoke(searchInfo)
else
defaultAction.invoke()
}
TypeMode.MAGIKEYBOARD -> selectionAction.invoke(
isIntentSenderMode(specialMode, typeMode),
typeMode,
searchInfo
)
TypeMode.PASSKEY ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
selectionAction.invoke(
isIntentSenderMode(specialMode, typeMode),
typeMode,
searchInfo
)
} else
defaultAction.invoke()
TypeMode.AUTOFILL -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
selectionAction.invoke(
isIntentSenderMode(specialMode, typeMode),
typeMode,
searchInfo
)
} else
defaultAction.invoke()
}
} else {
if (searchInfo != null)
searchAction.invoke(searchInfo)
else
defaultAction.invoke()
}
} else {
if (searchInfo != null)
searchAction.invoke(searchInfo)
else
defaultAction.invoke()
}
}
SpecialMode.REGISTRATION -> {
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
if (!isIntentSenderMode(
specialMode = retrieveSpecialModeFromIntent(intent),
typeMode = retrieveTypeModeFromIntent(intent))
) {
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
val typeMode = intent.retrieveTypeMode()
val intentSenderMode = isIntentSenderMode(specialMode, typeMode)
if (!intentSenderMode) {
intent.removeModes()
intent.removeInfo()
}
when (retrieveTypeModeFromIntent(intent)) {
TypeMode.AUTOFILL -> {
autofillRegistrationAction.invoke(registerInfo)
}
TypeMode.PASSKEY -> {
passkeyRegistrationAction.invoke(registerInfo)
}
else -> {
// Do other registration type
}
if (registerInfo != null)
registrationAction.invoke(
intentSenderMode,
typeMode,
registerInfo
)
else {
defaultAction.invoke()
}
}
}
@@ -367,7 +414,7 @@ object EntrySelectionHelper {
try {
database.iconDrawableFactory.getBitmapFromIcon(context,
this.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
return Icon.createWithBitmap(bitmap)
return IconCompat.createWithBitmap(bitmap).toIcon(context)
}
} catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)

View File

@@ -3,7 +3,6 @@ package com.kunzisoft.keepass.credentialprovider
enum class SpecialMode {
DEFAULT,
SEARCH,
SAVE,
SELECTION,
REGISTRATION;
}

View File

@@ -1,5 +1,5 @@
package com.kunzisoft.keepass.credentialprovider
enum class TypeMode {
DEFAULT, MAGIKEYBOARD, AUTOFILL, PASSKEY
DEFAULT, MAGIKEYBOARD, PASSKEY, AUTOFILL
}

View File

@@ -27,34 +27,48 @@ import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.getRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.getSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.getSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.autofill.CompatInlineSuggestionsRequest
import com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.retrieveAutofillComponent
import com.kunzisoft.keepass.credentialprovider.viewmodel.AutofillLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.utils.AppUtil
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch
@RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : DatabaseModeActivity() {
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.buildActivityResultLauncher(lockDatabase = true)
private val autofillLauncherViewModel: AutofillLauncherViewModel by viewModels()
private var mAutofillSelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
autofillLauncherViewModel.manageSelectionResult(it)
}
private var mAutofillRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
autofillLauncherViewModel.manageRegistrationResult(it)
}
override fun applyCustomStyle(): Boolean {
return false
@@ -64,208 +78,137 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
return true
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
// Retrieve selection mode
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
when (specialMode) {
SpecialMode.SELECTION -> {
intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle ->
// To pass extra inline request
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
compatInlineSuggestionsRequest = bundle.getParcelableCompat(
KEY_INLINE_SUGGESTION
)
}
// Build search param
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
AppUtil.getConcreteWebDomain(
this,
searchInfo.webDomain
) { concreteWebDomain ->
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val assistStructure = AutofillHelper
.retrieveAutofillComponent(intent)
?.assistStructure
val newAutofillComponent = if (assistStructure != null) {
AutofillComponent(
assistStructure,
compatInlineSuggestionsRequest
)
} else {
null
}
searchInfo.webDomain = concreteWebDomain
launchSelection(database, newAutofillComponent, searchInfo)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
// To apply the bypass https://github.com/Kunzisoft/KeePassDX/issues/2238
// before managing intent in super class
intent.retrieveSelectionBundle()?.apply {
intent.addSpecialMode(getSpecialMode())
intent.addSearchInfo(getSearchInfo())
intent.addRegisterInfo(getRegisterInfo())
intent.addAutofillComponent(retrieveAutofillComponent())
}
super.onCreate(savedInstanceState)
autofillLauncherViewModel.initialize()
lifecycleScope.launch {
// Initialize the parameters
autofillLauncherViewModel.uiState.collect { uiState ->
when (uiState) {
AutofillLauncherViewModel.UIState.Loading -> {}
is AutofillLauncherViewModel.UIState.ShowBlockRestartMessage -> {
showBlockRestartMessage()
autofillLauncherViewModel.cancelResult()
}
// Remove bundle
intent.removeExtra(KEY_SELECTION_BUNDLE)
}
SpecialMode.REGISTRATION -> {
// To register info
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(
KEY_REGISTER_INFO
)
val searchInfo = SearchInfo(registerInfo?.searchInfo)
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launchRegistration(database, searchInfo, registerInfo)
is AutofillLauncherViewModel.UIState.ShowAutofillSuggestionMessage -> {
showAutofillSuggestionMessage()
}
}
else -> {
// Not an autofill call
setResult(RESULT_CANCELED)
finish()
}
}
lifecycleScope.launch {
// Retrieve the UI
autofillLauncherViewModel.credentialUiState.collect { uiState ->
when (uiState) {
is CredentialLauncherViewModel.CredentialState.Loading -> {}
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
GroupActivity.launchForSelection(
context = this@AutofillLauncherActivity,
database = uiState.database,
searchInfo = uiState.searchInfo,
typeMode = uiState.typeMode,
activityResultLauncher = mAutofillSelectionActivityResultLauncher,
)
}
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
GroupActivity.launchForRegistration(
context = this@AutofillLauncherActivity,
database = uiState.database,
registerInfo = uiState.registerInfo,
typeMode = uiState.typeMode,
activityResultLauncher = mAutofillRegistrationActivityResultLauncher
)
}
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
FileDatabaseSelectActivity.launchForSelection(
context = this@AutofillLauncherActivity,
searchInfo = uiState.searchInfo,
typeMode = uiState.typeMode,
activityResultLauncher = mAutofillSelectionActivityResultLauncher
)
}
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForRegistration -> {
FileDatabaseSelectActivity.launchForRegistration(
context = this@AutofillLauncherActivity,
registerInfo = uiState.registerInfo,
typeMode = uiState.typeMode,
activityResultLauncher = mAutofillRegistrationActivityResultLauncher,
)
}
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
setActivityResult(
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is CredentialLauncherViewModel.CredentialState.ShowError -> {
toastError(uiState.error)
autofillLauncherViewModel.cancelResult()
}
}
}
}
}
private fun launchSelection(database: ContextualDatabase?,
autofillComponent: AutofillComponent?,
searchInfo: SearchInfo) {
if (autofillComponent == null) {
setResult(RESULT_CANCELED)
finish()
} else if (KeeAutofillService.autofillAllowedFor(
applicationId = searchInfo.applicationId,
webDomain = searchInfo.webDomain,
context = this
)) {
// If database is open
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
// Items found
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
finish()
},
onItemNotFound = { openedDatabase ->
// Show the database UI to select the entry
GroupActivity.launchForAutofillSelectionResult(
this,
openedDatabase,
mCredentialActivityResultLauncher,
autofillComponent,
searchInfo,
false
)
},
onDatabaseClosed = {
// If database not open
FileDatabaseSelectActivity.launchForAutofillResult(
this,
mCredentialActivityResultLauncher,
autofillComponent,
searchInfo
)
}
)
} else {
showBlockRestartMessage()
setResult(RESULT_CANCELED)
finish()
}
}
private fun launchRegistration(database: ContextualDatabase?,
searchInfo: SearchInfo,
registerInfo: RegisterInfo?) {
if (KeeAutofillService.autofillAllowedFor(
applicationId = searchInfo.applicationId,
webDomain = searchInfo.webDomain,
context = this
)) {
val readOnly = database?.isReadOnly != false
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, _ ->
if (!readOnly) {
// Show the database UI to select the entry
GroupActivity.launchForRegistration(
context = this,
activityResultLauncher = null, // TODO Autofill result launcher #765
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
} else {
showReadOnlySaveMessage()
}
},
onItemNotFound = { openedDatabase ->
if (!readOnly) {
// Show the database UI to select the entry
GroupActivity.launchForRegistration(
context = this,
activityResultLauncher = null, // TODO Autofill result launcher #765
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
} else {
showReadOnlySaveMessage()
}
},
onDatabaseClosed = {
// If database not open
FileDatabaseSelectActivity.launchForRegistration(
context = this,
activityResultLauncher = null, // TODO Autofill result launcher #765
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
}
)
} else {
showBlockRestartMessage()
setResult(RESULT_CANCELED)
}
finish()
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onUnknownDatabaseRetrieved(database)
autofillLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
}
private fun showBlockRestartMessage() {
// If item not allowed, show a toast
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
Toast.makeText(
applicationContext,
R.string.autofill_block_restart,
Toast.LENGTH_LONG
).show()
}
private fun showReadOnlySaveMessage() {
toastError(RegisterInReadOnlyDatabaseException())
private fun showAutofillSuggestionMessage() {
Toast.makeText(
applicationContext,
R.string.autofill_inline_suggestions_keyboard,
Toast.LENGTH_SHORT
).show()
}
companion object {
private const val KEY_PENDING_INTENT_BUNDLE = "com.kunzisoft.keepass.extra.BUNDLE"
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"
fun Intent.retrieveSelectionBundle(): Bundle? {
return this.getBundleExtra(KEY_PENDING_INTENT_BUNDLE)
}
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
fun getPendingIntentForSelection(context: Context,
searchInfo: SearchInfo? = null,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent? {
fun getPendingIntentForSelection(
context: Context,
searchInfo: SearchInfo? = null,
autofillComponent: AutofillComponent
): PendingIntent? {
try {
// Doesn't work with direct extra Parcelable in Android 11 (don't know why?)
// https://github.com/Kunzisoft/KeePassDX/issues/2238
// Wrap into a bundle to bypass the problem
val tempBundle = Bundle().apply {
addSpecialMode(SpecialMode.SELECTION)
addSearchInfo(searchInfo)
addAutofillComponent(autofillComponent)
}
return PendingIntent.getActivity(
context, 0,
// Doesn't work with direct extra Parcelable (don't know why?)
// Wrap into a bundle to bypass the problem
context,
randomRequestCode(),
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)
}
})
putExtra(KEY_PENDING_INTENT_BUNDLE, tempBundle)
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
@@ -279,14 +222,21 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
}
}
fun getPendingIntentForRegistration(context: Context,
registerInfo: RegisterInfo): PendingIntent? {
fun getPendingIntentForRegistration(
context: Context,
registerInfo: RegisterInfo
): PendingIntent? {
try {
// Bypass intent issue
val tempBundle = Bundle().apply {
addSpecialMode(SpecialMode.REGISTRATION)
addRegisterInfo(registerInfo)
}
return PendingIntent.getActivity(
context, 0,
context,
randomRequestCode(),
Intent(context, AutofillLauncherActivity::class.java).apply {
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
putExtra(KEY_REGISTER_INFO, registerInfo)
putExtra(KEY_PENDING_INTENT_BUNDLE, tempBundle)
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
@@ -299,14 +249,5 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
return null
}
}
fun launchForRegistration(context: Context,
registerInfo: RegisterInfo) {
val intent = Intent(context, AutofillLauncherActivity::class.java)
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
intent.putExtra(KEY_REGISTER_INFO, registerInfo)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}
}

View File

@@ -22,22 +22,22 @@ package com.kunzisoft.keepass.credentialprovider.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.core.net.toUri
import com.kunzisoft.keepass.R
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.EntrySelectionViewModel
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.utils.AppUtil
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch
/**
* Activity to search or select entry in database,
@@ -45,198 +45,129 @@ import com.kunzisoft.keepass.view.toastError
*/
class EntrySelectionLauncherActivity : DatabaseModeActivity() {
override fun applyCustomStyle(): Boolean {
return false
}
private val entrySelectionViewModel: EntrySelectionViewModel by viewModels()
override fun finishActivityIfReloadRequested(): Boolean {
return false
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
if (keySelectionBundle != null) {
// To manage package name
var searchInfo = SearchInfo()
keySelectionBundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { mSearchInfo ->
searchInfo = mSearchInfo
}
launch(database, searchInfo)
} else {
// To manage share
var sharedWebDomain: String? = null
var otpString: String? = null
when (intent?.action) {
Intent.ACTION_SEND -> {
if ("text/plain" == intent.type) {
// Retrieve web domain or OTP
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
else
sharedWebDomain = extra.toUri().host
}
}
launchSelection(database, sharedWebDomain, otpString)
}
Intent.ACTION_VIEW -> {
// Retrieve OTP
intent.dataString?.let { extra ->
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
}
launchSelection(database, sharedWebDomain, otpString)
}
else -> {
if (database != null) {
GroupActivity.launch(this, database)
} else {
FileDatabaseSelectActivity.launch(this)
}
}
}
}
finish()
}
private fun launchSelection(database: ContextualDatabase?,
sharedWebDomain: String?,
otpString: String?) {
// Build domain search param
val searchInfo = SearchInfo().apply {
this.webDomain = sharedWebDomain
this.otpString = otpString
private var mEntrySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
entrySelectionViewModel.manageSelectionResult(it)
}
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launch(database, searchInfo)
}
}
override fun applyCustomStyle() = false
private fun launch(database: ContextualDatabase?,
searchInfo: SearchInfo) {
override fun finishActivityIfReloadRequested() = false
// Setting to integrate Magikeyboard
val searchShareForMagikeyboard = isKeyboardActivatedInSettings()
// If database is open
val readOnly = database?.isReadOnly != false
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
// Items found
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(
this,
openedDatabase,
searchInfo,
false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
entrySelectionViewModel.initialize()
lifecycleScope.launch {
// Initialize the parameters
entrySelectionViewModel.uiState.collect { uiState ->
when (uiState) {
is EntrySelectionViewModel.UIState.Loading -> {}
is EntrySelectionViewModel.UIState.PopulateKeyboard -> {
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(
context = this@EntrySelectionLauncherActivity,
entry = uiState.entryInfo,
toast = true
)
} else {
toastError(RegisterInReadOnlyDatabaseException())
}
} else if (searchShareForMagikeyboard) {
MagikeyboardService.performSelection(
items,
{ entryInfo ->
// Automatically populate keyboard
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
this,
entryInfo
)
},
{ autoSearch ->
GroupActivity.launchForKeyboardSelectionResult(
this,
openedDatabase,
searchInfo,
autoSearch
)
}
)
} else {
GroupActivity.launchForSearchResult(
this,
openedDatabase,
searchInfo,
true
)
}
},
onItemNotFound = { openedDatabase ->
// Show the database UI to select the entry
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(
this,
openedDatabase,
searchInfo,
false
is EntrySelectionViewModel.UIState.LaunchFileDatabaseSelectForSearch -> {
FileDatabaseSelectActivity.launchForSearch(
context = this@EntrySelectionLauncherActivity,
searchInfo = uiState.searchInfo
)
}
is EntrySelectionViewModel.UIState.LaunchGroupActivityForSearch -> {
GroupActivity.launchForSearch(
context = this@EntrySelectionLauncherActivity,
database = uiState.database,
searchInfo = uiState.searchInfo
)
} else {
toastError(RegisterInReadOnlyDatabaseException())
}
} else if (searchShareForMagikeyboard) {
GroupActivity.launchForKeyboardSelectionResult(
this,
openedDatabase,
searchInfo,
false
)
} else {
GroupActivity.launchForSearchResult(
this,
openedDatabase,
searchInfo,
false
)
}
},
onDatabaseClosed = {
// If database not open
if (searchInfo.otpString != null) {
FileDatabaseSelectActivity.launchForSaveResult(
this,
searchInfo
)
} else if (searchShareForMagikeyboard) {
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(
this,
searchInfo
)
} else {
FileDatabaseSelectActivity.launchForSearchResult(
this,
searchInfo
)
}
}
)
}
lifecycleScope.launch {
// Retrieve the UI
entrySelectionViewModel.credentialUiState.collect { uiState ->
when (uiState) {
is CredentialLauncherViewModel.CredentialState.Loading -> {}
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
GroupActivity.launchForSelection(
context = this@EntrySelectionLauncherActivity,
database = uiState.database,
searchInfo = uiState.searchInfo,
typeMode = uiState.typeMode,
activityResultLauncher = mEntrySelectionActivityResultLauncher
)
}
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
GroupActivity.launchForRegistration(
context = this@EntrySelectionLauncherActivity,
database = uiState.database,
registerInfo = uiState.registerInfo,
typeMode = uiState.typeMode,
activityResultLauncher = null // Null to not get any callback
)
finish()
}
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
FileDatabaseSelectActivity.launchForSelection(
context = this@EntrySelectionLauncherActivity,
searchInfo = uiState.searchInfo,
typeMode = uiState.typeMode,
activityResultLauncher = mEntrySelectionActivityResultLauncher
)
}
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForRegistration -> {
FileDatabaseSelectActivity.launchForRegistration(
context = this@EntrySelectionLauncherActivity,
registerInfo = uiState.registerInfo,
typeMode = uiState.typeMode,
activityResultLauncher = null // Null to not get any callback
)
finish()
}
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
setActivityResult(
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is CredentialLauncherViewModel.CredentialState.ShowError -> {
toastError(uiState.error)
entrySelectionViewModel.cancelResult()
}
}
}
}
}
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onUnknownDatabaseRetrieved(database)
entrySelectionViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
}
override fun onDestroy() {
super.onDestroy()
}
companion object {
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
fun launch(context: Context,
searchInfo: SearchInfo? = null) {
val intent = Intent(context, EntrySelectionLauncherActivity::class.java).apply {
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
putParcelable(KEY_SEARCH_INFO, searchInfo)
})
}
// New task needed because don't launch from an Activity context
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
fun launch(
context: Context,
searchInfo: SearchInfo? = null
) {
context.startActivity(Intent(
context,
EntrySelectionLauncherActivity::class.java
).apply {
addSearchInfo(searchInfo)
// New task needed because don't launch from an Activity context
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
})
}
}
}

View File

@@ -0,0 +1,170 @@
package com.kunzisoft.keepass.credentialprovider.activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.addHardwareKey
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.addSeed
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.buildHardwareKeyChallenge
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.isYubikeyDriverAvailable
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.UIState
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch
/**
* Special activity to deal with hardware key drivers,
* return the response to the database service once finished
*/
class HardwareKeyActivity: DatabaseModeActivity(){
private val mHardwareKeyLauncherViewModel: HardwareKeyLauncherViewModel by viewModels()
private var activityResultLauncher: ActivityResultLauncher<Intent> =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
mHardwareKeyLauncherViewModel.manageSelectionResult(it)
}
override fun applyCustomStyle(): Boolean = false
override fun showDatabaseDialog(): Boolean = false
override fun manageDatabaseInfo(): Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
mHardwareKeyLauncherViewModel.uiState.collect { uiState ->
when (uiState) {
is UIState.Loading -> {}
is UIState.ShowHardwareKeyDriverNeeded -> {
showHardwareKeyDriverNeeded(
this@HardwareKeyActivity,
uiState.hardwareKey
) {
mDatabaseViewModel.onChallengeResponded(null)
finish()
}
}
is UIState.LaunchChallengeActivityForResponse -> {
// Send to the driver
activityResultLauncher.launch(
buildHardwareKeyChallenge(uiState.challenge)
)
}
is UIState.OnChallengeResponded -> {
mDatabaseViewModel.onChallengeResponded(uiState.response)
}
}
}
}
lifecycleScope.launch {
mHardwareKeyLauncherViewModel.credentialUiState.collect { uiState ->
when (uiState) {
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
setActivityResult(
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is CredentialLauncherViewModel.CredentialState.ShowError -> {
toastError(uiState.error)
mHardwareKeyLauncherViewModel.cancelResult()
}
else -> {}
}
}
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mHardwareKeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
}
override fun onDatabaseActionFinished(
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
finish()
}
private fun showHardwareKeyDriverNeeded(
context: Context,
hardwareKey: HardwareKey?,
onDialogDismissed: DialogInterface.OnDismissListener
) {
val builder = AlertDialog.Builder(context)
builder
.setMessage(
context.getString(R.string.error_driver_required, hardwareKey.toString())
)
.setPositiveButton(R.string.download) { _, _ ->
context.openExternalApp(
context.getString(R.string.key_driver_app_id),
context.getString(R.string.key_driver_url)
)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setOnDismissListener(onDialogDismissed)
builder.create().show()
}
companion object {
private val TAG = HardwareKeyActivity::class.java.simpleName
fun launchHardwareKeyActivity(
context: Context,
hardwareKey: HardwareKey,
seed: ByteArray?
) {
context.startActivity(
Intent(
context,
HardwareKeyActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_MULTIPLE_TASK
addHardwareKey(hardwareKey)
addSeed(seed)
})
}
fun isHardwareKeyAvailable(
context: Context,
hardwareKey: HardwareKey?
): Boolean {
if (hardwareKey == null)
return false
return when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
false
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Check available intent
isYubikeyDriverAvailable(context)
}
}
}
}
}

View File

@@ -36,6 +36,8 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
@@ -44,14 +46,14 @@ import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch
import java.util.UUID
@@ -79,10 +81,6 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
return false
}
override fun finishActivityIfDatabaseNotLoaded(): Boolean {
return false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
@@ -105,61 +103,69 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
nodeId = uiState.nodeId
)
}
is PasskeyLauncherViewModel.UIState.SetActivityResult -> {
setActivityResult(
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is PasskeyLauncherViewModel.UIState.ShowError -> {
toastError(uiState.error)
passkeyLauncherViewModel.cancelResult()
}
is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForSelection -> {
GroupActivity.launchForPasskeySelectionResult(
context = this@PasskeyLauncherActivity,
database = uiState.database,
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
searchInfo = null,
autoSearch = false
)
}
is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> {
GroupActivity.launchForRegistration(
context = this@PasskeyLauncherActivity,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
database = uiState.database,
registerInfo = uiState.registerInfo,
typeMode = uiState.typeMode
)
}
is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> {
FileDatabaseSelectActivity.launchForPasskeySelectionResult(
activity = this@PasskeyLauncherActivity,
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
searchInfo = uiState.searchInfo,
)
}
is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> {
FileDatabaseSelectActivity.launchForRegistration(
context = this@PasskeyLauncherActivity,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
registerInfo = uiState.registerInfo,
typeMode = uiState.typeMode
)
}
is PasskeyLauncherViewModel.UIState.UpdateEntry -> {
updateEntry(uiState.oldEntry, uiState.newEntry)
}
}
}
}
lifecycleScope.launch {
passkeyLauncherViewModel.credentialUiState.collect { uiState ->
when (uiState) {
is CredentialLauncherViewModel.CredentialState.Loading -> {}
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
setActivityResult(
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is CredentialLauncherViewModel.CredentialState.ShowError -> {
toastError(uiState.error)
passkeyLauncherViewModel.cancelResult()
}
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
GroupActivity.launchForSelection(
context = this@PasskeyLauncherActivity,
database = uiState.database,
typeMode = uiState.typeMode,
searchInfo = uiState.searchInfo,
activityResultLauncher = mPasskeySelectionActivityResultLauncher
)
}
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
GroupActivity.launchForRegistration(
context = this@PasskeyLauncherActivity,
database = uiState.database,
typeMode = uiState.typeMode,
registerInfo = uiState.registerInfo,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher
)
}
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
FileDatabaseSelectActivity.launchForSelection(
context = this@PasskeyLauncherActivity,
typeMode = uiState.typeMode,
searchInfo = uiState.searchInfo,
activityResultLauncher = mPasskeySelectionActivityResultLauncher
)
}
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForRegistration -> {
FileDatabaseSelectActivity.launchForRegistration(
context = this@PasskeyLauncherActivity,
typeMode = uiState.typeMode,
registerInfo = uiState.registerInfo,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
)
}
}
}
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
passkeyLauncherViewModel.launchPasskeyActionIfNeeded(intent, mSpecialMode, database)
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onUnknownDatabaseRetrieved(database)
passkeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
}
override fun onDatabaseActionFinished(
@@ -170,7 +176,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
passkeyLauncherViewModel.autoSelectPasskey(result, database)
// TODO When auto save is enabled, WARNING filter by the calling activity
// passkeyLauncherViewModel.autoSelectPasskey(result, database)
}
}
}
@@ -234,6 +241,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
)
.append("\n\n")
.append(getString(R.string.passkeys_missing_signature_app_ask_explanation))
.append("\n\n")
.append(getString(R.string.passkeys_missing_signature_app_ask_question))
.toString()
)
setPositiveButton(android.R.string.ok) { _, _ ->
@@ -273,7 +282,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
): PendingIntent? {
return PendingIntent.getActivity(
context,
(Math.random() * Integer.MAX_VALUE).toInt(),
randomRequestCode(),
Intent(context, PasskeyLauncherActivity::class.java).apply {
addSpecialMode(specialMode)
addTypeMode(TypeMode.PASSKEY)

View File

@@ -2,5 +2,7 @@ package com.kunzisoft.keepass.credentialprovider.autofill
import android.app.assist.AssistStructure
data class AutofillComponent(val assistStructure: AssistStructure,
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)
data class AutofillComponent(
val assistStructure: AssistStructure,
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?
)

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.credentialprovider.autofill
import android.annotation.SuppressLint
import android.app.Activity
import android.app.PendingIntent
import android.app.assist.AssistStructure
import android.content.Context
@@ -28,6 +27,7 @@ import android.content.Intent
import android.graphics.BlendMode
import android.graphics.drawable.Icon
import android.os.Build
import android.os.Bundle
import android.service.autofill.Dataset
import android.service.autofill.Field
import android.service.autofill.FillResponse
@@ -38,7 +38,6 @@ import android.view.autofill.AutofillId
import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue
import android.widget.RemoteViews
import android.widget.Toast
import android.widget.inline.InlinePresentationSpec
import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions
@@ -54,21 +53,63 @@ import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import java.io.IOException
import kotlin.math.min
@RequiresApi(api = Build.VERSION_CODES.O)
object AutofillHelper {
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
private const val EXTRA_BASE_STRUCTURE = "com.kunzisoft.keepass.autofill.BASE_STRUCTURE"
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
fun Intent.addAutofillComponent(autofillComponent: AutofillComponent?): Intent {
autofillComponent?.let {
this.putExtra(EXTRA_BASE_STRUCTURE, autofillComponent.assistStructure)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
autofillComponent.compatInlineSuggestionsRequest?.let {
this.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
}
}
}
return this
}
fun Intent.retrieveAutofillComponent(): AutofillComponent? {
this.getParcelableExtraCompat<AssistStructure>(EXTRA_BASE_STRUCTURE)?.let { assistStructure ->
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AutofillComponent(assistStructure,
intent.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
AutofillComponent(
assistStructure,
this.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
} else {
AutofillComponent(assistStructure, null)
}
}
return null
}
fun Bundle.addAutofillComponent(autofillComponent: AutofillComponent?): Bundle {
autofillComponent?.let {
this.putParcelable(EXTRA_BASE_STRUCTURE, autofillComponent.assistStructure)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
autofillComponent.compatInlineSuggestionsRequest?.let {
this.putParcelable(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
}
}
}
return this
}
fun Bundle.retrieveAutofillComponent(): AutofillComponent? {
this.getParcelableCompat<AssistStructure>(EXTRA_BASE_STRUCTURE)?.let { assistStructure ->
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AutofillComponent(
assistStructure,
this.getParcelableCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST)
)
} else {
AutofillComponent(assistStructure, null)
}
@@ -127,11 +168,13 @@ object AutofillHelper {
return this
}
private fun buildDatasetForEntry(context: Context,
database: ContextualDatabase,
entryInfo: EntryInfo,
struct: StructureParser.Result,
inlinePresentation: InlinePresentation?): Dataset {
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) {
@@ -291,11 +334,13 @@ object AutofillHelper {
@SuppressLint("RestrictedApi")
@RequiresApi(Build.VERSION_CODES.R)
private fun buildInlinePresentationForEntry(context: Context,
database: ContextualDatabase,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
positionItem: Int,
entryInfo: EntryInfo): InlinePresentation? {
private fun buildInlinePresentationForEntry(
context: Context,
database: ContextualDatabase,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
positionItem: Int,
entryInfo: EntryInfo
): InlinePresentation? {
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
@@ -314,7 +359,7 @@ object AutofillHelper {
// Build the content for IME UI
val pendingIntent = PendingIntent.getActivity(
context,
0,
randomRequestCode(),
Intent(context, AutofillSettingsActivity::class.java),
PendingIntent.FLAG_IMMUTABLE
)
@@ -341,9 +386,11 @@ object AutofillHelper {
@RequiresApi(Build.VERSION_CODES.R)
@SuppressLint("RestrictedApi")
private fun buildInlinePresentationForManualSelection(context: Context,
inlinePresentationSpec: InlinePresentationSpec,
pendingIntent: PendingIntent): InlinePresentation? {
private fun buildInlinePresentationForManualSelection(
context: Context,
inlinePresentationSpec: InlinePresentationSpec,
pendingIntent: PendingIntent
): InlinePresentation? {
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
@@ -360,11 +407,13 @@ object AutofillHelper {
}.build().slice, inlinePresentationSpec, false)
}
fun buildResponse(context: Context,
database: ContextualDatabase,
entriesInfo: List<EntryInfo>,
parseResult: StructureParser.Result,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
fun buildResponse(
context: Context,
database: ContextualDatabase,
entriesInfo: List<EntryInfo>,
parseResult: StructureParser.Result,
autofillComponent: AutofillComponent
): FillResponse? {
val responseBuilder = FillResponse.Builder()
// Add Header
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -385,7 +434,8 @@ object AutofillHelper {
// Add inline suggestion for new IME and dataset
var numberInlineSuggestions = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
autofillComponent.compatInlineSuggestionsRequest
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
@@ -401,21 +451,27 @@ object AutofillHelper {
var inlinePresentation: InlinePresentation? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& numberInlineSuggestions > 0
&& compatInlineSuggestionsRequest != null) {
&& autofillComponent.compatInlineSuggestionsRequest != null) {
inlinePresentation = buildInlinePresentationForEntry(
context,
database,
compatInlineSuggestionsRequest,
autofillComponent.compatInlineSuggestionsRequest,
numberInlineSuggestions--,
entry
)
}
// Create dataset for each entry
responseBuilder.addDataset(
buildDatasetForEntry(context, database, entry, parseResult, inlinePresentation)
buildDatasetForEntry(
context = context,
database = database,
entryInfo = entry,
struct = parseResult,
inlinePresentation = inlinePresentation
)
)
} catch (e: Exception) {
Log.e(TAG, "Unable to add dataset")
Log.e(TAG, "Unable to add dataset", e)
}
}
@@ -427,21 +483,28 @@ object AutofillHelper {
webScheme = parseResult.webScheme
manualSelection = true
}
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
AutofillLauncherActivity.getPendingIntentForSelection(context,
searchInfo, compatInlineSuggestionsRequest)?.let { pendingIntent ->
val manualSelectionView = RemoteViews(
context.packageName,
R.layout.item_autofill_select_entry
)
AutofillLauncherActivity.getPendingIntentForSelection(
context,
searchInfo,
autofillComponent
)?.let { pendingIntent ->
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
)
}
autofillComponent.compatInlineSuggestionsRequest
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpec =
inlineSuggestionsRequest.inlinePresentationSpecs[0]
inlinePresentation = buildInlinePresentationForManualSelection(
context,
inlinePresentationSpec,
pendingIntent
)
}
}
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -486,61 +549,31 @@ object AutofillHelper {
}
/**
* Build the Autofill response for one entry
* Build the Autofill response
*/
fun buildResponseAndSetResult(activity: Activity,
database: ContextualDatabase,
entryInfo: EntryInfo) {
buildResponseAndSetResult(activity, database, ArrayList<EntryInfo>().apply { add(entryInfo) })
}
/**
* Build the Autofill response for many entry
*/
fun buildResponseAndSetResult(activity: Activity,
database: ContextualDatabase,
entriesInfo: List<EntryInfo>) {
fun buildResponse(
context: Context,
autofillComponent: AutofillComponent,
database: ContextualDatabase,
entriesInfo: List<EntryInfo>,
onIntentCreated: (Intent) -> Unit
) {
if (entriesInfo.isEmpty()) {
activity.setResult(Activity.RESULT_CANCELED)
throw IOException("No entries found")
} else {
var setResultOk = false
activity.intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
StructureParser(structure).parse()?.let { result ->
// New Response
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat<CompatInlineSuggestionsRequest>(
EXTRA_INLINE_SUGGESTIONS_REQUEST
)
if (compatInlineSuggestionsRequest != null) {
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
}
buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest)
} else {
buildResponse(activity, database, entriesInfo, result, null)
}
val mReplyIntent = Intent()
Log.d(activity.javaClass.name, "Success Autofill auth.")
mReplyIntent.putExtra(
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
response)
setResultOk = true
activity.setResult(Activity.RESULT_OK, mReplyIntent)
}
}
if (!setResultOk) {
Log.w(activity.javaClass.name, "Failed Autofill auth.")
activity.setResult(Activity.RESULT_CANCELED)
}
}
}
fun Intent.addAutofillComponent(context: Context, autofillComponent: AutofillComponent) {
this.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(context)) {
autofillComponent.compatInlineSuggestionsRequest?.let {
this.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
}
StructureParser(autofillComponent.assistStructure).parse()?.let { result ->
// New Response
onIntentCreated(Intent().putExtra(
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
buildResponse(
context = context,
database = database,
entriesInfo = entriesInfo,
parseResult = result,
autofillComponent = autofillComponent
)
))
} ?: throw IOException("Unable to parse the structure")
}
}

View File

@@ -53,7 +53,7 @@ import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.AppUtil
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import org.joda.time.DateTime
@@ -92,10 +92,11 @@ class KeeAutofillService : AutofillService() {
autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this)
}
override fun onFillRequest(request: FillRequest,
cancellationSignal: CancellationSignal,
callback: FillCallback) {
override fun onFillRequest(
request: FillRequest,
cancellationSignal: CancellationSignal,
callback: FillCallback
) {
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
@@ -120,67 +121,64 @@ class KeeAutofillService : AutofillService() {
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
}
AppUtil.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)
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
CompatInlineSuggestionsRequest(request)
} else {
null
}
val autofillComponent = AutofillComponent(
latestStructure,
inlineSuggestionsRequest
)
SearchHelper.checkAutoSearchInfo(
context = this,
database = mDatabase,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
callback.onSuccess(
AutofillHelper.buildResponse(
context = this,
database = openedDatabase,
entriesInfo = items,
parseResult = parseResult,
autofillComponent = autofillComponent
)
)
},
onItemNotFound = { openedDatabase ->
// Show UI if no search result
showUIForEntrySelection(parseResult, openedDatabase,
searchInfo, autofillComponent, callback)
},
onDatabaseClosed = {
// Show UI if database not open
showUIForEntrySelection(parseResult, null,
searchInfo, autofillComponent, callback)
}
)
}
}
}
private fun launchSelection(database: ContextualDatabase?,
searchInfo: SearchInfo,
parseResult: StructureParser.Result,
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
callback: FillCallback) {
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
callback.onSuccess(
AutofillHelper.buildResponse(
this, openedDatabase,
items, parseResult, inlineSuggestionsRequest
)
)
},
onItemNotFound = { openedDatabase ->
// Show UI if no search result
showUIForEntrySelection(parseResult, openedDatabase,
searchInfo, inlineSuggestionsRequest, callback)
},
onDatabaseClosed = {
// Show UI if database not open
showUIForEntrySelection(parseResult, null,
searchInfo, inlineSuggestionsRequest, callback)
}
)
}
@SuppressLint("RestrictedApi")
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
database: ContextualDatabase?,
searchInfo: SearchInfo,
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
callback: FillCallback) {
private fun showUIForEntrySelection(
parseResult: StructureParser.Result,
database: ContextualDatabase?,
searchInfo: SearchInfo,
autofillComponent: AutofillComponent,
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.
AutofillLauncherActivity.getPendingIntentForSelection(this,
searchInfo, inlineSuggestionsRequest)?.intentSender?.let { intentSender ->
AutofillLauncherActivity.getPendingIntentForSelection(
this,
searchInfo,
autofillComponent
)?.intentSender?.let { intentSender ->
val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (database == null) {
if (!parseResult.webDomain.isNullOrEmpty()) {
@@ -271,7 +269,8 @@ class KeeAutofillService : AutofillService() {
&& autofillInlineSuggestionsEnabled
) {
var inlinePresentation: InlinePresentation? = null
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
autofillComponent.compatInlineSuggestionsRequest
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs =
inlineSuggestionsRequest.inlinePresentationSpecs
if (inlineSuggestionsRequest.maxSuggestionCount > 0
@@ -289,7 +288,7 @@ class KeeAutofillService : AutofillService() {
InlineSuggestionUi.newContentBuilder(
PendingIntent.getActivity(
this,
0,
randomRequestCode(),
Intent(this, AutofillSettingsActivity::class.java),
PendingIntent.FLAG_IMMUTABLE
)
@@ -361,7 +360,7 @@ class KeeAutofillService : AutofillService() {
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
var success = false
if (askToSaveData) {
if (askToSaveData && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val latestStructure = request.fillContexts.last().structure
StructureParser(latestStructure).parse(true)?.let { parseResult ->
@@ -387,32 +386,32 @@ class KeeAutofillService : AutofillService() {
}
// Show UI to save data
val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
}
val registerInfo = RegisterInfo(
searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
},
searchInfo = searchInfo,
username = parseResult.usernameValue?.textValue?.toString(),
password = parseResult.passwordValue?.textValue?.toString(),
creditCard =
creditCard = parseResult.creditCardNumber?.let { cardNumber ->
CreditCard(
parseResult.creditCardHolder,
parseResult.creditCardNumber,
cardNumber,
expiration,
parseResult.cardVerificationValue
)
)
}
)
// TODO Callback in each activity #765
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
// registerInfo))
//} else {
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
success = true
callback.onSuccess()
//}
AutofillLauncherActivity.getPendingIntentForRegistration(
this,
registerInfo
)?.intentSender?.let { intentSender ->
success = true
callback.onSuccess(intentSender)
}
}
}
}

View File

@@ -362,8 +362,8 @@ class StructureParser(private val structure: AssistStructure) {
if (result?.passwordId == null) {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
}
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
}
inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {

View File

@@ -43,6 +43,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.FieldsAdapter
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
@@ -461,9 +462,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
}
fun performSelection(items: List<EntryInfo>,
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
fun performSelection(
items: List<EntryInfo>,
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
actionEntrySelection: (autoSearch: Boolean) -> Unit
) {
EntrySelectionHelper.performSelection(
items = items,
actionPopulateCredentialProvider = { itemFound ->
@@ -477,15 +480,5 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
actionEntrySelection = actionEntrySelection
)
}
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
entry: EntryInfo,
toast: Boolean = true) {
// Populate Magikeyboard with entry
addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
// Consume the selection mode
EntrySelectionHelper.removeModesFromIntent(activity.intent)
activity.moveTaskToBack(true)
}
}
}

View File

@@ -93,22 +93,18 @@ class PasskeyProviderService : CredentialProviderService() {
private fun buildPasskeySearchInfo(relyingParty: String): SearchInfo {
return SearchInfo().apply {
this.relyingParty = relyingParty
this.isAPasskeySearch = true
this.query = relyingParty
}
}
override fun onBeginGetCredentialRequest(
request: BeginGetCredentialRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>
) {
Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called")
try {
processGetCredentialsRequest(request)?.let { response ->
processGetCredentialsRequest(request) { response ->
callback.onResult(response)
} ?: run {
callback.onError(GetCredentialUnknownException())
}
} catch (e: Exception) {
Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e)
@@ -116,24 +112,30 @@ class PasskeyProviderService : CredentialProviderService() {
}
}
private fun processGetCredentialsRequest(request: BeginGetCredentialRequest): BeginGetCredentialResponse? {
val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
private fun processGetCredentialsRequest(
request: BeginGetCredentialRequest,
callback: (BeginGetCredentialResponse?) -> Unit
) {
var knownOption = false
for (option in request.beginGetCredentialOptions) {
when (option) {
is BeginGetPublicKeyCredentialOption -> {
credentialEntries.addAll(
populatePasskeyData(option)
)
return BeginGetCredentialResponse(credentialEntries)
knownOption = true
populatePasskeyData(option) { listCredentials ->
callback(BeginGetCredentialResponse(listCredentials))
}
}
}
}
Log.w(javaClass.simpleName, "unknown beginGetCredentialOption")
return null
if (knownOption.not()) {
throw IOException("unknown type of beginGetCredentialOption")
}
}
private fun populatePasskeyData(option: BeginGetPublicKeyCredentialOption): List<CredentialEntry> {
private fun populatePasskeyData(
option: BeginGetPublicKeyCredentialOption,
callback: (List<CredentialEntry>) -> Unit
) {
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
@@ -169,6 +171,7 @@ class PasskeyProviderService : CredentialProviderService() {
)
}
}
callback(passkeyEntries)
},
onItemNotFound = { _ ->
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
@@ -191,6 +194,7 @@ class PasskeyProviderService : CredentialProviderService() {
)
)
}
callback(passkeyEntries)
},
onDatabaseClosed = {
Log.d(TAG, "Add pending intent for passkey selection in closed database")
@@ -213,9 +217,9 @@ class PasskeyProviderService : CredentialProviderService() {
)
)
}
callback(passkeyEntries)
}
)
return passkeyEntries
}
override fun onBeginCreateCredentialRequest(
@@ -225,7 +229,9 @@ class PasskeyProviderService : CredentialProviderService() {
) {
Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
try {
callback.onResult(processCreateCredentialRequest(request))
processCreateCredentialRequest(request) {
callback.onResult(BeginCreateCredentialResponse(it))
}
} catch (e: Exception) {
Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e)
toastError(e)
@@ -233,15 +239,20 @@ class PasskeyProviderService : CredentialProviderService() {
}
}
private fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse {
private fun processCreateCredentialRequest(
request: BeginCreateCredentialRequest,
callback: (List<CreateEntry>) -> Unit
) {
when (request) {
is BeginCreatePublicKeyCredentialRequest -> {
// Request is passkey type
return handleCreatePasskeyQuery(request)
handleCreatePasskeyQuery(request, callback)
}
else -> {
// request type not supported
throw IOException("unknown type of BeginCreateCredentialRequest")
}
}
// request type not supported
throw IOException("unknown type of BeginCreateCredentialRequest")
}
private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry(
@@ -266,9 +277,15 @@ class PasskeyProviderService : CredentialProviderService() {
}
}
private fun handleCreatePasskeyQuery(request: BeginCreatePublicKeyCredentialRequest): BeginCreateCredentialResponse {
val accountName = mDatabase?.name ?: getString(R.string.passkey_database_username)
private fun handleCreatePasskeyQuery(
request: BeginCreatePublicKeyCredentialRequest,
callback: (List<CreateEntry>) -> Unit
) {
val databaseName = mDatabase?.name
val accountName =
if (databaseName?.isBlank() != false)
getString(R.string.passkey_database_username)
else databaseName
val createEntries: MutableList<CreateEntry> = mutableListOf()
val relyingPartyId = PublicKeyCredentialCreationOptions(
requestJson = request.requestJson,
@@ -309,6 +326,7 @@ class PasskeyProviderService : CredentialProviderService() {
}
}*/
}
callback(createEntries)
},
onItemNotFound = { database ->
// To create a new entry
@@ -317,6 +335,7 @@ class PasskeyProviderService : CredentialProviderService() {
} else {
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
}
callback(createEntries)
},
onDatabaseClosed = {
// Launch the passkey launcher activity to open the database
@@ -334,10 +353,9 @@ class PasskeyProviderService : CredentialProviderService() {
)
)
}
callback(createEntries)
}
)
return BeginCreateCredentialResponse(createEntries)
}
override fun onClearCredentialStateRequest(

View File

@@ -24,7 +24,6 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.ParcelUuid
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Log
@@ -44,6 +43,7 @@ import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
import com.kunzisoft.encrypt.Signature
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
@@ -60,7 +60,6 @@ import com.kunzisoft.keepass.model.AndroidOrigin
import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Passkey
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.utils.AppUtil
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
@@ -88,10 +87,7 @@ object PasskeyHelper {
private const val HMAC_TYPE = "HmacSHA256"
private const val EXTRA_SEARCH_INFO = "com.kunzisoft.keepass.extra.searchInfo"
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.nodeId"
private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
@@ -110,38 +106,6 @@ object PasskeyHelper {
private val internalSecureRandom: SecureRandom = SecureRandom()
/**
* Build the Passkey response for one entry
*/
fun Activity.buildPasskeyResponseAndSetResult(
entryInfo: EntryInfo,
extras: Bundle? = null
) {
try {
entryInfo.passkey?.let { passkey ->
val mReplyIntent = Intent()
Log.d(javaClass.name, "Success Passkey manual selection")
mReplyIntent.addPasskey(passkey)
mReplyIntent.addAppOrigin(entryInfo.appOrigin)
mReplyIntent.addNodeId(entryInfo.id)
extras?.let {
mReplyIntent.putExtras(it)
}
setResult(Activity.RESULT_OK, mReplyIntent)
} ?: run {
throw IOException("No passkey found")
}
} catch (e: Exception) {
Log.e(javaClass.name, "Unable to add the passkey as result", e)
Toast.makeText(
this,
getString(R.string.error_passkey_result),
Toast.LENGTH_SHORT
).show()
setResult(Activity.RESULT_CANCELED)
}
}
/**
* Add an authentication code generated by an entry to the intent
*/
@@ -181,22 +145,6 @@ object PasskeyHelper {
return this.removeExtra(EXTRA_PASSKEY)
}
/**
* Add the search info to the intent
*/
fun Intent.addSearchInfo(searchInfo: SearchInfo?) {
searchInfo?.let {
putExtra(EXTRA_SEARCH_INFO, searchInfo)
}
}
/**
* Retrieve the search info from the intent
*/
fun Intent.retrieveSearchInfo(): SearchInfo? {
return this.getParcelableExtraCompat(EXTRA_SEARCH_INFO)
}
/**
* Add the app origin to the intent
*/
@@ -221,21 +169,37 @@ object PasskeyHelper {
}
/**
* Add the node id to the intent, useful for auto passkey selection
* Build the Passkey response for one entry
*/
fun Intent.addNodeId(nodeId: UUID?) {
nodeId?.let {
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId))
fun Activity.buildPasskeyResponseAndSetResult(
entryInfo: EntryInfo,
extras: Bundle? = null
) {
try {
entryInfo.passkey?.let { passkey ->
val mReplyIntent = Intent()
Log.d(javaClass.name, "Success Passkey manual selection")
mReplyIntent.addPasskey(passkey)
mReplyIntent.addAppOrigin(entryInfo.appOrigin)
mReplyIntent.addNodeId(entryInfo.id)
extras?.let {
mReplyIntent.putExtras(it)
}
setResult(Activity.RESULT_OK, mReplyIntent)
} ?: run {
throw IOException("No passkey found")
}
} catch (e: Exception) {
Log.e(javaClass.name, "Unable to add the passkey as result", e)
Toast.makeText(
this,
getString(R.string.error_passkey_result),
Toast.LENGTH_SHORT
).show()
setResult(Activity.RESULT_CANCELED)
}
}
/**
* Retrieve the node id from the intent
*/
fun Intent.retrieveNodeId(): UUID? {
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
}
/**
* Check the timestamp and authentication code transmitted via PendingIntent
*/
@@ -424,11 +388,15 @@ object PasskeyHelper {
* Utility method to create a passkey and the associated creation request parameters
* [intent] allows to retrieve the request
* [context] context to manage package verification files
* [defaultBackupEligibility] the default backup eligibility to add the the passkey entry
* [defaultBackupState] the default backup state to add the the passkey entry
* [passkeyCreated] is called asynchronously when the passkey has been created
*/
suspend fun retrievePasskeyCreationRequestParameters(
intent: Intent,
context: Context,
defaultBackupEligibility: Boolean?,
defaultBackupState: Boolean?,
passkeyCreated: suspend (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
) {
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
@@ -456,7 +424,9 @@ object PasskeyHelper {
privateKeyPem = privateKeyPem,
credentialId = b64Encode(credentialId),
userHandle = b64Encode(userHandle),
relyingParty = relyingParty
relyingParty = relyingParty,
backupEligibility = defaultBackupEligibility,
backupState = defaultBackupState
)
// create new entry in database
@@ -590,8 +560,8 @@ object PasskeyHelper {
requestOptions: PublicKeyCredentialRequestOptions,
clientDataResponse: ClientDataResponse,
passkey: Passkey,
backupEligibility: Boolean,
backupState: Boolean
defaultBackupEligibility: Boolean,
defaultBackupState: Boolean
): PublicKeyCredential {
val getCredentialResponse = FidoPublicKeyCredential(
id = passkey.credentialId,
@@ -599,8 +569,8 @@ object PasskeyHelper {
requestOptions = requestOptions,
userPresent = true,
userVerified = true,
backupEligibility = backupEligibility,
backupState = backupState,
backupEligibility = passkey.backupEligibility ?: defaultBackupEligibility,
backupState = passkey.backupState ?: defaultBackupState,
userHandle = passkey.userHandle,
privateKey = passkey.privateKeyPem,
clientDataResponse = clientDataResponse

View File

@@ -0,0 +1,279 @@
package com.kunzisoft.keepass.credentialprovider.viewmodel
import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK
import android.app.Application
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.annotation.RequiresApi
import androidx.lifecycle.viewModelScope
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveAndRemoveEntries
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.retrieveAutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
@RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherViewModel(application: Application): CredentialLauncherViewModel(application) {
private var mAutofillComponent: AutofillComponent? = null
private var mLockDatabaseAfterSelection: Boolean = false
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
fun initialize() {
mLockDatabaseAfterSelection = PreferencesUtil.isAutofillCloseDatabaseEnable(getApplication())
}
override fun onResult() {
super.onResult()
mAutofillComponent = null
}
override suspend fun launchAction(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
// Retrieve selection mode
when (intent.retrieveSpecialMode()) {
SpecialMode.SELECTION -> {
val searchInfo = intent.retrieveSearchInfo()
if (searchInfo == null)
throw IOException("Search info is null")
mAutofillComponent = intent.retrieveAutofillComponent()
// Build search param
launchSelection(database, mAutofillComponent, searchInfo)
}
SpecialMode.REGISTRATION -> {
// To register info
val registerInfo = intent.retrieveRegisterInfo()
if (registerInfo == null)
throw IOException("Register info is null")
launchRegistration(database, registerInfo)
}
else -> {
// Not an autofill call
cancelResult()
}
}
}
private suspend fun launchSelection(
database: ContextualDatabase?,
autofillComponent: AutofillComponent?,
searchInfo: SearchInfo
) {
withContext(Dispatchers.IO) {
if (autofillComponent == null) {
throw IOException("Autofill component is null")
}
if (KeeAutofillService.autofillAllowedFor(
applicationId = searchInfo.applicationId,
webDomain = searchInfo.webDomain,
context = getApplication()
)
) {
// If database is open
SearchHelper.checkAutoSearchInfo(
context = getApplication(),
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
// Items found
if (autofillComponent.compatInlineSuggestionsRequest != null) {
mUiState.value = UIState.ShowAutofillSuggestionMessage
}
AutofillHelper.buildResponse(
context = getApplication(),
autofillComponent = autofillComponent,
database = openedDatabase,
entriesInfo = items
) { intent ->
setResult(intent, lockDatabase = mLockDatabaseAfterSelection)
}
},
onItemNotFound = { openedDatabase ->
// Show the database UI to select the entry
mCredentialUiState.value =
CredentialState.LaunchGroupActivityForSelection(
database = openedDatabase,
searchInfo = searchInfo,
typeMode = TypeMode.AUTOFILL
)
},
onDatabaseClosed = {
// If database not open
mCredentialUiState.value =
CredentialState.LaunchFileDatabaseSelectActivityForSelection(
searchInfo = searchInfo,
typeMode = TypeMode.AUTOFILL
)
}
)
} else {
mUiState.value = UIState.ShowBlockRestartMessage
}
}
}
override fun manageSelectionResult(
database: ContextualDatabase,
activityResult: ActivityResult
) {
super.manageSelectionResult(database, activityResult)
val intent = activityResult.data
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create selection response for autofill", e)
showError(e)
}) {
when (activityResult.resultCode) {
RESULT_OK -> {
withContext(Dispatchers.IO) {
Log.d(TAG, "Autofill selection result")
if (intent == null)
throw IOException("Intent is null")
val entries = intent.retrieveAndRemoveEntries(database)
val autofillComponent = mAutofillComponent
if (autofillComponent == null)
throw IOException("Autofill component is null")
withContext(Dispatchers.Main) {
AutofillHelper.buildResponse(
context = getApplication(),
autofillComponent = autofillComponent,
database = database,
entriesInfo = entries
) { intent ->
setResult(intent, lockDatabase = mLockDatabaseAfterSelection)
}
}
}
}
RESULT_CANCELED -> {
withContext(Dispatchers.Main) {
cancelResult()
}
}
}
}
}
// -------------
// Registration
// -------------
private fun launchRegistration(
database: ContextualDatabase?,
registerInfo: RegisterInfo
) {
val searchInfo = registerInfo.searchInfo
if (KeeAutofillService.autofillAllowedFor(
applicationId = searchInfo.applicationId,
webDomain = searchInfo.webDomain,
context = getApplication()
)) {
val readOnly = database?.isReadOnly != false
SearchHelper.checkAutoSearchInfo(
context = getApplication(),
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, _ ->
if (!readOnly) {
// Show the database UI to select the entry
mCredentialUiState.value =
CredentialState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
} else {
mCredentialUiState.value = CredentialState.ShowError(
RegisterInReadOnlyDatabaseException()
)
}
},
onItemNotFound = { openedDatabase ->
if (!readOnly) {
// Show the database UI to select the entry
mCredentialUiState.value =
CredentialState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
} else {
mCredentialUiState.value = CredentialState.ShowError(
RegisterInReadOnlyDatabaseException()
)
}
},
onDatabaseClosed = {
// If database not open
mCredentialUiState.value =
CredentialState.LaunchFileDatabaseSelectActivityForRegistration(
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
}
)
} else {
mUiState.value = UIState.ShowBlockRestartMessage
}
}
override fun manageRegistrationResult(activityResult: ActivityResult) {
isResultLauncherRegistered = false
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create registration response for autofill", e)
showError(e)
}) {
val responseIntent = Intent()
when (activityResult.resultCode) {
RESULT_OK -> {
Log.d(TAG, "Autofill registration result")
withContext(Dispatchers.Main) {
setResult(responseIntent)
}
}
RESULT_CANCELED -> {
withContext(Dispatchers.Main) {
cancelResult()
}
}
}
}
}
sealed class UIState {
object Loading: UIState()
object ShowBlockRestartMessage: UIState()
object ShowAutofillSuggestionMessage: UIState()
}
companion object {
private val TAG = AutofillLauncherViewModel::class.java.name
}
}

View File

@@ -0,0 +1,151 @@
package com.kunzisoft.keepass.credentialprovider.viewmodel
import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK
import android.app.Application
import android.content.Intent
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
abstract class CredentialLauncherViewModel(application: Application): AndroidViewModel(application) {
protected var mDatabase: ContextualDatabase? = null
protected var isResultLauncherRegistered: Boolean = false
private var mSelectionResult: ActivityResult? = null
protected val mCredentialUiState = MutableStateFlow<CredentialState>(CredentialState.Loading)
val credentialUiState: StateFlow<CredentialState> = mCredentialUiState
fun showError(error: Throwable) {
Log.e(TAG, "Error on credential provider launch", error)
mCredentialUiState.value = CredentialState.ShowError(error)
}
open fun onResult() {
isResultLauncherRegistered = false
mSelectionResult = null
}
fun setResult(intent: Intent, lockDatabase: Boolean = false) {
// Remove the launcher register
onResult()
mCredentialUiState.value = CredentialState.SetActivityResult(
lockDatabase = lockDatabase,
resultCode = RESULT_OK,
data = intent
)
}
fun cancelResult(lockDatabase: Boolean = false) {
onResult()
mCredentialUiState.value = CredentialState.SetActivityResult(
lockDatabase = lockDatabase,
resultCode = RESULT_CANCELED
)
}
private fun onDatabaseRetrieved(database: ContextualDatabase) {
mDatabase = database
mSelectionResult?.let { selectionResult ->
manageSelectionResult(database, selectionResult)
}
}
fun manageSelectionResult(activityResult: ActivityResult) {
// Waiting for the database if needed
when (activityResult.resultCode) {
RESULT_OK -> {
mSelectionResult = activityResult
mDatabase?.let { database ->
manageSelectionResult(database, activityResult)
}
}
RESULT_CANCELED -> {
cancelResult()
}
}
}
open fun manageSelectionResult(database: ContextualDatabase, activityResult: ActivityResult) {
mSelectionResult = null
}
open fun manageRegistrationResult(activityResult: ActivityResult) {}
open fun onExceptionOccurred(e: Throwable) {
showError(e)
}
open fun launchActionIfNeeded(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
if (database != null) {
onDatabaseRetrieved(database)
}
if (isResultLauncherRegistered.not()) {
isResultLauncherRegistered = true
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
onExceptionOccurred(e)
}) {
launchAction(intent, specialMode, database)
}
}
}
/**
* Launch the main action
*/
protected abstract suspend fun launchAction(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
)
sealed class CredentialState {
object Loading : CredentialState()
data class LaunchGroupActivityForSelection(
val database: ContextualDatabase,
val searchInfo: SearchInfo?,
val typeMode: TypeMode
): CredentialState()
data class LaunchGroupActivityForRegistration(
val database: ContextualDatabase,
val registerInfo: RegisterInfo?,
val typeMode: TypeMode
): CredentialState()
data class LaunchFileDatabaseSelectActivityForSelection(
val searchInfo: SearchInfo?,
val typeMode: TypeMode
): CredentialState()
data class LaunchFileDatabaseSelectActivityForRegistration(
val registerInfo: RegisterInfo?,
val typeMode: TypeMode
): CredentialState()
data class SetActivityResult(
val lockDatabase: Boolean,
val resultCode: Int,
val data: Intent? = null
): CredentialState()
data class ShowError(
val error: Throwable
): CredentialState()
}
companion object {
private val TAG = CredentialLauncherViewModel::class.java.name
}
}

View File

@@ -0,0 +1,297 @@
package com.kunzisoft.keepass.credentialprovider.viewmodel
import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK
import android.app.Application
import android.content.Intent
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.core.net.toUri
import androidx.lifecycle.viewModelScope
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveAndRemoveEntries
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNodeId
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
class EntrySelectionViewModel(application: Application): CredentialLauncherViewModel(application) {
private var searchShareForMagikeyboard: Boolean = false
private var mLockDatabaseAfterSelection: Boolean = false
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
fun initialize() {
searchShareForMagikeyboard = getApplication<Application>().isKeyboardActivatedInSettings()
mLockDatabaseAfterSelection = false // TODO Close database after selection
}
override fun launchActionIfNeeded(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
// Launch with database when a nodeId is present
if ((database != null && database.loaded) || intent.retrieveNodeId() == null) {
super.launchActionIfNeeded(intent, specialMode, database)
}
}
override suspend fun launchAction(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
val searchInfo: SearchInfo? = intent.retrieveSearchInfo()
if (searchInfo != null) {
launch(database, searchInfo)
} else {
// To manage share
var sharedWebDomain: String? = null
var otpString: String? = null
when (intent.action) {
Intent.ACTION_SEND -> {
if ("text/plain" == intent.type) {
// Retrieve web domain or OTP
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
else
sharedWebDomain = extra.toUri().host
}
}
launchSelection(database, sharedWebDomain, otpString)
}
Intent.ACTION_VIEW -> {
// Retrieve OTP
intent.dataString?.let { extra ->
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
}
launchSelection(database, null, otpString)
}
else -> {
if (database != null && database.loaded) {
mUiState.value = UIState.LaunchGroupActivityForSearch(
database = database,
searchInfo = SearchInfo()
)
} else {
mUiState.value = UIState.LaunchFileDatabaseSelectForSearch(
searchInfo = SearchInfo()
)
}
}
}
}
}
// -------------
// Selection
// -------------
private fun launchSelection(
database: ContextualDatabase?,
sharedWebDomain: String?,
otpString: String?
) {
// Build domain search param
val searchInfo = SearchInfo().apply {
this.webDomain = sharedWebDomain
this.otpString = otpString
}
launch(database, searchInfo)
}
private fun launch(
database: ContextualDatabase?,
searchInfo: SearchInfo
) {
// If database is open
val readOnly = database?.isReadOnly != false
SearchHelper.checkAutoSearchInfo(
context = getApplication(),
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
// Items found
if (searchInfo.otpString != null) {
if (!readOnly) {
mCredentialUiState.value =
CredentialState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = searchInfo.toRegisterInfo(),
typeMode = TypeMode.DEFAULT
)
} else {
mCredentialUiState.value = CredentialState.ShowError(
RegisterInReadOnlyDatabaseException()
)
}
} else if (searchShareForMagikeyboard) {
MagikeyboardService.performSelection(
items,
{ entryInfo ->
populateKeyboard(entryInfo)
},
{ autoSearch ->
mCredentialUiState.value = CredentialState.LaunchGroupActivityForSelection(
database = openedDatabase,
searchInfo = searchInfo,
typeMode = TypeMode.MAGIKEYBOARD
)
}
)
} else {
mUiState.value = UIState.LaunchGroupActivityForSearch(
database = openedDatabase,
searchInfo = searchInfo
)
}
},
onItemNotFound = { openedDatabase ->
// Show the database UI to select the entry
if (searchInfo.otpString != null) {
if (!readOnly) {
mCredentialUiState.value =
CredentialState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = searchInfo.toRegisterInfo(),
typeMode = TypeMode.DEFAULT
)
} else {
mCredentialUiState.value = CredentialState.ShowError(
RegisterInReadOnlyDatabaseException()
)
}
} else if (searchShareForMagikeyboard) {
mCredentialUiState.value = CredentialState.LaunchGroupActivityForSelection(
database = openedDatabase,
searchInfo = searchInfo,
typeMode = TypeMode.MAGIKEYBOARD
)
} else {
mUiState.value = UIState.LaunchGroupActivityForSearch(
database = openedDatabase,
searchInfo = searchInfo
)
}
},
onDatabaseClosed = {
// If database not open
if (searchInfo.otpString != null) {
mCredentialUiState.value = CredentialState.LaunchFileDatabaseSelectActivityForRegistration(
registerInfo = searchInfo.toRegisterInfo(),
typeMode = TypeMode.DEFAULT
)
} else if (searchShareForMagikeyboard) {
mCredentialUiState.value = CredentialState.LaunchFileDatabaseSelectActivityForSelection(
searchInfo = searchInfo,
typeMode = TypeMode.MAGIKEYBOARD
)
} else {
mUiState.value = UIState.LaunchFileDatabaseSelectForSearch(
searchInfo = searchInfo
)
}
}
)
}
private fun populateKeyboard(entryInfo: EntryInfo) {
// Automatically populate keyboard
mUiState.value = UIState.PopulateKeyboard(entryInfo)
setResult(Intent(), lockDatabase = mLockDatabaseAfterSelection)
}
override fun manageSelectionResult(
database: ContextualDatabase,
activityResult: ActivityResult
) {
super.manageSelectionResult(database, activityResult)
val intent = activityResult.data
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create selection response for Magikeyboard", e)
showError(e)
}) {
when (activityResult.resultCode) {
RESULT_OK -> {
withContext(Dispatchers.IO) {
Log.d(TAG, "Magikeyboard selection result")
if (intent == null)
throw IOException("Intent is null")
val entries = intent.retrieveAndRemoveEntries(database)
withContext(Dispatchers.Main) {
// Populate Magikeyboard with entry
entries.firstOrNull()?.let { entryInfo ->
populateKeyboard(entryInfo)
} // TODO Manage multiple entries in Magikeyboard
}
}
}
RESULT_CANCELED -> {
withContext(Dispatchers.Main) {
cancelResult()
}
}
}
}
}
override fun manageRegistrationResult(activityResult: ActivityResult) {
super.manageRegistrationResult(activityResult)
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create selection response for Magikeyboard", e)
showError(e)
}) {
when (activityResult.resultCode) {
RESULT_OK -> {
// Empty data result
// TODO Show Toast indicating value is saved
withContext(Dispatchers.Main) {
setResult(Intent(), lockDatabase = false)
}
}
RESULT_CANCELED -> {
withContext(Dispatchers.Main) {
cancelResult()
}
}
}
}
}
sealed class UIState {
object Loading: UIState()
data class PopulateKeyboard(
val entryInfo: EntryInfo
): UIState()
data class LaunchFileDatabaseSelectForSearch(
val searchInfo: SearchInfo
): UIState()
data class LaunchGroupActivityForSearch(
val database: ContextualDatabase,
val searchInfo: SearchInfo
): UIState()
}
companion object {
private val TAG = EntrySelectionViewModel::class.java.name
}
}

View File

@@ -0,0 +1,147 @@
package com.kunzisoft.keepass.credentialprovider.viewmodel
import android.app.Activity.RESULT_OK
import android.app.Application
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.activity.result.ActivityResult
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity.Companion.isHardwareKeyAvailable
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.hardware.HardwareKey
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class HardwareKeyLauncherViewModel(application: Application): CredentialLauncherViewModel(application) {
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
override suspend fun launchAction(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
val hardwareKey = HardwareKey.Companion.getHardwareKeyFromString(
intent.getStringExtra(DATA_HARDWARE_KEY)
)
if (isHardwareKeyAvailable(getApplication(), hardwareKey)) {
when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
throw Exception("FIDO2 not implemented")
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
launchYubikeyChallengeForResponse(intent.getByteArrayExtra(DATA_SEED))
}
else -> {
UIState.OnChallengeResponded(null)
}
}
} else {
mUiState.value = UIState.ShowHardwareKeyDriverNeeded(hardwareKey)
}
}
private fun launchYubikeyChallengeForResponse(seed: ByteArray?) {
// Transform the seed before sending
var challenge: ByteArray? = null
if (seed != null) {
challenge = ByteArray(64)
seed.copyInto(challenge, 0, 0, 32)
challenge.fill(32, 32, 64)
}
mUiState.value = UIState.LaunchChallengeActivityForResponse(challenge)
Log.d(TAG, "Challenge sent")
}
override fun manageSelectionResult(
database: ContextualDatabase,
activityResult: ActivityResult
) {
super.manageSelectionResult(database, activityResult)
if (activityResult.resultCode == RESULT_OK) {
val challengeResponse: ByteArray? =
activityResult.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
Log.d(TAG, "Response form challenge")
mUiState.value = UIState.OnChallengeResponded(challengeResponse)
} else {
Log.e(TAG, "Response from challenge error")
mUiState.value = UIState.OnChallengeResponded(null)
}
}
sealed class UIState {
object Loading : UIState()
data class ShowHardwareKeyDriverNeeded(
val hardwareKey: HardwareKey?
): UIState()
data class LaunchChallengeActivityForResponse(
val challenge: ByteArray?,
): UIState() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as LaunchChallengeActivityForResponse
return challenge.contentEquals(other.challenge)
}
override fun hashCode(): Int {
return challenge?.contentHashCode() ?: 0
}
}
data class OnChallengeResponded(
val response: ByteArray?
): UIState() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as OnChallengeResponded
return response.contentEquals(other.response)
}
override fun hashCode(): Int {
return response?.contentHashCode() ?: 0
}
}
}
companion object {
private val TAG = HardwareKeyLauncherViewModel::class.java.name
private const val DATA_HARDWARE_KEY = "DATA_HARDWARE_KEY"
private const val DATA_SEED = "DATA_SEED"
// Driver call
private const val YUBIKEY_CHALLENGE_RESPONSE_INTENT = "android.yubikey.intent.action.CHALLENGE_RESPONSE"
private const val HARDWARE_KEY_CHALLENGE_KEY = "challenge"
private const val HARDWARE_KEY_RESPONSE_KEY = "response"
fun isYubikeyDriverAvailable(context: Context): Boolean {
return Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT)
.resolveActivity(context.packageManager) != null
}
fun buildHardwareKeyChallenge(challenge: ByteArray?): Intent {
return Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
}
}
fun Intent.addHardwareKey(hardwareKey: HardwareKey) {
putExtra(DATA_HARDWARE_KEY, hardwareKey.value)
}
fun Intent.addSeed(seed: ByteArray?) {
putExtra(DATA_SEED, seed)
}
}
}

View File

@@ -11,8 +11,11 @@ import androidx.annotation.RequiresApi
import androidx.credentials.GetCredentialResponse
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.PendingIntentHandler
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeNodeId
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNodeId
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
@@ -25,11 +28,9 @@ import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVe
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps
import com.kunzisoft.keepass.database.ContextualDatabase
@@ -56,22 +57,21 @@ import java.io.InvalidObjectException
import java.util.UUID
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
class PasskeyLauncherViewModel(application: Application): AndroidViewModel(application) {
class PasskeyLauncherViewModel(application: Application): CredentialLauncherViewModel(application) {
private var mUsageParameters: PublicKeyCredentialUsageParameters? = null
private var mCreationParameters: PublicKeyCredentialCreationParameters? = null
private var mPasskey: Passkey? = null
private var mLockDatabaseAfterSelection: Boolean = false
private var mBackupEligibility: Boolean = true
private var mBackupState: Boolean = false
private var mLockDatabase: Boolean = true
private var isResultLauncherRegistered: Boolean = false
private val _uiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = _uiState
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
fun initialize() {
mLockDatabaseAfterSelection = PreferencesUtil.isPasskeyCloseDatabaseEnable(getApplication())
mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication())
mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication())
}
@@ -79,19 +79,14 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
fun showAppPrivilegedDialog(
temptingApp: AndroidPrivilegedApp
) {
_uiState.value = UIState.ShowAppPrivilegedDialog(temptingApp)
mUiState.value = UIState.ShowAppPrivilegedDialog(temptingApp)
}
fun showAppSignatureDialog(
temptingApp: AppOrigin,
nodeId: UUID
) {
_uiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId)
}
fun showError(error: Throwable) {
Log.e(TAG, "Error on passkey launch", error)
_uiState.value = UIState.ShowError(error)
mUiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId)
}
fun saveCustomPrivilegedApp(
@@ -107,7 +102,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
context = getApplication(),
privilegedApps = listOf(temptingApp)
)
launchPasskeyAction(
launchAction(
intent = intent,
specialMode = specialMode,
database = database
@@ -139,54 +134,33 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
)
entryInfo.saveAppOrigin(database, temptingApp)
newEntry.setEntryInfo(database, entryInfo)
_uiState.value = UIState.UpdateEntry(
mUiState.value = UIState.UpdateEntry(
oldEntry = entry,
newEntry = newEntry
)
}
}
fun setResult(intent: Intent) {
// Remove the launcher register
isResultLauncherRegistered = false
_uiState.value = UIState.SetActivityResult(
lockDatabase = mLockDatabase,
resultCode = RESULT_OK,
data = intent
)
override fun onExceptionOccurred(e: Throwable) {
if (e is PrivilegedAllowLists.PrivilegedException) {
showAppPrivilegedDialog(e.temptingApp)
} else {
super.onExceptionOccurred(e)
}
}
fun cancelResult() {
isResultLauncherRegistered = false
_uiState.value = UIState.SetActivityResult(
lockDatabase = mLockDatabase,
resultCode = RESULT_CANCELED
)
}
fun launchPasskeyActionIfNeeded(
override fun launchActionIfNeeded(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
if (isResultLauncherRegistered.not()) {
isResultLauncherRegistered = true
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
if (e is PrivilegedAllowLists.PrivilegedException) {
showAppPrivilegedDialog(e.temptingApp)
} else {
showError(e)
}
}) {
launchPasskeyAction(intent, specialMode, database)
}
// Launch with database when a nodeId is present
if ((database != null && database.loaded) || intent.retrieveNodeId() == null) {
super.launchActionIfNeeded(intent, specialMode, database)
}
}
/**
* Launch the main action to manage Passkey
*/
private suspend fun launchPasskeyAction(
override suspend fun launchAction(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
@@ -194,6 +168,9 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
val nodeId = intent.retrieveNodeId()
intent.removeInfo()
intent.removeAppOrigin()
intent.removeNodeId()
checkSecurity(intent, nodeId)
when (specialMode) {
SpecialMode.SELECTION -> {
@@ -260,15 +237,19 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
TAG, "No Passkey found for selection," +
"launch manual selection in opened database"
)
_uiState.value = UIState.LaunchGroupActivityForSelection(
database = openedDatabase
)
mCredentialUiState.value =
CredentialState.LaunchGroupActivityForSelection(
database = openedDatabase,
searchInfo = searchInfo,
typeMode = TypeMode.PASSKEY
)
},
onDatabaseClosed = {
Log.d(TAG, "Manual passkey selection in closed database")
_uiState.value =
UIState.LaunchFileDatabaseSelectActivityForSelection(
searchInfo = searchInfo
mCredentialUiState.value =
CredentialState.LaunchFileDatabaseSelectActivityForSelection(
searchInfo = searchInfo,
typeMode = TypeMode.PASSKEY
)
}
)
@@ -326,12 +307,12 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
appOrigin = appOrigin
),
passkey = passkey,
backupEligibility = mBackupEligibility,
backupState = mBackupState
defaultBackupEligibility = mBackupEligibility,
defaultBackupState = mBackupState
)
)
)
setResult(result)
setResult(result, lockDatabase = mLockDatabaseAfterSelection)
} catch (e: SignatureNotFoundException) {
// Request the dialog if signature exception
showAppSignatureDialog(e.temptingApp, nodeId)
@@ -340,9 +321,11 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
}
}
fun manageSelectionResult(
override fun manageSelectionResult(
database: ContextualDatabase,
activityResult: ActivityResult
) {
super.manageSelectionResult(database, activityResult)
val intent = activityResult.data
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create selection response for passkey", e)
@@ -380,8 +363,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
appOrigin = appOrigin
),
passkey = passkey,
backupEligibility = mBackupEligibility,
backupState = mBackupState
defaultBackupEligibility = mBackupEligibility,
defaultBackupState = mBackupState
)
)
)
@@ -389,7 +372,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
throw IOException("Usage parameters is null")
}
withContext(Dispatchers.Main) {
setResult(responseIntent)
setResult(responseIntent, lockDatabase = mLockDatabaseAfterSelection)
}
}
}
@@ -417,6 +400,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
retrievePasskeyCreationRequestParameters(
intent = intent,
context = getApplication(),
defaultBackupEligibility = mBackupEligibility,
defaultBackupState = mBackupState,
passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters ->
// Save the requested parameters
mPasskey = passkey
@@ -440,24 +425,26 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
TAG, "Passkey found for registration, " +
"but launch manual registration for a new entry"
)
_uiState.value = UIState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
mCredentialUiState.value =
CredentialState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
},
onItemNotFound = { openedDatabase ->
Log.d(TAG, "Launch new manual registration in opened database")
_uiState.value = UIState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
mCredentialUiState.value =
CredentialState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
},
onDatabaseClosed = {
Log.d(TAG, "Manual passkey registration in closed database")
_uiState.value =
UIState.LaunchFileDatabaseSelectActivityForRegistration(
mCredentialUiState.value =
CredentialState.LaunchFileDatabaseSelectActivityForRegistration(
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
@@ -490,7 +477,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
}
}
fun manageRegistrationResult(activityResult: ActivityResult) {
override fun manageRegistrationResult(activityResult: ActivityResult) {
val intent = activityResult.data
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create registration response for passkey", e)
@@ -518,8 +505,10 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
intent = responseIntent,
response = buildCreatePublicKeyCredentialResponse(
publicKeyCredentialCreationParameters = it,
backupEligibility = mBackupEligibility,
backupState = mBackupState
backupEligibility = passkey?.backupEligibility
?: mBackupEligibility,
backupState = passkey?.backupState
?: mBackupState
)
)
}
@@ -549,29 +538,6 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
val temptingApp: AppOrigin,
val nodeId: UUID
): UIState()
data class LaunchGroupActivityForSelection(
val database: ContextualDatabase
): UIState()
data class LaunchGroupActivityForRegistration(
val database: ContextualDatabase,
val registerInfo: RegisterInfo,
val typeMode: TypeMode
): UIState()
data class LaunchFileDatabaseSelectActivityForSelection(
val searchInfo: SearchInfo
): UIState()
data class LaunchFileDatabaseSelectActivityForRegistration(
val registerInfo: RegisterInfo,
val typeMode: TypeMode
): UIState()
data class SetActivityResult(
val lockDatabase: Boolean,
val resultCode: Int,
val data: Intent? = null
): UIState()
data class ShowError(
val error: Throwable
): UIState()
data class UpdateEntry(
val oldEntry: Entry,
val newEntry: Entry

View File

@@ -19,7 +19,6 @@
*/
package com.kunzisoft.keepass.database
import android.Manifest
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
@@ -29,23 +28,15 @@ import android.content.Context.BIND_IMPORTANT
import android.content.Intent
import android.content.IntentFilter
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.widget.Toast
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
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Entry
@@ -55,7 +46,6 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK
@@ -89,13 +79,9 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.utils.putParcelableList
import kotlinx.coroutines.launch
import java.util.UUID
/**
@@ -103,121 +89,29 @@ import java.util.UUID
* Useful to retrieve a database instance and sending tasks commands
*/
class DatabaseTaskProvider(
private var context: Context,
private var showDialog: Boolean = true
private var context: Context
) {
// To show dialog only if context is an activity
private var activity: FragmentActivity? = try {
context as? FragmentActivity?
} catch (_: Exception) {
null
}
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
var onActionFinish: ((
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) -> Unit)? = null
private var intentDatabaseTask: Intent = Intent(
context.applicationContext,
DatabaseTaskNotificationService::class.java
)
var onStartActionRequested: ((bundle: Bundle?, actionTask: String) -> Unit)? = null
var actionTaskListener: DatabaseTaskNotificationService.ActionTaskListener? = null
var databaseInfoListener: DatabaseTaskNotificationService.DatabaseInfoListener? = null
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
private var serviceConnection: ServiceConnection? = null
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
fun destroy() {
this.activity = null
this.onDatabaseRetrieved = null
this.onActionFinish = null
this.databaseTaskBroadcastReceiver = null
this.mBinder = null
this.serviceConnection = null
this.progressTaskDialogFragment = null
this.databaseChangedDialogFragment = null
}
private val actionTaskListener = object : DatabaseTaskNotificationService.ActionTaskListener {
override fun onActionStarted(
database: ContextualDatabase,
progressMessage: ProgressMessage
) {
if (showDialog)
startDialog(progressMessage)
}
override fun onActionUpdated(
database: ContextualDatabase,
progressMessage: ProgressMessage
) {
if (showDialog)
updateDialog(progressMessage)
}
override fun onActionStopped(
database: ContextualDatabase
) {
// Remove the progress task
stopDialog()
}
override fun onActionFinished(
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
onActionFinish?.invoke(database, actionTask, result)
onActionStopped(database)
}
}
private val mActionDatabaseListener =
object : DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
override fun validateDatabaseChanged() {
mBinder?.getService()?.saveDatabaseInfo()
}
}
private var databaseInfoListener = object :
DatabaseTaskNotificationService.DatabaseInfoListener {
override fun onDatabaseInfoChanged(
previousDatabaseInfo: SnapFileDatabaseInfo,
newDatabaseInfo: SnapFileDatabaseInfo,
readOnlyDatabase: Boolean
) {
activity?.let { activity ->
activity.lifecycleScope.launch {
if (databaseChangedDialogFragment == null) {
databaseChangedDialogFragment = activity.supportFragmentManager
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
databaseChangedDialogFragment?.actionDatabaseListener =
mActionDatabaseListener
}
if (progressTaskDialogFragment == null) {
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
previousDatabaseInfo,
newDatabaseInfo,
readOnlyDatabase
)
databaseChangedDialogFragment?.actionDatabaseListener =
mActionDatabaseListener
databaseChangedDialogFragment?.show(
activity.supportFragmentManager,
DATABASE_CHANGED_DIALOG_TAG
)
}
}
}
}
fun onDatabaseChangeValidated() {
mBinder?.getService()?.saveDatabaseInfo()
}
private var databaseListener = object : DatabaseTaskNotificationService.DatabaseListener {
@@ -226,48 +120,18 @@ class DatabaseTaskProvider(
}
}
private fun startDialog(progressMessage: ProgressMessage) {
activity?.let { activity ->
activity.lifecycleScope.launch {
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = activity.supportFragmentManager
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
}
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = ProgressTaskDialogFragment()
progressTaskDialogFragment?.show(
activity.supportFragmentManager,
PROGRESS_TASK_DIALOG_TAG
)
}
updateDialog(progressMessage)
}
}
}
private fun updateDialog(progressMessage: ProgressMessage) {
progressTaskDialogFragment?.apply {
updateTitle(progressMessage.titleId)
updateMessage(progressMessage.messageId)
updateWarning(progressMessage.warningId)
setCancellable(progressMessage.cancelable)
}
}
private fun stopDialog() {
progressTaskDialogFragment?.dismissAllowingStateLoss()
progressTaskDialogFragment = null
}
private fun initServiceConnection() {
actionTaskListener?.onActionStopped()
if (serviceConnection == null) {
serviceConnection = object : ServiceConnection {
override fun onBindingDied(name: ComponentName?) {
stopDialog()
actionTaskListener?.onActionStopped()
onDatabaseRetrieved?.invoke(null)
}
override fun onNullBinding(name: ComponentName?) {
stopDialog()
actionTaskListener?.onActionStopped()
onDatabaseRetrieved?.invoke(null)
}
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
@@ -290,21 +154,33 @@ class DatabaseTaskProvider(
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
service?.addDatabaseListener(databaseListener)
service?.addDatabaseFileInfoListener(databaseInfoListener)
service?.addActionTaskListener(actionTaskListener)
databaseInfoListener?.let { infoListener ->
service?.addDatabaseFileInfoListener(infoListener)
}
actionTaskListener?.let { taskListener ->
service?.addActionTaskListener(taskListener)
}
}
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
service?.removeActionTaskListener(actionTaskListener)
service?.removeDatabaseFileInfoListener(databaseInfoListener)
actionTaskListener?.let { taskListener ->
service?.removeActionTaskListener(taskListener)
}
databaseInfoListener?.let { infoListener ->
service?.removeDatabaseFileInfoListener(infoListener)
}
service?.removeDatabaseListener(databaseListener)
onDatabaseRetrieved?.invoke(null)
}
private fun bindService() {
initServiceConnection()
serviceConnection?.let {
context.bindService(
intentDatabaseTask,
Intent(
context.applicationContext,
DatabaseTaskNotificationService::class.java
),
it,
BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT
)
@@ -368,58 +244,9 @@ class DatabaseTaskProvider(
}
}
private val tempServiceParameters = mutableListOf<Pair<Bundle?, String>>()
private val requestPermissionLauncher = activity?.registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { _ ->
// Whether or not the user has accepted, the service can be started,
// There just won't be any notification if it's not allowed.
tempServiceParameters.removeFirstOrNull()?.let {
startService(it.first, it.second)
}
}
private fun start(bundle: Bundle? = null, actionTask: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val contextActivity = activity
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED
) {
startService(bundle, actionTask)
} else if (contextActivity != null && shouldShowRequestPermissionRationale(
contextActivity,
Manifest.permission.POST_NOTIFICATIONS
)
) {
// it's not the first time, so the user deliberately chooses not to display the notification
startService(bundle, actionTask)
} else {
AlertDialog.Builder(context)
.setMessage(R.string.warning_database_notification_permission)
.setNegativeButton(R.string.later) { _, _ ->
// Refuses the notification, so start the service
startService(bundle, actionTask)
}
.setPositiveButton(R.string.ask) { _, _ ->
// Save the temp parameters to ask the permission
tempServiceParameters.add(Pair(bundle, actionTask))
requestPermissionLauncher?.launch(Manifest.permission.POST_NOTIFICATIONS)
}.create().show()
}
} else {
startService(bundle, actionTask)
}
}
private fun startService(bundle: Bundle? = null, actionTask: String) {
try {
if (bundle != null)
intentDatabaseTask.putExtras(bundle)
intentDatabaseTask.action = actionTask
context.startService(intentDatabaseTask)
} catch (e: Exception) {
Log.e(TAG, "Unable to perform database action", e)
Toast.makeText(context, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
onStartActionRequested?.invoke(bundle, actionTask) ?: run {
context.startDatabaseService(bundle, actionTask)
}
}
@@ -842,5 +669,21 @@ class DatabaseTaskProvider(
companion object {
private val TAG = DatabaseTaskProvider::class.java.name
fun Context.startDatabaseService(bundle: Bundle? = null, actionTask: String) {
try {
val intentDatabaseTask = Intent(
applicationContext,
DatabaseTaskNotificationService::class.java
)
if (bundle != null)
intentDatabaseTask.putExtras(bundle)
intentDatabaseTask.action = actionTask
startService(intentDatabaseTask)
} catch (e: Exception) {
Log.e(TAG, "Unable to perform database action", e)
Toast.makeText(this, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
}
}
}
}

View File

@@ -47,6 +47,8 @@ import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
import com.kunzisoft.keepass.database.exception.VersionDatabaseException
import com.kunzisoft.keepass.database.exception.XMLMalformedDatabaseException
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_CREDENTIAL_ID
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_FLAG_BE
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_FLAG_BS
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_PRIVATE_KEY
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_RELYING_PARTY
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USERNAME
@@ -146,6 +148,8 @@ fun TemplateField.getLocalizedName(context: Context?, name: String): String {
FIELD_CREDENTIAL_ID.equals(name, true) -> context.getString(R.string.passkey_credential_id)
FIELD_USER_HANDLE.equals(name, true) -> context.getString(R.string.passkey_user_handle)
FIELD_RELYING_PARTY.equals(name, true) -> context.getString(R.string.passkey_relying_party)
FIELD_FLAG_BE.equals(name, true) -> context.getString(R.string.passkey_backup_eligibility)
FIELD_FLAG_BS.equals(name, true) -> context.getString(R.string.passkey_backup_state)
else -> name
}

View File

@@ -21,9 +21,16 @@ package com.kunzisoft.keepass.database.helper
import android.content.Context
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil.searchSubDomains
import com.kunzisoft.keepass.timeout.TimeoutHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
object SearchHelper {
@@ -40,6 +47,76 @@ object SearchHelper {
}
}
/**
* Get the concrete web domain AKA without sub domain if needed
*/
private fun getConcreteWebDomain(
context: Context,
webDomain: String?,
concreteWebDomain: (searchSubDomains: Boolean, concreteWebDomain: String?) -> Unit
) {
val domain = webDomain
val searchSubDomains = searchSubDomains(context)
if (domain != null) {
// Warning, web domain can contains IP, don't crop in this case
if (searchSubDomains
|| Regex(SearchInfo.WEB_IP_REGEX).matches(domain)) {
concreteWebDomain.invoke(searchSubDomains, webDomain)
} else {
CoroutineScope(Dispatchers.IO).launch {
val publicSuffixList = PublicSuffixList(context)
val publicSuffix = publicSuffixList
.getPublicSuffixPlusOne(domain).await()
withContext(Dispatchers.Main) {
concreteWebDomain.invoke(false, publicSuffix)
}
}
}
} else {
concreteWebDomain.invoke(searchSubDomains, null)
}
}
/**
* Create search parameters asynchronously from [SearchInfo]
*/
fun SearchInfo.getSearchParametersFromSearchInfo(
context: Context,
callback: (SearchParameters) -> Unit
) {
getConcreteWebDomain(
context,
webDomain
) { searchSubDomains, concreteDomain ->
var query = this.toString()
if (isDomainSearch && concreteDomain != null)
query = concreteDomain
callback.invoke(
SearchParameters().apply {
searchQuery = query
allowEmptyQuery = false
searchInTitles = false
searchInUsernames = false
searchInPasswords = false
searchInAppIds = isAppIdSearch
searchInUrls = isDomainSearch
searchByDomain = true
searchBySubDomain = searchSubDomains
searchInRelyingParty = isPasskeySearch
searchInNotes = false
searchInOTP = isOTPSearch
searchInOther = false
searchInUUIDs = false
searchInTags = isTagSearch
searchInCurrentGroup = false
searchInSearchableGroup = true
searchInRecycleBin = false
searchInTemplates = false
}
)
}
}
/**
* Utility method to perform actions if item is found or not after an auto search in [database]
*/
@@ -52,28 +129,31 @@ object SearchHelper {
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
onDatabaseClosed: () -> Unit
) {
// Do not place coroutine at start, bug in Passkey implementation
if (database == null || !database.loaded) {
onDatabaseClosed.invoke()
} else if (TimeoutHelper.checkTime(context)) {
var searchWithoutUI = false
if (searchInfo != null
&& !searchInfo.manualSelection
&& !searchInfo.containsOnlyNullValues()) {
// If search provide results
database.createVirtualGroupFromSearchInfo(
searchInfo,
MAX_SEARCH_ENTRY
)?.let { searchGroup ->
if (searchGroup.numberOfChildEntries > 0) {
searchWithoutUI = true
onItemsFound.invoke(database,
searchGroup.getChildEntriesInfo(database))
}
&& !searchInfo.containsOnlyNullValues()
) {
searchInfo.getSearchParametersFromSearchInfo(context) { searchParameters ->
// If search provide results
database.createVirtualGroupFromSearchInfo(
searchParameters = searchParameters,
max = MAX_SEARCH_ENTRY
)?.let { searchGroup ->
if (searchGroup.numberOfChildEntries > 0) {
onItemsFound.invoke(
database,
searchGroup.getChildEntriesInfo(database)
)
} else
onItemNotFound.invoke(database)
} ?: onItemNotFound.invoke(database)
}
}
if (!searchWithoutUI) {
} else
onItemNotFound.invoke(database)
}
}
}
}

View File

@@ -1,170 +0,0 @@
package com.kunzisoft.keepass.hardware
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
/**
* Special activity to deal with hardware key drivers,
* return the response to the database service once finished
*/
class HardwareKeyActivity: DatabaseModeActivity(){
// To manage hardware key challenge response
private val resultCallback = ActivityResultCallback<ActivityResult> { result ->
if (result.resultCode == Activity.RESULT_OK) {
val challengeResponse: ByteArray? = result.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
Log.d(TAG, "Response form challenge")
mDatabaseTaskProvider?.startChallengeResponded(challengeResponse ?: ByteArray(0))
} else {
Log.e(TAG, "Response from challenge error")
mDatabaseTaskProvider?.startChallengeResponded(ByteArray(0))
}
finish()
}
private var activityResultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
resultCallback
)
override fun applyCustomStyle(): Boolean {
return false
}
override fun showDatabaseDialog(): Boolean {
return false
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
val hardwareKey = HardwareKey.getHardwareKeyFromString(
intent.getStringExtra(DATA_HARDWARE_KEY)
)
if (isHardwareKeyAvailable(this, hardwareKey, true) {
mDatabaseTaskProvider?.startChallengeResponded(ByteArray(0))
}) {
when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
throw Exception("FIDO2 not implemented")
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
launchYubikeyChallengeForResponse(intent.getByteArrayExtra(DATA_SEED))
}
else -> {
finish()
}
}
}
}
private fun launchYubikeyChallengeForResponse(seed: ByteArray?) {
// Transform the seed before sending
var challenge: ByteArray? = null
if (seed != null) {
challenge = ByteArray(64)
seed.copyInto(challenge, 0, 0, 32)
challenge.fill(32, 32, 64)
}
// Send to the driver
activityResultLauncher.launch(
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
}
)
Log.d(TAG, "Challenge sent")
}
companion object {
private val TAG = HardwareKeyActivity::class.java.simpleName
private const val DATA_HARDWARE_KEY = "DATA_HARDWARE_KEY"
private const val DATA_SEED = "DATA_SEED"
private const val YUBIKEY_CHALLENGE_RESPONSE_INTENT = "android.yubikey.intent.action.CHALLENGE_RESPONSE"
private const val HARDWARE_KEY_CHALLENGE_KEY = "challenge"
private const val HARDWARE_KEY_RESPONSE_KEY = "response"
fun launchHardwareKeyActivity(
context: Context,
hardwareKey: HardwareKey,
seed: ByteArray?
) {
context.startActivity(Intent(context, HardwareKeyActivity::class.java).apply {
flags = FLAG_ACTIVITY_NEW_TASK
putExtra(DATA_HARDWARE_KEY, hardwareKey.value)
putExtra(DATA_SEED, seed)
})
}
fun isHardwareKeyAvailable(
context: Context,
hardwareKey: HardwareKey?,
showDialog: Boolean = true,
onDialogDismissed: DialogInterface.OnDismissListener? = null
): Boolean {
if (hardwareKey == null)
return false
return when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
if (showDialog)
UnderDevelopmentFeatureDialogFragment()
.show(activity.supportFragmentManager, "underDevFeatureDialog")
false
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Check available intent
val yubikeyDriverAvailable =
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT)
.resolveActivity(context.packageManager) != null
if (showDialog && !yubikeyDriverAvailable
&& context is Activity)
showHardwareKeyDriverNeeded(context, hardwareKey) {
onDialogDismissed?.onDismiss(it)
context.finish()
}
yubikeyDriverAvailable
}
}
}
private fun showHardwareKeyDriverNeeded(
context: Context,
hardwareKey: HardwareKey,
onDialogDismissed: DialogInterface.OnDismissListener
) {
val builder = AlertDialog.Builder(context)
builder
.setMessage(
context.getString(R.string.error_driver_required, hardwareKey.toString())
)
.setPositiveButton(R.string.download) { _, _ ->
context.openExternalApp(
context.getString(R.string.key_driver_app_id),
context.getString(R.string.key_driver_url)
)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setOnDismissListener(onDialogDismissed)
builder.create().show()
}
}
}

View File

@@ -33,20 +33,22 @@ import java.util.*
class PasswordGenerator(private val resources: Resources) {
@Throws(IllegalArgumentException::class)
fun generatePassword(length: Int,
upperCase: Boolean,
lowerCase: Boolean,
digits: Boolean,
minus: Boolean,
underline: Boolean,
space: Boolean,
specials: Boolean,
brackets: Boolean,
extended: Boolean,
considerChars: String,
ignoreChars: String,
atLeastOneFromEach: Boolean,
excludeAmbiguousChar: Boolean): String {
fun generatePassword(
length: Int,
upperCase: Boolean,
lowerCase: Boolean,
digits: Boolean,
minus: Boolean,
underline: Boolean,
space: Boolean,
specials: Boolean,
brackets: Boolean,
extended: Boolean,
considerChars: String,
ignoreChars: String,
atLeastOneFromEach: Boolean,
excludeAmbiguousChar: Boolean
): String {
// Desired password length is 0 or less
if (length <= 0) {
throw IllegalArgumentException(resources.getString(R.string.error_wrong_length))
@@ -228,7 +230,7 @@ class PasswordGenerator(private val resources: Resources) {
private const val MINUS_CHAR = "-"
private const val UNDERLINE_CHAR = "_"
private const val SPACE_CHAR = " "
private const val SPECIAL_CHARS = "!\"#$%&'*+,./:;=?@\\^`"
private const val SPECIAL_CHARS = "&/,^@.#:%\\='$!?*`;+\"|~"
private const val BRACKET_CHARS = "[]{}()<>"
private const val AMBIGUOUS_CHARS = "iI|lLoO01"

View File

@@ -36,6 +36,7 @@ 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.AppUtil.randomRequestCode
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import kotlinx.coroutines.CoroutineScope
@@ -194,7 +195,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
private fun newNotification(attachmentNotification: AttachmentNotification) {
val pendingContentIntent = PendingIntent.getActivity(this,
0,
randomRequestCode(),
Intent().apply {
action = Intent.ACTION_VIEW
setDataAndType(attachmentNotification.uri,
@@ -208,7 +209,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
)
val pendingDeleteIntent = PendingIntent.getService(this,
0,
randomRequestCode(),
Intent(this, AttachmentFileNotificationService::class.java).apply {
// No action to delete the service
putExtra(FILE_URI_KEY, attachmentNotification.uri)

View File

@@ -61,13 +61,14 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.utils.LOCK_ACTION
@@ -175,7 +176,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
progressMessage: ProgressMessage
)
fun onActionStopped(
database: ContextualDatabase
database: ContextualDatabase? = null
)
fun onActionFinished(
database: ContextualDatabase,
@@ -550,7 +551,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
// Build Intents for notification action
val pendingDatabaseIntent = PendingIntent.getActivity(
this,
0,
randomRequestCode(),
Intent(this, GroupActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
@@ -675,6 +676,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
override fun actionOnLock() {
if (!TimeoutHelper.temporarilyDisableLock) {
closeDatabase(mDatabase)
// Remove the database during the lock
// And notify each subscriber
mDatabase = null
mDatabaseListeners.forEach { listener ->
listener.onDatabaseRetrieved(null)
}
// Remove the lock timer (no more needed if it exists)
TimeoutHelper.cancelLockTimer(this)
// Service is stopped after receive the broadcast
@@ -709,9 +716,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
notifyProgressMessage()
HardwareKeyActivity
.launchHardwareKeyActivity(
this@DatabaseTaskNotificationService,
hardwareKey,
seed
context = this@DatabaseTaskNotificationService,
hardwareKey = hardwareKey,
seed = seed
)
// Wait the response
mProgressMessage.apply {

View File

@@ -41,6 +41,11 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
autofillInlineSuggestionsPreference?.isVisible = false
}
val autofillAskSaveDataPreference: TwoStatePreference? = findPreference(getString(R.string.autofill_ask_to_save_data_key))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
autofillAskSaveDataPreference?.isVisible = false
}
}
override fun onDisplayPreferenceDialog(preference: Preference) {

View File

@@ -21,20 +21,25 @@ package com.kunzisoft.keepass.settings
import android.content.Context
import android.os.Bundle
import android.view.View
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
class MainPreferenceFragment : PreferenceFragmentCompat() {
private var mCallback: Callback? = null
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private var mDatabaseLoaded: Boolean = false
private val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
override fun onAttach(context: Context) {
super.onAttach(context)
@@ -50,20 +55,24 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
mCallback = null
super.onDetach()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
mDatabaseLoaded = database?.loaded == true
checkDatabaseLoaded()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database ->
checkDatabaseLoaded(database?.loaded == true)
}
}
}
super.onViewCreated(view, savedInstanceState)
}
private fun checkDatabaseLoaded() {
private fun checkDatabaseLoaded(isDatabaseLoaded: Boolean) {
findPreference<Preference>(getString(R.string.settings_database_key))
?.isEnabled = mDatabaseLoaded
?.isEnabled = isDatabaseLoaded
findPreference<PreferenceCategory>(getString(R.string.settings_database_category_key))
?.isVisible = mDatabaseLoaded
?.isVisible = isDatabaseLoaded
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -119,7 +128,7 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
}
}
checkDatabaseLoaded()
checkDatabaseLoaded(mDatabase?.loaded == true)
}
interface Callback {

View File

@@ -19,13 +19,21 @@
*/
package com.kunzisoft.keepass.settings
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.*
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.toColorInt
import androidx.core.view.MenuProvider
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.TwoStatePreference
@@ -39,19 +47,40 @@ import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.helper.*
import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.settings.preference.*
import com.kunzisoft.keepass.settings.preferencedialogfragment.*
import com.kunzisoft.keepass.settings.preference.DialogColorPreference
import com.kunzisoft.keepass.settings.preference.DialogListExplanationPreference
import com.kunzisoft.keepass.settings.preference.InputKdfNumberPreference
import com.kunzisoft.keepass.settings.preference.InputKdfSizePreference
import com.kunzisoft.keepass.settings.preference.InputNumberPreference
import com.kunzisoft.keepass.settings.preference.InputTextPreference
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseColorPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDataCompressionPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDefaultUsernamePreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDescriptionPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseKeyDerivationPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMaxHistorySizePreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMemoryUsagePreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseNamePreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseParallelismPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRecycleBinGroupPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRoundsPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseTemplatesGroupPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getSerializableCompat
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private var mDatabase: ContextualDatabase? = null
private val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
private var mDatabaseReadOnly: Boolean = false
private var mMergeDataAllowed: Boolean = false
private var mDatabaseAutoSaveEnabled: Boolean = true
@@ -114,19 +143,46 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mDatabaseViewModel.actionState.collect { uiState ->
when (uiState) {
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished(
uiState.database,
uiState.actionTask,
uiState.result
)
}
else -> {}
}
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database ->
database?.let {
onDatabaseRetrieved(database)
}
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
mDatabase = database
view.resetAppTimeoutWhenViewTouchedOrFocused(requireContext(), database?.loaded)
onDatabaseRetrieved(database)
}
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) {
onDatabaseActionFinished(it.database, it.actionTask, it.result)
viewLifecycleOwner.lifecycleScope.launch {
mDatabaseViewModel.databaseState.collect { database ->
view.resetAppTimeoutWhenViewTouchedOrFocused(
context = requireContext(),
databaseLoaded = database?.loaded
)
}
}
}
@@ -167,29 +223,26 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
mDatabaseViewModel.reloadDatabase(false)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
mDatabase = database
mDatabaseReadOnly = database?.isReadOnly == true
mMergeDataAllowed = database?.isMergeDataAllowed() == true
override fun onDatabaseRetrieved(database: ContextualDatabase) {
mDatabaseReadOnly = database.isReadOnly
mMergeDataAllowed = database.isMergeDataAllowed()
mDatabase?.let {
if (it.loaded) {
when (mScreen) {
Screen.DATABASE -> {
onCreateDatabasePreference(it)
}
Screen.DATABASE_SECURITY -> {
onCreateDatabaseSecurityPreference(it)
}
Screen.DATABASE_MASTER_KEY -> {
onCreateDatabaseMasterKeyPreference(it)
}
else -> {
}
if (database.loaded) {
when (mScreen) {
Screen.DATABASE -> {
onCreateDatabasePreference(database)
}
Screen.DATABASE_SECURITY -> {
onCreateDatabaseSecurityPreference(database)
}
Screen.DATABASE_MASTER_KEY -> {
onCreateDatabaseMasterKeyPreference(database)
}
else -> {
}
} else {
Log.e(javaClass.name, "Database isn't ready")
}
} else {
Log.e(javaClass.name, "Database isn't ready")
}
}
@@ -458,7 +511,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newDefaultUsername
} else {
mDatabase?.defaultUsername = oldDefaultUsername
database.defaultUsername = oldDefaultUsername
oldDefaultUsername
}
dbDefaultUsernamePref?.summary = defaultUsernameToShow
@@ -471,7 +524,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newColor
} else {
mDatabase?.customColor = Color.parseColor(oldColor)
database.customColor = oldColor.toColorInt()
oldColor
}
dbCustomColorPref?.summary = defaultColorToShow
@@ -483,7 +536,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newCompression
} else {
mDatabase?.compressionAlgorithm = oldCompression
database.compressionAlgorithm = oldCompression
oldCompression
}
dbDataCompressionPref?.summary = algorithmToShow?.getLocalizedName(resources)
@@ -497,7 +550,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} else {
oldRecycleBin
}
mDatabase?.setRecycleBin(recycleBinToShow)
database.setRecycleBin(recycleBinToShow)
refreshRecycleBinGroup(database)
}
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK -> {
@@ -509,7 +562,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} else {
oldTemplatesGroup
}
mDatabase?.setTemplatesGroup(templatesGroupToShow)
database.setTemplatesGroup(templatesGroupToShow)
refreshTemplatesGroup(database)
}
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK -> {
@@ -519,7 +572,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newMaxHistoryItems
} else {
mDatabase?.historyMaxItems = oldMaxHistoryItems
database.historyMaxItems = oldMaxHistoryItems
oldMaxHistoryItems
}
dbMaxHistoryItemsPref?.summary = maxHistoryItemsToShow.toString()
@@ -531,7 +584,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newMaxHistorySize
} else {
mDatabase?.historyMaxSize = oldMaxHistorySize
database.historyMaxSize = oldMaxHistorySize
oldMaxHistorySize
}
dbMaxHistorySizePref?.summary = maxHistorySizeToShow.toString()
@@ -549,7 +602,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newEncryption
} else {
mDatabase?.encryptionAlgorithm = oldEncryption
database.encryptionAlgorithm = oldEncryption
oldEncryption
}
mEncryptionAlgorithmPref?.summary = algorithmToShow.toString()
@@ -561,7 +614,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newKeyDerivationEngine
} else {
mDatabase?.kdfEngine = oldKeyDerivationEngine
database.kdfEngine = oldKeyDerivationEngine
oldKeyDerivationEngine
}
mKeyDerivationPref?.summary = kdfEngineToShow.toString()
@@ -578,7 +631,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newIterations
} else {
mDatabase?.numberKeyEncryptionRounds = oldIterations
database.numberKeyEncryptionRounds = oldIterations
oldIterations
}
mRoundPref?.summary = roundsToShow.toString()
@@ -590,7 +643,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newMemoryUsage
} else {
mDatabase?.memoryUsage = oldMemoryUsage
database.memoryUsage = oldMemoryUsage
oldMemoryUsage
}
mMemoryPref?.summary = memoryToShow.toString()
@@ -602,7 +655,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newParallelism
} else {
mDatabase?.parallelism = oldParallelism
database.parallelism = oldParallelism
oldParallelism
}
mParallelismPref?.summary = parallelismToShow.toString()

View File

@@ -108,7 +108,7 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.auto_focus_search_default))
}
fun searchSubdomains(context: Context): Boolean {
fun searchSubDomains(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.subdomain_search_key),
context.resources.getBoolean(R.bool.subdomain_search_default))
@@ -352,6 +352,8 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.search_option_username_default))
searchInPasswords = prefs.getBoolean(context.getString(R.string.search_option_password_key),
context.resources.getBoolean(R.bool.search_option_password_default))
searchInAppIds = prefs.getBoolean(context.getString(R.string.search_option_application_id_key),
context.resources.getBoolean(R.bool.search_option_application_id_default))
searchInUrls = prefs.getBoolean(context.getString(R.string.search_option_url_key),
context.resources.getBoolean(R.bool.search_option_url_default))
searchInExpired = prefs.getBoolean(context.getString(R.string.search_option_expired_key),
@@ -389,6 +391,8 @@ object PreferencesUtil {
searchParameters.searchInUsernames)
putBoolean(context.getString(R.string.search_option_password_key),
searchParameters.searchInPasswords)
putBoolean(context.getString(R.string.search_option_application_id_key),
searchParameters.searchInAppIds)
putBoolean(context.getString(R.string.search_option_url_key),
searchParameters.searchInUrls)
putBoolean(context.getString(R.string.search_option_expired_key),
@@ -686,6 +690,12 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.keyboard_previous_lock_default))
}
fun isPasskeyCloseDatabaseEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.passkeys_close_database_key),
context.resources.getBoolean(R.bool.passkeys_close_database_default))
}
fun isPasskeyBackupEligibilityEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.passkeys_backup_eligibility_key),
@@ -854,6 +864,10 @@ object PreferencesUtil {
context.getString(R.string.keyboard_previous_search_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_previous_fill_in_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_previous_lock_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.passkeys_close_database_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.passkeys_auto_select_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.passkeys_backup_eligibility_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.passkeys_backup_state_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean())

View File

@@ -70,8 +70,12 @@ open class SettingsActivity
// To apply navigation bar with background color
/* TODO Settings nav bar
setTransparentNavigationBar {
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
coordinatorLayout?.applyWindowInsets(EnumSet.of(
WindowInsetPosition.TOP_MARGINS,
WindowInsetPosition.BOTTOM_MARGINS,
WindowInsetPosition.START_MARGINS,
WindowInsetPosition.END_MARGINS,
))
}*/
mExternalFileHelper = ExternalFileHelper(this)
@@ -155,10 +159,6 @@ open class SettingsActivity
return coordinatorLayout
}
override fun finishActivityIfDatabaseNotLoaded(): Boolean {
return false
}
override fun onDatabaseActionFinished(
database: ContextualDatabase,
actionTask: String,
@@ -188,7 +188,7 @@ open class SettingsActivity
}
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
assignPassword(mainCredential)
assignMainCredential(mainCredential)
}
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}

View File

@@ -95,20 +95,16 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
return dialog
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
database?.let {
var initColor = it.customColor
if (initColor != null) {
enableSwitchView.isChecked = true
} else {
enableSwitchView.isChecked = false
initColor = DEFAULT_COLOR
}
chromaColorView.currentColor = initColor
arguments?.putInt(ARG_INITIAL_COLOR, initColor)
override fun onDatabaseRetrieved(database: ContextualDatabase) {
var initColor = database.customColor
if (initColor != null) {
enableSwitchView.isChecked = true
} else {
enableSwitchView.isChecked = false
initColor = DEFAULT_COLOR
}
chromaColorView.currentColor = initColor
arguments?.putInt(ARG_INITIAL_COLOR, initColor)
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -50,16 +50,14 @@ class DatabaseDataCompressionPreferenceDialogFragmentCompat
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
override fun onDatabaseRetrieved(database: ContextualDatabase) {
setExplanationText(R.string.database_data_compression_summary)
mRecyclerView?.adapter = mCompressionAdapter
database?.let {
compressionSelected = it.compressionAlgorithm
mCompressionAdapter?.setItems(it.availableCompressionAlgorithms, compressionSelected)
}
compressionSelected = database.compressionAlgorithm
mCompressionAdapter?.setItems(
items = database.availableCompressionAlgorithms,
itemUsed = compressionSelected
)
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
class DatabaseDefaultUsernamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
inputText = database?.defaultUsername?: ""
override fun onDatabaseRetrieved(database: ContextualDatabase) {
inputText = database.defaultUsername
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
class DatabaseDescriptionPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
inputText = database?.description ?: ""
override fun onDatabaseRetrieved(database: ContextualDatabase) {
inputText = database.description
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -51,12 +51,9 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
database?.let {
algorithmSelected = database.encryptionAlgorithm
mEncryptionAlgorithmAdapter?.setItems(database.availableEncryptionAlgorithms, algorithmSelected)
}
override fun onDatabaseRetrieved(database: ContextualDatabase) {
algorithmSelected = database.encryptionAlgorithm
mEncryptionAlgorithmAdapter?.setItems(database.availableEncryptionAlgorithms, algorithmSelected)
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -54,12 +54,12 @@ class DatabaseKeyDerivationPreferenceDialogFragmentCompat
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
database?.let {
kdfEngineSelected = database.kdfEngine
mKdfAdapter?.setItems(database.availableKdfEngines, kdfEngineSelected)
}
override fun onDatabaseRetrieved(database: ContextualDatabase) {
kdfEngineSelected = database.kdfEngine
mKdfAdapter?.setItems(
items = database.availableKdfEngines,
itemUsed = kdfEngineSelected
)
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -31,19 +31,17 @@ class DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat : DatabaseSavePrefer
setExplanationText(R.string.max_history_items_summary)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
database?.historyMaxItems?.let { maxItemsDatabase ->
inputText = maxItemsDatabase.toString()
setSwitchAction({ isChecked ->
inputText = if (!isChecked) {
NONE_MAX_HISTORY_ITEMS.toString()
} else {
DEFAULT_MAX_HISTORY_ITEMS.toString()
}
showInputText(isChecked)
}, maxItemsDatabase > NONE_MAX_HISTORY_ITEMS)
}
override fun onDatabaseRetrieved(database: ContextualDatabase) {
val maxItemsDatabase = database.historyMaxItems
inputText = maxItemsDatabase.toString()
setSwitchAction({ isChecked ->
inputText = if (!isChecked) {
NONE_MAX_HISTORY_ITEMS.toString()
} else {
DEFAULT_MAX_HISTORY_ITEMS.toString()
}
showInputText(isChecked)
}, maxItemsDatabase > NONE_MAX_HISTORY_ITEMS)
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -34,31 +34,29 @@ class DatabaseMaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePrefere
setExplanationText(R.string.max_history_size_summary)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
database?.historyMaxSize?.let { maxItemsDatabase ->
dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE)
.toBetterByteFormat()
inputText = dataByte.number.toString()
if (dataByte.number >= 0) {
setUnitText(dataByte.format.stringId)
} else {
unitText = null
}
setSwitchAction({ isChecked ->
if (!isChecked) {
dataByte = INFINITE_MAX_HISTORY_SIZE_DATA_BYTE
inputText = INFINITE_MAX_HISTORY_SIZE.toString()
unitText = null
} else {
dataByte = DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE
inputText = dataByte.number.toString()
setUnitText(dataByte.format.stringId)
}
showInputText(isChecked)
}, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
override fun onDatabaseRetrieved(database: ContextualDatabase) {
val maxItemsDatabase = database.historyMaxSize
dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE)
.toBetterByteFormat()
inputText = dataByte.number.toString()
if (dataByte.number >= 0) {
setUnitText(dataByte.format.stringId)
} else {
unitText = null
}
setSwitchAction({ isChecked ->
if (!isChecked) {
dataByte = INFINITE_MAX_HISTORY_SIZE_DATA_BYTE
inputText = INFINITE_MAX_HISTORY_SIZE.toString()
unitText = null
} else {
dataByte = DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE
inputText = dataByte.number.toString()
setUnitText(dataByte.format.stringId)
}
showInputText(isChecked)
}, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -34,15 +34,12 @@ class DatabaseMemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreference
setExplanationText(R.string.memory_usage_explanation)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
database?.let {
val memoryBytes = database.memoryUsage
dataByte = DataByte(memoryBytes, DataByte.ByteFormat.BYTE)
.toBetterByteFormat()
inputText = dataByte.number.toString()
setUnitText(dataByte.format.stringId)
}
override fun onDatabaseRetrieved(database: ContextualDatabase) {
val memoryBytes = database.memoryUsage
dataByte = DataByte(memoryBytes, DataByte.ByteFormat.BYTE)
.toBetterByteFormat()
inputText = dataByte.number.toString()
setUnitText(dataByte.format.stringId)
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
class DatabaseNamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
inputText = database?.name ?: ""
override fun onDatabaseRetrieved(database: ContextualDatabase) {
inputText = database.name
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -31,9 +31,8 @@ class DatabaseParallelismPreferenceDialogFragmentCompat : DatabaseSavePreference
setExplanationText(R.string.parallelism_explanation)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
inputText = database?.parallelism?.toString() ?: MIN_PARALLELISM.toString()
override fun onDatabaseRetrieved(database: ContextualDatabase) {
inputText = database.parallelism.toString()
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -48,12 +48,9 @@ class DatabaseRecycleBinGroupPreferenceDialogFragmentCompat
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
database?.let {
mGroupRecycleBin = database.recycleBin
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupRecycleBin)
}
override fun onDatabaseRetrieved(database: ContextualDatabase) {
mGroupRecycleBin = database.recycleBin
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupRecycleBin)
}
override fun onItemSelected(item: Group) {

View File

@@ -46,6 +46,8 @@ class DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat : DatabaseSavePre
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase) {}
companion object {
fun newInstance(key: String): DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat {

View File

@@ -32,9 +32,8 @@ class DatabaseRoundsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialo
explanationText = getString(R.string.rounds_explanation)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
inputText = database?.numberKeyEncryptionRounds?.toString() ?: MIN_ITERATIONS.toString()
override fun onDatabaseRetrieved(database: ContextualDatabase) {
inputText = database.numberKeyEncryptionRounds.toString()
}
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {

View File

@@ -22,6 +22,9 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.database.ContextualDatabase
@@ -32,13 +35,15 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
abstract class DatabaseSavePreferenceDialogFragmentCompat
: InputPreferenceDialogFragmentCompat(), DatabaseRetrieval {
private var mDatabaseAutoSaveEnable = true
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private var mDatabase: ContextualDatabase? = null
protected val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
override fun onAttach(context: Context) {
super.onAttach(context)
@@ -47,18 +52,32 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDatabaseViewModel.database.observe(this) { database ->
onDatabaseRetrieved(database)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mDatabaseViewModel.actionState.collect { uiState ->
when (uiState) {
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished(
uiState.database,
uiState.actionTask,
uiState.result
)
}
else -> {}
}
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database ->
database?.let {
onDatabaseRetrieved(database)
}
}
}
}
}
override fun onResume() {
super.onResume()
onDatabaseRetrieved(mDatabase)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
this.mDatabase = database
}
override fun onDatabaseActionFinished(
@@ -77,8 +96,10 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
// To inherit to save element in database
}
protected fun saveColor(oldColor: Int?,
newColor: Int?) {
protected fun saveColor(
oldColor: Int?,
newColor: Int?
) {
val oldColorString = if (oldColor != null)
ChromaUtil.getFormattedColorString(oldColor, false)
else
@@ -87,77 +108,158 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
ChromaUtil.getFormattedColorString(newColor, false)
else
""
mDatabaseViewModel.saveColor(oldColorString, newColorString, mDatabaseAutoSaveEnable)
mDatabaseViewModel.saveColor(
oldColorString,
newColorString,
mDatabaseAutoSaveEnable
)
}
protected fun saveCompression(oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm
protected fun saveCompression(
oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm
) {
mDatabaseViewModel.saveCompression(oldCompression, newCompression, mDatabaseAutoSaveEnable)
mDatabaseViewModel.saveCompression(
oldCompression,
newCompression,
mDatabaseAutoSaveEnable
)
}
protected fun saveDefaultUsername(oldUsername: String,
newUsername: String) {
mDatabaseViewModel.saveDefaultUsername(oldUsername, newUsername, mDatabaseAutoSaveEnable)
protected fun saveDefaultUsername(
oldUsername: String,
newUsername: String
) {
mDatabaseViewModel.saveDefaultUsername(
oldUsername,
newUsername,
mDatabaseAutoSaveEnable
)
}
protected fun saveDescription(oldDescription: String,
newDescription: String) {
mDatabaseViewModel.saveDescription(oldDescription, newDescription, mDatabaseAutoSaveEnable)
protected fun saveDescription(
oldDescription: String,
newDescription: String
) {
mDatabaseViewModel.saveDescription(
oldDescription,
newDescription,
mDatabaseAutoSaveEnable
)
}
protected fun saveEncryption(oldEncryption: EncryptionAlgorithm,
newEncryptionAlgorithm: EncryptionAlgorithm) {
mDatabaseViewModel.saveEncryption(oldEncryption, newEncryptionAlgorithm, mDatabaseAutoSaveEnable)
protected fun saveEncryption(
oldEncryption: EncryptionAlgorithm,
newEncryptionAlgorithm: EncryptionAlgorithm
) {
mDatabaseViewModel.saveEncryption(
oldEncryption,
newEncryptionAlgorithm,
mDatabaseAutoSaveEnable
)
}
protected fun saveKeyDerivation(oldKeyDerivation: KdfEngine,
newKeyDerivation: KdfEngine) {
mDatabaseViewModel.saveKeyDerivation(oldKeyDerivation, newKeyDerivation, mDatabaseAutoSaveEnable)
protected fun saveKeyDerivation(
oldKeyDerivation: KdfEngine,
newKeyDerivation: KdfEngine
) {
mDatabaseViewModel.saveKeyDerivation(
oldKeyDerivation,
newKeyDerivation,
mDatabaseAutoSaveEnable
)
}
protected fun saveName(oldName: String,
newName: String) {
mDatabaseViewModel.saveName(oldName, newName, mDatabaseAutoSaveEnable)
protected fun saveName(
oldName: String,
newName: String
) {
mDatabaseViewModel.saveName(
oldName,
newName,
mDatabaseAutoSaveEnable
)
}
protected fun saveRecycleBin(oldGroup: Group?,
newGroup: Group?) {
mDatabaseViewModel.saveRecycleBin(oldGroup, newGroup, mDatabaseAutoSaveEnable)
protected fun saveRecycleBin(
oldGroup: Group?,
newGroup: Group?
) {
mDatabaseViewModel.saveRecycleBin(
oldGroup,
newGroup,
mDatabaseAutoSaveEnable
)
}
protected fun removeUnlinkedData() {
mDatabaseViewModel.removeUnlinkedData(mDatabaseAutoSaveEnable)
}
protected fun saveTemplatesGroup(oldGroup: Group?,
newGroup: Group?) {
mDatabaseViewModel.saveTemplatesGroup(oldGroup, newGroup, mDatabaseAutoSaveEnable)
protected fun saveTemplatesGroup(
oldGroup: Group?,
newGroup: Group?
) {
mDatabaseViewModel.saveTemplatesGroup(
oldGroup,
newGroup,
mDatabaseAutoSaveEnable
)
}
protected fun saveMaxHistoryItems(oldNumber: Int,
newNumber: Int) {
mDatabaseViewModel.saveMaxHistoryItems(oldNumber, newNumber, mDatabaseAutoSaveEnable)
protected fun saveMaxHistoryItems(
oldNumber: Int,
newNumber: Int
) {
mDatabaseViewModel.saveMaxHistoryItems(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
}
protected fun saveMaxHistorySize(oldNumber: Long,
newNumber: Long) {
mDatabaseViewModel.saveMaxHistorySize(oldNumber, newNumber, mDatabaseAutoSaveEnable)
protected fun saveMaxHistorySize(
oldNumber: Long,
newNumber: Long
) {
mDatabaseViewModel.saveMaxHistorySize(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
}
protected fun saveMemoryUsage(oldNumber: Long,
newNumber: Long) {
mDatabaseViewModel.saveMemoryUsage(oldNumber, newNumber, mDatabaseAutoSaveEnable)
protected fun saveMemoryUsage(
oldNumber: Long,
newNumber: Long
) {
mDatabaseViewModel.saveMemoryUsage(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
}
protected fun saveParallelism(oldNumber: Long,
newNumber: Long) {
mDatabaseViewModel.saveParallelism(oldNumber, newNumber, mDatabaseAutoSaveEnable)
protected fun saveParallelism(
oldNumber: Long,
newNumber: Long
) {
mDatabaseViewModel.saveParallelism(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
}
protected fun saveIterations(oldNumber: Long,
newNumber: Long) {
mDatabaseViewModel.saveIterations(oldNumber, newNumber, mDatabaseAutoSaveEnable)
protected fun saveIterations(
oldNumber: Long,
newNumber: Long
) {
mDatabaseViewModel.saveIterations(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
}
companion object {

View File

@@ -48,12 +48,9 @@ class DatabaseTemplatesGroupPreferenceDialogFragmentCompat
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
database?.let {
mGroupTemplates = database.templatesGroup
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupTemplates)
}
override fun onDatabaseRetrieved(database: ContextualDatabase) {
mGroupTemplates = database.templatesGroup
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupTemplates)
}
override fun onItemSelected(item: Group) {

View File

@@ -13,15 +13,13 @@ import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
object AppUtil {
fun randomRequestCode(): Int {
return (Math.random() * Integer.MAX_VALUE).toInt()
}
fun Context.isExternalAppInstalled(packageName: String, showError: Boolean = true): Boolean {
try {
this.applicationContext.packageManager.getPackageInfoCompat(
@@ -79,29 +77,6 @@ object AppUtil {
)
}
/**
* Get the concrete web domain AKA without sub domain if needed
*/
fun getConcreteWebDomain(context: Context,
webDomain: String?,
concreteWebDomain: (String?) -> Unit) {
CoroutineScope(Dispatchers.Main).launch {
if (webDomain != null) {
// Warning, web domain can contains IP, don't crop in this case
if (PreferencesUtil.searchSubdomains(context)
|| Regex(SearchInfo.WEB_IP_REGEX).matches(webDomain)) {
concreteWebDomain.invoke(webDomain)
} else {
val publicSuffixList = PublicSuffixList(context)
concreteWebDomain.invoke(publicSuffixList
.getPublicSuffixPlusOne(webDomain).await())
}
} else {
concreteWebDomain.invoke(null)
}
}
}
@RequiresApi(Build.VERSION_CODES.P)
fun getInstalledBrowsersWithSignatures(context: Context): List<AndroidPrivilegedApp> {
val packageManager = context.packageManager

View File

@@ -19,30 +19,39 @@
*/
package com.kunzisoft.keepass.utils
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.coroutines.EmptyCoroutineContext
/**
* Class to invoke action in a separate IO thread
*/
class IOActionTask<T>(
private val action: () -> T ,
private val afterActionListener: ((T?) -> Unit)? = null) {
private val mainScope = CoroutineScope(Dispatchers.Main)
private val action: () -> T,
private val onActionComplete: ((T?) -> Unit)? = null,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main),
private val exceptionHandler: CoroutineExceptionHandler? = null
) {
fun execute() {
mainScope.launch {
scope.launch(exceptionHandler ?: EmptyCoroutineContext) {
withContext(Dispatchers.IO) {
val asyncResult: Deferred<T?> = async {
try {
action.invoke()
} catch (e: Exception) {
e.printStackTrace()
null
}
exceptionHandler?.let {
action.invoke()
} ?: try {
action.invoke()
} catch (e: Exception) {
e.printStackTrace()
null
}
}
withContext(Dispatchers.Main) {
afterActionListener?.invoke(asyncResult.await())
onActionComplete?.invoke(asyncResult.await())
}
}
}

View File

@@ -3,7 +3,6 @@ package com.kunzisoft.keepass.view
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.ImageView
@@ -30,8 +29,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
private var searchTitle: CompoundButton
private var searchUsername: CompoundButton
private var searchPassword: CompoundButton
private var searchApplicationId: CompoundButton
private var searchURL: CompoundButton
private var searchByURLDomain: Boolean = false
private var searchByURLSubDomain: Boolean = false
private var searchExpired: CompoundButton
private var searchNotes: CompoundButton
private var searchOther: CompoundButton
@@ -50,8 +51,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
this.searchInTitles = searchTitle.isChecked
this.searchInUsernames = searchUsername.isChecked
this.searchInPasswords = searchPassword.isChecked
this.searchInAppIds = searchApplicationId.isChecked
this.searchInUrls = searchURL.isChecked
this.searchByDomain = searchByURLDomain
this.searchBySubDomain = searchByURLSubDomain
this.searchInExpired = searchExpired.isChecked
this.searchInNotes = searchNotes.isChecked
this.searchInOther = searchOther.isChecked
@@ -71,8 +74,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchTitle.isChecked = value.searchInTitles
searchUsername.isChecked = value.searchInUsernames
searchPassword.isChecked = value.searchInPasswords
searchApplicationId.isChecked = value.searchInAppIds
searchURL.isChecked = value.searchInUrls
searchByURLDomain = value.searchByDomain
searchByURLSubDomain = value.searchBySubDomain
searchExpired.isChecked = value.searchInExpired
searchNotes.isChecked = value.searchInNotes
searchOther.isChecked = value.searchInOther
@@ -87,7 +92,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
var onParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = null
private var mOnParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = {
// To recalculate height
if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) {
if (searchAdvanceFiltersContainer?.visibility == VISIBLE) {
searchAdvanceFiltersContainer?.expand(
false,
searchAdvanceFiltersContainer?.getFullHeight()
@@ -110,6 +115,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchTitle = findViewById(R.id.search_chip_title)
searchUsername = findViewById(R.id.search_chip_username)
searchPassword = findViewById(R.id.search_chip_password)
searchApplicationId = findViewById(R.id.search_chip_application_id)
searchURL = findViewById(R.id.search_chip_url)
searchExpired = findViewById(R.id.search_chip_expires)
searchNotes = findViewById(R.id.search_chip_note)
@@ -125,7 +131,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
// Expand menu with button
searchExpandButton.setOnClickListener {
val isVisible = searchAdvanceFiltersContainer?.visibility == View.VISIBLE
val isVisible = searchAdvanceFiltersContainer?.visibility == VISIBLE
if (isVisible)
closeAdvancedFilters()
else
@@ -156,6 +162,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchParameters.searchInPasswords = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchApplicationId.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInAppIds = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchURL.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInUrls = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
@@ -200,10 +210,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchNumbers.text = SearchHelper.showNumberOfSearchResults(numbers)
}
fun setCurrentGroupText(text: String) {
fun setCurrentGroupText(text: String?) {
val maxChars = 12
searchCurrentGroup.text = when {
text.isEmpty() -> context.getString(R.string.current_group)
text.isNullOrEmpty() -> context.getString(R.string.current_group)
text.length > maxChars -> text.substring(0, maxChars) + ""
else -> text
}
@@ -213,6 +223,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchOther.isVisible = available
}
fun availableApplicationIds(available: Boolean) {
searchApplicationId.isVisible = available
}
fun availableTags(available: Boolean) {
searchTag.isVisible = available
}
@@ -243,16 +257,20 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
)
}
fun showSearchExpandButton(show: Boolean) {
searchExpandButton.isVisible = show
}
override fun setVisibility(visibility: Int) {
when (visibility) {
View.VISIBLE -> {
searchAdvanceFiltersContainer?.visibility = View.GONE
VISIBLE -> {
searchAdvanceFiltersContainer?.visibility = GONE
searchContainer.showByFading()
}
else -> {
searchContainer.hideByFading()
if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) {
searchAdvanceFiltersContainer?.visibility = View.INVISIBLE
if (searchAdvanceFiltersContainer?.visibility == VISIBLE) {
searchAdvanceFiltersContainer?.visibility = INVISIBLE
searchAdvanceFiltersContainer?.collapse()
}
}

View File

@@ -24,7 +24,6 @@ import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PorterDuff
@@ -66,6 +65,7 @@ import com.kunzisoft.keepass.database.exception.LocalizedException
import com.kunzisoft.keepass.database.helper.getLocalizedMessage
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import java.util.EnumSet
/**
@@ -317,9 +317,7 @@ fun CollapsingToolbarLayout.changeTitleColor(color: Int) {
@Suppress("DEPRECATION")
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) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
WindowCompat.setDecorFitsSystemWindows(window, false)
window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector)
if (applyToStatusBar) {
@@ -335,7 +333,7 @@ fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, appl
/**
* Apply a margin to a view to fix the window inset
*/
fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.BOTTOM) {
fun View.applyWindowInsets(positions: EnumSet<WindowInsetPosition>) {
ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
var consumed = false
@@ -351,52 +349,78 @@ fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.B
}
}
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
when (position) {
WindowInsetPosition.TOP -> {
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()
or WindowInsetsCompat.Type.displayCutout()
or WindowInsetsCompat.Type.ime())
val isRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL
val wantTopMargins = positions.contains(WindowInsetPosition.TOP_MARGINS)
val wantBottomMargins = positions.contains(WindowInsetPosition.BOTTOM_MARGINS)
val wantStartMargins = positions.contains(WindowInsetPosition.START_MARGINS)
val wantEndMargins = positions.contains(WindowInsetPosition.END_MARGINS)
if (view.layoutParams is ViewGroup.MarginLayoutParams
&& (wantTopMargins || wantBottomMargins || wantStartMargins || wantEndMargins)) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
if (wantTopMargins) {
topMargin = insets.top
}
if (wantBottomMargins) {
bottomMargin = insets.bottom
}
if (wantStartMargins) {
if (isRtl) {
rightMargin = insets.right
} else {
leftMargin = insets.left
}
}
}
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 (wantEndMargins) {
if (isRtl) {
leftMargin = insets.left
} else {
rightMargin = insets.right
}
}
}
}
val wantTopPadding = positions.contains(WindowInsetPosition.TOP_PADDING)
val wantBottomPadding = positions.contains(WindowInsetPosition.BOTTOM_PADDING)
val wantStartPadding = positions.contains(WindowInsetPosition.START_PADDING)
val wantEndPadding = positions.contains(WindowInsetPosition.END_PADDING)
if (wantTopPadding || wantBottomPadding || wantStartPadding || wantEndPadding) {
val topPadding = if (wantTopPadding) insets.top else 0
val bottomPadding = if (wantBottomPadding) insets.bottom else 0
var leftPadding = 0
var rightPadding = 0
if (wantStartPadding) {
if (isRtl) {
rightPadding = insets.right
} else {
leftPadding = insets.left
}
}
if (wantEndPadding) {
if (isRtl) {
leftPadding = insets.left
} else {
rightPadding = insets.right
}
}
setPadding(leftPadding, topPadding, rightPadding, bottomPadding)
}
// 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
TOP_MARGINS, BOTTOM_MARGINS, START_MARGINS, END_MARGINS,
TOP_PADDING, BOTTOM_PADDING, START_PADDING, END_PADDING,
}

View File

@@ -1,214 +1,500 @@
package com.kunzisoft.keepass.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import android.app.Application
import android.net.Uri
import android.os.Bundle
import androidx.lifecycle.AndroidViewModel
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.ProgressMessage
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.tasks.ActionRunnable
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.UUID
class DatabaseViewModel: ViewModel() {
class DatabaseViewModel(application: Application): AndroidViewModel(application) {
val database : LiveData<ContextualDatabase?> get() = _database
private val _database = MutableLiveData<ContextualDatabase?>()
private val mDatabaseState = MutableStateFlow<ContextualDatabase?>(null)
val databaseState: StateFlow<ContextualDatabase?> = mDatabaseState
val actionFinished : LiveData<ActionResult> get() = _actionFinished
private val _actionFinished = SingleLiveEvent<ActionResult>()
val database: ContextualDatabase?
get() = databaseState.value
val saveDatabase : LiveData<Boolean> get() = _saveDatabase
private val _saveDatabase = SingleLiveEvent<Boolean>()
private val mActionState = MutableStateFlow<ActionState>(ActionState.Loading)
val actionState: StateFlow<ActionState> = mActionState
val mergeDatabase : LiveData<Boolean> get() = _mergeDatabase
private val _mergeDatabase = SingleLiveEvent<Boolean>()
private var mDatabaseTaskProvider: DatabaseTaskProvider = DatabaseTaskProvider(
context = application
)
val reloadDatabase : LiveData<Boolean> get() = _reloadDatabase
private val _reloadDatabase = SingleLiveEvent<Boolean>()
init {
mDatabaseTaskProvider.onDatabaseRetrieved = { databaseRetrieved ->
val databaseWasReloaded = databaseRetrieved?.wasReloaded == true
if (databaseWasReloaded) {
mActionState.value = ActionState.OnDatabaseReloaded
}
if (database == null || database != databaseRetrieved || databaseWasReloaded) {
databaseRetrieved?.wasReloaded = false
mDatabaseState.value = databaseRetrieved
}
}
mDatabaseTaskProvider.onStartActionRequested = { bundle, actionTask ->
mActionState.value = ActionState.OnDatabaseActionRequested(bundle, actionTask)
}
mDatabaseTaskProvider.databaseInfoListener = object : DatabaseTaskNotificationService.DatabaseInfoListener {
override fun onDatabaseInfoChanged(
previousDatabaseInfo: SnapFileDatabaseInfo,
newDatabaseInfo: SnapFileDatabaseInfo,
readOnlyDatabase: Boolean
) {
mActionState.value = ActionState.OnDatabaseInfoChanged(
previousDatabaseInfo,
newDatabaseInfo,
readOnlyDatabase
)
}
}
mDatabaseTaskProvider.actionTaskListener = object : DatabaseTaskNotificationService.ActionTaskListener {
override fun onActionStarted(
database: ContextualDatabase,
progressMessage: ProgressMessage
) {
mActionState.value = ActionState.OnDatabaseActionStarted(database, progressMessage)
}
val saveName : LiveData<SuperString> get() = _saveName
private val _saveName = SingleLiveEvent<SuperString>()
override fun onActionUpdated(
database: ContextualDatabase,
progressMessage: ProgressMessage
) {
mActionState.value = ActionState.OnDatabaseActionUpdated(database, progressMessage)
}
val saveDescription : LiveData<SuperString> get() = _saveDescription
private val _saveDescription = SingleLiveEvent<SuperString>()
override fun onActionStopped(database: ContextualDatabase?) {
mActionState.value = ActionState.OnDatabaseActionStopped(database)
}
val saveDefaultUsername : LiveData<SuperString> get() = _saveDefaultUsername
private val _saveDefaultUsername = SingleLiveEvent<SuperString>()
override fun onActionFinished(
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
mActionState.value = ActionState.OnDatabaseActionFinished(database, actionTask, result)
}
}
val saveColor : LiveData<SuperString> get() = _saveColor
private val _saveColor = SingleLiveEvent<SuperString>()
val saveCompression : LiveData<SuperCompression> get() = _saveCompression
private val _saveCompression = SingleLiveEvent<SuperCompression>()
val removeUnlinkData : LiveData<Boolean> get() = _removeUnlinkData
private val _removeUnlinkData = SingleLiveEvent<Boolean>()
val saveRecycleBin : LiveData<SuperGroup> get() = _saveRecycleBin
private val _saveRecycleBin = SingleLiveEvent<SuperGroup>()
val saveTemplatesGroup : LiveData<SuperGroup> get() = _saveTemplatesGroup
private val _saveTemplatesGroup = SingleLiveEvent<SuperGroup>()
val saveMaxHistoryItems : LiveData<SuperInt> get() = _saveMaxHistoryItems
private val _saveMaxHistoryItems = SingleLiveEvent<SuperInt>()
val saveMaxHistorySize : LiveData<SuperLong> get() = _saveMaxHistorySize
private val _saveMaxHistorySize = SingleLiveEvent<SuperLong>()
val saveEncryption : LiveData<SuperEncryption> get() = _saveEncryption
private val _saveEncryption = SingleLiveEvent<SuperEncryption>()
val saveKeyDerivation : LiveData<SuperKeyDerivation> get() = _saveKeyDerivation
private val _saveKeyDerivation = SingleLiveEvent<SuperKeyDerivation>()
val saveIterations : LiveData<SuperLong> get() = _saveIterations
private val _saveIterations = SingleLiveEvent<SuperLong>()
val saveMemoryUsage : LiveData<SuperLong> get() = _saveMemoryUsage
private val _saveMemoryUsage = SingleLiveEvent<SuperLong>()
val saveParallelism : LiveData<SuperLong> get() = _saveParallelism
private val _saveParallelism = SingleLiveEvent<SuperLong>()
fun defineDatabase(database: ContextualDatabase?) {
this._database.value = database
mDatabaseTaskProvider.registerProgressTask()
}
fun onActionFinished(database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result) {
this._actionFinished.value = ActionResult(database, actionTask, result)
/*
* Main database actions
*/
fun loadDatabase(
databaseUri: Uri,
mainCredential: MainCredential,
readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean
) {
mDatabaseTaskProvider.startDatabaseLoad(
databaseUri,
mainCredential,
readOnly,
cipherEncryptDatabase,
fixDuplicateUuid
)
}
fun saveDatabase(save: Boolean) {
_saveDatabase.value = save
fun createDatabase(
databaseUri: Uri,
mainCredential: MainCredential
) {
mDatabaseTaskProvider.startDatabaseCreate(databaseUri, mainCredential)
}
fun mergeDatabase(save: Boolean) {
_mergeDatabase.value = save
fun assignMainCredential(
databaseUri: Uri?,
mainCredential: MainCredential
) {
if (databaseUri != null) {
mDatabaseTaskProvider.startDatabaseAssignCredential(databaseUri, mainCredential)
}
}
fun saveDatabase(save: Boolean, saveToUri: Uri? = null) {
mDatabaseTaskProvider.startDatabaseSave(save, saveToUri)
}
fun mergeDatabase(
save: Boolean,
fromDatabaseUri: Uri? = null,
mainCredential: MainCredential? = null
) {
mDatabaseTaskProvider.startDatabaseMerge(save, fromDatabaseUri, mainCredential)
}
fun reloadDatabase(fixDuplicateUuid: Boolean) {
_reloadDatabase.value = fixDuplicateUuid
mDatabaseTaskProvider.askToStartDatabaseReload(
conditionToAsk = database?.dataModifiedSinceLastLoading != false
) {
mDatabaseTaskProvider.startDatabaseReload(fixDuplicateUuid)
}
}
fun saveName(oldValue: String,
newValue: String,
save: Boolean) {
_saveName.value = SuperString(oldValue, newValue, save)
fun onDatabaseChangeValidated() {
mDatabaseTaskProvider.onDatabaseChangeValidated()
}
fun saveDescription(oldValue: String,
newValue: String,
save: Boolean) {
_saveDescription.value = SuperString(oldValue, newValue, save)
/*
* Nodes actions
*/
fun createEntry(
newEntry: Entry,
parent: Group,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseCreateEntry(
newEntry,
parent,
save
)
}
fun saveDefaultUsername(oldValue: String,
newValue: String,
save: Boolean) {
_saveDefaultUsername.value = SuperString(oldValue, newValue, save)
fun updateEntry(
oldEntry: Entry,
entryToUpdate: Entry,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseUpdateEntry(
oldEntry,
entryToUpdate,
save
)
}
fun saveColor(oldValue: String,
newValue: String,
save: Boolean) {
_saveColor.value = SuperString(oldValue, newValue, save)
fun restoreEntryHistory(
mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseRestoreEntryHistory(
mainEntryId,
entryHistoryPosition,
save
)
}
fun saveCompression(oldValue: CompressionAlgorithm,
newValue: CompressionAlgorithm,
save: Boolean) {
_saveCompression.value = SuperCompression(oldValue, newValue, save)
fun deleteEntryHistory(
mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseDeleteEntryHistory(
mainEntryId,
entryHistoryPosition,
save
)
}
fun createGroup(
newGroup: Group,
parent: Group,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseCreateGroup(
newGroup,
parent,
save
)
}
fun updateGroup(
oldGroup: Group,
groupToUpdate: Group,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseUpdateGroup(
oldGroup,
groupToUpdate,
save
)
}
fun copyNodes(
nodesToCopy: List<Node>,
newParent: Group,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseCopyNodes(
nodesToCopy,
newParent,
save
)
}
fun moveNodes(
nodesToMove: List<Node>,
newParent: Group,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseMoveNodes(
nodesToMove,
newParent,
save
)
}
fun deleteNodes(
nodes: List<Node>,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseDeleteNodes(
nodes,
save
)
}
/*
* Attributes
*/
fun buildNewAttachment(): BinaryData? {
return database?.buildNewBinaryAttachment()
}
/*
* Settings actions
*/
fun saveName(
oldValue: String,
newValue: String,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveName(
oldValue,
newValue,
save
)
}
fun saveDescription(
oldValue: String,
newValue: String,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveDescription(
oldValue,
newValue,
save
)
}
fun saveDefaultUsername(
oldValue: String,
newValue: String,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveDefaultUsername(
oldValue,
newValue,
save
)
}
fun saveColor(
oldValue: String,
newValue: String,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveColor(
oldValue,
newValue,
save
)
}
fun saveCompression(
oldValue: CompressionAlgorithm,
newValue: CompressionAlgorithm,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveCompression(
oldValue,
newValue,
save
)
}
fun removeUnlinkedData(save: Boolean) {
_removeUnlinkData.value = save
mDatabaseTaskProvider.startDatabaseRemoveUnlinkedData(save)
}
fun saveRecycleBin(oldValue: Group?,
newValue: Group?,
save: Boolean) {
_saveRecycleBin.value = SuperGroup(oldValue, newValue, save)
fun saveRecycleBin(
oldValue: Group?,
newValue: Group?,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveRecycleBin(
oldValue,
newValue,
save
)
}
fun saveTemplatesGroup(oldValue: Group?,
newValue: Group?,
save: Boolean) {
_saveTemplatesGroup.value = SuperGroup(oldValue, newValue, save)
fun saveTemplatesGroup(
oldValue: Group?,
newValue: Group?,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveTemplatesGroup(
oldValue,
newValue,
save
)
}
fun saveMaxHistoryItems(oldValue: Int,
newValue: Int,
save: Boolean) {
_saveMaxHistoryItems.value = SuperInt(oldValue, newValue, save)
fun saveMaxHistoryItems(
oldValue: Int,
newValue: Int,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveMaxHistoryItems(
oldValue,
newValue,
save
)
}
fun saveMaxHistorySize(oldValue: Long,
newValue: Long,
save: Boolean) {
_saveMaxHistorySize.value = SuperLong(oldValue, newValue, save)
fun saveMaxHistorySize(
oldValue: Long,
newValue: Long,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveMaxHistorySize(
oldValue,
newValue,
save
)
}
fun saveEncryption(oldValue: EncryptionAlgorithm,
newValue: EncryptionAlgorithm,
save: Boolean) {
_saveEncryption.value = SuperEncryption(oldValue, newValue, save)
fun saveEncryption(
oldValue: EncryptionAlgorithm,
newValue: EncryptionAlgorithm,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveEncryption(
oldValue,
newValue,
save
)
}
fun saveKeyDerivation(oldValue: KdfEngine,
newValue: KdfEngine,
save: Boolean) {
_saveKeyDerivation.value = SuperKeyDerivation(oldValue, newValue, save)
fun saveKeyDerivation(
oldValue: KdfEngine,
newValue: KdfEngine,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveKeyDerivation(
oldValue,
newValue,
save
)
}
fun saveIterations(oldValue: Long,
newValue: Long,
save: Boolean) {
_saveIterations.value = SuperLong(oldValue, newValue, save)
fun saveIterations(
oldValue: Long,
newValue: Long,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveIterations(
oldValue,
newValue,
save
)
}
fun saveMemoryUsage(oldValue: Long,
newValue: Long,
save: Boolean) {
_saveMemoryUsage.value = SuperLong(oldValue, newValue, save)
fun saveMemoryUsage(
oldValue: Long,
newValue: Long,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveMemoryUsage(
oldValue,
newValue,
save
)
}
fun saveParallelism(oldValue: Long,
newValue: Long,
save: Boolean) {
_saveParallelism.value = SuperLong(oldValue, newValue, save)
fun saveParallelism(
oldValue: Long,
newValue: Long,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveParallelism(
oldValue,
newValue,
save
)
}
data class ActionResult(val database: ContextualDatabase,
val actionTask: String,
val result: ActionRunnable.Result)
data class SuperString(val oldValue: String,
val newValue: String,
val save: Boolean)
data class SuperInt(val oldValue: Int,
val newValue: Int,
val save: Boolean)
data class SuperLong(val oldValue: Long,
val newValue: Long,
val save: Boolean)
data class SuperMerge(val fixDuplicateUuid: Boolean,
val save: Boolean)
data class SuperCompression(val oldValue: CompressionAlgorithm,
val newValue: CompressionAlgorithm,
val save: Boolean)
data class SuperEncryption(val oldValue: EncryptionAlgorithm,
val newValue: EncryptionAlgorithm,
val save: Boolean)
data class SuperKeyDerivation(val oldValue: KdfEngine,
val newValue: KdfEngine,
val save: Boolean)
data class SuperGroup(val oldValue: Group?,
val newValue: Group?,
val save: Boolean)
/*
* Hardware Key
*/
fun onChallengeResponded(challengeResponse: ByteArray?) {
mDatabaseTaskProvider.startChallengeResponded(
challengeResponse ?: ByteArray(0)
)
}
override fun onCleared() {
super.onCleared()
mDatabaseTaskProvider.unregisterProgressTask()
mDatabaseTaskProvider.destroy()
}
sealed class ActionState {
object Loading: ActionState()
object OnDatabaseReloaded: ActionState()
data class OnDatabaseActionRequested(
val bundle: Bundle? = null,
val actionTask: String
): ActionState()
data class OnDatabaseInfoChanged(
val previousDatabaseInfo: SnapFileDatabaseInfo,
val newDatabaseInfo: SnapFileDatabaseInfo,
val readOnlyDatabase: Boolean
): ActionState()
data class OnDatabaseActionStarted(
var database: ContextualDatabase,
val progressMessage: ProgressMessage
): ActionState()
data class OnDatabaseActionUpdated(
var database: ContextualDatabase,
val progressMessage: ProgressMessage
): ActionState()
data class OnDatabaseActionStopped(
var database: ContextualDatabase?
): ActionState()
data class OnDatabaseActionFinished(
var database: ContextualDatabase,
val actionTask: String,
val result: ActionRunnable.Result
): ActionState()
}
}

View File

@@ -3,6 +3,7 @@ package com.kunzisoft.keepass.viewmodels
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Entry
@@ -16,10 +17,11 @@ import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.utils.IOActionTask
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.UUID
@@ -28,12 +30,18 @@ class EntryEditViewModel: NodeEditViewModel() {
private var mEntryId: NodeId<UUID>? = null
private var mParentId: NodeId<*>? = null
private var mRegisterInfo: RegisterInfo? = null
private var mSearchInfo: SearchInfo? = null
private var mParent: Group? = null
private var mEntry: Entry? = null
private var mIsTemplate: Boolean = false
private val mTempAttachments = mutableListOf<EntryAttachmentState>()
// To show dialog only one time
var backPressedAlreadyApproved = false
var warningOverwriteDataAlreadyApproved = false
// Useful to not relaunch a current action
private var actionLocked: Boolean = false
val templatesEntry : LiveData<TemplatesEntry?> get() = _templatesEntry
private val _templatesEntry = MutableLiveData<TemplatesEntry?>()
@@ -73,24 +81,28 @@ class EntryEditViewModel: NodeEditViewModel() {
val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded
private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>()
fun loadDatabase(database: ContextualDatabase?) {
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo, mSearchInfo)
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
fun loadTemplateEntry(database: ContextualDatabase?) {
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo)
}
fun loadTemplateEntry(database: ContextualDatabase?,
entryId: NodeId<UUID>?,
parentId: NodeId<*>?,
registerInfo: RegisterInfo?,
searchInfo: SearchInfo?) {
fun loadTemplateEntry(
database: ContextualDatabase?,
entryId: NodeId<UUID>?,
parentId: NodeId<*>?,
registerInfo: RegisterInfo?
) {
this.mEntryId = entryId
this.mParentId = parentId
this.mRegisterInfo = registerInfo
this.mSearchInfo = searchInfo
database?.let {
mEntryId?.let {
IOActionTask(
{
scope = viewModelScope,
action = {
// Create an Entry copy to modify from the database entry
mEntry = database.getEntryById(it)
// Retrieve the parent
@@ -105,21 +117,24 @@ class EntryEditViewModel: NodeEditViewModel() {
database,
entry,
mIsTemplate,
registerInfo,
searchInfo
registerInfo
)
}
},
{ templatesEntry ->
onActionComplete = { templatesEntry ->
mEntryId = null
_templatesEntry.value = templatesEntry
if (templatesEntry?.overwrittenData == true) {
mUiState.value = UIState.ShowOverwriteMessage
}
}
).execute()
}
mParentId?.let {
IOActionTask(
{
scope = viewModelScope,
action = {
mParent = database.getGroupById(it)
mParent?.let { parentGroup ->
mEntry = database.createEntry()?.apply {
@@ -145,12 +160,11 @@ class EntryEditViewModel: NodeEditViewModel() {
database,
mEntry,
mIsTemplate,
registerInfo,
searchInfo
registerInfo
)
}
},
{ templatesEntry ->
onActionComplete = { templatesEntry ->
mParentId = null
_templatesEntry.value = templatesEntry
}
@@ -159,33 +173,37 @@ class EntryEditViewModel: NodeEditViewModel() {
}
}
private fun decodeTemplateEntry(database: ContextualDatabase,
entry: Entry?,
isTemplate: Boolean,
registerInfo: RegisterInfo?,
searchInfo: SearchInfo?): TemplatesEntry {
private fun decodeTemplateEntry(
database: ContextualDatabase,
entry: Entry?,
isTemplate: Boolean,
registerInfo: RegisterInfo?
): TemplatesEntry {
val templates = database.getTemplates(isTemplate)
val entryTemplate = entry?.let { database.getTemplate(it) }
?: Template.STANDARD
var entryInfo: EntryInfo? = null
var overwrittenData = false
// Decode the entry / load entry info
entry?.let {
database.decodeEntryWithTemplateConfiguration(it).let { entry ->
// Load entry info
entry.getEntryInfo(database, true).let { tempEntryInfo ->
// Retrieve data from registration
// TODO only save registration
searchInfo?.let { tempSearchInfo ->
tempEntryInfo.saveSearchInfo(database, tempSearchInfo)
}
registerInfo?.let { regInfo ->
tempEntryInfo.saveRegisterInfo(database, regInfo)
overwrittenData = tempEntryInfo.saveRegisterInfo(database, regInfo)
}
entryInfo = tempEntryInfo
}
}
}
return TemplatesEntry(isTemplate, templates, entryTemplate, entryInfo)
return TemplatesEntry(
isTemplate,
templates,
entryTemplate,
entryInfo,
overwrittenData
)
}
fun changeTemplate(template: Template) {
@@ -198,44 +216,52 @@ class EntryEditViewModel: NodeEditViewModel() {
_requestEntryInfoUpdate.value = EntryUpdate(database, mEntry, mParent)
}
fun unlockAction() {
actionLocked = false
}
fun saveEntryInfo(database: ContextualDatabase?, entry: Entry?, parent: Group?, entryInfo: EntryInfo) {
IOActionTask(
{
removeTempAttachmentsNotCompleted(entryInfo)
entry?.let { oldEntry ->
// Create a clone
var newEntry = Entry(oldEntry)
if (actionLocked.not()) {
actionLocked = true
IOActionTask(
scope = viewModelScope,
action = {
removeTempAttachmentsNotCompleted(entryInfo)
entry?.let { oldEntry ->
// Create a clone
var newEntry = Entry(oldEntry)
// Build info
newEntry.setEntryInfo(database, entryInfo)
// Build info
newEntry.setEntryInfo(database, entryInfo)
// Encode entry properties for template
_onTemplateChanged.value?.let { template ->
newEntry =
database?.encodeEntryWithTemplateConfiguration(newEntry, template)
?: newEntry
}
// Encode entry properties for template
_onTemplateChanged.value?.let { template ->
newEntry =
database?.encodeEntryWithTemplateConfiguration(newEntry, template)
?: newEntry
}
// Delete temp attachment if not used
mTempAttachments.forEach { tempAttachmentState ->
val tempAttachment = tempAttachmentState.attachment
database?.attachmentPool?.let { binaryPool ->
if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) {
database.removeAttachmentIfNotUsed(tempAttachment)
// Delete temp attachment if not used
mTempAttachments.forEach { tempAttachmentState ->
val tempAttachment = tempAttachmentState.attachment
database?.attachmentPool?.let { binaryPool ->
if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) {
database.removeAttachmentIfNotUsed(tempAttachment)
}
}
}
}
// Return entry to save
EntrySave(oldEntry, newEntry, parent)
// Return entry to save
EntrySave(oldEntry, newEntry, parent)
}
},
onActionComplete = { entrySave ->
entrySave?.let {
_onEntrySaved.value = it
}
}
},
{ entrySave ->
entrySave?.let {
_onEntrySaved.value = it
}
}
).execute()
).execute()
}
}
private fun removeTempAttachmentsNotCompleted(entryInfo: EntryInfo) {
@@ -322,10 +348,13 @@ class EntryEditViewModel: NodeEditViewModel() {
_onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition)
}
data class TemplatesEntry(val isTemplate: Boolean,
val templates: List<Template>,
val defaultTemplate: Template,
val entryInfo: EntryInfo?)
data class TemplatesEntry(
val isTemplate: Boolean,
val templates: List<Template>,
val defaultTemplate: Template,
val entryInfo: EntryInfo?,
val overwrittenData: Boolean = false
)
data class EntryUpdate(val database: ContextualDatabase?, val entry: Entry?, val parent: Group?)
data class EntrySave(val oldEntry: Entry, val newEntry: Entry, val parent: Group?)
data class FieldEdition(val oldField: Field?, val newField: Field?)
@@ -333,6 +362,11 @@ class EntryEditViewModel: NodeEditViewModel() {
data class AttachmentUpload(val attachmentToUploadUri: Uri, val attachment: Attachment)
data class AttachmentPosition(val entryAttachmentState: EntryAttachmentState, val viewPosition: Float)
sealed class UIState {
object Loading: UIState()
object ShowOverwriteMessage: UIState()
}
companion object {
private val TAG = EntryEditViewModel::class.java.name
}

View File

@@ -20,6 +20,7 @@
<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:id="@+id/activity_entry_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:filterTouchesWhenObscured="true">

View File

@@ -101,6 +101,13 @@
android:checked="false"
style="@style/KeepassDXStyle.Chip.Filter"
android:text="@string/entry_password"/>
<com.google.android.material.chip.Chip
android:id="@+id/search_chip_application_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
style="@style/KeepassDXStyle.Chip.Filter"
android:text="@string/entry_application_id"/>
<com.google.android.material.chip.Chip
android:id="@+id/search_chip_url"
android:layout_width="wrap_content"

View File

@@ -335,7 +335,7 @@
<string name="device_unlock_explanation_summary">استخدم إلغاء القفل الجهاز لفتح قاعدة البيانات بسهولة</string>
<string name="lock_database_show_button_summary">يعرض زر القَفل في الواجهة</string>
<string name="lock_database_show_button_title">أظهر زر القفل</string>
<string name="lock_database_back_root_summary">قفل قاعدة البيانات عند النقر على زر الرجوع في الشاشة الرئيسية</string>
<string name="lock_database_back_root_summary">اضغط على \"رجوع\" لقفل قاعدة البيانات إذا كنت في الشاشة الجذر لقاعدة البيانات</string>
<string name="lock_database_back_root_title">اضغط على \"رجوع\" للإقفال</string>
<string name="clipboard_explanation_summary">انسخ حقول المدخل باستخدام الحافظة</string>
<string name="database_opened">قاعدة البيانات مفتوحة</string>
@@ -452,7 +452,6 @@
<string name="menu_form_filling_settings">ملء النموذج</string>
<string name="menu_reload_database">أعد تحميل البيانات</string>
<string name="menu_external_icon">أيقونة خارجية</string>
<string name="registration_mode">وضع التسجيل</string>
<string name="import_app_properties_title">استورد خصائص التطبيق</string>
<string name="import_app_properties_summary">اختر ملفًا لاستيراد إعدادات التطبيق</string>
<string name="export_app_properties_title">صدّر إعدادات التطبيق</string>
@@ -643,8 +642,8 @@
<string name="show_uuid_title">أظهر \"المعرّف العام المميز\" UUID</string>
<string name="unlock_and_link_biometric">رابط فتح الجهاز</string>
<string name="device_unlock_invalid_key">لا يمكن قراءة مفتاح فتح الجهاز. يرجى حذفه وتكرار إجراء التعرف على الفتح.</string>
<string name="menu_appearance_settings_summary">المظاهر والألوان والسمات</string>
<string name="autofill_explanation_summary">تمكين الملء التلقائي لملء النماذج بسرعة في التطبيقات الأخرى</string>
<string name="menu_appearance_settings_summary">المظاهر والألوان والأيقونات والخطوط والسمات</string>
<string name="autofill_explanation_summary">اضبط الملء التلقائي لملء النماذج بسرعة في التطبيقات الأخرى</string>
<string name="device_credential_unlock_enable_summary">يتيح لك استخدام بيانات اعتماد جهازك لفتح قاعدة البيانات</string>
<string name="unlock">فتح</string>
<string name="menu_app_settings_summary">البحث، القفل، التاريخ، الخصائص</string>
@@ -692,4 +691,45 @@
<string name="warning_large_keyfile">لا يُنصح بإضافة ملف مفتاحي كبير، فقد يؤدي هذا إلى منع فتح قاعدة البيانات.</string>
<string name="hide_templates_title">أخفِ القوالب</string>
<string name="error_otp_secret_length">يجب أن يتكوّن المفتاح السري من %1$d أحرف على الأقل.</string>
<string name="entry_application_id">معرّف التطبيق</string>
<string name="warning_overwrite_data_title">أتريد الكتابة فوق البيانات الموجودة؟</string>
<string name="warning_overwrite_data_description">سيؤدي هذا الإجراء إلى استبدال البيانات الموجودة في الإدخال، ويمكنك استرداد البيانات القديمة إذا كانت المحفوظات مفعلة.</string>
<string name="credential_provider">موفّر بيانات الاعتماد</string>
<string name="passkeys">مفاتيح المرور</string>
<string name="passkeys_explanation_summary">اضبط مفاتيح المرور لتسجيل دخول سريع وآمن بدون كلمة سر</string>
<string name="passkeys_preference_title">إعدادات مفاتيح المرور</string>
<string name="passkeys_close_database_title">أغلق قاعدة البيانات</string>
<string name="passkeys_close_database_summary">أغلق قاعدة البيانات بعد اختيار مفتاح المرور</string>
<string name="passkeys_privileged_apps_title">التطبيقات المتميزة</string>
<string name="passkeys_privileged_apps_summary">أدر المتصفحات في القائمة المخصّصة للتطبيقات المتميزة</string>
<string name="passkeys_privileged_apps_explanation">تحذير: يعمل تطبيق مميز كبوابة لاسترداد أصل الاستيثاق. تأكد من شرعيته لتجنب المشكلات الأمنية.</string>
<string name="passkeys_privileged_apps_ask_title">التطبيق غير معروف</string>
<string name="passkeys_privileged_apps_ask_message">يحاول %1$s تنفيذ إجراء مفتاح المرور.\n\nأتريد إضافته إلى قائمة التطبيقات المتميزة؟</string>
<string name="passkeys_missing_signature_app_ask_title">التوقيع مفقود</string>
<string name="passkeys_missing_signature_app_ask_explanation">تحذير: أُنشئ مفتاح المرور من عميل آخر أو حُذف التوقيع. تأكد من أن التطبيق الذي تريد الاستيثاق عليه جزء من نفس الخدمة وأنه شرعي لتجنب المشكلات الأمنية.\nإذا كان التطبيق متصفحًا، فلا تضف توقيعه إلى الإدخال، بل إلى قائمة التطبيقات الموثوقة في الإعدادات.</string>
<string name="passkeys_missing_signature_app_ask_message">%1$s غير معروف ويحاول الاستيثاق باستخدام مفتاح مرور موجود.</string>
<string name="passkeys_missing_signature_app_ask_question">إضافة توقيع التطبيق إلى إدخال مفتاح المرور؟</string>
<string name="passkeys_auto_select_title">تحديد تلقائي</string>
<string name="passkeys_auto_select_summary">حدّد تلقائي إذا كان هناك إدخال واحد فقط وقاعدة البيانات مفتوحة، فقط إذا كان التطبيق الطالب متوافقًا</string>
<string name="passkeys_backup_eligibility_title">أهلية النسخ الاحتياطي</string>
<string name="passkeys_backup_eligibility_summary">تحديد وقت الإنشاء ما إذا كان مسموحًا بنسخ مصدر بيانات اعتماد المفتاح العام احتياطيًا</string>
<string name="passkeys_backup_state_title">حالة النسخ الاحتياطي</string>
<string name="passkeys_backup_state_summary">أشر إلى أن بيانات الاعتماد مدعومة ومحمية ضد فقدان جهاز واحد</string>
<string name="credential_provider_service_subtitle">مفاتيح المرور، موفّر بيانات اعتماد الملء التلقائي</string>
<string name="passkey">مفتاح المرور</string>
<string name="passkey_service_name">موفّر بيانات اعتماد KeePassDX</string>
<string name="passkey_creation_description">احفظ مفتاح المرور في مدخل جديد</string>
<string name="passkey_update_description">حدِّث مفتاح المرور في %1$s</string>
<string name="passkey_selection_username">لم يُعثر على مفتاح مرور</string>
<string name="passkey_selection_description">حدّد مفتاح مرور موجود</string>
<string name="passkey_database_username">قاعدة بيانات KeePassDX</string>
<string name="passkey_locked_database_description">حدّد لفتح القفل</string>
<string name="passkey_username">اسم مستخدم مفتاح المرور</string>
<string name="passkey_private_key">المفتاح الخاص لمفتاح المرور</string>
<string name="passkey_credential_id">معرّف بيانات مفتاح المرور</string>
<string name="passkey_user_handle">معرّف مستخدم مفتاح المرور</string>
<string name="passkey_relying_party">الطرف المعتمد لمفتاح المرور</string>
<string name="passkey_backup_eligibility">أهلية النسخ الاحتياطي لمفتاح المرور</string>
<string name="passkey_backup_state">حالة النسخ الاحتياطي لمفتاح المرور</string>
<string name="error_passkey_result">تعذر إرجاع مفتاح المرور</string>
</resources>

View File

@@ -485,7 +485,6 @@
<string name="search_mode">Axtarış modu</string>
<string name="save_mode">Yadda saxlama modu</string>
<string name="selection_mode">Seçim modu</string>
<string name="registration_mode">Qeydiyyat modu</string>
<string name="remember_database_locations_title">Məlumat bazalarının yerlərini xatırlayın</string>
<string name="remember_database_locations_summary">Məlumat bazalarının harada saxlanıldığını izlə</string>
<string name="remember_hardware_key_summary">Aparat-təchizat açarlarının harada istifadə olunduğunu izlə</string>

View File

@@ -336,7 +336,6 @@
<string name="menu_keystore_remove_key">Izbrišite ključ za otključavanje uređaja</string>
<string name="subdomain_search_summary">Pretražujte veb domene sa ograničenjima poddomena</string>
<string name="export_app_properties_title">Izvezite podešavanja aplikacije</string>
<string name="registration_mode">Režim registracije</string>
<string name="remember_database_locations_title">Zapamtite lokacije baza podataka</string>
<string name="remember_hardware_key_title">Zapamtite hardverske ključeve</string>
<string name="remember_hardware_key_summary">Vodi evidenciju o korišćenim hardverskim ključevima</string>

View File

@@ -296,7 +296,6 @@
<string name="search_mode">Рэжым пошуку</string>
<string name="save_mode">Рэжым захавання</string>
<string name="selection_mode">Рэжым выбару</string>
<string name="registration_mode">Рэжым рэгістрацыі</string>
<string name="remember_database_locations_title">Запамінаць размяшчэнне баз дадзеных</string>
<string name="remember_database_locations_summary">Адсочвае, дзе захоўваюцца базы дадзеных</string>
<string name="remember_keyfile_locations_title">Запамінаць размяшчэнне файлаў ключоў</string>

View File

@@ -232,7 +232,7 @@
<string name="auto_focus_search_title">Бързо търсене</string>
<string name="subdomain_search_title">Търсене на поддомейни</string>
<string name="menu_delete">Изтриване</string>
<string name="menu_appearance_settings_summary">Теми, цветове, атрибути</string>
<string name="menu_appearance_settings_summary">Теми, цветове, икони, шрифтове, атрибути</string>
<string name="download_initialization">Подготвяне…</string>
<string name="content_description_entry_background_color">Цвят на фона на запис</string>
<string name="html_about_licence">KeePassDX © %1$d Kunzisoft е приложение с &lt;strong&gt;отворен код&lt;/strong&gt; и &lt;strong&gt;без реклами&lt;/strong&gt;. \nРазпространява се под лиценза &lt;strong&gt;GPLv3&lt;/strong&gt; без каквато и да е гаранция.</string>
@@ -283,11 +283,11 @@
<string name="search">Търсене</string>
<string name="uppercase">Горен регистър</string>
<string name="warning">Внимание</string>
<string name="lock_database_back_root_summary">Заключва хранилището при докосване на бутона „Назад“ на началния екран</string>
<string name="lock_database_back_root_summary">Бутонът „Назад“ в кореновата папка заключва хранилището</string>
<string name="read_only">Само за четене</string>
<string name="contains_duplicate_uuid">Хранилището съдържа повтарящ се идентификатор.</string>
<string name="biometric">Биометричен ключ</string>
<string name="set_credential_provider_service_title">Задаване на подразбирана услуга за автоматично попълване</string>
<string name="set_credential_provider_service_title">Услуга за автоматично попълване на регистрации</string>
<string name="password_size_title">Дължина на създаваните пароли</string>
<string name="lock_database_back_root_title">Заключване при „Назад“</string>
<string name="content">Съдържание</string>
@@ -398,7 +398,7 @@
<string name="education_search_summary">Търсете по заглавие, потребител или съдържание на други полета, за да намерите своите пароли.</string>
<string name="html_about_contribution">За &lt;strong&gt;запазване на нашата независимост&lt;/strong&gt;, &lt;strong&gt;отстраняване на дефекти&lt;/strong&gt;, &lt;strong&gt;добавяне на нови възможности&lt;/strong&gt; и &lt;strong&gt;поддържане на активна разработка&lt;/strong&gt;, разчитаме на вашата &lt;strong&gt;поддръжка&lt;/strong&gt;.</string>
<string name="lock_database_show_button_title">Бутон за заключване</string>
<string name="autofill_explanation_summary">Включете услугата за попълване на формуляри в други приложения</string>
<string name="autofill_explanation_summary">Настройки на услугата за попълване на формуляри в други приложения</string>
<string name="properties">Свойства</string>
<string name="education_validate_entry_summary">Не забравяйте да потвърдите записа и да го запазите в хранилището.
\n
@@ -462,7 +462,6 @@
<string name="error_invalid_OTP">Неприемлива тайна за OTP.</string>
<string name="error_no_name">Въведете име.</string>
<string name="hide_broken_locations_summary">Скрива вече несъществуващи хранилища от списъка с последно отваряните</string>
<string name="registration_mode">Режим регистрация</string>
<string name="remember_database_locations_title">Запомняне използваните хранилища</string>
<string name="show_recent_files_title">Показване на последните хранилища</string>
<string name="search_mode">Режим търсене</string>
@@ -654,7 +653,7 @@
<string name="biometric_auto_open_prompt_summary">Автоматична заявка за отключване на устройството ако хранилището се отключва с устройството</string>
<string name="allow_copy_password_summary">Разрешава копиране на паролите и защитените полета от записите в междинната памет</string>
<string name="error_rebuild_list">Списъкът не може да бъде изграден отново.</string>
<string name="error_otp_type">Формулярът не разпознава този вид OTP и може да не създава правилни кодове за достъп.</string>
<string name="error_otp_type">Формулярът не разпознава този вид OTP и може да не създава верни кодове за достъп.</string>
<string name="warning_keyfile_integrity">Отпечатъкът от файла не е сигурен, защото Андроид може да променя данните в движение. Променете разширението на файла на .bin, за бъде невредим.</string>
<string name="allow_copy_password_title">Доверяване на междинната памет</string>
<string name="warning_exact_alarm">Приложението няма права за използване на точен будилник. В резултат на това дейностите, които зависят от него няма да се изпълняват на време.</string>
@@ -682,4 +681,45 @@
<string name="hide_templates_summary">Шаблоните не се показват</string>
<string name="hide_templates_title">Скриване на шаблоните</string>
<string name="error_otp_secret_length">Тайният ключ трябва да бъде най-малко %1$d знаци.</string>
<string name="entry_application_id">Идент. приложение</string>
<string name="warning_overwrite_data_title">Презаписване на информация?</string>
<string name="warning_overwrite_data_description">Чрез това действие ще бъде презаоисана информация на записа. Може да намерите старите данни ако историята е включена.</string>
<string name="credential_provider">Доставчик на регистрации</string>
<string name="passkeys">Ключове за достъп</string>
<string name="passkeys_explanation_summary">Настройки на ключове за достъп за бърз и сигурен вход без парола</string>
<string name="passkeys_preference_title">Настройки на ключове за достъп</string>
<string name="passkeys_close_database_title">Затваряне на хранилище</string>
<string name="passkeys_close_database_summary">Затваряне на хранилището след избор на ключ за достъп</string>
<string name="passkeys_privileged_apps_explanation">ВНИМАНИЕ: Привилегированото приложение работи като шлюз, който получава източника на удостоверяването. За да избегнете проблеми със сигурността се уверете в неговата автентичност.</string>
<string name="passkeys_privileged_apps_title">Привилегировани приложения</string>
<string name="passkeys_privileged_apps_summary">Управление на мрежови четци в потребителския списък с привилегировани приложения</string>
<string name="passkeys_privileged_apps_ask_title">Неразпознато приложение</string>
<string name="passkeys_privileged_apps_ask_message">Приложението „%1$s“ опитва да извърши действия с ключ за достъп .\n\nДа бъде ли добавено в списъка с привилегировани приложения?</string>
<string name="passkeys_missing_signature_app_ask_title">Липсващ подпис</string>
<string name="passkeys_missing_signature_app_ask_message">Приложението „%1$s“ не е разпознато, но се опитва да извърши удостоверяване с ключ за достъп.</string>
<string name="passkeys_missing_signature_app_ask_question">Добавяне на подпис към запис на ключ за достъп?</string>
<string name="passkeys_auto_select_title">Автоматичен избор</string>
<string name="passkeys_auto_select_summary">Работи при отключено хранилище, съвпадение на един запис и съвместимо запитващо приложение</string>
<string name="passkeys_backup_eligibility_title">Възможност за резервно копие</string>
<string name="passkeys_backup_eligibility_summary">Настройка, определяща още при създаване разрешението за резервно копие на публичния ключ на източника на регистрацията</string>
<string name="passkeys_backup_state_title">Наличие на резервно копие</string>
<string name="passkeys_backup_state_summary">Указва дали регистрацията е включена в резервно копие и е защитена от загуба на едно устройство</string>
<string name="credential_provider_service_subtitle">Услуга за автоматично попълване на ключове за достъп и регистрации</string>
<string name="passkey">Ключ за достъп</string>
<string name="passkey_service_name">Доставчик на регистрации на KeePassDX</string>
<string name="passkey_creation_description">Запазване ключа за достъп в друг запис</string>
<string name="passkey_update_description">Обновяване ключа за достъп в/ъв %1$s</string>
<string name="passkey_selection_username">Ключ за достъп не е намерен</string>
<string name="passkey_selection_description">Изберете съществуващ ключ за достъп</string>
<string name="passkey_database_username">Хранилище на KeePassDX</string>
<string name="passkey_locked_database_description">Изберете за отключване</string>
<string name="passkey_username">Потребител на ключа за достъп</string>
<string name="passkey_private_key">Частен ключ на ключа за достъп</string>
<string name="passkey_credential_id">Идент. на регистрация на ключа за достъп</string>
<string name="passkey_user_handle">Идент. на потребител на ключа за достъп</string>
<string name="passkey_backup_eligibility">Възможност за резервно копие на ключа за достъп</string>
<string name="passkey_backup_state">Състояние на резервно копие на ключа за достъп</string>
<string name="error_passkey_result">Грешка при връщане на ключ за достъп</string>
<string name="passkey_relying_party">Доверяваща страна на ключа за достъп</string>
<string name="passkeys_missing_signature_app_ask_explanation">ВНИМАНИЕ: Ключът за достъп е създаден от друг клиент или подписът е премахнат. За да избегнете проблеми със сигурността се уверете, че приложението, което удостоверявате е част от същата услуга и е автентично.\nАко приложението е мрежов четец не добавяйте подписа му към записа, а в настройките го добавете в списъка с привилегировани приложения.</string>
</resources>

View File

@@ -209,7 +209,6 @@
<string name="search_mode">অনুসন্ধান মোড</string>
<string name="save_mode">সেভ মোড</string>
<string name="selection_mode">নির্বাচন মোড</string>
<string name="registration_mode">রেজিস্ট্রেশন মোড</string>
<string name="remember_keyfile_locations_summary">কী ফাইলগুলি কোথায় সংরক্ষণ করা হয় তা ট্র্যাক রাখে</string>
<string name="show_recent_files_title">সাম্প্রতিক ফাইল দেখান</string>
<string name="show_recent_files_summary">সাম্প্রতিক ডাটাবেসের অবস্থান দেখান</string>

View File

@@ -347,7 +347,6 @@
<string name="place_of_issue">Lloc d\'expedició</string>
<string name="style_brightness_summary">Escull tema clar o fosc</string>
<string name="hardware_key">Clau física</string>
<string name="registration_mode">Mode de registre</string>
<string name="ignore_chars_filter">Ignora caràcters</string>
<string name="ask">Pregunta</string>
<string name="searchable">Cercable</string>

View File

@@ -180,8 +180,8 @@
<string name="general">Obecné</string>
<string name="autofill">Samovyplnění</string>
<string name="autofill_sign_in_prompt">Přihlásit se s KeePassDX</string>
<string name="set_credential_provider_service_title">Nastavit výchozí službu samovyplnění</string>
<string name="autofill_explanation_summary">Zapnout samovyplnění formulářů za účelem rychlého vyplnění v ostatních aplikacích</string>
<string name="set_credential_provider_service_title">Služba poskytovatele údajů</string>
<string name="autofill_explanation_summary">Nastavit automatické vyplnění formulářů za účelem rychlého vyplnění v ostatních aplikacích</string>
<string name="password_size_title">Délka generovaného hesla</string>
<string name="password_size_summary">Nastavení výchozí délky generovaných hesel</string>
<string name="list_password_generator_options_title">Znaky hesla</string>
@@ -206,7 +206,7 @@
<string name="assign_master_key">Přiřadit hlavní klíč</string>
<string name="create_keepass_file">Vytvořit nový trezor</string>
<string name="recycle_bin_title">Využití koše</string>
<string name="recycle_bin_summary">Před smazáním přesune vybrané položky do skupiny s názvem \"Koš\"</string>
<string name="recycle_bin_summary">Před smazáním přesune vybrané záznamy do skupiny s názvem Koš</string>
<string name="monospace_font_fields_enable_title">Písmo kolonek</string>
<string name="monospace_font_fields_enable_summary">Čitelnost znaků v kolonkách můžete přizpůsobit změnou písma</string>
<string name="allow_copy_password_title">Důvěřovat schránce</string>
@@ -283,9 +283,9 @@
<string name="keyboard_setting_label">Magikeyboard nastavení</string>
<string name="keyboard_entry_category">Záznam</string>
<string name="keyboard_entry_timeout_title">Časový limit</string>
<string name="keyboard_entry_timeout_summary">Doba uchování položky v Magikeyboardu</string>
<string name="keyboard_entry_timeout_summary">Doba uchování položky v klávesnici</string>
<string name="keyboard_notification_entry_title">Informace o oznámení</string>
<string name="keyboard_notification_entry_summary">Zobrazit oznámení, když je položka dostupná</string>
<string name="keyboard_notification_entry_summary">Zobrazit oznámení, když je záznam dostupný</string>
<string name="keyboard_notification_entry_content_title_text">Záznam</string>
<string name="keyboard_notification_entry_content_title">%1$s dostupné v Magikeyboardu</string>
<string name="keyboard_notification_entry_content_text">%1$s</string>
@@ -299,7 +299,7 @@
<string name="selection_mode">Režim výběru</string>
<string name="do_not_kill_app">Nezavírejte aplikaci…</string>
<string name="lock_database_back_root_title">K uzamknutí stiskněte Zpět</string>
<string name="lock_database_back_root_summary">Zamknout obrazovku, pokud uživatel stiskne tlačítko Zpět v hlavním panelu</string>
<string name="lock_database_back_root_summary">Stiskněte „Zpět“ pro uzamčení databáze, pokud se nacházíte na hlavní obrazovce databáze</string>
<string name="clear_clipboard_notification_title">Vymazat při ukončení</string>
<string name="clear_clipboard_notification_summary">Uzamknout databázi, jakmile trvání schránky vyprší nebo po uzavření oznámení</string>
<string name="recycle_bin">Koš</string>
@@ -480,7 +480,6 @@
<string name="biometric_security_update_required">Vyžadována aktualizace biometrického zabezpečení.</string>
<string name="configure_biometric">Žádné přihlašovací ani biometrické údaje nejsou registrovány.</string>
<string name="warning_empty_recycle_bin">Trvale odstranit všechny uzly z koše\?</string>
<string name="registration_mode">Registrace</string>
<string name="save_mode">Režim ukládání</string>
<string name="search_mode">Vyhledávání</string>
<string name="error_field_name_already_exists">Jméno kolonky již existuje.</string>
@@ -525,7 +524,7 @@
<string name="unit_mebibyte">MiB</string>
<string name="unit_kibibyte">KiB</string>
<string name="unit_byte">B</string>
<string name="error_otp_type">Nemohu rozpoznat existující typ OTP v této formě, jeho validace patrně nebude generovat správný token.</string>
<string name="error_otp_type">Nemohu rozpoznat existující typ OTP v této formě a jeho validace patrně nebude generovat správný token.</string>
<string name="download_canceled">Zrušeno!</string>
<string name="icon_section_custom">Vlastní</string>
<string name="icon_section_standard">Standardní</string>
@@ -680,7 +679,7 @@
<string name="ask">Zeptat se</string>
<string name="configure">Nastavit</string>
<string name="unlock_and_link_biometric">Propojení s odemykáním zařízení</string>
<string name="menu_appearance_settings_summary">Motivy, barvy, atributy</string>
<string name="menu_appearance_settings_summary">Motivy, barvy, ikony, písma, atributy</string>
<string name="unlock">Odemknout</string>
<string name="education_validate_entry_title">Ověřit vstup</string>
<string name="education_validate_entry_summary">Nezapomeňte ověřit svůj vstup a uložit databázi.
@@ -706,4 +705,45 @@
<string name="hide_templates_title">Skrýt šablony</string>
<string name="hide_templates_summary">Šablony nejsou zobrazeny</string>
<string name="error_otp_secret_length">Tajný klíč musí obsahovat alespoň %1$d znaků.</string>
<string name="entry_application_id">ID aplikace</string>
<string name="warning_overwrite_data_title">Přepsat existující data?</string>
<string name="warning_overwrite_data_description">Tato akce nahradí existující data v položce, pokud je povolena historie, můžete původní data získat.</string>
<string name="credential_provider">Poskytovatel údajů</string>
<string name="passkeys">Přístupové klíče</string>
<string name="passkeys_explanation_summary">Nastavte přístupové klíče pro rychlé a bezpečné přihlášení bez hesla</string>
<string name="passkeys_preference_title">Nastavení přístupových klíčů</string>
<string name="passkeys_close_database_title">Zavřít databázi</string>
<string name="passkeys_close_database_summary">Zavřít databázi po výběru přístupového klíče</string>
<string name="passkeys_privileged_apps_title">Privilegované aplikace</string>
<string name="passkeys_privileged_apps_summary">Spravovat prohlížeče ve vlastním seznamu privilegovaných aplikací</string>
<string name="passkeys_privileged_apps_explanation">VAROVÁNÍ: Privilegovaná aplikace funguje jako brána k získání původu autentifikace. Ujistěte se, že se jedná o důvěryhodnou aplikaci, pro zabránění bezpečnostním problémům.</string>
<string name="passkeys_privileged_apps_ask_title">Aplikace nerozpoznána</string>
<string name="passkeys_privileged_apps_ask_message">%1$s se pokouší provést akci přísupového klíče.\n\nChcete ji přidat do seznamu privilegovaných aplikací?</string>
<string name="passkeys_missing_signature_app_ask_title">Chybějící podpis</string>
<string name="passkeys_missing_signature_app_ask_explanation">VAROVÁNÍ: Přístupový klíč byl vytvořen z jiného klienta nebo byl vymazán podpis. Ujistěte se, že aplikace, kterou chcete autentifikovat, je součástí stejné služby a že je legitimní pro zabránění bezpečnostním problémům.\nPokud je aplikace prohlížeč, nepřidávejte její podpis do záznamu, ale do seznamu privilegovaných aplikací v nastavení.</string>
<string name="passkeys_missing_signature_app_ask_message">%1$s je nerozpoznaná a pokouší se o autentifikaci s existujícím přístupovým klíčem.</string>
<string name="passkeys_missing_signature_app_ask_question">Přidat podpis aplikace do záznamu přístupového klíče?</string>
<string name="passkeys_auto_select_title">Automatický výběr</string>
<string name="passkeys_auto_select_summary">Automaticky vybrat, pokud existuje pouze jeden záznam a aplikace je otevřená, pouze pokud je žádající aplikace kompatibilní</string>
<string name="passkeys_backup_eligibility_title">Možnost zálohy</string>
<string name="passkeys_backup_eligibility_summary">Při vytváření určit, zda je povoleno zálohovat zdroj ověřovacích údajů veřejného klíče</string>
<string name="passkeys_backup_state_title">Stav zálohy</string>
<string name="passkeys_backup_state_summary">Uvést, že přihlašovací údaje jsou zálohovány a chráněny proti ztrátě jednoho zařízení</string>
<string name="credential_provider_service_subtitle">Přístupové klíče, poskytovatel automatického vyplnění</string>
<string name="passkey">Přístupový klíč</string>
<string name="passkey_service_name">Poskytovatel údajů KeePassDX</string>
<string name="passkey_creation_description">Uložit přístupový klíč do nového záznamu</string>
<string name="passkey_update_description">Aktualizovat přístupový klíč v %1$s</string>
<string name="passkey_selection_username">Nenalezeny žádné přístupové klíče</string>
<string name="passkey_selection_description">Vyberte existující přístupový klíč</string>
<string name="passkey_database_username">Databáze KeePassDX</string>
<string name="passkey_locked_database_description">Vyberte k odemčení</string>
<string name="passkey_username">Uživatelské jméno přístupového klíče</string>
<string name="passkey_private_key">Soukromý klíč přístupového klíče</string>
<string name="passkey_credential_id">ID údaje přístupového klíče</string>
<string name="passkey_user_handle">Uživatelská adresa přístupového klíče</string>
<string name="passkey_relying_party">Strana předávající přístupový klíč</string>
<string name="passkey_backup_eligibility">Způsobilost přístupového klíče k zálohování</string>
<string name="passkey_backup_state">Stav zálohy přístupového klíče</string>
<string name="error_passkey_result">Nepodařilo se vrátit přístupový klíč</string>
</resources>

View File

@@ -475,7 +475,6 @@
\n
\nDatabasen kan blive meget stor og reducere ydeevnen med denne overførelse.</string>
<string name="warning_empty_recycle_bin">Slet alle noder permanent fra papirkurven\?</string>
<string name="registration_mode">Registreringstilstand</string>
<string name="save_mode">Gem-tilstand</string>
<string name="search_mode">Søgetilstand</string>
<string name="error_registration_read_only">Det er ikke tilladt at gemme et nyt element i en skrivebeskyttet database.</string>

View File

@@ -153,7 +153,7 @@
<string name="menu_appearance_settings">Erscheinungsbild</string>
<string name="password_size_title">Generierte Passwortlänge</string>
<string name="password_size_summary">Legt die Standardlänge des generierten Passworts fest</string>
<string name="clipboard_notifications_title">Kopier-Benachrichtigung</string>
<string name="clipboard_notifications_title">Kopierbenachrichtigung</string>
<string name="clipboard_notifications_summary">Benachrichtigung anzeigen, um beim Betrachten eines Eintrags Felder kopieren zu können</string>
<string name="lock_database_screen_off_title">Bildschirmsperre</string>
<string name="lock_database_screen_off_summary">Datenbank wenige Sekunden nach Bildschirmabschaltung sperren</string>
@@ -197,7 +197,7 @@
<string name="autofill">Automatisches Ausfüllen</string>
<string name="autofill_sign_in_prompt">Mit KeePassDX anmelden</string>
<string name="set_credential_provider_service_title">Standard-Autofill-Service festlegen</string>
<string name="autofill_explanation_summary">Automatisches Ausfüllen aktivieren, um Formulare in anderen Apps schnell auszufüllen</string>
<string name="autofill_explanation_summary">Automatisches Ausfüllen konfigurieren, um Formulare in anderen Apps schnell auszufüllen</string>
<string name="autofill_select_entry">Eintrag auswählen </string>
<string name="clipboard">Zwischenablage</string>
<string name="biometric_delete_all_key_title">Verschlüsselungsschlüssel löschen</string>
@@ -223,8 +223,8 @@
<string name="reset_education_screens_summary">Alle Hilfsinfos nochmal anzeigen</string>
<string name="reset_education_screens_text">Hilfeanzeige zurückgesetzt</string>
<string name="education_create_database_title">Datenbankdatei erstellen</string>
<string name="education_create_database_summary">Erste Datei zur Passwortverwaltung erstellen.</string>
<string name="education_select_database_title">Existierende Datenbank öffnen</string>
<string name="education_create_database_summary">Eine erste Datei zur Passwortverwaltung erstellen.</string>
<string name="education_select_database_title">Vorhandene Datenbank öffnen</string>
<string name="education_select_database_summary">Öffnet über den Dateimanager eine früher erstellte Datenbankdatei, um sie weiterzuverwenden.</string>
<string name="education_new_node_title">Datenbankelemente hinzufügen</string>
<string name="education_new_node_summary">Einträge helfen, digitale Konten zu verwalten.
@@ -257,7 +257,7 @@
<string name="html_text_dev_feature_buy_pro">Durch den Kauf der &lt;strong&gt;Pro-Version&lt;/strong&gt;,</string>
<string name="html_text_dev_feature_contibute">Durch deinen &lt;strong&gt;Beitrag&lt;/strong&gt;,</string>
<string name="html_text_dev_feature_encourage">ermutigst du die Entwickler, &lt;strong&gt;neue Funktionen&lt;/strong&gt; einzuführen und gemäß deinen Anmerkungen &lt;strong&gt;Fehler zu beheben&lt;/strong&gt;.</string>
<string name="html_text_dev_feature_thanks">Vielen Dank für deine Unterstützung.</string>
<string name="html_text_dev_feature_thanks">Vielen Dank für die Unterstützung.</string>
<string name="html_text_dev_feature_work_hard">Wir bemühen uns, diese Funktion bald zu veröffentlichen.</string>
<string name="html_text_dev_feature_upgrade">Denke daran, die App durch die Installation neuer Versionen auf dem aktuellsten Stand zu halten.</string>
<string name="download">Download</string>
@@ -312,7 +312,7 @@
<string name="hide_broken_locations_title">Defekte Datenbankverknüpfungen ausblenden</string>
<string name="hide_broken_locations_summary">Defekte Verknüpfungen in der Liste der zuletzt verwendeten Datenbanken ausblenden</string>
<string name="do_not_kill_app">App nicht beenden </string>
<string name="lock_database_back_root_summary">Datenbank sperren, wenn auf dem Hauptbildschirm die Taste „Zurück“ gedrückt wird</string>
<string name="lock_database_back_root_summary">„Zurück“ drücken, um die Datenbank zu sperren, wenn man sich auf dem Hauptbildschirm der Datenbank befindet</string>
<string name="clear_clipboard_notification_title">Beim Schließen löschen</string>
<string name="recycle_bin">Papierkorb</string>
<string name="keyboard_selection_entry_title">Eintragsauswahl</string>
@@ -486,7 +486,6 @@
<string name="notification">Benachrichtigung</string>
<string name="biometric_security_update_required">Biometrische Sicherheitsaktualisierung erforderlich.</string>
<string name="configure_biometric">Es sind weder Biometrie- noch Geräteanmeldedaten registriert.</string>
<string name="registration_mode">Registrierungsmodus</string>
<string name="save_mode">Speichermodus</string>
<string name="search_mode">Suchmodus</string>
<string name="error_registration_read_only">Speichern eines neuen Elements in einer schreibgeschützten Datenbank ist unzulässig.</string>
@@ -509,7 +508,7 @@
<string name="temp_device_unlock_timeout_title">Ablauf der Geräteentsperrung</string>
<string name="temp_device_unlock_enable_summary">Bei Nutzung der Geräteentsperrung keine verschlüsselten Inhalte speichern</string>
<string name="temp_device_unlock_enable_title">Zeitlich begrenzte Geräteentsperrung</string>
<string name="device_credential_unlock_enable_summary">Ermöglicht das Öffnen der Datenbank mit deinen Geräteanmeldedaten</string>
<string name="device_credential_unlock_enable_summary">Ermöglicht das Öffnen der Datenbank mit den persönlichen Geräteanmeldedaten</string>
<string name="device_unlock_tap_delete">Drücken, um alle Geräteentsperrschlüssel zu löschen</string>
<string name="content">Inhalt</string>
<string name="device_unlock_prompt_extract_credential_title">Datenbank mit Geräteentsperrdaten öffnen</string>
@@ -537,7 +536,7 @@
<string name="error_upload_file">Beim Hochladen der Datei ist ein Fehler aufgetreten.</string>
<string name="import_app_properties_title">App-Einstellungen importieren</string>
<string name="error_start_database_action">Beim Ausführen einer Aktion in der Datenbank ist ein Fehler aufgetreten.</string>
<string name="error_otp_type">Der vorhandene OTP-Typ wird von diesem Formular nicht erkannt, seine Validierung kann Token möglicherweise nicht mehr korrekt erzeugen.</string>
<string name="error_otp_type">Der vorhandene OTP-Typ wird von diesem Formular nicht erkannt, und seine Validierung kann Token möglicherweise nicht mehr korrekt erzeugen.</string>
<string name="content_description_otp_information">Informationen zu Einmalpasswörtern</string>
<string name="warning_database_revoked">Auf die Datei kann nicht zugegriffen werden. Bitte die Datenbank schließen und von ihrem Speicherort aus erneut öffnen.</string>
<string name="error_export_app_properties">Fehler beim Exportieren der App-Einstellungen.</string>
@@ -683,7 +682,7 @@
<string name="ask">Fragen</string>
<string name="later">Später</string>
<string name="unlock_and_link_biometric">Geräteentsperrverknüpfung</string>
<string name="menu_appearance_settings_summary">Design, Farben, Attribute</string>
<string name="menu_appearance_settings_summary">Design, Farben, Symbole, Schriftarten, Attribute</string>
<string name="warning_database_notification_permission">Die Benachrichtigungsberechtigung ermöglicht es, den Status der Datenbank anzuzeigen und sie mit einer leicht zugänglichen Taste zu sperren.
\n
\nWird diese Berechtigung nicht aktiviert, ist die im Hintergrund geöffnete Datenbank nicht sichtbar, wenn eine Anwendung im Vordergrund läuft.</string>
@@ -705,4 +704,28 @@
<string name="recursive_number_entries_title">Rekursive Anzahl der Einträge</string>
<string name="generate_keyfile">Schlüsseldatei generieren</string>
<string name="error_otp_secret_length">Geheimschlüssel muss mindestens %1$d Zeichen lang sein.</string>
<string name="entry_application_id">App-ID</string>
<string name="warning_overwrite_data_title">Bestehende Daten überschreiben?</string>
<string name="warning_overwrite_data_description">Diese Aktion ersetzt die bestehenden Daten im Eintrag. Die alten Daten können wiederhergestellt werden, wenn der Verlauf aktiviert ist.</string>
<string name="passkeys">Passkeys</string>
<string name="passkeys_preference_title">Passkeys-Einstellungen</string>
<string name="passkeys_close_database_title">Datenbank schließen</string>
<string name="passkeys_privileged_apps_title">Vertrauliche Apps</string>
<string name="passkeys_missing_signature_app_ask_title">Signatur fehlt</string>
<string name="passkeys_auto_select_title">Automatische Auswahl</string>
<string name="passkeys_backup_eligibility_title">Backup-Erlaubnis</string>
<string name="passkeys_backup_state_title">Backup-Status</string>
<string name="passkeys_privileged_apps_ask_title">App nicht erkannt</string>
<string name="passkey">Passkey</string>
<string name="passkey_selection_username">Kein Passkey gefunden</string>
<string name="passkey_creation_description">Passkey in neuem Eintrag speichern</string>
<string name="passkey_update_description">Passkey in %1$s aktualisieren</string>
<string name="passkey_selection_description">Vorhandenen Passkey auswählen</string>
<string name="passkey_database_username">KeePassDX-Datenbank</string>
<string name="passkey_username">Passkey-Benutzername</string>
<string name="passkey_backup_state">Passkey-Backup-Status</string>
<string name="passkey_backup_eligibility">Passkey-Backup-Erlaubnis</string>
<string name="passkeys_close_database_summary">Datenbank nach der Passwortauswahl schließen</string>
<string name="credential_provider">Anmeldeinformationsanbieter</string>
<string name="passkeys_explanation_summary">Passkeys für eine schnelle und sichere Anmeldung ohne Passwort konfigurieren</string>
</resources>

View File

@@ -19,7 +19,7 @@
--><resources>
<string name="feedback">Σχόλια</string>
<string name="homepage">Αρχική Σελίδα</string>
<string name="about_description">Το KeePassDX είναι μία εφαρμογή Android του διαχειριστή κωδικών KeePass</string>
<string name="about_description">Υλοποίηση του διαχειριστή κωδικών πρόσβασης KeePass για Android.</string>
<string name="accept">Αποδοχή</string>
<string name="add_entry">Προσθήκη καταχώρησης</string>
<string name="add_group">Προσθήκη ομάδας</string>
@@ -479,7 +479,6 @@
<string name="biometric_security_update_required">Απαιτείται ενημέρωση βιομετρικής ασφάλειας.</string>
<string name="configure_biometric">Κανένα πιστοποιητικό βιομετρίας ή συσκευής δεν είναι εγγεγραμμένο.</string>
<string name="warning_empty_recycle_bin">Να διαγραφούν οριστικά όλοι οι κόμβοι από τον κάδο ανακύκλωσης;</string>
<string name="registration_mode">Τρόπος εγγραφής</string>
<string name="save_mode">Λειτουργία αποθήκευσης</string>
<string name="search_mode">Λειτουργία αναζήτησης</string>
<string name="error_registration_read_only">Η αποθήκευση ενός νέου αντικειμένου δεν επιτρέπεται σε μια βάση δεδομένων μόνο για ανάγνωση.</string>

View File

@@ -21,7 +21,7 @@
--><resources>
<string name="feedback">Comentarios</string>
<string name="homepage">Página de inicio</string>
<string name="about_description">Implementación para Android del gestor de contraseñas KeePass</string>
<string name="about_description">Implementación para Android del gestor de contraseñas KeePass.</string>
<string name="accept">Aceptar</string>
<string name="add_entry">Añadir apunte</string>
<string name="add_group">Añadir grupo</string>
@@ -452,7 +452,6 @@
<string name="configure_biometric">No se ha inscrito ninguna credencial biométrica o del dispositivo.</string>
<string name="warning_empty_keyfile_explanation">El contenido del archivo de clave nunca debe modificarse y, en el mejor de los casos, debe contener datos generados al azar.</string>
<string name="warning_empty_recycle_bin">¿Borrar permanentemente todos los nodos de la papelera de reciclaje\?</string>
<string name="registration_mode">Modo de registro</string>
<string name="save_mode">Modo de guardado</string>
<string name="search_mode">Modo de búsqueda</string>
<string name="contains_duplicate_uuid_procedure">¿Solucionar el problema generando nuevos UUID para que los duplicados continúen?</string>
@@ -690,4 +689,5 @@
<string name="generate_keyfile">Generar archivo de claves</string>
<string name="recursive_number_entries_title">Número recursivo de entradas</string>
<string name="hide_templates_summary">Las plantillas no se muestran</string>
<string name="error_otp_secret_length">La clave secreta debe tener al menos %1$d caracteres.</string>
</resources>

View File

@@ -383,7 +383,6 @@
<string name="search_mode">Otsinguviis</string>
<string name="save_mode">Salvestusviis</string>
<string name="selection_mode">Valikuviis</string>
<string name="registration_mode">Registreerimisviis</string>
<string name="invalid_credentials">Salasõna või võtmefaili ei õnnestunud lugeda.</string>
<string name="protection">Kaitse</string>
<string name="underline">Allajoonitud</string>
@@ -404,7 +403,7 @@
<string name="database_history">Ajalugu</string>
<string name="properties">Omadused</string>
<string name="menu_appearance_settings">Välimus</string>
<string name="menu_appearance_settings_summary">Välimus, värvid, omadused</string>
<string name="menu_appearance_settings_summary">Välimus, värvid, ikoonid, kirjatüübid, omadused</string>
<string name="html_text_dev_feature_upgrade">Ära unusta paigaldada viimaseid versioone ja tagada, et rakendus on alati uuendatud.</string>
<string name="at_least_one_char">Vähemalt üks tähemärk igast</string>
<string name="exclude_ambiguous_chars">Välista mitmetähenduslikud tähemärgid</string>
@@ -453,7 +452,7 @@
<string name="lock_database_screen_off_summary">Mõni sekund peale ekraani väljalülitumist lukusta andmebaas</string>
<string name="lock_database_back_root_title">Vajuta nuppu „Tagasi“</string>
<string name="lock_database_show_button_title">Näita lukustuse nuppu</string>
<string name="lock_database_back_root_summary">Lukusta andmebaas peale juurkaustas „Tagasi“ nupu klõpsimist</string>
<string name="lock_database_back_root_summary">Kui sa pole andmebaasi juurkasutas, siis lukusta andmebaas peale „Tagasi“ nupu klõpsimist</string>
<string name="allow_copy_password_title">Lõikelaua usaldamine</string>
<string name="allow_copy_password_summary">Luba sisestatud salasõna ja kaitstud väljade kopeerimise lõikelauale</string>
<string name="education_unlock_title">Eemalda oma andmebaasi lukustus</string>
@@ -471,7 +470,7 @@
<string name="allow_copy_password_warning">Hoiatus: süsteemiülene lõikelaud on kõikide rakenduste kasutuses. Kui sa kopeerid sinna delikaatseid andmeid, siis muu tarkvara võib seda seal näha. Kui sa kasutad KDE Connecti või muud lõikelaua jagamise teenust, siis sõltuvalt seadistustest võivad need delikaatsed andmed olla nähtavad ka muudes seadmetes.</string>
<string name="warning_database_revoked">Failihaldur on blokeerinud ligipääsu failile. Sulge andmebaas ja ava ta uuesti oma asukohast.</string>
<string name="autofill_sign_in_prompt">Logi sisse KeePassDX abil</string>
<string name="autofill_explanation_summary">Täitmaks andmevorme teistes rakenduses, luba automaattäite teenus</string>
<string name="autofill_explanation_summary">Täitmaks andmevorme teistes rakenduses, seadista automaattäite teenus</string>
<string name="autofill_select_entry">Vali kirje…</string>
<string name="set_credential_provider_service_title">Vali vaikimisi kasutatav automaattäite teenus</string>
<string name="autofill_preference_title">Automaattäite teenuse seadistused</string>
@@ -663,4 +662,8 @@
<string name="keyboard_previous_fill_in_summary">Peale tegevust „Automaatne võtmetoiming“ lülita automaatselt tagasi eelmisele klahvistikule</string>
<string name="education_read_only_summary">Saad juhtida sessioonil kasutatavat avamisviisi.\n\n„Kirjutuskaitstud“ tagab, et juhuslike muudatustega ei läheks andmeid kaotsi.\n„Muudetav“ võimaldab sul lisada, kustutada või muuta kõiki andmebaasi kirjeid.</string>
<string name="error_otp_secret_length">Salavõti peab olema vähemalt %1$d tähemärki pikk.</string>
<string name="entry_application_id">Rakenduse tunnus</string>
<string name="passkeys_close_database_title">Sulge andmebaas</string>
<string name="passkeys_privileged_apps_ask_title">Rakendust ei õnnestu tuvastada</string>
<string name="warning_overwrite_data_title">Kas soovid olemasolevad andmed üle kirjutada?</string>
</resources>

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