Compare commits

...

248 Commits

Author SHA1 Message Date
J-Jamet
5bdc72aa67 fix: Remembering database location #2262 2025-11-13 10:59:16 +01:00
J-Jamet
2be32e6884 fix: Upgrade to 4.2.4 2025-11-13 10:31:11 +01:00
J-Jamet
9ddd66ce85 Merge tag '4.2.3' into develop
4.2.3
2025-10-29 18:37:28 +01:00
J-Jamet
e3b69789bf Merge branch 'release/4.2.3' 2025-10-29 18:37:19 +01:00
J-Jamet
54f2ed9fab fix: Small fixes 2025-10-29 18:27:54 +01:00
J-Jamet
2fea019b95 fix: Save search info if URL present #2255 2025-10-29 18:12:44 +01:00
J-Jamet
9ac7ef2d22 fix: Credential allow action during orientation change #2253 2025-10-29 17:12:27 +01:00
J-Jamet
6d452fa49c fix: Credential allow action #2253 2025-10-29 16:44:48 +01:00
J-Jamet
d99edb6b4d fix: database dialog subtitle #2254 2025-10-29 16:18:51 +01:00
J-Jamet
cb679f0d59 fix: Multiple Passkey selection #2253 2025-10-29 15:15:44 +01:00
J-Jamet
5dd9f75095 Merge tag '4.2.2' into develop
4.2.2
2025-10-28 17:44:51 +01:00
J-Jamet
403021d38b Merge branch 'release/4.2.2' 2025-10-28 17:44:43 +01:00
J-Jamet
fea7b30d6f Merge branch 'develop' into release/4.2.2 2025-10-28 17:19:08 +01:00
J-Jamet
ab5c859db4 fix: Tags in translations 2025-10-28 17:18:41 +01:00
J-Jamet
3fcbc65de0 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-10-28 17:16:39 +01:00
J-Jamet
3f1ee6bbea fix: Upgrade CHANGELOG 2025-10-28 17:14:44 +01:00
J-Jamet
37ce2ab781 fix: Snackbar error 2025-10-28 15:59:20 +01:00
J-Jamet
ffaf4a761a fix: Service stop 2025-10-28 15:48:07 +01:00
J-Jamet
56b7cc9118 fix: Progress message 2025-10-28 14:49:10 +01:00
J-Jamet
987f3f9047 fix: Revert fragment view 2025-10-28 14:15:19 +01:00
J-Jamet
3039efc67c fix: Cancelable during save 2025-10-28 14:07:32 +01:00
J-Jamet
26daac4637 fix: Dialog progress tasks 2025-10-28 13:28:20 +01:00
J-Jamet
88a93829a9 fix: Lock finish 2025-10-27 21:28:29 +01:00
J-Jamet
7923a63d36 fix: Database dialog stopped 2025-10-27 20:53:30 +01:00
J-Jamet
9a5c782d5d fix: Small fix, info dialog 2025-10-27 20:45:40 +01:00
J-Jamet
c39e4ba693 fix: Play service as privileged app for Passkey Cross Device Authentication #2244 2025-10-27 20:15:02 +01:00
J-Jamet
7db3d0502f fix: Save search info #2243 2025-10-27 19:32:00 +01:00
J-Jamet
d557e8b516 fix: merge algorithm #2223 2025-10-27 18:51:19 +01:00
Priit Jõerüüt
d6ae17657b Translated using Weblate (Estonian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-10-27 18:11:18 +01:00
solokot
3468b0f6f5 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-27 18:11:16 +01:00
J-Jamet
79777801e8 fix: merge algorithm 2025-10-27 16:34:38 +01:00
안세훈
a202f66d48 Translated using Weblate (Korean)
Currently translated at 52.6% (370 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ko/
2025-10-27 03:02:44 +00:00
Kunzisoft
ba58d5d47c Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 33.3% (1 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/zh_Hant/
2025-10-25 16:03:05 +00:00
Kunzisoft
46685592df Translated using Weblate (Croatian)
Currently translated at 33.3% (1 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/hr/
2025-10-25 16:03:03 +00:00
Kunzisoft
ba9e2892ef Translated using Weblate (Indonesian)
Currently translated at 33.3% (1 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/id/
2025-10-25 16:03:02 +00:00
Kunzisoft
a1da3b4fbd 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-25 16:03:01 +00:00
Kunzisoft
8bee0ec220 Translated using Weblate (Macedonian)
Currently translated at 33.3% (1 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/mk/
2025-10-25 16:02:59 +00:00
Kunzisoft
aebf6b21de 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-25 16:02:58 +00:00
Kunzisoft
0cf9253ea4 Translated using Weblate (Spanish)
Currently translated at 33.3% (1 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/es/
2025-10-25 16:02:55 +00:00
Kunzisoft
b63ceb37a4 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-25 16:02:54 +00:00
solokot
c462dae6f5 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-25 16:02:52 +00:00
Kunzisoft
ddf890b861 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-25 16:02:51 +00:00
Besnik Bleta
252eb30b13 Translated using Weblate (Albanian)
Currently translated at 92.0% (647 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2025-10-25 16:02:43 +00:00
J-Jamet
62ab11cc56 Merge tag '4.2.1' into develop
4.2.1
2025-10-24 18:16:25 +02:00
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
248 changed files with 6296 additions and 4287 deletions

View File

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

View File

@@ -1,5 +1,39 @@
KeePassDX(4.2.4)
* Fix remembering database location #2262
KeePassDX(4.2.3)
* Fix multiple Passkey selection #2253
* Fix database dialog subtitle #2254
* Fix save search info if URL present #2255
* Small fixes
KeePassDX(4.2.2)
* Fix database merge algorithm #2223
* Fix save search info #2243
* Fix Play Service as privileged app for Passkey Cross Device Authentication #2244
* Small fixes
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) 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) KeePassDX(4.1.8)
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP) * Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)

View File

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

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 35 targetSdkVersion 35
versionCode = 142 versionCode = 149
versionName = "4.2.0beta02" versionName = "4.2.4"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" 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", "type": "android",
"info": { "info": {

View File

@@ -178,18 +178,22 @@
<activity <activity
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" /> android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
<activity <activity
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity" android:name="com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity"
android:theme="@style/Theme.Transparent" /> android:theme="@style/Theme.Transparent"
android:exported="false"
android:excludeFromRecents="true" />
<activity <activity
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity" android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
android:theme="@style/Theme.Transparent" android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:excludeFromRecents="true"/> android:exported="false"
android:excludeFromRecents="true" />
<activity <activity
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity" android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
android:theme="@style/Theme.Transparent" android:theme="@style/Theme.Transparent"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:exported="true"> android:exported="true"
android:excludeFromRecents="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@@ -208,8 +212,8 @@
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity" android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
android:theme="@style/Theme.Transparent" android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:excludeFromRecents="true"
android:exported="false" android:exported="false"
android:excludeFromRecents="true"
tools:targetApi="upside_down_cake" /> tools:targetApi="upside_down_cake" />
<service <service
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService" 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.setTransparentNavigationBar
import com.kunzisoft.keepass.view.showActionErrorIfNeeded import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.EntryViewModel import com.kunzisoft.keepass.viewmodels.EntryViewModel
import java.util.EnumSet
import java.util.UUID import java.util.UUID
class EntryActivity : DatabaseLockActivity() { class EntryActivity : DatabaseLockActivity() {
private var footer: ViewGroup? = null private var footer: ViewGroup? = null
private var container: View? = null
private var coordinatorLayout: CoordinatorLayout? = null private var coordinatorLayout: CoordinatorLayout? = null
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var appBarLayout: AppBarLayout? = null private var appBarLayout: AppBarLayout? = null
@@ -123,6 +125,8 @@ class EntryActivity : DatabaseLockActivity() {
private var mBackgroundColor: Int? = null private var mBackgroundColor: Int? = null
private var mForegroundColor: Int? = null private var mForegroundColor: Int? = null
override fun manageDatabaseInfo(): Boolean = true
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -135,6 +139,7 @@ class EntryActivity : DatabaseLockActivity() {
// Get views // Get views
footer = findViewById(R.id.activity_entry_footer) footer = findViewById(R.id.activity_entry_footer)
container = findViewById(R.id.activity_entry_container)
coordinatorLayout = findViewById(R.id.toolbar_coordinator) coordinatorLayout = findViewById(R.id.toolbar_coordinator)
collapsingToolbarLayout = findViewById(R.id.toolbar_layout) collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
appBarLayout = findViewById(R.id.app_bar) appBarLayout = findViewById(R.id.app_bar)
@@ -150,8 +155,12 @@ class EntryActivity : DatabaseLockActivity() {
setTransparentNavigationBar { setTransparentNavigationBar {
// To fix margin with API 27 // To fix margin with API 27
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null) ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP) container?.applyWindowInsets(EnumSet.of(
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM) WindowInsetPosition.TOP_MARGINS,
WindowInsetPosition.BOTTOM_MARGINS,
WindowInsetPosition.START_MARGINS,
WindowInsetPosition.END_MARGINS,
))
} }
// Empty title // Empty title
@@ -305,11 +314,11 @@ class EntryActivity : DatabaseLockActivity() {
mEntryViewModel.historySelected.observe(this) { historySelected -> mEntryViewModel.historySelected.observe(this) { historySelected ->
mDatabase?.let { database -> mDatabase?.let { database ->
launch( launch(
this, activity = this,
database, database = database,
historySelected.nodeId, entryId = historySelected.nodeId,
historySelected.historyPosition, historyPosition = historySelected.historyPosition,
mEntryActivityResultLauncher activityResultLauncher = mEntryActivityResultLauncher
) )
} }
} }
@@ -323,9 +332,8 @@ class EntryActivity : DatabaseLockActivity() {
return coordinatorLayout return coordinatorLayout
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
mEntryViewModel.loadDatabase(database) mEntryViewModel.loadDatabase(database)
} }
@@ -471,11 +479,12 @@ class EntryActivity : DatabaseLockActivity() {
R.id.menu_edit -> { R.id.menu_edit -> {
mDatabase?.let { database -> mDatabase?.let { database ->
mMainEntryId?.let { entryId -> mMainEntryId?.let { entryId ->
EntryEditActivity.launchToUpdate( EntryEditActivity.launch(
this, activity = this,
database, database = database,
entryId, registrationType = EntryEditActivity.RegistrationType.UPDATE,
mEntryActivityResultLauncher nodeId = entryId,
activityResultLauncher = mEntryActivityResultLauncher
) )
} }
} }
@@ -513,7 +522,7 @@ class EntryActivity : DatabaseLockActivity() {
// Transit data in previous Activity after an update // Transit data in previous Activity after an update
Intent().apply { Intent().apply {
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId) putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
setResult(Activity.RESULT_OK, this) setResult(RESULT_OK, this)
} }
super.finish() super.finish()
} }
@@ -527,34 +536,22 @@ class EntryActivity : DatabaseLockActivity() {
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG" const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
/** /**
* Open standard Entry activity * Open standard or history Entry activity
*/ */
fun launch(activity: Activity, fun launch(
database: ContextualDatabase, activity: Activity,
entryId: NodeId<UUID>, database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>) { entryId: NodeId<UUID>,
historyPosition: Int? = null,
activityResultLauncher: ActivityResultLauncher<Intent>
) {
if (database.loaded) { if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java) val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId) intent.putExtra(KEY_ENTRY, entryId)
activityResultLauncher.launch(intent) historyPosition?.let {
} intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
} }
}
/**
* 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)
activityResultLauncher.launch(intent) activityResultLauncher.launch(intent)
} }
} }

View File

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

View File

@@ -19,7 +19,6 @@
*/ */
package com.kunzisoft.keepass.activities package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@@ -33,8 +32,6 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
@@ -50,20 +47,16 @@ import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
@@ -99,8 +92,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? = override fun manageDatabaseInfo(): Boolean = false
this.buildActivityResultLauncher()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -131,7 +123,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri -> mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let { uri?.let {
launchPasswordActivityWithPath(uri) launchMainCredentialActivityWithPath(uri)
} }
} }
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri -> mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
@@ -160,7 +152,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
} }
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen -> mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri -> fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
launchPasswordActivity( launchMainCredentialActivity(
databaseFileUri, databaseFileUri,
fileDatabaseHistoryEntityToOpen.keyFileUri, fileDatabaseHistoryEntityToOpen.keyFileUri,
fileDatabaseHistoryEntityToOpen.hardwareKey fileDatabaseHistoryEntityToOpen.hardwareKey
@@ -179,7 +171,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
// Load default database the first time // Load default database the first time
databaseFilesViewModel.doForDefaultDatabase { databaseFileUri -> databaseFilesViewModel.doForDefaultDatabase { databaseFileUri ->
launchPasswordActivityWithPath(databaseFileUri) launchMainCredentialActivityWithPath(databaseFileUri)
} }
// Retrieve the database URI provided by file manager after an orientation change // Retrieve the database URI provided by file manager after an orientation change
@@ -222,13 +214,16 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
// Retrieve settings for default database // Retrieve settings for default database
mAdapterDatabaseHistory?.setDefaultDatabase(it) mAdapterDatabaseHistory?.setDefaultDatabase(it)
} }
// Remove all the remember locations if needed
if (PreferencesUtil.rememberDatabaseLocations(applicationContext).not()) {
FileDatabaseHistoryAction.getInstance(applicationContext)
.deleteAll()
}
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) launchGroupActivityIfLoaded(database)
if (database != null) {
launchGroupActivityIfLoaded(database)
}
} }
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
@@ -236,25 +231,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
actionTask: String, actionTask: String,
result: ActionRunnable.Result result: ActionRunnable.Result
) { ) {
super.onDatabaseActionFinished(database, actionTask, result)
if (result.isSuccess) { if (result.isSuccess) {
// Update list
when (actionTask) {
ACTION_DATABASE_CREATE_TASK,
ACTION_DATABASE_LOAD_TASK -> {
result.data?.getParcelableCompat<Uri>(DATABASE_URI_KEY)?.let { databaseUri ->
val mainCredential =
result.data?.getParcelableCompat(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY)
?: MainCredential()
databaseFilesViewModel.addDatabaseFile(
databaseUri,
mainCredential.keyFileUri,
mainCredential.hardwareKey
)
}
}
}
// Launch activity // Launch activity
when (actionTask) { when (actionTask) {
ACTION_DATABASE_CREATE_TASK -> { ACTION_DATABASE_CREATE_TASK -> {
@@ -263,13 +240,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
database, database,
false false
) )
coordinatorLayout.showActionErrorIfNeeded(result)
} }
ACTION_DATABASE_LOAD_TASK -> { ACTION_DATABASE_LOAD_TASK -> {
launchGroupActivityIfLoaded(database) launchGroupActivityIfLoaded(database)
} }
} }
} }
coordinatorLayout.showActionErrorIfNeeded(result)
} }
/** /**
@@ -287,17 +264,58 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show() Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
} }
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) { private fun launchMainCredentialActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
MainCredentialActivity.launch(this, try {
databaseUri, EntrySelectionHelper.doSpecialAction(
keyFile, intent = this.intent,
hardwareKey, defaultAction = {
{ exception -> MainCredentialActivity.launch(
fileNoFoundAction(exception) activity = this,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey
)
}, },
{ onCancelSpecialMode() }, searchAction = { searchInfo ->
{ onLaunchActivitySpecialMode() }, MainCredentialActivity.launchForSearchResult(
mCredentialActivityResultLauncher) 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) { private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
@@ -307,12 +325,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
{ onValidateSpecialMode() }, { onValidateSpecialMode() },
{ onCancelSpecialMode() }, { onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() }, { onLaunchActivitySpecialMode() },
mCredentialActivityResultLauncher) mCredentialActivityResultLauncher
)
} }
} }
private fun launchPasswordActivityWithPath(databaseUri: Uri) { private fun launchMainCredentialActivityWithPath(databaseUri: Uri) {
launchPasswordActivity(databaseUri, null, null) launchMainCredentialActivity(databaseUri, null, null)
// Delete flickering for kitkat <= // Delete flickering for kitkat <=
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
@@ -336,10 +355,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
} }
} }
mDatabase?.let { database ->
launchGroupActivityIfLoaded(database)
}
// Show recent files if allowed // Show recent files if allowed
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) { if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
databaseFilesViewModel.loadListOfDatabases() databaseFilesViewModel.loadListOfDatabases()
@@ -358,7 +373,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
try { try {
mDatabaseFileUri?.let { databaseUri -> mDatabaseFileUri?.let { databaseUri ->
// Create the new database // Create the new database
createDatabase(databaseUri, mainCredential) mDatabaseViewModel.createDatabase(databaseUri, mainCredential)
} }
} catch (e: Exception) { } catch (e: Exception) {
val error = getString(R.string.error_create_database_file) val error = getString(R.string.error_create_database_file)
@@ -442,71 +457,35 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
* ------------------------- * -------------------------
*/ */
fun launchForSearchResult(context: Context, fun launchForSearch(
searchInfo: SearchInfo) { context: Context,
EntrySelectionHelper.startActivityForSearchModeResult(context, searchInfo: SearchInfo
Intent(context, FileDatabaseSelectActivity::class.java), ) {
searchInfo) EntrySelectionHelper.startActivityForSearchModeResult(
context = context,
intent = Intent(context, FileDatabaseSelectActivity::class.java),
searchInfo = searchInfo
)
} }
/* /*
* ------------------------- * -------------------------
* Save Launch * Selection Launch
* ------------------------- * -------------------------
*/ */
fun launchForSaveResult(context: Context, fun launchForSelection(
searchInfo: SearchInfo) { context: Context,
EntrySelectionHelper.startActivityForSaveModeResult(context, typeMode: TypeMode,
Intent(context, FileDatabaseSelectActivity::class.java), searchInfo: SearchInfo? = null,
searchInfo) activityResultLauncher: ActivityResultLauncher<Intent>?,
} ) {
EntrySelectionHelper.startActivityForSelectionModeResult(
/* context = context,
* ------------------------- intent = Intent(context, FileDatabaseSelectActivity::class.java),
* Keyboard Launch searchInfo = searchInfo,
* ------------------------- typeMode = typeMode,
*/ activityResultLauncher = activityResultLauncher
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
) )
} }
@@ -515,16 +494,18 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
* Registration Launch * Registration Launch
* ------------------------- * -------------------------
*/ */
fun launchForRegistration(context: Context, fun launchForRegistration(
activityResultLauncher: ActivityResultLauncher<Intent>?, context: Context,
registerInfo: RegisterInfo? = null, typeMode: TypeMode,
typeMode: TypeMode) { registerInfo: RegisterInfo? = null,
activityResultLauncher: ActivityResultLauncher<Intent>?,
) {
EntrySelectionHelper.startActivityForRegistrationModeResult( EntrySelectionHelper.startActivityForRegistrationModeResult(
context, context = context,
activityResultLauncher, intent = Intent(context, FileDatabaseSelectActivity::class.java),
Intent(context, FileDatabaseSelectActivity::class.java), registerInfo = registerInfo,
registerInfo, typeMode = typeMode,
typeMode activityResultLauncher = activityResultLauncher
) )
} }
} }

View File

@@ -78,6 +78,8 @@ class IconPickerActivity : DatabaseLockActivity() {
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
override fun manageDatabaseInfo(): Boolean = true
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -174,10 +176,10 @@ class IconPickerActivity : DatabaseLockActivity() {
return true return true
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
if (database?.allowCustomIcons == true) { if (database.allowCustomIcons) {
uploadButton.setOpenDocumentClickListener(mExternalFileHelper) uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
} else { } else {
uploadButton.visibility = View.GONE uploadButton.visibility = View.GONE

View File

@@ -45,6 +45,8 @@ class ImageViewerActivity : DatabaseLockActivity() {
private lateinit var imageView: ImageView private lateinit var imageView: ImageView
private lateinit var progressView: View private lateinit var progressView: View
override fun manageDatabaseInfo(): Boolean = false
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -101,7 +103,7 @@ class ImageViewerActivity : DatabaseLockActivity() {
return true return true
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
try { try {
@@ -119,18 +121,16 @@ class ImageViewerActivity : DatabaseLockActivity() {
resources.displayMetrics.heightPixels * 2 resources.displayMetrics.heightPixels * 2
) )
database?.let { database -> BinaryDatabaseManager.loadBitmap(
BinaryDatabaseManager.loadBitmap( database,
database, attachment.binaryData,
attachment.binaryData, mImagePreviewMaxWidth
mImagePreviewMaxWidth ) { bitmapLoaded ->
) { bitmapLoaded -> if (bitmapLoaded == null) {
if (bitmapLoaded == null) { finish()
finish() } else {
} else { progressView.visibility = View.GONE
progressView.visibility = View.GONE imageView.setImageBitmap(bitmapLoaded)
imageView.setImageBitmap(bitmapLoaded)
}
} }
} }
} ?: finish() } ?: finish()

View File

@@ -28,6 +28,8 @@ class KeyGeneratorActivity : DatabaseLockActivity() {
private lateinit var validationButton: View private lateinit var validationButton: View
private var lockView: View? = null private var lockView: View? = null
override fun manageDatabaseInfo(): Boolean = true
private val keyGeneratorViewModel: KeyGeneratorViewModel by viewModels() private val keyGeneratorViewModel: KeyGeneratorViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {

View File

@@ -36,8 +36,6 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.coordinatorlayout.widget.CoordinatorLayout 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.DeviceUnlockManager
import com.kunzisoft.keepass.biometric.deviceUnlockError import com.kunzisoft.keepass.biometric.deviceUnlockError
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
@@ -128,8 +124,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
private var mReadOnly: Boolean = false private var mReadOnly: Boolean = false
private var mForceReadOnly: Boolean = false private var mForceReadOnly: Boolean = false
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? = override fun manageDatabaseInfo(): Boolean = false
this.buildActivityResultLauncher()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -310,26 +305,20 @@ class MainCredentialActivity : DatabaseModeActivity() {
mDatabaseFileUri?.let { databaseFileUri -> mDatabaseFileUri?.let { databaseFileUri ->
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri) mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
} }
mDatabase?.let { database ->
launchGroupActivityIfLoaded(database)
}
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
if (database != null) { // Trying to load another database
// Trying to load another database if (mDatabaseFileUri != null
if (mDatabaseFileUri != null && database.fileUri != null
&& database.fileUri != null && mDatabaseFileUri != database.fileUri) {
&& mDatabaseFileUri != database.fileUri) { Toast.makeText(this,
Toast.makeText(this, R.string.warning_database_already_opened,
R.string.warning_database_already_opened, Toast.LENGTH_LONG
Toast.LENGTH_LONG ).show()
).show()
}
launchGroupActivityIfLoaded(database)
} }
launchGroupActivityIfLoaded(database)
} }
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
@@ -514,10 +503,11 @@ class MainCredentialActivity : DatabaseModeActivity() {
val password = intent.getStringExtra(KEY_PASSWORD) val password = intent.getStringExtra(KEY_PASSWORD)
// Consume the intent extra password // Consume the intent extra password
intent.removeExtra(KEY_PASSWORD) intent.removeExtra(KEY_PASSWORD)
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
if (password != null) { if (password != null) {
mainCredentialView?.populatePasswordTextView(password) mainCredentialView?.populatePasswordTextView(password)
} }
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
intent.removeExtra(KEY_LAUNCH_IMMEDIATELY)
if (launchImmediately) { if (launchImmediately) {
loadDatabase() loadDatabase()
} else { } else {
@@ -572,10 +562,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
clearCredentialsViews() clearCredentialsViews()
} }
if (mReadOnly && ( if (mReadOnly && mSpecialMode == SpecialMode.REGISTRATION) {
mSpecialMode == SpecialMode.SAVE
|| mSpecialMode == SpecialMode.REGISTRATION)
) {
Log.e(TAG, getString(R.string.error_save_read_only)) Log.e(TAG, getString(R.string.error_save_read_only))
Snackbar.make(coordinatorLayout, Snackbar.make(coordinatorLayout,
R.string.error_save_read_only, R.string.error_save_read_only,
@@ -599,7 +586,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
readOnly: Boolean, readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?, cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUUID: Boolean) { fixDuplicateUUID: Boolean) {
loadDatabase( mDatabaseViewModel.loadDatabase(
databaseUri, databaseUri,
mainCredential, mainCredential,
readOnly, readOnly,
@@ -752,11 +739,13 @@ class MainCredentialActivity : DatabaseModeActivity() {
private const val KEY_PASSWORD = "password" private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately" private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private fun buildAndLaunchIntent(activity: Activity, private fun buildAndLaunchIntent(
databaseFile: Uri, activity: Activity,
keyFile: Uri?, databaseFile: Uri,
hardwareKey: HardwareKey?, keyFile: Uri?,
intentBuildLauncher: (Intent) -> Unit) { hardwareKey: HardwareKey?,
intentBuildLauncher: (Intent) -> Unit
) {
val intent = Intent(activity, MainCredentialActivity::class.java) val intent = Intent(activity, MainCredentialActivity::class.java)
intent.putExtra(KEY_FILENAME, databaseFile) intent.putExtra(KEY_FILENAME, databaseFile)
if (keyFile != null) if (keyFile != null)
@@ -773,10 +762,12 @@ class MainCredentialActivity : DatabaseModeActivity() {
*/ */
@Throws(FileNotFoundException::class) @Throws(FileNotFoundException::class)
fun launch(activity: Activity, fun launch(
databaseFile: Uri, activity: Activity,
keyFile: Uri?, databaseFile: Uri,
hardwareKey: HardwareKey?) { keyFile: Uri?,
hardwareKey: HardwareKey?
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
activity.startActivity(intent) activity.startActivity(intent)
} }
@@ -789,245 +780,73 @@ class MainCredentialActivity : DatabaseModeActivity() {
*/ */
@Throws(FileNotFoundException::class) @Throws(FileNotFoundException::class)
fun launchForSearchResult(activity: Activity, fun launchForSearchResult(
databaseFile: Uri, activity: Activity,
keyFile: Uri?, databaseFile: Uri,
hardwareKey: HardwareKey?, keyFile: Uri?,
searchInfo: SearchInfo) { hardwareKey: HardwareKey?,
searchInfo: SearchInfo
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSearchModeResult( EntrySelectionHelper.startActivityForSearchModeResult(
activity, context = activity,
intent, intent = intent,
searchInfo) searchInfo = 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
) )
} }
} }
/* /*
* ------------------------- * -------------------------
* Registration Launch * Selection Launch
* ------------------------- * -------------------------
*/ */
fun launchForRegistration(
@Throws(FileNotFoundException::class)
fun launchForSelection(
activity: Activity, activity: Activity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?, keyFile: Uri?,
hardwareKey: HardwareKey?, hardwareKey: HardwareKey?,
typeMode: TypeMode, typeMode: TypeMode,
registerInfo: RegisterInfo? searchInfo: SearchInfo?,
activityResultLauncher: ActivityResultLauncher<Intent>?
) { ) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForRegistrationModeResult( EntrySelectionHelper.startActivityForSelectionModeResult(
context = activity, context = activity,
activityResultLauncher = activityResultLauncher,
intent = intent, intent = intent,
typeMode = typeMode, 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 { @Throws(FileNotFoundException::class)
EntrySelectionHelper.doSpecialAction( fun launchForRegistration(
intent = activity.intent, activity: Activity,
defaultAction = { databaseFile: Uri,
launch( keyFile: Uri?,
activity = activity, hardwareKey: HardwareKey?,
databaseFile = databaseUri, typeMode: TypeMode,
keyFile = keyFile, registerInfo: RegisterInfo?,
hardwareKey = hardwareKey activityResultLauncher: ActivityResultLauncher<Intent>?
) ) {
}, buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
searchAction = { searchInfo -> EntrySelectionHelper.startActivityForRegistrationModeResult(
launchForSearchResult( context = activity,
activity = activity, intent = intent,
databaseFile = databaseUri, typeMode = typeMode,
keyFile = keyFile, registerInfo = registerInfo,
hardwareKey = hardwareKey, activityResultLauncher = activityResultLauncher,
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()
}
) )
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,10 +45,10 @@ class IconEditDialogFragment : DatabaseDialogFragment() {
private var mCustomIcon: IconImageCustom? = null private var mCustomIcon: IconImageCustom? = null
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon -> mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon) database.iconDrawableFactory.assignDatabaseIcon(imageView, icon)
} }
mCustomIcon?.let { customIcon -> mCustomIcon?.let { customIcon ->
populateViewsWithCustomIcon(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.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
import com.kunzisoft.keepass.password.PasswordEntropy import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.UriUtil.openUrl import com.kunzisoft.keepass.utils.UriUtil.openUrl
@@ -258,8 +258,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
showEmptyPasswordConfirmationDialog() showEmptyPasswordConfirmationDialog()
} else if (!error } else if (!error
&& hardwareKey != null && hardwareKey != null
&& !HardwareKeyActivity.isHardwareKeyAvailable( && !HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)
requireActivity(), hardwareKey, false)
) { ) {
// show hardware driver dialog if required // show hardware driver dialog if required
error = true error = true

View File

@@ -4,36 +4,59 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels 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.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval { abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels() protected val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
protected var mDatabase: ContextualDatabase? = null protected val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database -> override fun onCreate(savedInstanceState: Bundle?) {
if (mDatabase == null || mDatabase != database) { super.onCreate(savedInstanceState)
this.mDatabase = database lifecycleScope.launch {
onDatabaseRetrieved(database) repeatOnLifecycle(Lifecycle.State.STARTED) {
mDatabaseViewModel.actionState.collect { uiState ->
when (uiState) {
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished(
uiState.database,
uiState.actionTask,
uiState.result
)
}
else -> {}
}
}
} }
} }
lifecycleScope.launch {
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result -> repeatOnLifecycle(Lifecycle.State.RESUMED) {
onDatabaseActionFinished(result.database, result.actionTask, result.result) mDatabaseViewModel.databaseState.collect { database ->
database?.let {
onDatabaseRetrieved(database)
}
}
}
} }
} }
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) { protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
context?.let { 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 // 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 attachmentToUploadUri = it.attachmentToUploadUri
val fileName = it.fileName val fileName = it.fileName
buildNewBinaryAttachment()?.let { binaryAttachment -> mDatabaseViewModel.buildNewAttachment()?.let { binaryAttachment ->
val entryAttachment = Attachment(fileName, binaryAttachment) val entryAttachment = Attachment(fileName, binaryAttachment)
// Ask to replace the current attachment // Ask to replace the current attachment
if ((!mAllowMultipleAttachments if ((!mAllowMultipleAttachments
@@ -273,13 +273,13 @@ class EntryEditFragment: DatabaseFragment() {
} }
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
templateView.populateIconMethod = { imageView, icon -> 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?.database = database
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize -> attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
@@ -290,12 +290,12 @@ class EntryEditFragment: DatabaseFragment() {
} }
} }
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool) tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
tagsCompletionView.apply { tagsCompletionView.apply {
threshold = 1 threshold = 1
setAdapter(tagsAdapter) 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?) { 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 -> context?.let { context ->
attachmentsAdapter = EntryAttachmentsItemsAdapter(context) attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
attachmentsAdapter?.database = database attachmentsAdapter?.database = database

View File

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

View File

@@ -71,8 +71,8 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
resetAppTimeoutWhenViewFocusedOrChanged(view) resetAppTimeoutWhenViewFocusedOrChanged(view)
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory iconPickerAdapter.iconDrawableFactory = database.iconDrawableFactory
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val populateList = 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, iconPickerPagerAdapter = IconPickerPagerAdapter(this,
if (database?.allowCustomIcons == true) 2 else 1) if (database.allowCustomIcons) 2 else 1)
viewPager.adapter = iconPickerPagerAdapter viewPager.adapter = iconPickerPagerAdapter
TabLayoutMediator(tabLayout, viewPager) { tab, position -> TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = when (position) { tab.text = when (position) {

View File

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

View File

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

View File

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

View File

@@ -1,104 +1,307 @@
package com.kunzisoft.keepass.activities.legacy package com.kunzisoft.keepass.activities.legacy
import android.net.Uri import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityOptionsCompat
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.activities.stylish.StylishActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider import com.kunzisoft.keepass.database.DatabaseTaskProvider.Companion.startDatabaseService
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.tasks.ActionRunnable 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.tasks.ProgressTaskViewModel
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval { abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
protected val mDatabaseViewModel: DatabaseViewModel by viewModels() protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null protected val mDatabase: ContextualDatabase?
protected var mDatabase: ContextualDatabase? = null get() = mDatabaseViewModel.database
private val progressTaskViewModel: ProgressTaskViewModel by viewModels()
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)
}
}
/**
* Useful to only waiting for the activity result and prevent any parallel action
*/
var credentialResultLaunched = false
/**
* Utility activity result launcher,
* Used recursively, close each activity with return data
*/
protected var mCredentialActivityResultLauncher: CredentialActivityResultLauncher =
CredentialActivityResultLauncher(
registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
setActivityResult(
lockDatabase = false,
resultCode = it.resultCode,
data = it.data
)
}
)
/**
* Custom ActivityResultLauncher to manage the database action
*/
protected inner class CredentialActivityResultLauncher(
val builder: ActivityResultLauncher<Intent>
) : ActivityResultLauncher<Intent>() {
override fun launch(
input: Intent?,
options: ActivityOptionsCompat?
) {
credentialResultLaunched = true
builder.launch(input, options)
}
override fun unregister() {
builder.unregister()
}
override fun getContract(): ActivityResultContract<Intent?, *> {
return builder.getContract()
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
mDatabaseTaskProvider = DatabaseTaskProvider(this, showDatabaseDialog()) if (savedInstanceState != null
&& savedInstanceState.containsKey(CREDENTIAL_RESULT_LAUNCHER_KEY)
) {
credentialResultLaunched = savedInstanceState.getBoolean(CREDENTIAL_RESULT_LAUNCHER_KEY)
}
mDatabaseTaskProvider?.onDatabaseRetrieved = { database -> lifecycleScope.launch {
val databaseWasReloaded = database?.wasReloaded == true repeatOnLifecycle(Lifecycle.State.STARTED) {
if (databaseWasReloaded && finishActivityIfReloadRequested()) { mDatabaseViewModel.actionState.collect { uiState ->
finish() if (credentialResultLaunched.not()) {
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) { when (uiState) {
database?.wasReloaded = false is DatabaseViewModel.ActionState.Wait -> {}
onDatabaseRetrieved(database) 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 -> {
progressTaskViewModel.show(uiState.progressMessage)
}
is DatabaseViewModel.ActionState.OnDatabaseActionUpdated -> {
progressTaskViewModel.show(uiState.progressMessage)
}
is DatabaseViewModel.ActionState.OnDatabaseActionStopped -> {
progressTaskViewModel.hide()
}
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished(
uiState.database,
uiState.actionTask,
uiState.result
)
progressTaskViewModel.hide()
}
}
}
}
} }
} }
mDatabaseTaskProvider?.onActionFinish = { database, actionTask, result -> lifecycleScope.launch {
onDatabaseActionFinished(database, actionTask, result) repeatOnLifecycle(Lifecycle.State.RESUMED) {
progressTaskViewModel.progressTaskState.collect { state ->
when (state) {
is ProgressTaskViewModel.ProgressTaskState.Show ->
startDialog()
is ProgressTaskViewModel.ProgressTaskState.Hide ->
stopDialog()
}
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database ->
if (credentialResultLaunched.not()) {
// Nullable function
onUnknownDatabaseRetrieved(database)
database?.let {
onDatabaseRetrieved(database)
}
}
}
}
} }
} }
protected open fun showDatabaseDialog(): Boolean { override fun onSaveInstanceState(outState: Bundle) {
return true outState.putBoolean(CREDENTIAL_RESULT_LAUNCHER_KEY, credentialResultLaunched)
super.onSaveInstanceState(outState)
} }
/**
override fun onDestroy() { * Nullable function to retrieve a database
mDatabaseTaskProvider?.destroy() */
mDatabaseTaskProvider = null open fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
mDatabase = null
super.onDestroy()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
mDatabase = database
mDatabaseViewModel.defineDatabase(database)
// optional method implementation // optional method implementation
} }
override fun onDatabaseRetrieved(database: ContextualDatabase) {
// optional method implementation
}
open fun manageDatabaseInfo(): Boolean = true
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
database: ContextualDatabase, database: ContextualDatabase,
actionTask: String, actionTask: String,
result: ActionRunnable.Result result: ActionRunnable.Result
) { ) {
mDatabaseViewModel.onActionFinished(database, actionTask, result)
// optional method implementation // optional method implementation
} }
fun createDatabase( private fun startDatabasePermissionService(bundle: Bundle?, actionTask: String) {
databaseUri: Uri, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
mainCredential: MainCredential 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( private fun startDialog() {
databaseUri: Uri, lifecycleScope.launch {
mainCredential: MainCredential, if (showDatabaseDialog()) {
readOnly: Boolean, if (progressTaskDialogFragment == null) {
cipherEncryptDatabase: CipherEncryptDatabase?, progressTaskDialogFragment = supportFragmentManager
fixDuplicateUuid: Boolean .findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
) { }
mDatabaseTaskProvider?.startDatabaseLoad( if (progressTaskDialogFragment == null) {
databaseUri, progressTaskDialogFragment = ProgressTaskDialogFragment()
mainCredential, progressTaskDialogFragment?.show(
readOnly, supportFragmentManager,
cipherEncryptDatabase, PROGRESS_TASK_DIALOG_TAG
fixDuplicateUuid )
) }
}
}
} }
protected fun closeDatabase() { private fun stopDialog() {
mDatabase?.clearAndClose(this.getBinaryDir()) progressTaskDialogFragment?.dismissAllowingStateLoss()
progressTaskDialogFragment = null
} }
override fun onResume() { protected open fun showDatabaseDialog(): Boolean {
super.onResume() return true
mDatabaseTaskProvider?.registerProgressTask()
} }
override fun onPause() { companion object {
mDatabaseTaskProvider?.unregisterProgressTask() const val CREDENTIAL_RESULT_LAUNCHER_KEY = "com.kunzisoft.keepass.CREDENTIAL_RESULT_LAUNCHER_KEY"
super.onPause()
} }
} }

View File

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

View File

@@ -5,8 +5,13 @@ import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode 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.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
@@ -21,7 +26,7 @@ import com.kunzisoft.keepass.view.ToolbarSpecial
abstract class DatabaseModeActivity : DatabaseActivity() { abstract class DatabaseModeActivity : DatabaseActivity() {
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
private var mTypeMode: TypeMode = TypeMode.DEFAULT protected var mTypeMode: TypeMode = TypeMode.DEFAULT
private var mToolbarSpecial: ToolbarSpecial? = null private var mToolbarSpecial: ToolbarSpecial? = null
@@ -50,8 +55,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
fun onLaunchActivitySpecialMode() { fun onLaunchActivitySpecialMode() {
if (!isIntentSender()) { if (!isIntentSender()) {
EntrySelectionHelper.removeModesFromIntent(intent) intent.removeModes()
EntrySelectionHelper.removeInfoFromIntent(intent) intent.removeInfo()
finish() finish()
} }
} }
@@ -60,8 +65,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
if (isIntentSender()) { if (isIntentSender()) {
super.finish() super.finish()
} else { } else {
EntrySelectionHelper.removeModesFromIntent(intent) intent.removeModes()
EntrySelectionHelper.removeInfoFromIntent(intent) intent.removeInfo()
if (mSpecialMode != SpecialMode.DEFAULT) { if (mSpecialMode != SpecialMode.DEFAULT) {
backToTheMainAppAndFinish() backToTheMainAppAndFinish()
} }
@@ -73,8 +78,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
// To get the app caller, only for IntentSender // To get the app caller, only for IntentSender
onRegularBackPressed() onRegularBackPressed()
} else { } else {
EntrySelectionHelper.removeModesFromIntent(intent) intent.removeModes()
EntrySelectionHelper.removeInfoFromIntent(intent) intent.removeInfo()
if (mSpecialMode != SpecialMode.DEFAULT) { if (mSpecialMode != SpecialMode.DEFAULT) {
backToTheMainAppAndFinish() backToTheMainAppAndFinish()
} }
@@ -105,18 +110,18 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
} }
}) })
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent) mSpecialMode = intent.retrieveSpecialMode()
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent) mTypeMode = intent.retrieveTypeMode()
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent) mSpecialMode = intent.retrieveSpecialMode()
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent) mTypeMode = intent.retrieveTypeMode()
val registerInfo: RegisterInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent) val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
val searchInfo: SearchInfo? = registerInfo?.searchInfo val searchInfo: SearchInfo? = registerInfo?.searchInfo
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent) ?: intent.retrieveSearchInfo()
// To show the selection mode // To show the selection mode
mToolbarSpecial = findViewById(R.id.special_mode_view) mToolbarSpecial = findViewById(R.id.special_mode_view)
@@ -125,9 +130,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
val selectionModeStringId = when (mSpecialMode) { val selectionModeStringId = when (mSpecialMode) {
SpecialMode.DEFAULT, // Not important because hidden SpecialMode.DEFAULT, // Not important because hidden
SpecialMode.SEARCH -> R.string.search_mode SpecialMode.SEARCH -> R.string.search_mode
SpecialMode.SAVE -> R.string.save_mode
SpecialMode.SELECTION -> R.string.selection_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) { val typeModeStringId = when (mTypeMode) {
TypeMode.DEFAULT, // Not important because hidden TypeMode.DEFAULT, // Not important because hidden
@@ -145,7 +149,6 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
visible = when (mSpecialMode) { visible = when (mSpecialMode) {
SpecialMode.DEFAULT -> false SpecialMode.DEFAULT -> false
SpecialMode.SEARCH -> true SpecialMode.SEARCH -> true
SpecialMode.SAVE -> true
SpecialMode.SELECTION -> true SpecialMode.SELECTION -> true
SpecialMode.REGISTRATION -> true SpecialMode.REGISTRATION -> true
} }

View File

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

View File

@@ -24,26 +24,31 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.os.Build import android.os.Build
import android.os.Bundle
import android.os.ParcelUuid
import android.util.Log import android.util.Log
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import com.kunzisoft.keepass.R 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.ContextualDatabase
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.getEnum
import com.kunzisoft.keepass.utils.getEnumExtra import com.kunzisoft.keepass.utils.getEnumExtra
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getParcelableExtraCompat 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.putEnumExtra
import com.kunzisoft.keepass.utils.putParcelableList
import java.io.IOException
import java.util.UUID
object EntrySelectionHelper { object EntrySelectionHelper {
@@ -51,6 +56,8 @@ object EntrySelectionHelper {
private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE" private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO" private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_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 * Finish the activity by passing the result code and by locking the database if necessary
@@ -58,7 +65,7 @@ object EntrySelectionHelper {
fun Activity.setActivityResult( fun Activity.setActivityResult(
lockDatabase: Boolean = false, lockDatabase: Boolean = false,
resultCode: Int, resultCode: Int,
data: Intent? = null, data: Intent? = null
) { ) {
when (resultCode) { when (resultCode) {
Activity.RESULT_OK -> Activity.RESULT_OK ->
@@ -68,170 +75,212 @@ object EntrySelectionHelper {
} }
this.finish() this.finish()
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(this)) { if (lockDatabase) {
// Close the database // Close the database
this.sendBroadcast(Intent(LOCK_ACTION)) this.sendBroadcast(Intent(LOCK_ACTION))
} }
} }
/** fun startActivityForSearchModeResult(
* Utility method to build a registerForActivityResult, context: Context,
* Used recursively, close each activity with return data intent: Intent,
*/ searchInfo: SearchInfo
fun AppCompatActivity.buildActivityResultLauncher( ) {
lockDatabase: Boolean = false, intent.addSpecialMode(SpecialMode.SEARCH)
dataTransformation: (data: Intent?) -> Intent? = { it }, intent.addSearchInfo(searchInfo)
): ActivityResultLauncher<Intent> { intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
return this.registerForActivityResult( context.startActivity(intent)
ActivityResultContracts.StartActivityForResult() }
) {
setActivityResult( fun startActivityForSelectionModeResult(
lockDatabase, context: Context,
it.resultCode, intent: Intent,
dataTransformation(it.data) 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
} }
} activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
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)
} }
fun startActivityForRegistrationModeResult( fun startActivityForRegistrationModeResult(
context: Context?, context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>?, activityResultLauncher: ActivityResultLauncher<Intent>?,
intent: Intent, intent: Intent,
registerInfo: RegisterInfo?, registerInfo: RegisterInfo?,
typeMode: TypeMode typeMode: TypeMode
) { ) {
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION) intent.addSpecialMode(SpecialMode.REGISTRATION)
addTypeModeInIntent(intent, typeMode) intent.addTypeMode(typeMode)
addRegisterInfoInIntent(intent, registerInfo) intent.addRegisterInfo(registerInfo)
if (activityResultLauncher == null) { if (activityResultLauncher == null) {
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
} }
activityResultLauncher?.launch(intent) ?: context?.startActivity(intent) ?: activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
throw IllegalStateException("At least Context or ActivityResultLauncher must not be null")
} }
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 { searchInfo?.let {
intent.putExtra(KEY_SEARCH_INFO, it) putExtra(KEY_SEARCH_INFO, it)
} }
return this
} }
fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? { fun Bundle.addSearchInfo(searchInfo: SearchInfo?): Bundle {
return intent.getParcelableExtraCompat(KEY_SEARCH_INFO) 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 { registerInfo?.let {
intent.putExtra(KEY_REGISTER_INFO, it) putExtra(KEY_REGISTER_INFO, it)
} }
return this
} }
fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? { fun Bundle.addRegisterInfo(registerInfo: RegisterInfo?): Bundle {
return intent.getParcelableExtraCompat(KEY_REGISTER_INFO) registerInfo?.let {
putParcelable(KEY_REGISTER_INFO, it)
}
return this
} }
fun removeInfoFromIntent(intent: Intent) { fun Intent.retrieveRegisterInfo(): RegisterInfo? {
intent.removeExtra(KEY_SEARCH_INFO) return getParcelableExtraCompat(KEY_REGISTER_INFO)
intent.removeExtra(KEY_REGISTER_INFO)
} }
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) { fun Bundle.getRegisterInfo(): RegisterInfo? {
// TODO Replace by Intent.addSpecialMode return getParcelableCompat(KEY_REGISTER_INFO)
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
} }
fun Intent.removeInfo() {
removeExtra(KEY_SEARCH_INFO)
removeExtra(KEY_REGISTER_INFO)
}
fun Intent.addSpecialMode(specialMode: SpecialMode): Intent { fun Intent.addSpecialMode(specialMode: SpecialMode): Intent {
this.putEnumExtra(KEY_SPECIAL_MODE, specialMode) this.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
return this return this
} }
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode { fun Bundle.addSpecialMode(specialMode: SpecialMode): Bundle {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { this.putEnum(KEY_SPECIAL_MODE, specialMode)
if (AutofillHelper.retrieveAutofillComponent(intent) != null) return this
return SpecialMode.SELECTION
}
return intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
} }
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) { fun Intent.retrieveSpecialMode(): SpecialMode {
// TODO Replace by Intent.addTypeMode return this.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
} }
fun Bundle.getSpecialMode(): SpecialMode {
return this.getEnum<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
}
fun Intent.addTypeMode(typeMode: TypeMode): Intent { fun Intent.addTypeMode(typeMode: TypeMode): Intent {
this.putEnumExtra(KEY_TYPE_MODE, typeMode) this.putEnumExtra(KEY_TYPE_MODE, typeMode)
return this return this
} }
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode { fun Intent.retrieveTypeMode(): TypeMode {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
return TypeMode.AUTOFILL
}
return intent.getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
} }
fun removeModesFromIntent(intent: Intent) { fun Intent.removeModes() {
intent.removeExtra(KEY_SPECIAL_MODE) removeExtra(KEY_SPECIAL_MODE)
intent.removeExtra(KEY_TYPE_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 { fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
return (specialMode == SpecialMode.SELECTION return (specialMode == SpecialMode.SELECTION
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY)) && (typeMode == TypeMode.MAGIKEYBOARD || typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
// TODO Autofill Registration callback #765 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|| (specialMode == SpecialMode.REGISTRATION || (specialMode == SpecialMode.REGISTRATION
&& typeMode == TypeMode.PASSKEY) && (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
} }
fun doSpecialAction(intent: Intent, fun doSpecialAction(
defaultAction: () -> Unit, intent: Intent,
searchAction: (searchInfo: SearchInfo) -> Unit, defaultAction: () -> Unit,
saveAction: (searchInfo: SearchInfo) -> Unit, searchAction: (searchInfo: SearchInfo) -> Unit,
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit, selectionAction: (
autofillSelectionAction: (searchInfo: SearchInfo?, intentSenderMode: Boolean,
autofillComponent: AutofillComponent) -> Unit, typeMode: TypeMode,
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit, searchInfo: SearchInfo?
passkeySelectionAction: (searchInfo: SearchInfo?) -> Unit, ) -> Unit,
passkeyRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) { registrationAction: (
intentSenderMode: Boolean,
when (retrieveSpecialModeFromIntent(intent)) { typeMode: TypeMode,
registerInfo: RegisterInfo?
) -> Unit
) {
when (val specialMode = intent.retrieveSpecialMode()) {
SpecialMode.DEFAULT -> { SpecialMode.DEFAULT -> {
removeModesFromIntent(intent) intent.removeModes()
removeInfoFromIntent(intent) intent.removeInfo()
defaultAction.invoke() defaultAction.invoke()
} }
SpecialMode.SEARCH -> { SpecialMode.SEARCH -> {
val searchInfo = retrieveSearchInfoFromIntent(intent) val searchInfo = intent.retrieveSearchInfo()
removeModesFromIntent(intent) intent.removeModes()
removeInfoFromIntent(intent) intent.removeInfo()
if (searchInfo != null) if (searchInfo != null)
searchAction.invoke(searchInfo) searchAction.invoke(searchInfo)
else { else {
defaultAction.invoke() defaultAction.invoke()
} }
} }
SpecialMode.SAVE -> {
val searchInfo = retrieveSearchInfoFromIntent(intent)
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
if (searchInfo != null)
saveAction.invoke(searchInfo)
else {
defaultAction.invoke()
}
}
SpecialMode.SELECTION -> { SpecialMode.SELECTION -> {
val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent) val searchInfo: SearchInfo? = intent.retrieveSearchInfo()
var autofillComponentInit = false if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { when (val typeMode = intent.retrieveTypeMode()) {
AutofillHelper.retrieveAutofillComponent(intent)?.let { autofillComponent -> TypeMode.DEFAULT -> {
autofillSelectionAction.invoke(searchInfo, autofillComponent) intent.removeModes()
autofillComponentInit = true if (searchInfo != null)
} searchAction.invoke(searchInfo)
} else
if (!autofillComponentInit) { defaultAction.invoke()
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) { }
when (retrieveTypeModeFromIntent(intent)) { TypeMode.MAGIKEYBOARD -> selectionAction.invoke(
TypeMode.DEFAULT -> { isIntentSenderMode(specialMode, typeMode),
removeModesFromIntent(intent) typeMode,
if (searchInfo != null) searchInfo
searchAction.invoke(searchInfo) )
else TypeMode.PASSKEY ->
defaultAction.invoke() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
} selectionAction.invoke(
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo) isIntentSenderMode(specialMode, typeMode),
TypeMode.PASSKEY -> passkeySelectionAction.invoke(searchInfo) typeMode,
else -> { searchInfo
// In this case, error )
removeModesFromIntent(intent) } else
removeInfoFromIntent(intent) 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 -> { SpecialMode.REGISTRATION -> {
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent) val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
if (!isIntentSenderMode( val typeMode = intent.retrieveTypeMode()
specialMode = retrieveSpecialModeFromIntent(intent), val intentSenderMode = isIntentSenderMode(specialMode, typeMode)
typeMode = retrieveTypeModeFromIntent(intent)) if (!intentSenderMode) {
) { intent.removeModes()
removeModesFromIntent(intent) intent.removeInfo()
removeInfoFromIntent(intent)
} }
when (retrieveTypeModeFromIntent(intent)) { if (registerInfo != null)
TypeMode.AUTOFILL -> { registrationAction.invoke(
autofillRegistrationAction.invoke(registerInfo) intentSenderMode,
} typeMode,
TypeMode.PASSKEY -> { registerInfo
passkeyRegistrationAction.invoke(registerInfo) )
} else {
else -> { defaultAction.invoke()
// Do other registration type
}
} }
} }
} }
@@ -367,7 +414,7 @@ object EntrySelectionHelper {
try { try {
database.iconDrawableFactory.getBitmapFromIcon(context, database.iconDrawableFactory.getBitmapFromIcon(context,
this.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap -> this.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
return Icon.createWithBitmap(bitmap) return IconCompat.createWithBitmap(bitmap).toIcon(context)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e) 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 { enum class SpecialMode {
DEFAULT, DEFAULT,
SEARCH, SEARCH,
SAVE,
SELECTION, SELECTION,
REGISTRATION; REGISTRATION;
} }

View File

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

View File

@@ -27,245 +27,186 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher 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.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent 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.credentialprovider.autofill.CompatInlineSuggestionsRequest import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.retrieveAutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService 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.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.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.utils.AppUtil import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.view.toastError import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : DatabaseModeActivity() { class AutofillLauncherActivity : DatabaseModeActivity() {
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? = private val autofillLauncherViewModel: AutofillLauncherViewModel by viewModels()
this.buildActivityResultLauncher(lockDatabase = true)
override fun applyCustomStyle(): Boolean { private var mAutofillSelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
return false this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
} autofillLauncherViewModel.manageSelectionResult(it)
}
override fun finishActivityIfReloadRequested(): Boolean { private var mAutofillRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
return true this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
} autofillLauncherViewModel.manageRegistrationResult(it)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun applyCustomStyle(): Boolean = false
super.onDatabaseRetrieved(database)
// Retrieve selection mode override fun finishActivityIfReloadRequested(): Boolean = true
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
when (specialMode) { override fun manageDatabaseInfo(): Boolean = false
SpecialMode.SELECTION -> {
intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle -> override fun onCreate(savedInstanceState: Bundle?) {
// To pass extra inline request // To apply the bypass https://github.com/Kunzisoft/KeePassDX/issues/2238
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null // before managing intent in super class
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { intent.retrieveSelectionBundle()?.apply {
compatInlineSuggestionsRequest = bundle.getParcelableCompat( intent.addSpecialMode(getSpecialMode())
KEY_INLINE_SUGGESTION intent.addSearchInfo(getSearchInfo())
) intent.addRegisterInfo(getRegisterInfo())
} intent.addAutofillComponent(retrieveAutofillComponent())
// Build search param }
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo -> super.onCreate(savedInstanceState)
AppUtil.getConcreteWebDomain( autofillLauncherViewModel.initialize()
this, lifecycleScope.launch {
searchInfo.webDomain // Initialize the parameters
) { concreteWebDomain -> autofillLauncherViewModel.uiState.collect { uiState ->
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE) when (uiState) {
val assistStructure = AutofillHelper AutofillLauncherViewModel.UIState.Loading -> {}
.retrieveAutofillComponent(intent) is AutofillLauncherViewModel.UIState.ShowBlockRestartMessage -> {
?.assistStructure showBlockRestartMessage()
val newAutofillComponent = if (assistStructure != null) { autofillLauncherViewModel.cancelResult()
AutofillComponent(
assistStructure,
compatInlineSuggestionsRequest
)
} else {
null
}
searchInfo.webDomain = concreteWebDomain
launchSelection(database, newAutofillComponent, searchInfo)
}
}
} }
// Remove bundle is AutofillLauncherViewModel.UIState.ShowAutofillSuggestionMessage -> {
intent.removeExtra(KEY_SELECTION_BUNDLE) showAutofillSuggestionMessage()
}
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)
} }
} }
else -> { }
// Not an autofill call }
setResult(RESULT_CANCELED) lifecycleScope.launch {
finish() // 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?, override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
autofillComponent: AutofillComponent?, super.onUnknownDatabaseRetrieved(database)
searchInfo: SearchInfo) { autofillLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
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()
} }
private fun showBlockRestartMessage() { private fun showBlockRestartMessage() {
// If item not allowed, show a toast // 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() { private fun showAutofillSuggestionMessage() {
toastError(RegisterInReadOnlyDatabaseException()) Toast.makeText(
applicationContext,
R.string.autofill_inline_suggestions_keyboard,
Toast.LENGTH_SHORT
).show()
} }
companion object { companion object {
private const val KEY_PENDING_INTENT_BUNDLE = "com.kunzisoft.keepass.extra.BUNDLE"
private val TAG = AutofillLauncherActivity::class.java.name private val TAG = AutofillLauncherActivity::class.java.name
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE" fun Intent.retrieveSelectionBundle(): Bundle? {
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO" return this.getBundleExtra(KEY_PENDING_INTENT_BUNDLE)
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION" }
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO" fun getPendingIntentForSelection(
context: Context,
fun getPendingIntentForSelection(context: Context, searchInfo: SearchInfo? = null,
searchInfo: SearchInfo? = null, autofillComponent: AutofillComponent
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent? { ): PendingIntent? {
try { 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( return PendingIntent.getActivity(
context, 0, context,
// Doesn't work with direct extra Parcelable (don't know why?) randomRequestCode(),
// Wrap into a bundle to bypass the problem
Intent(context, AutofillLauncherActivity::class.java).apply { Intent(context, AutofillLauncherActivity::class.java).apply {
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply { putExtra(KEY_PENDING_INTENT_BUNDLE, tempBundle)
putParcelable(KEY_SEARCH_INFO, searchInfo)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
}
})
}, },
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
@@ -279,14 +220,21 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
} }
} }
fun getPendingIntentForRegistration(context: Context, fun getPendingIntentForRegistration(
registerInfo: RegisterInfo): PendingIntent? { context: Context,
registerInfo: RegisterInfo
): PendingIntent? {
try { try {
// Bypass intent issue
val tempBundle = Bundle().apply {
addSpecialMode(SpecialMode.REGISTRATION)
addRegisterInfo(registerInfo)
}
return PendingIntent.getActivity( return PendingIntent.getActivity(
context, 0, context,
randomRequestCode(),
Intent(context, AutofillLauncherActivity::class.java).apply { Intent(context, AutofillLauncherActivity::class.java).apply {
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION) putExtra(KEY_PENDING_INTENT_BUNDLE, tempBundle)
putExtra(KEY_REGISTER_INFO, registerInfo)
}, },
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
@@ -299,14 +247,5 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
return null 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.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import androidx.activity.result.ActivityResultLauncher
import androidx.core.net.toUri import androidx.activity.result.contract.ActivityResultContracts
import com.kunzisoft.keepass.R import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity 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.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.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.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 com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch
/** /**
* Activity to search or select entry in database, * Activity to search or select entry in database,
@@ -45,198 +45,133 @@ import com.kunzisoft.keepass.view.toastError
*/ */
class EntrySelectionLauncherActivity : DatabaseModeActivity() { class EntrySelectionLauncherActivity : DatabaseModeActivity() {
override fun applyCustomStyle(): Boolean { private val entrySelectionViewModel: EntrySelectionViewModel by viewModels()
return false
}
override fun finishActivityIfReloadRequested(): Boolean { private var mEntrySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
return false this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
} entrySelectionViewModel.manageSelectionResult(it)
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
} }
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain -> override fun applyCustomStyle() = false
searchInfo.webDomain = concreteWebDomain
launch(database, searchInfo)
}
}
private fun launch(database: ContextualDatabase?, override fun finishActivityIfReloadRequested() = false
searchInfo: SearchInfo) {
// Setting to integrate Magikeyboard override fun manageDatabaseInfo(): Boolean = false
val searchShareForMagikeyboard = isKeyboardActivatedInSettings()
// If database is open override fun onCreate(savedInstanceState: Bundle?) {
val readOnly = database?.isReadOnly != false super.onCreate(savedInstanceState)
SearchHelper.checkAutoSearchInfo( entrySelectionViewModel.initialize()
context = this, lifecycleScope.launch {
database = database, // Initialize the parameters
searchInfo = searchInfo, entrySelectionViewModel.uiState.collect { uiState ->
onItemsFound = { openedDatabase, items -> when (uiState) {
// Items found is EntrySelectionViewModel.UIState.Loading -> {}
if (searchInfo.otpString != null) { is EntrySelectionViewModel.UIState.PopulateKeyboard -> {
if (!readOnly) { MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(
GroupActivity.launchForSaveResult( context = this@EntrySelectionLauncherActivity,
this, entry = uiState.entryInfo,
openedDatabase, toast = true
searchInfo,
false
) )
} else {
toastError(RegisterInReadOnlyDatabaseException())
} }
} else if (searchShareForMagikeyboard) { is EntrySelectionViewModel.UIState.LaunchFileDatabaseSelectForSearch -> {
MagikeyboardService.performSelection( FileDatabaseSelectActivity.launchForSearch(
items, context = this@EntrySelectionLauncherActivity,
{ entryInfo -> searchInfo = uiState.searchInfo
// 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
) )
} else { finish()
toastError(RegisterInReadOnlyDatabaseException()) }
is EntrySelectionViewModel.UIState.LaunchGroupActivityForSearch -> {
GroupActivity.launchForSearch(
context = this@EntrySelectionLauncherActivity,
database = uiState.database,
searchInfo = uiState.searchInfo
)
finish()
} }
} 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 { companion object {
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE" fun launch(
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO" context: Context,
searchInfo: SearchInfo? = null
fun launch(context: Context, ) {
searchInfo: SearchInfo? = null) { context.startActivity(Intent(
val intent = Intent(context, EntrySelectionLauncherActivity::class.java).apply { context,
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply { EntrySelectionLauncherActivity::class.java
putParcelable(KEY_SEARCH_INFO, searchInfo) ).apply {
}) addSearchInfo(searchInfo)
} // New task needed because don't launch from an Activity context
// New task needed because don't launch from an Activity context flags = Intent.FLAG_ACTIVITY_NEW_TASK or
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
Intent.FLAG_ACTIVITY_CLEAR_TASK })
context.startActivity(intent)
} }
} }
} }

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.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity 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.addSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult 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.data.AndroidPrivilegedApp
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin 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.addAuthCode
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addNodeId import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.model.AppOrigin import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.view.toastError import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
@@ -79,10 +81,6 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
return false return false
} }
override fun finishActivityIfDatabaseNotLoaded(): Boolean {
return false
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
lifecycleScope.launch { lifecycleScope.launch {
@@ -105,61 +103,69 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
nodeId = uiState.nodeId 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 -> { is PasskeyLauncherViewModel.UIState.UpdateEntry -> {
updateEntry(uiState.oldEntry, uiState.newEntry) 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?) { override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database) super.onUnknownDatabaseRetrieved(database)
passkeyLauncherViewModel.launchPasskeyActionIfNeeded(intent, mSpecialMode, database) passkeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
} }
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
@@ -170,7 +176,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
super.onDatabaseActionFinished(database, actionTask, result) super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) { when (actionTask) {
ACTION_DATABASE_UPDATE_ENTRY_TASK -> { 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("\n\n")
.append(getString(R.string.passkeys_missing_signature_app_ask_explanation)) .append(getString(R.string.passkeys_missing_signature_app_ask_explanation))
.append("\n\n")
.append(getString(R.string.passkeys_missing_signature_app_ask_question))
.toString() .toString()
) )
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(android.R.string.ok) { _, _ ->
@@ -273,7 +282,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
): PendingIntent? { ): PendingIntent? {
return PendingIntent.getActivity( return PendingIntent.getActivity(
context, context,
(Math.random() * Integer.MAX_VALUE).toInt(), randomRequestCode(),
Intent(context, PasskeyLauncherActivity::class.java).apply { Intent(context, PasskeyLauncherActivity::class.java).apply {
addSpecialMode(specialMode) addSpecialMode(specialMode)
addTypeMode(TypeMode.PASSKEY) addTypeMode(TypeMode.PASSKEY)

View File

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

View File

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

View File

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

View File

@@ -362,8 +362,8 @@ class StructureParser(private val structure: AssistStructure) {
if (result?.passwordId == null) { if (result?.passwordId == null) {
usernameIdCandidate = autofillId usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue 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, inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> { 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.R
import com.kunzisoft.keepass.adapters.FieldsAdapter import com.kunzisoft.keepass.adapters.FieldsAdapter
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider import com.kunzisoft.keepass.database.DatabaseTaskProvider
@@ -461,9 +462,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast) KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
} }
fun performSelection(items: List<EntryInfo>, fun performSelection(
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit, items: List<EntryInfo>,
actionEntrySelection: (autoSearch: Boolean) -> Unit) { actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
actionEntrySelection: (autoSearch: Boolean) -> Unit
) {
EntrySelectionHelper.performSelection( EntrySelectionHelper.performSelection(
items = items, items = items,
actionPopulateCredentialProvider = { itemFound -> actionPopulateCredentialProvider = { itemFound ->
@@ -477,15 +480,5 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
actionEntrySelection = actionEntrySelection 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 { private fun buildPasskeySearchInfo(relyingParty: String): SearchInfo {
return SearchInfo().apply { return SearchInfo().apply {
this.relyingParty = relyingParty this.relyingParty = relyingParty
this.isAPasskeySearch = true
this.query = relyingParty
} }
} }
override fun onBeginGetCredentialRequest( override fun onBeginGetCredentialRequest(
request: BeginGetCredentialRequest, request: BeginGetCredentialRequest,
cancellationSignal: CancellationSignal, cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>, callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>
) { ) {
Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called") Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called")
try { try {
processGetCredentialsRequest(request)?.let { response -> processGetCredentialsRequest(request) { response ->
callback.onResult(response) callback.onResult(response)
} ?: run {
callback.onError(GetCredentialUnknownException())
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e) Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e)
@@ -116,24 +112,30 @@ class PasskeyProviderService : CredentialProviderService() {
} }
} }
private fun processGetCredentialsRequest(request: BeginGetCredentialRequest): BeginGetCredentialResponse? { private fun processGetCredentialsRequest(
val credentialEntries: MutableList<CredentialEntry> = mutableListOf() request: BeginGetCredentialRequest,
callback: (BeginGetCredentialResponse?) -> Unit
) {
var knownOption = false
for (option in request.beginGetCredentialOptions) { for (option in request.beginGetCredentialOptions) {
when (option) { when (option) {
is BeginGetPublicKeyCredentialOption -> { is BeginGetPublicKeyCredentialOption -> {
credentialEntries.addAll( knownOption = true
populatePasskeyData(option) populatePasskeyData(option) { listCredentials ->
) callback(BeginGetCredentialResponse(listCredentials))
return BeginGetCredentialResponse(credentialEntries) }
} }
} }
} }
Log.w(javaClass.simpleName, "unknown beginGetCredentialOption") if (knownOption.not()) {
return null 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() val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
@@ -169,6 +171,7 @@ class PasskeyProviderService : CredentialProviderService() {
) )
} }
} }
callback(passkeyEntries)
}, },
onItemNotFound = { _ -> onItemNotFound = { _ ->
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId") Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
@@ -191,6 +194,7 @@ class PasskeyProviderService : CredentialProviderService() {
) )
) )
} }
callback(passkeyEntries)
}, },
onDatabaseClosed = { onDatabaseClosed = {
Log.d(TAG, "Add pending intent for passkey selection in closed database") 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( override fun onBeginCreateCredentialRequest(
@@ -225,7 +229,9 @@ class PasskeyProviderService : CredentialProviderService() {
) { ) {
Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called") Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
try { try {
callback.onResult(processCreateCredentialRequest(request)) processCreateCredentialRequest(request) {
callback.onResult(BeginCreateCredentialResponse(it))
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e) Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e)
toastError(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) { when (request) {
is BeginCreatePublicKeyCredentialRequest -> { is BeginCreatePublicKeyCredentialRequest -> {
// Request is passkey type // 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( private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry(
@@ -266,9 +277,15 @@ class PasskeyProviderService : CredentialProviderService() {
} }
} }
private fun handleCreatePasskeyQuery(request: BeginCreatePublicKeyCredentialRequest): BeginCreateCredentialResponse { private fun handleCreatePasskeyQuery(
request: BeginCreatePublicKeyCredentialRequest,
val accountName = mDatabase?.name ?: getString(R.string.passkey_database_username) 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 createEntries: MutableList<CreateEntry> = mutableListOf()
val relyingPartyId = PublicKeyCredentialCreationOptions( val relyingPartyId = PublicKeyCredentialCreationOptions(
requestJson = request.requestJson, requestJson = request.requestJson,
@@ -309,6 +326,7 @@ class PasskeyProviderService : CredentialProviderService() {
} }
}*/ }*/
} }
callback(createEntries)
}, },
onItemNotFound = { database -> onItemNotFound = { database ->
// To create a new entry // To create a new entry
@@ -317,6 +335,7 @@ class PasskeyProviderService : CredentialProviderService() {
} else { } else {
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo) createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
} }
callback(createEntries)
}, },
onDatabaseClosed = { onDatabaseClosed = {
// Launch the passkey launcher activity to open the database // Launch the passkey launcher activity to open the database
@@ -334,10 +353,9 @@ class PasskeyProviderService : CredentialProviderService() {
) )
) )
} }
callback(createEntries)
} }
) )
return BeginCreateCredentialResponse(createEntries)
} }
override fun onClearCredentialStateRequest( override fun onClearCredentialStateRequest(

View File

@@ -24,7 +24,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.ParcelUuid
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Log 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
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
import com.kunzisoft.keepass.R 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.AuthenticatorAssertionResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor 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.AppOrigin
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Passkey import com.kunzisoft.keepass.model.Passkey
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.utils.AppUtil import com.kunzisoft.keepass.utils.AppUtil
import com.kunzisoft.keepass.utils.StringUtil.toHexString import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.getParcelableExtraCompat import com.kunzisoft.keepass.utils.getParcelableExtraCompat
@@ -88,10 +87,7 @@ object PasskeyHelper {
private const val HMAC_TYPE = "HmacSHA256" 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_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_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode" private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
@@ -110,38 +106,6 @@ object PasskeyHelper {
private val internalSecureRandom: SecureRandom = SecureRandom() 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 * Add an authentication code generated by an entry to the intent
*/ */
@@ -181,22 +145,6 @@ object PasskeyHelper {
return this.removeExtra(EXTRA_PASSKEY) 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 * 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?) { fun Activity.buildPasskeyResponseAndSetResult(
nodeId?.let { entryInfo: EntryInfo,
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId)) 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 * 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 * Utility method to create a passkey and the associated creation request parameters
* [intent] allows to retrieve the request * [intent] allows to retrieve the request
* [context] context to manage package verification files * [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 * [passkeyCreated] is called asynchronously when the passkey has been created
*/ */
suspend fun retrievePasskeyCreationRequestParameters( suspend fun retrievePasskeyCreationRequestParameters(
intent: Intent, intent: Intent,
context: Context, context: Context,
defaultBackupEligibility: Boolean?,
defaultBackupState: Boolean?,
passkeyCreated: suspend (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit passkeyCreated: suspend (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
) { ) {
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent) val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
@@ -456,7 +424,9 @@ object PasskeyHelper {
privateKeyPem = privateKeyPem, privateKeyPem = privateKeyPem,
credentialId = b64Encode(credentialId), credentialId = b64Encode(credentialId),
userHandle = b64Encode(userHandle), userHandle = b64Encode(userHandle),
relyingParty = relyingParty relyingParty = relyingParty,
backupEligibility = defaultBackupEligibility,
backupState = defaultBackupState
) )
// create new entry in database // create new entry in database
@@ -590,8 +560,8 @@ object PasskeyHelper {
requestOptions: PublicKeyCredentialRequestOptions, requestOptions: PublicKeyCredentialRequestOptions,
clientDataResponse: ClientDataResponse, clientDataResponse: ClientDataResponse,
passkey: Passkey, passkey: Passkey,
backupEligibility: Boolean, defaultBackupEligibility: Boolean,
backupState: Boolean defaultBackupState: Boolean
): PublicKeyCredential { ): PublicKeyCredential {
val getCredentialResponse = FidoPublicKeyCredential( val getCredentialResponse = FidoPublicKeyCredential(
id = passkey.credentialId, id = passkey.credentialId,
@@ -599,8 +569,8 @@ object PasskeyHelper {
requestOptions = requestOptions, requestOptions = requestOptions,
userPresent = true, userPresent = true,
userVerified = true, userVerified = true,
backupEligibility = backupEligibility, backupEligibility = passkey.backupEligibility ?: defaultBackupEligibility,
backupState = backupState, backupState = passkey.backupState ?: defaultBackupState,
userHandle = passkey.userHandle, userHandle = passkey.userHandle,
privateKey = passkey.privateKeyPem, privateKey = passkey.privateKeyPem,
clientDataResponse = clientDataResponse 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.GetCredentialResponse
import androidx.credentials.exceptions.GetCredentialUnknownException import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.PendingIntentHandler import androidx.credentials.provider.PendingIntentHandler
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope 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.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp 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.removeAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey 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.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.retrievePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters 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.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
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
@@ -56,22 +57,21 @@ import java.io.InvalidObjectException
import java.util.UUID import java.util.UUID
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @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 mUsageParameters: PublicKeyCredentialUsageParameters? = null
private var mCreationParameters: PublicKeyCredentialCreationParameters? = null private var mCreationParameters: PublicKeyCredentialCreationParameters? = null
private var mPasskey: Passkey? = null private var mPasskey: Passkey? = null
private var mLockDatabaseAfterSelection: Boolean = false
private var mBackupEligibility: Boolean = true private var mBackupEligibility: Boolean = true
private var mBackupState: Boolean = false private var mBackupState: Boolean = false
private var mLockDatabase: Boolean = true
private var isResultLauncherRegistered: Boolean = false private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
private val _uiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = _uiState
fun initialize() { fun initialize() {
mLockDatabaseAfterSelection = PreferencesUtil.isPasskeyCloseDatabaseEnable(getApplication())
mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication()) mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication())
mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication()) mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication())
} }
@@ -79,19 +79,14 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
fun showAppPrivilegedDialog( fun showAppPrivilegedDialog(
temptingApp: AndroidPrivilegedApp temptingApp: AndroidPrivilegedApp
) { ) {
_uiState.value = UIState.ShowAppPrivilegedDialog(temptingApp) mUiState.value = UIState.ShowAppPrivilegedDialog(temptingApp)
} }
fun showAppSignatureDialog( fun showAppSignatureDialog(
temptingApp: AppOrigin, temptingApp: AppOrigin,
nodeId: UUID nodeId: UUID
) { ) {
_uiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId) mUiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId)
}
fun showError(error: Throwable) {
Log.e(TAG, "Error on passkey launch", error)
_uiState.value = UIState.ShowError(error)
} }
fun saveCustomPrivilegedApp( fun saveCustomPrivilegedApp(
@@ -107,7 +102,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
context = getApplication(), context = getApplication(),
privilegedApps = listOf(temptingApp) privilegedApps = listOf(temptingApp)
) )
launchPasskeyAction( launchAction(
intent = intent, intent = intent,
specialMode = specialMode, specialMode = specialMode,
database = database database = database
@@ -139,54 +134,33 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
) )
entryInfo.saveAppOrigin(database, temptingApp) entryInfo.saveAppOrigin(database, temptingApp)
newEntry.setEntryInfo(database, entryInfo) newEntry.setEntryInfo(database, entryInfo)
_uiState.value = UIState.UpdateEntry( mUiState.value = UIState.UpdateEntry(
oldEntry = entry, oldEntry = entry,
newEntry = newEntry newEntry = newEntry
) )
} }
} }
fun setResult(intent: Intent) { override fun onExceptionOccurred(e: Throwable) {
// Remove the launcher register if (e is PrivilegedAllowLists.PrivilegedException) {
isResultLauncherRegistered = false showAppPrivilegedDialog(e.temptingApp)
_uiState.value = UIState.SetActivityResult( } else {
lockDatabase = mLockDatabase, super.onExceptionOccurred(e)
resultCode = RESULT_OK, }
data = intent
)
} }
fun cancelResult() { override fun launchActionIfNeeded(
isResultLauncherRegistered = false
_uiState.value = UIState.SetActivityResult(
lockDatabase = mLockDatabase,
resultCode = RESULT_CANCELED
)
}
fun launchPasskeyActionIfNeeded(
intent: Intent, intent: Intent,
specialMode: SpecialMode, specialMode: SpecialMode,
database: ContextualDatabase? database: ContextualDatabase?
) { ) {
if (isResultLauncherRegistered.not()) { // Launch with database when a nodeId is present
isResultLauncherRegistered = true if ((database != null && database.loaded) || intent.retrieveNodeId() == null) {
viewModelScope.launch(CoroutineExceptionHandler { _, e -> super.launchActionIfNeeded(intent, specialMode, database)
if (e is PrivilegedAllowLists.PrivilegedException) {
showAppPrivilegedDialog(e.temptingApp)
} else {
showError(e)
}
}) {
launchPasskeyAction(intent, specialMode, database)
}
} }
} }
/** override suspend fun launchAction(
* Launch the main action to manage Passkey
*/
private suspend fun launchPasskeyAction(
intent: Intent, intent: Intent,
specialMode: SpecialMode, specialMode: SpecialMode,
database: ContextualDatabase? database: ContextualDatabase?
@@ -194,6 +168,9 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo() val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false) val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
val nodeId = intent.retrieveNodeId() val nodeId = intent.retrieveNodeId()
intent.removeInfo()
intent.removeAppOrigin()
intent.removeNodeId()
checkSecurity(intent, nodeId) checkSecurity(intent, nodeId)
when (specialMode) { when (specialMode) {
SpecialMode.SELECTION -> { SpecialMode.SELECTION -> {
@@ -260,15 +237,19 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
TAG, "No Passkey found for selection," + TAG, "No Passkey found for selection," +
"launch manual selection in opened database" "launch manual selection in opened database"
) )
_uiState.value = UIState.LaunchGroupActivityForSelection( mCredentialUiState.value =
database = openedDatabase CredentialState.LaunchGroupActivityForSelection(
) database = openedDatabase,
searchInfo = searchInfo,
typeMode = TypeMode.PASSKEY
)
}, },
onDatabaseClosed = { onDatabaseClosed = {
Log.d(TAG, "Manual passkey selection in closed database") Log.d(TAG, "Manual passkey selection in closed database")
_uiState.value = mCredentialUiState.value =
UIState.LaunchFileDatabaseSelectActivityForSelection( CredentialState.LaunchFileDatabaseSelectActivityForSelection(
searchInfo = searchInfo searchInfo = searchInfo,
typeMode = TypeMode.PASSKEY
) )
} }
) )
@@ -326,12 +307,12 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
appOrigin = appOrigin appOrigin = appOrigin
), ),
passkey = passkey, passkey = passkey,
backupEligibility = mBackupEligibility, defaultBackupEligibility = mBackupEligibility,
backupState = mBackupState defaultBackupState = mBackupState
) )
) )
) )
setResult(result) setResult(result, lockDatabase = mLockDatabaseAfterSelection)
} catch (e: SignatureNotFoundException) { } catch (e: SignatureNotFoundException) {
// Request the dialog if signature exception // Request the dialog if signature exception
showAppSignatureDialog(e.temptingApp, nodeId) showAppSignatureDialog(e.temptingApp, nodeId)
@@ -340,9 +321,11 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
} }
} }
fun manageSelectionResult( override fun manageSelectionResult(
database: ContextualDatabase,
activityResult: ActivityResult activityResult: ActivityResult
) { ) {
super.manageSelectionResult(database, activityResult)
val intent = activityResult.data val intent = activityResult.data
viewModelScope.launch(CoroutineExceptionHandler { _, e -> viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create selection response for passkey", e) Log.e(TAG, "Unable to create selection response for passkey", e)
@@ -380,8 +363,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
appOrigin = appOrigin appOrigin = appOrigin
), ),
passkey = passkey, passkey = passkey,
backupEligibility = mBackupEligibility, defaultBackupEligibility = mBackupEligibility,
backupState = mBackupState defaultBackupState = mBackupState
) )
) )
) )
@@ -389,7 +372,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
throw IOException("Usage parameters is null") throw IOException("Usage parameters is null")
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
setResult(responseIntent) setResult(responseIntent, lockDatabase = mLockDatabaseAfterSelection)
} }
} }
} }
@@ -417,6 +400,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
retrievePasskeyCreationRequestParameters( retrievePasskeyCreationRequestParameters(
intent = intent, intent = intent,
context = getApplication(), context = getApplication(),
defaultBackupEligibility = mBackupEligibility,
defaultBackupState = mBackupState,
passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters -> passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters ->
// Save the requested parameters // Save the requested parameters
mPasskey = passkey mPasskey = passkey
@@ -440,24 +425,26 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
TAG, "Passkey found for registration, " + TAG, "Passkey found for registration, " +
"but launch manual registration for a new entry" "but launch manual registration for a new entry"
) )
_uiState.value = UIState.LaunchGroupActivityForRegistration( mCredentialUiState.value =
database = openedDatabase, CredentialState.LaunchGroupActivityForRegistration(
registerInfo = registerInfo, database = openedDatabase,
typeMode = TypeMode.PASSKEY registerInfo = registerInfo,
) typeMode = TypeMode.PASSKEY
)
}, },
onItemNotFound = { openedDatabase -> onItemNotFound = { openedDatabase ->
Log.d(TAG, "Launch new manual registration in opened database") Log.d(TAG, "Launch new manual registration in opened database")
_uiState.value = UIState.LaunchGroupActivityForRegistration( mCredentialUiState.value =
database = openedDatabase, CredentialState.LaunchGroupActivityForRegistration(
registerInfo = registerInfo, database = openedDatabase,
typeMode = TypeMode.PASSKEY registerInfo = registerInfo,
) typeMode = TypeMode.PASSKEY
)
}, },
onDatabaseClosed = { onDatabaseClosed = {
Log.d(TAG, "Manual passkey registration in closed database") Log.d(TAG, "Manual passkey registration in closed database")
_uiState.value = mCredentialUiState.value =
UIState.LaunchFileDatabaseSelectActivityForRegistration( CredentialState.LaunchFileDatabaseSelectActivityForRegistration(
registerInfo = registerInfo, registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY 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 val intent = activityResult.data
viewModelScope.launch(CoroutineExceptionHandler { _, e -> viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create registration response for passkey", e) Log.e(TAG, "Unable to create registration response for passkey", e)
@@ -518,8 +505,10 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
intent = responseIntent, intent = responseIntent,
response = buildCreatePublicKeyCredentialResponse( response = buildCreatePublicKeyCredentialResponse(
publicKeyCredentialCreationParameters = it, publicKeyCredentialCreationParameters = it,
backupEligibility = mBackupEligibility, backupEligibility = passkey?.backupEligibility
backupState = mBackupState ?: mBackupEligibility,
backupState = passkey?.backupState
?: mBackupState
) )
) )
} }
@@ -549,29 +538,6 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
val temptingApp: AppOrigin, val temptingApp: AppOrigin,
val nodeId: UUID val nodeId: UUID
): UIState() ): 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( data class UpdateEntry(
val oldEntry: Entry, val oldEntry: Entry,
val newEntry: Entry val newEntry: Entry

View File

@@ -19,7 +19,6 @@
*/ */
package com.kunzisoft.keepass.database package com.kunzisoft.keepass.database
import android.Manifest
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
@@ -29,23 +28,15 @@ import android.content.Context.BIND_IMPORTANT
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.ServiceConnection import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED 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.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.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Entry 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.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK 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_RECYCLE_BIN_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_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.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_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.utils.putParcelableList import com.kunzisoft.keepass.utils.putParcelableList
import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
/** /**
@@ -103,121 +89,29 @@ import java.util.UUID
* Useful to retrieve a database instance and sending tasks commands * Useful to retrieve a database instance and sending tasks commands
*/ */
class DatabaseTaskProvider( class DatabaseTaskProvider(
private var context: Context, private var context: Context
private var showDialog: Boolean = true
) { ) {
// 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 onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
var onActionFinish: (( var onStartActionRequested: ((bundle: Bundle?, actionTask: String) -> Unit)? = null
database: ContextualDatabase, var actionTaskListener: DatabaseTaskNotificationService.ActionTaskListener? = null
actionTask: String, var databaseInfoListener: DatabaseTaskNotificationService.DatabaseInfoListener? = null
result: ActionRunnable.Result
) -> Unit)? = null
private var intentDatabaseTask: Intent = Intent(
context.applicationContext,
DatabaseTaskNotificationService::class.java
)
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
private var serviceConnection: ServiceConnection? = null private var serviceConnection: ServiceConnection? = null
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
fun destroy() { fun destroy() {
this.activity = null
this.onDatabaseRetrieved = null this.onDatabaseRetrieved = null
this.onActionFinish = null
this.databaseTaskBroadcastReceiver = null this.databaseTaskBroadcastReceiver = null
this.mBinder = null this.mBinder = null
this.serviceConnection = null this.serviceConnection = null
this.progressTaskDialogFragment = null
this.databaseChangedDialogFragment = null
} }
private val actionTaskListener = object : DatabaseTaskNotificationService.ActionTaskListener { fun onDatabaseChangeValidated() {
override fun onActionStarted( mBinder?.getService()?.saveDatabaseInfo()
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
)
}
}
}
}
} }
private var databaseListener = object : DatabaseTaskNotificationService.DatabaseListener { private var databaseListener = object : DatabaseTaskNotificationService.DatabaseListener {
@@ -226,48 +120,17 @@ 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() { private fun initServiceConnection() {
if (serviceConnection == null) { if (serviceConnection == null) {
serviceConnection = object : ServiceConnection { serviceConnection = object : ServiceConnection {
override fun onBindingDied(name: ComponentName?) { override fun onBindingDied(name: ComponentName?) {
stopDialog() actionTaskListener?.onActionStopped()
onDatabaseRetrieved?.invoke(null)
} }
override fun onNullBinding(name: ComponentName?) { override fun onNullBinding(name: ComponentName?) {
stopDialog() actionTaskListener?.onActionStopped()
onDatabaseRetrieved?.invoke(null)
} }
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) { override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
@@ -290,21 +153,33 @@ class DatabaseTaskProvider(
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) { private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
service?.addDatabaseListener(databaseListener) service?.addDatabaseListener(databaseListener)
service?.addDatabaseFileInfoListener(databaseInfoListener) databaseInfoListener?.let { infoListener ->
service?.addActionTaskListener(actionTaskListener) service?.addDatabaseFileInfoListener(infoListener)
}
actionTaskListener?.let { taskListener ->
service?.addActionTaskListener(taskListener)
}
} }
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) { private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
service?.removeActionTaskListener(actionTaskListener) actionTaskListener?.let { taskListener ->
service?.removeDatabaseFileInfoListener(databaseInfoListener) service?.removeActionTaskListener(taskListener)
}
databaseInfoListener?.let { infoListener ->
service?.removeDatabaseFileInfoListener(infoListener)
}
service?.removeDatabaseListener(databaseListener) service?.removeDatabaseListener(databaseListener)
onDatabaseRetrieved?.invoke(null)
} }
private fun bindService() { private fun bindService() {
initServiceConnection() initServiceConnection()
serviceConnection?.let { serviceConnection?.let {
context.bindService( context.bindService(
intentDatabaseTask, Intent(
context.applicationContext,
DatabaseTaskNotificationService::class.java
),
it, it,
BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT
) )
@@ -368,58 +243,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) { private fun start(bundle: Bundle? = null, actionTask: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { onStartActionRequested?.invoke(bundle, actionTask) ?: run {
val contextActivity = activity context.startDatabaseService(bundle, actionTask)
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()
} }
} }
@@ -842,5 +668,21 @@ class DatabaseTaskProvider(
companion object { companion object {
private val TAG = DatabaseTaskProvider::class.java.name 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

@@ -4,7 +4,7 @@ import androidx.annotation.StringRes
data class ProgressMessage( data class ProgressMessage(
@StringRes @StringRes
var titleId: Int, var titleId: Int? = null,
@StringRes @StringRes
var messageId: Int? = null, var messageId: Int? = null,
@StringRes @StringRes

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.VersionDatabaseException
import com.kunzisoft.keepass.database.exception.XMLMalformedDatabaseException import com.kunzisoft.keepass.database.exception.XMLMalformedDatabaseException
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_CREDENTIAL_ID 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_PRIVATE_KEY
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_RELYING_PARTY import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_RELYING_PARTY
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USERNAME 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_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_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_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 else -> name
} }

View File

@@ -21,9 +21,16 @@ package com.kunzisoft.keepass.database.helper
import android.content.Context import android.content.Context
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil.searchSubDomains
import com.kunzisoft.keepass.timeout.TimeoutHelper 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 { 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] * 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, onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
onDatabaseClosed: () -> Unit onDatabaseClosed: () -> Unit
) { ) {
// Do not place coroutine at start, bug in Passkey implementation
if (database == null || !database.loaded) { if (database == null || !database.loaded) {
onDatabaseClosed.invoke() onDatabaseClosed.invoke()
} else if (TimeoutHelper.checkTime(context)) { } else if (TimeoutHelper.checkTime(context)) {
var searchWithoutUI = false
if (searchInfo != null if (searchInfo != null
&& !searchInfo.manualSelection && !searchInfo.manualSelection
&& !searchInfo.containsOnlyNullValues()) { && !searchInfo.containsOnlyNullValues()
// If search provide results ) {
database.createVirtualGroupFromSearchInfo( searchInfo.getSearchParametersFromSearchInfo(context) { searchParameters ->
searchInfo, // If search provide results
MAX_SEARCH_ENTRY database.createVirtualGroupFromSearchInfo(
)?.let { searchGroup -> searchParameters = searchParameters,
if (searchGroup.numberOfChildEntries > 0) { max = MAX_SEARCH_ENTRY
searchWithoutUI = true )?.let { searchGroup ->
onItemsFound.invoke(database, if (searchGroup.numberOfChildEntries > 0) {
searchGroup.getChildEntriesInfo(database)) onItemsFound.invoke(
} database,
searchGroup.getChildEntriesInfo(database)
)
} else
onItemNotFound.invoke(database)
} ?: onItemNotFound.invoke(database)
} }
} } else
if (!searchWithoutUI) {
onItemNotFound.invoke(database) 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) { class PasswordGenerator(private val resources: Resources) {
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
fun generatePassword(length: Int, fun generatePassword(
upperCase: Boolean, length: Int,
lowerCase: Boolean, upperCase: Boolean,
digits: Boolean, lowerCase: Boolean,
minus: Boolean, digits: Boolean,
underline: Boolean, minus: Boolean,
space: Boolean, underline: Boolean,
specials: Boolean, space: Boolean,
brackets: Boolean, specials: Boolean,
extended: Boolean, brackets: Boolean,
considerChars: String, extended: Boolean,
ignoreChars: String, considerChars: String,
atLeastOneFromEach: Boolean, ignoreChars: String,
excludeAmbiguousChar: Boolean): String { atLeastOneFromEach: Boolean,
excludeAmbiguousChar: Boolean
): String {
// Desired password length is 0 or less // Desired password length is 0 or less
if (length <= 0) { if (length <= 0) {
throw IllegalArgumentException(resources.getString(R.string.error_wrong_length)) 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 MINUS_CHAR = "-"
private const val UNDERLINE_CHAR = "_" private const val UNDERLINE_CHAR = "_"
private const val SPACE_CHAR = " " private const val SPACE_CHAR = " "
private const val SPECIAL_CHARS = "!\"#$%&'*+,./:;=?@\\^`" private const val SPECIAL_CHARS = "&/,^@.#:%\\='$!?*`;+\"|~"
private const val BRACKET_CHARS = "[]{}()<>" private const val BRACKET_CHARS = "[]{}()<>"
private const val AMBIGUOUS_CHARS = "iI|lLoO01" 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.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager 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.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.getParcelableExtraCompat import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -164,7 +165,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
} }
} }
if (attachmentNotificationList.isEmpty()) { if (attachmentNotificationList.isEmpty()) {
stopSelf() stopService()
} }
} }
} }
@@ -194,7 +195,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
private fun newNotification(attachmentNotification: AttachmentNotification) { private fun newNotification(attachmentNotification: AttachmentNotification) {
val pendingContentIntent = PendingIntent.getActivity(this, val pendingContentIntent = PendingIntent.getActivity(this,
0, randomRequestCode(),
Intent().apply { Intent().apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
setDataAndType(attachmentNotification.uri, setDataAndType(attachmentNotification.uri,
@@ -208,7 +209,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
) )
val pendingDeleteIntent = PendingIntent.getService(this, val pendingDeleteIntent = PendingIntent.getService(this,
0, randomRequestCode(),
Intent(this, AttachmentFileNotificationService::class.java).apply { Intent(this, AttachmentFileNotificationService::class.java).apply {
// No action to delete the service // No action to delete the service
putExtra(FILE_URI_KEY, attachmentNotification.uri) putExtra(FILE_URI_KEY, attachmentNotification.uri)

View File

@@ -62,7 +62,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
sendBroadcast(Intent(LOCK_ACTION)) sendBroadcast(Intent(LOCK_ACTION))
} }
// Stop the service // Stop the service
stopSelf() stopService()
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {

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.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.hardware.HardwareKey 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.CipherEncryptDatabase
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.timeout.TimeoutHelper 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_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.LOCK_ACTION
@@ -175,7 +176,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
progressMessage: ProgressMessage progressMessage: ProgressMessage
) )
fun onActionStopped( fun onActionStopped(
database: ContextualDatabase database: ContextualDatabase? = null
) )
fun onActionFinished( fun onActionFinished(
database: ContextualDatabase, database: ContextualDatabase,
@@ -261,11 +262,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
) )
} }
} else { } else {
/* Do not stopped here, service cannot be connected
mActionTaskListeners.forEach { actionTaskListener -> mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onActionStopped( actionTaskListener.onActionStopped(
database database
) )
} }*/
} }
} }
} }
@@ -337,7 +339,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
val intentAction = intent?.action val intentAction = intent?.action
if (intentAction == null && !database.loaded) { if (intentAction == null && !database.loaded) {
stopSelf() stopService()
} }
val actionRunnable: ActionRunnable? = when (intentAction) { val actionRunnable: ActionRunnable? = when (intentAction) {
@@ -446,10 +448,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
TimeoutHelper.releaseTemporarilyDisableTimeout() TimeoutHelper.releaseTemporarilyDisableTimeout()
// Stop service after save if user remove task // Stop service after save if user remove task
if (save && mTaskRemovedRequested) { if (save && mTaskRemovedRequested) {
actionOnLock() stopService()
} else if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) { } else if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) {
if (!database.loaded) { if (!database.loaded) {
stopSelf() stopService()
} else { } else {
// Restart the service to open lock notification // Restart the service to open lock notification
try { try {
@@ -534,11 +536,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
val notificationBuilder = buildNewNotification().apply { val notificationBuilder = buildNewNotification().apply {
setSmallIcon(iconId) setSmallIcon(iconId)
intent?.let { val titleId = mProgressMessage.titleId?.let {
setContentTitle(getString( intent?.getIntExtra(DATABASE_TASK_TITLE_KEY, it)
intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mProgressMessage.titleId)) } ?: R.string.app_name
) setContentTitle(getString(titleId))
}
setAutoCancel(false) setAutoCancel(false)
setContentIntent(null) setContentIntent(null)
} }
@@ -550,7 +551,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
// Build Intents for notification action // Build Intents for notification action
val pendingDatabaseIntent = PendingIntent.getActivity( val pendingDatabaseIntent = PendingIntent.getActivity(
this, this,
0, randomRequestCode(),
Intent(this, GroupActivity::class.java), Intent(this, GroupActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
@@ -660,7 +661,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
} }
private fun updateMessage(resId: Int) { private fun updateMessage(resId: Int) {
mProgressMessage.messageId = resId mProgressMessage = mProgressMessage.copy(
messageId = resId
)
notifyProgressMessage() notifyProgressMessage()
} }
@@ -672,13 +675,19 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
updateMessage(R.string.decrypting_db) updateMessage(R.string.decrypting_db)
} }
override fun actionOnLock() { override fun stopService() {
if (!TimeoutHelper.temporarilyDisableLock) { if (!TimeoutHelper.temporarilyDisableLock) {
closeDatabase(mDatabase) 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) // Remove the lock timer (no more needed if it exists)
TimeoutHelper.cancelLockTimer(this) TimeoutHelper.cancelLockTimer(this)
// Service is stopped after receive the broadcast // Service is stopped after receive the broadcast
super.actionOnLock() super.stopService()
} }
} }
@@ -709,9 +718,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
notifyProgressMessage() notifyProgressMessage()
HardwareKeyActivity HardwareKeyActivity
.launchHardwareKeyActivity( .launchHardwareKeyActivity(
this@DatabaseTaskNotificationService, context = this@DatabaseTaskNotificationService,
hardwareKey, hardwareKey = hardwareKey,
seed seed = seed
) )
// Wait the response // Wait the response
mProgressMessage.apply { mProgressMessage.apply {
@@ -722,7 +731,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
// Close channels // Close channels
closeChallengeResponse() closeChallengeResponse()
// Restore previous message // Restore previous message
mProgressMessage = previousMessage mProgressMessage = previousMessage.apply {
cancelable = null
}
notifyProgressMessage() notifyProgressMessage()
} }
return response return response

View File

@@ -55,7 +55,7 @@ class KeyboardEntryNotificationService : LockNotificationService() {
sendBroadcast(Intent(LOCK_ACTION)) sendBroadcast(Intent(LOCK_ACTION))
} }
// Stop the service // Stop the service
stopSelf() stopService()
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.services package com.kunzisoft.keepass.services
import android.content.Intent import android.content.Intent
import androidx.core.app.ServiceCompat
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.LockReceiver import com.kunzisoft.keepass.utils.LockReceiver
import com.kunzisoft.keepass.utils.registerLockReceiver import com.kunzisoft.keepass.utils.registerLockReceiver
@@ -29,13 +28,7 @@ import com.kunzisoft.keepass.utils.unregisterLockReceiver
abstract class LockNotificationService : NotificationService() { abstract class LockNotificationService : NotificationService() {
private var mLockReceiver: LockReceiver = LockReceiver { private var mLockReceiver: LockReceiver = LockReceiver {
actionOnLock() stopService()
}
protected open fun actionOnLock() {
// Stop the service in all cases
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
stopSelf()
} }
override fun onCreate() { override fun onCreate() {
@@ -46,7 +39,7 @@ abstract class LockNotificationService : NotificationService() {
override fun onTaskRemoved(rootIntent: Intent?) { override fun onTaskRemoved(rootIntent: Intent?) {
if (!TimeoutHelper.temporarilyDisableLock) { if (!TimeoutHelper.temporarilyDisableLock) {
actionOnLock() stopService()
} }
super.onTaskRemoved(rootIntent) super.onTaskRemoved(rootIntent)
} }

View File

@@ -17,6 +17,7 @@ import android.util.TypedValue
import android.widget.Toast import android.widget.Toast
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.Stylish import com.kunzisoft.keepass.activities.stylish.Stylish
@@ -114,6 +115,12 @@ abstract class NotificationService : Service() {
} }
} }
protected open fun stopService() {
// Stop the service in all cases
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
stopSelf()
}
protected fun defineTimerJob(builder: NotificationCompat.Builder, protected fun defineTimerJob(builder: NotificationCompat.Builder,
type: NotificationServiceType, type: NotificationServiceType,
timeoutMilliseconds: Long, timeoutMilliseconds: Long,

View File

@@ -41,6 +41,11 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
autofillInlineSuggestionsPreference?.isVisible = false 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) { override fun onDisplayPreferenceDialog(preference: Preference) {

View File

@@ -13,6 +13,8 @@ abstract class ExternalSettingsActivity : DatabaseModeActivity() {
private var lockView: FloatingActionButton? = null private var lockView: FloatingActionButton? = null
override fun manageDatabaseInfo(): Boolean = true
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

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

View File

@@ -19,13 +19,21 @@
*/ */
package com.kunzisoft.keepass.settings package com.kunzisoft.keepass.settings
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.util.Log 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.core.view.MenuProvider
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import androidx.preference.TwoStatePreference 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.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm 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.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.settings.preference.* import com.kunzisoft.keepass.settings.preference.DialogColorPreference
import com.kunzisoft.keepass.settings.preferencedialogfragment.* 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.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.getParcelableCompat import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getSerializableCompat import com.kunzisoft.keepass.utils.getSerializableCompat
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval { class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels() 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 mDatabaseReadOnly: Boolean = false
private var mMergeDataAllowed: Boolean = false private var mMergeDataAllowed: Boolean = false
private var mDatabaseAutoSaveEnabled: Boolean = true 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
activity?.addMenuProvider(menuProvider, viewLifecycleOwner) activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
viewLifecycleOwner.lifecycleScope.launch {
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database -> mDatabaseViewModel.databaseState.collect { database ->
mDatabase = database view.resetAppTimeoutWhenViewTouchedOrFocused(
view.resetAppTimeoutWhenViewTouchedOrFocused(requireContext(), database?.loaded) context = requireContext(),
onDatabaseRetrieved(database) databaseLoaded = database?.loaded
} )
}
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) {
onDatabaseActionFinished(it.database, it.actionTask, it.result)
} }
} }
@@ -167,29 +223,26 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
mDatabaseViewModel.reloadDatabase(false) mDatabaseViewModel.reloadDatabase(false)
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
mDatabase = database mDatabaseReadOnly = database.isReadOnly
mDatabaseReadOnly = database?.isReadOnly == true mMergeDataAllowed = database.isMergeDataAllowed()
mMergeDataAllowed = database?.isMergeDataAllowed() == true
mDatabase?.let { if (database.loaded) {
if (it.loaded) { when (mScreen) {
when (mScreen) { Screen.DATABASE -> {
Screen.DATABASE -> { onCreateDatabasePreference(database)
onCreateDatabasePreference(it) }
} Screen.DATABASE_SECURITY -> {
Screen.DATABASE_SECURITY -> { onCreateDatabaseSecurityPreference(database)
onCreateDatabaseSecurityPreference(it) }
} Screen.DATABASE_MASTER_KEY -> {
Screen.DATABASE_MASTER_KEY -> { onCreateDatabaseMasterKeyPreference(database)
onCreateDatabaseMasterKeyPreference(it) }
} else -> {
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) { if (result.isSuccess) {
newDefaultUsername newDefaultUsername
} else { } else {
mDatabase?.defaultUsername = oldDefaultUsername database.defaultUsername = oldDefaultUsername
oldDefaultUsername oldDefaultUsername
} }
dbDefaultUsernamePref?.summary = defaultUsernameToShow dbDefaultUsernamePref?.summary = defaultUsernameToShow
@@ -471,7 +524,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newColor newColor
} else { } else {
mDatabase?.customColor = Color.parseColor(oldColor) database.customColor = oldColor.toColorInt()
oldColor oldColor
} }
dbCustomColorPref?.summary = defaultColorToShow dbCustomColorPref?.summary = defaultColorToShow
@@ -483,7 +536,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newCompression newCompression
} else { } else {
mDatabase?.compressionAlgorithm = oldCompression database.compressionAlgorithm = oldCompression
oldCompression oldCompression
} }
dbDataCompressionPref?.summary = algorithmToShow?.getLocalizedName(resources) dbDataCompressionPref?.summary = algorithmToShow?.getLocalizedName(resources)
@@ -497,7 +550,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} else { } else {
oldRecycleBin oldRecycleBin
} }
mDatabase?.setRecycleBin(recycleBinToShow) database.setRecycleBin(recycleBinToShow)
refreshRecycleBinGroup(database) refreshRecycleBinGroup(database)
} }
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK -> { DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK -> {
@@ -509,7 +562,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} else { } else {
oldTemplatesGroup oldTemplatesGroup
} }
mDatabase?.setTemplatesGroup(templatesGroupToShow) database.setTemplatesGroup(templatesGroupToShow)
refreshTemplatesGroup(database) refreshTemplatesGroup(database)
} }
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK -> { DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK -> {
@@ -519,7 +572,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newMaxHistoryItems newMaxHistoryItems
} else { } else {
mDatabase?.historyMaxItems = oldMaxHistoryItems database.historyMaxItems = oldMaxHistoryItems
oldMaxHistoryItems oldMaxHistoryItems
} }
dbMaxHistoryItemsPref?.summary = maxHistoryItemsToShow.toString() dbMaxHistoryItemsPref?.summary = maxHistoryItemsToShow.toString()
@@ -531,7 +584,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newMaxHistorySize newMaxHistorySize
} else { } else {
mDatabase?.historyMaxSize = oldMaxHistorySize database.historyMaxSize = oldMaxHistorySize
oldMaxHistorySize oldMaxHistorySize
} }
dbMaxHistorySizePref?.summary = maxHistorySizeToShow.toString() dbMaxHistorySizePref?.summary = maxHistorySizeToShow.toString()
@@ -549,7 +602,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newEncryption newEncryption
} else { } else {
mDatabase?.encryptionAlgorithm = oldEncryption database.encryptionAlgorithm = oldEncryption
oldEncryption oldEncryption
} }
mEncryptionAlgorithmPref?.summary = algorithmToShow.toString() mEncryptionAlgorithmPref?.summary = algorithmToShow.toString()
@@ -561,7 +614,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newKeyDerivationEngine newKeyDerivationEngine
} else { } else {
mDatabase?.kdfEngine = oldKeyDerivationEngine database.kdfEngine = oldKeyDerivationEngine
oldKeyDerivationEngine oldKeyDerivationEngine
} }
mKeyDerivationPref?.summary = kdfEngineToShow.toString() mKeyDerivationPref?.summary = kdfEngineToShow.toString()
@@ -578,7 +631,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newIterations newIterations
} else { } else {
mDatabase?.numberKeyEncryptionRounds = oldIterations database.numberKeyEncryptionRounds = oldIterations
oldIterations oldIterations
} }
mRoundPref?.summary = roundsToShow.toString() mRoundPref?.summary = roundsToShow.toString()
@@ -590,7 +643,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newMemoryUsage newMemoryUsage
} else { } else {
mDatabase?.memoryUsage = oldMemoryUsage database.memoryUsage = oldMemoryUsage
oldMemoryUsage oldMemoryUsage
} }
mMemoryPref?.summary = memoryToShow.toString() mMemoryPref?.summary = memoryToShow.toString()
@@ -602,7 +655,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) { if (result.isSuccess) {
newParallelism newParallelism
} else { } else {
mDatabase?.parallelism = oldParallelism database.parallelism = oldParallelism
oldParallelism oldParallelism
} }
mParallelismPref?.summary = parallelismToShow.toString() mParallelismPref?.summary = parallelismToShow.toString()

View File

@@ -108,7 +108,7 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.auto_focus_search_default)) context.resources.getBoolean(R.bool.auto_focus_search_default))
} }
fun searchSubdomains(context: Context): Boolean { fun searchSubDomains(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.subdomain_search_key), return prefs.getBoolean(context.getString(R.string.subdomain_search_key),
context.resources.getBoolean(R.bool.subdomain_search_default)) context.resources.getBoolean(R.bool.subdomain_search_default))
@@ -352,6 +352,8 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.search_option_username_default)) context.resources.getBoolean(R.bool.search_option_username_default))
searchInPasswords = prefs.getBoolean(context.getString(R.string.search_option_password_key), searchInPasswords = prefs.getBoolean(context.getString(R.string.search_option_password_key),
context.resources.getBoolean(R.bool.search_option_password_default)) 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), searchInUrls = prefs.getBoolean(context.getString(R.string.search_option_url_key),
context.resources.getBoolean(R.bool.search_option_url_default)) context.resources.getBoolean(R.bool.search_option_url_default))
searchInExpired = prefs.getBoolean(context.getString(R.string.search_option_expired_key), searchInExpired = prefs.getBoolean(context.getString(R.string.search_option_expired_key),
@@ -389,6 +391,8 @@ object PreferencesUtil {
searchParameters.searchInUsernames) searchParameters.searchInUsernames)
putBoolean(context.getString(R.string.search_option_password_key), putBoolean(context.getString(R.string.search_option_password_key),
searchParameters.searchInPasswords) searchParameters.searchInPasswords)
putBoolean(context.getString(R.string.search_option_application_id_key),
searchParameters.searchInAppIds)
putBoolean(context.getString(R.string.search_option_url_key), putBoolean(context.getString(R.string.search_option_url_key),
searchParameters.searchInUrls) searchParameters.searchInUrls)
putBoolean(context.getString(R.string.search_option_expired_key), putBoolean(context.getString(R.string.search_option_expired_key),
@@ -686,6 +690,12 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.keyboard_previous_lock_default)) 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 { fun isPasskeyBackupEligibilityEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.passkeys_backup_eligibility_key), 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_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_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.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_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_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean())

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,9 +32,8 @@ class DatabaseRoundsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialo
explanationText = getString(R.string.rounds_explanation) explanationText = getString(R.string.rounds_explanation)
} }
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database) inputText = database.numberKeyEncryptionRounds.toString()
inputText = database?.numberKeyEncryptionRounds?.toString() ?: MIN_ITERATIONS.toString()
} }
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) { 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.content.Context
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.activityViewModels 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.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.database.ContextualDatabase 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.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
abstract class DatabaseSavePreferenceDialogFragmentCompat abstract class DatabaseSavePreferenceDialogFragmentCompat
: InputPreferenceDialogFragmentCompat(), DatabaseRetrieval { : InputPreferenceDialogFragmentCompat(), DatabaseRetrieval {
private var mDatabaseAutoSaveEnable = true private var mDatabaseAutoSaveEnable = true
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels() private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private var mDatabase: ContextualDatabase? = null protected val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
@@ -47,18 +52,32 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
mDatabaseViewModel.database.observe(this) { database -> lifecycleScope.launch {
onDatabaseRetrieved(database) 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( override fun onDatabaseActionFinished(
@@ -77,8 +96,10 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
// To inherit to save element in database // To inherit to save element in database
} }
protected fun saveColor(oldColor: Int?, protected fun saveColor(
newColor: Int?) { oldColor: Int?,
newColor: Int?
) {
val oldColorString = if (oldColor != null) val oldColorString = if (oldColor != null)
ChromaUtil.getFormattedColorString(oldColor, false) ChromaUtil.getFormattedColorString(oldColor, false)
else else
@@ -87,77 +108,158 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
ChromaUtil.getFormattedColorString(newColor, false) ChromaUtil.getFormattedColorString(newColor, false)
else else
"" ""
mDatabaseViewModel.saveColor(oldColorString, newColorString, mDatabaseAutoSaveEnable) mDatabaseViewModel.saveColor(
oldColorString,
newColorString,
mDatabaseAutoSaveEnable
)
} }
protected fun saveCompression(oldCompression: CompressionAlgorithm, protected fun saveCompression(
newCompression: CompressionAlgorithm oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm
) { ) {
mDatabaseViewModel.saveCompression(oldCompression, newCompression, mDatabaseAutoSaveEnable) mDatabaseViewModel.saveCompression(
oldCompression,
newCompression,
mDatabaseAutoSaveEnable
)
} }
protected fun saveDefaultUsername(oldUsername: String, protected fun saveDefaultUsername(
newUsername: String) { oldUsername: String,
mDatabaseViewModel.saveDefaultUsername(oldUsername, newUsername, mDatabaseAutoSaveEnable) newUsername: String
) {
mDatabaseViewModel.saveDefaultUsername(
oldUsername,
newUsername,
mDatabaseAutoSaveEnable
)
} }
protected fun saveDescription(oldDescription: String, protected fun saveDescription(
newDescription: String) { oldDescription: String,
mDatabaseViewModel.saveDescription(oldDescription, newDescription, mDatabaseAutoSaveEnable) newDescription: String
) {
mDatabaseViewModel.saveDescription(
oldDescription,
newDescription,
mDatabaseAutoSaveEnable
)
} }
protected fun saveEncryption(oldEncryption: EncryptionAlgorithm, protected fun saveEncryption(
newEncryptionAlgorithm: EncryptionAlgorithm) { oldEncryption: EncryptionAlgorithm,
mDatabaseViewModel.saveEncryption(oldEncryption, newEncryptionAlgorithm, mDatabaseAutoSaveEnable) newEncryptionAlgorithm: EncryptionAlgorithm
) {
mDatabaseViewModel.saveEncryption(
oldEncryption,
newEncryptionAlgorithm,
mDatabaseAutoSaveEnable
)
} }
protected fun saveKeyDerivation(oldKeyDerivation: KdfEngine, protected fun saveKeyDerivation(
newKeyDerivation: KdfEngine) { oldKeyDerivation: KdfEngine,
mDatabaseViewModel.saveKeyDerivation(oldKeyDerivation, newKeyDerivation, mDatabaseAutoSaveEnable) newKeyDerivation: KdfEngine
) {
mDatabaseViewModel.saveKeyDerivation(
oldKeyDerivation,
newKeyDerivation,
mDatabaseAutoSaveEnable
)
} }
protected fun saveName(oldName: String, protected fun saveName(
newName: String) { oldName: String,
mDatabaseViewModel.saveName(oldName, newName, mDatabaseAutoSaveEnable) newName: String
) {
mDatabaseViewModel.saveName(
oldName,
newName,
mDatabaseAutoSaveEnable
)
} }
protected fun saveRecycleBin(oldGroup: Group?, protected fun saveRecycleBin(
newGroup: Group?) { oldGroup: Group?,
mDatabaseViewModel.saveRecycleBin(oldGroup, newGroup, mDatabaseAutoSaveEnable) newGroup: Group?
) {
mDatabaseViewModel.saveRecycleBin(
oldGroup,
newGroup,
mDatabaseAutoSaveEnable
)
} }
protected fun removeUnlinkedData() { protected fun removeUnlinkedData() {
mDatabaseViewModel.removeUnlinkedData(mDatabaseAutoSaveEnable) mDatabaseViewModel.removeUnlinkedData(mDatabaseAutoSaveEnable)
} }
protected fun saveTemplatesGroup(oldGroup: Group?, protected fun saveTemplatesGroup(
newGroup: Group?) { oldGroup: Group?,
mDatabaseViewModel.saveTemplatesGroup(oldGroup, newGroup, mDatabaseAutoSaveEnable) newGroup: Group?
) {
mDatabaseViewModel.saveTemplatesGroup(
oldGroup,
newGroup,
mDatabaseAutoSaveEnable
)
} }
protected fun saveMaxHistoryItems(oldNumber: Int, protected fun saveMaxHistoryItems(
newNumber: Int) { oldNumber: Int,
mDatabaseViewModel.saveMaxHistoryItems(oldNumber, newNumber, mDatabaseAutoSaveEnable) newNumber: Int
) {
mDatabaseViewModel.saveMaxHistoryItems(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
} }
protected fun saveMaxHistorySize(oldNumber: Long, protected fun saveMaxHistorySize(
newNumber: Long) { oldNumber: Long,
mDatabaseViewModel.saveMaxHistorySize(oldNumber, newNumber, mDatabaseAutoSaveEnable) newNumber: Long
) {
mDatabaseViewModel.saveMaxHistorySize(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
} }
protected fun saveMemoryUsage(oldNumber: Long, protected fun saveMemoryUsage(
newNumber: Long) { oldNumber: Long,
mDatabaseViewModel.saveMemoryUsage(oldNumber, newNumber, mDatabaseAutoSaveEnable) newNumber: Long
) {
mDatabaseViewModel.saveMemoryUsage(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
} }
protected fun saveParallelism(oldNumber: Long, protected fun saveParallelism(
newNumber: Long) { oldNumber: Long,
mDatabaseViewModel.saveParallelism(oldNumber, newNumber, mDatabaseAutoSaveEnable) newNumber: Long
) {
mDatabaseViewModel.saveParallelism(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
} }
protected fun saveIterations(oldNumber: Long, protected fun saveIterations(
newNumber: Long) { oldNumber: Long,
mDatabaseViewModel.saveIterations(oldNumber, newNumber, mDatabaseAutoSaveEnable) newNumber: Long
) {
mDatabaseViewModel.saveIterations(
oldNumber,
newNumber,
mDatabaseAutoSaveEnable
)
} }
companion object { companion object {

View File

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

View File

@@ -27,32 +27,27 @@ import android.view.View
import android.widget.Button import android.widget.Button
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
open class ProgressTaskDialogFragment : DialogFragment() { open class ProgressTaskDialogFragment : DialogFragment() {
@StringRes
private var title = UNDEFINED
@StringRes
private var message = UNDEFINED
@StringRes
private var warning = UNDEFINED
private var cancellable: (() -> Unit)? = null
private var titleView: TextView? = null private var titleView: TextView? = null
private var messageView: TextView? = null private var messageView: TextView? = null
private var warningView: TextView? = null private var warningView: TextView? = null
private var cancelButton: Button? = null private var cancelButton: Button? = null
private var progressView: ProgressBar? = null private var progressView: ProgressBar? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { private val progressTaskViewModel: ProgressTaskViewModel by activityViewModels()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
try { try {
activity?.let { activity?.let {
val builder = AlertDialog.Builder(it) val builder = AlertDialog.Builder(it)
@@ -71,68 +66,63 @@ open class ProgressTaskDialogFragment : DialogFragment() {
cancelButton = root.findViewById(R.id.progress_dialog_cancel) cancelButton = root.findViewById(R.id.progress_dialog_cancel)
progressView = root.findViewById(R.id.progress_dialog_bar) progressView = root.findViewById(R.id.progress_dialog_bar)
updateTitle(title)
updateMessage(message)
updateWarning(warning)
setCancellable(cancellable)
isCancelable = false isCancelable = false
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
progressTaskViewModel.progressTaskState.collect { state ->
when (state) {
is ProgressTaskViewModel.ProgressTaskState.Show -> {
val value = state.value
updateView(
titleView,
value.titleId?.let { title ->
getString(title)
})
updateView(
messageView,
value.messageId?.let { message ->
getString(message)
})
updateView(
warningView,
value.warningId?.let { warning ->
getString(warning)
})
cancelButton?.apply {
isVisible = value.cancelable != null
setOnClickListener {
value.cancelable?.invoke()
}
}
}
else -> {
// Nothing here, this fragment is stopped externally
}
}
}
}
}
return builder.create() return builder.create()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to create progress dialog") Log.e(TAG, "Unable to create progress dialog", e)
} }
return super.onCreateDialog(savedInstanceState) return super.onCreateDialog(savedInstanceState)
} }
fun setTitle(@StringRes titleId: Int) { private fun updateView(textView: TextView?, value: String?) {
this.title = titleId if (value == null) {
} textView?.visibility = View.GONE
} else {
private fun updateView(textView: TextView?, @StringRes resId: Int) { textView?.text = value
activity?.lifecycleScope?.launch { textView?.visibility = View.VISIBLE
if (resId == UNDEFINED) {
textView?.visibility = View.GONE
} else {
textView?.setText(resId)
textView?.visibility = View.VISIBLE
}
} }
} }
private fun updateCancelable() {
activity?.lifecycleScope?.launch {
cancelButton?.isVisible = cancellable != null
cancelButton?.setOnClickListener {
cancellable?.invoke()
}
}
}
fun updateTitle(@StringRes resId: Int?) {
this.title = resId ?: UNDEFINED
updateView(titleView, title)
}
fun updateMessage(@StringRes resId: Int?) {
this.message = resId ?: UNDEFINED
updateView(messageView, message)
}
fun updateWarning(@StringRes resId: Int?) {
this.warning = resId ?: UNDEFINED
updateView(warningView, warning)
}
fun setCancellable(cancellable: (() -> Unit)?) {
this.cancellable = cancellable
updateCancelable()
}
companion object { companion object {
private val TAG = ProgressTaskDialogFragment::class.java.simpleName private val TAG = ProgressTaskDialogFragment::class.java.simpleName
const val PROGRESS_TASK_DIALOG_TAG = "progressDialogFragment" const val PROGRESS_TASK_DIALOG_TAG = "progressDialogFragment"
const val UNDEFINED = -1
} }
} }

View File

@@ -0,0 +1,28 @@
package com.kunzisoft.keepass.tasks
import androidx.lifecycle.ViewModel
import com.kunzisoft.keepass.database.ProgressMessage
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
class ProgressTaskViewModel: ViewModel() {
private val mProgressTaskState = MutableStateFlow<ProgressTaskState>(ProgressTaskState.Hide)
val progressTaskState: StateFlow<ProgressTaskState> = mProgressTaskState
fun show(value: ProgressMessage) {
mProgressTaskState.update { currentState ->
ProgressTaskState.Show(value)
}
}
fun hide() {
mProgressTaskState.value = ProgressTaskState.Hide
}
sealed class ProgressTaskState {
data class Show(val value: ProgressMessage): ProgressTaskState()
object Hide: ProgressTaskState()
}
}

View File

@@ -13,15 +13,13 @@ import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
import com.kunzisoft.keepass.education.Education 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 { object AppUtil {
fun randomRequestCode(): Int {
return (Math.random() * Integer.MAX_VALUE).toInt()
}
fun Context.isExternalAppInstalled(packageName: String, showError: Boolean = true): Boolean { fun Context.isExternalAppInstalled(packageName: String, showError: Boolean = true): Boolean {
try { try {
this.applicationContext.packageManager.getPackageInfoCompat( 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) @RequiresApi(Build.VERSION_CODES.P)
fun getInstalledBrowsersWithSignatures(context: Context): List<AndroidPrivilegedApp> { fun getInstalledBrowsersWithSignatures(context: Context): List<AndroidPrivilegedApp> {
val packageManager = context.packageManager val packageManager = context.packageManager
@@ -123,25 +98,43 @@ object AppUtil {
} }
val processedPackageNames = mutableSetOf<String>() val processedPackageNames = mutableSetOf<String>()
for (resolveInfo in resolveInfoList) { for (resolveInfo in resolveInfoList) {
val packageName = resolveInfo.activityInfo.packageName val packageName = resolveInfo.activityInfo.packageName
if (packageName != null && !processedPackageNames.contains(packageName)) { if (packageName != null && !processedPackageNames.contains(packageName)) {
try { buildAndroidPrivilegedApp(packageManager, packageName)?.let { privilegedApp ->
val packageInfo = packageManager.getPackageInfo( browserList.add(privilegedApp)
packageName, processedPackageNames.add(packageName)
PackageManager.GET_SIGNING_CERTIFICATES
)
val signatureFingerprints = packageInfo.signingInfo?.getAllFingerprints()
signatureFingerprints?.let {
browserList.add(AndroidPrivilegedApp(packageName, signatureFingerprints))
processedPackageNames.add(packageName)
}
} catch (e: Exception) {
Log.e(AppUtil::class.simpleName, "Error processing package: $packageName", e)
} }
} }
} }
// Add the Play Service
val gServices = "com.google.android.gms"
buildAndroidPrivilegedApp(packageManager, gServices)?.let { privilegedApp ->
browserList.add(privilegedApp)
processedPackageNames.add(gServices)
}
return browserList.distinctBy { it.packageName } // Ensure uniqueness just in case return browserList.distinctBy { it.packageName } // Ensure uniqueness just in case
} }
@RequiresApi(Build.VERSION_CODES.P)
private fun buildAndroidPrivilegedApp(
packageManager: PackageManager,
packageName: String
): AndroidPrivilegedApp? {
return try {
val packageInfo = packageManager.getPackageInfo(
packageName,
PackageManager.GET_SIGNING_CERTIFICATES
)
val signatureFingerprints = packageInfo.signingInfo?.getAllFingerprints()
signatureFingerprints?.let {
AndroidPrivilegedApp(packageName, signatureFingerprints)
}
} catch (e: Exception) {
Log.e(AppUtil::class.simpleName, "Error processing package: $packageName", e)
null
}
}
} }

View File

@@ -19,30 +19,39 @@
*/ */
package com.kunzisoft.keepass.utils 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 to invoke action in a separate IO thread
*/ */
class IOActionTask<T>( class IOActionTask<T>(
private val action: () -> T , private val action: () -> T,
private val afterActionListener: ((T?) -> Unit)? = null) { private val onActionComplete: ((T?) -> Unit)? = null,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main),
private val mainScope = CoroutineScope(Dispatchers.Main) private val exceptionHandler: CoroutineExceptionHandler? = null
) {
fun execute() { fun execute() {
mainScope.launch { scope.launch(exceptionHandler ?: EmptyCoroutineContext) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val asyncResult: Deferred<T?> = async { val asyncResult: Deferred<T?> = async {
try { exceptionHandler?.let {
action.invoke() action.invoke()
} catch (e: Exception) { } ?: try {
e.printStackTrace() action.invoke()
null } catch (e: Exception) {
} e.printStackTrace()
null
}
} }
withContext(Dispatchers.Main) { 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.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.CompoundButton import android.widget.CompoundButton
import android.widget.ImageView import android.widget.ImageView
@@ -30,8 +29,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
private var searchTitle: CompoundButton private var searchTitle: CompoundButton
private var searchUsername: CompoundButton private var searchUsername: CompoundButton
private var searchPassword: CompoundButton private var searchPassword: CompoundButton
private var searchApplicationId: CompoundButton
private var searchURL: CompoundButton private var searchURL: CompoundButton
private var searchByURLDomain: Boolean = false private var searchByURLDomain: Boolean = false
private var searchByURLSubDomain: Boolean = false
private var searchExpired: CompoundButton private var searchExpired: CompoundButton
private var searchNotes: CompoundButton private var searchNotes: CompoundButton
private var searchOther: CompoundButton private var searchOther: CompoundButton
@@ -50,8 +51,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
this.searchInTitles = searchTitle.isChecked this.searchInTitles = searchTitle.isChecked
this.searchInUsernames = searchUsername.isChecked this.searchInUsernames = searchUsername.isChecked
this.searchInPasswords = searchPassword.isChecked this.searchInPasswords = searchPassword.isChecked
this.searchInAppIds = searchApplicationId.isChecked
this.searchInUrls = searchURL.isChecked this.searchInUrls = searchURL.isChecked
this.searchByDomain = searchByURLDomain this.searchByDomain = searchByURLDomain
this.searchBySubDomain = searchByURLSubDomain
this.searchInExpired = searchExpired.isChecked this.searchInExpired = searchExpired.isChecked
this.searchInNotes = searchNotes.isChecked this.searchInNotes = searchNotes.isChecked
this.searchInOther = searchOther.isChecked this.searchInOther = searchOther.isChecked
@@ -71,8 +74,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchTitle.isChecked = value.searchInTitles searchTitle.isChecked = value.searchInTitles
searchUsername.isChecked = value.searchInUsernames searchUsername.isChecked = value.searchInUsernames
searchPassword.isChecked = value.searchInPasswords searchPassword.isChecked = value.searchInPasswords
searchApplicationId.isChecked = value.searchInAppIds
searchURL.isChecked = value.searchInUrls searchURL.isChecked = value.searchInUrls
searchByURLDomain = value.searchByDomain searchByURLDomain = value.searchByDomain
searchByURLSubDomain = value.searchBySubDomain
searchExpired.isChecked = value.searchInExpired searchExpired.isChecked = value.searchInExpired
searchNotes.isChecked = value.searchInNotes searchNotes.isChecked = value.searchInNotes
searchOther.isChecked = value.searchInOther searchOther.isChecked = value.searchInOther
@@ -87,7 +92,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
var onParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = null var onParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = null
private var mOnParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = { private var mOnParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = {
// To recalculate height // To recalculate height
if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) { if (searchAdvanceFiltersContainer?.visibility == VISIBLE) {
searchAdvanceFiltersContainer?.expand( searchAdvanceFiltersContainer?.expand(
false, false,
searchAdvanceFiltersContainer?.getFullHeight() searchAdvanceFiltersContainer?.getFullHeight()
@@ -110,6 +115,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchTitle = findViewById(R.id.search_chip_title) searchTitle = findViewById(R.id.search_chip_title)
searchUsername = findViewById(R.id.search_chip_username) searchUsername = findViewById(R.id.search_chip_username)
searchPassword = findViewById(R.id.search_chip_password) searchPassword = findViewById(R.id.search_chip_password)
searchApplicationId = findViewById(R.id.search_chip_application_id)
searchURL = findViewById(R.id.search_chip_url) searchURL = findViewById(R.id.search_chip_url)
searchExpired = findViewById(R.id.search_chip_expires) searchExpired = findViewById(R.id.search_chip_expires)
searchNotes = findViewById(R.id.search_chip_note) searchNotes = findViewById(R.id.search_chip_note)
@@ -125,7 +131,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
// Expand menu with button // Expand menu with button
searchExpandButton.setOnClickListener { searchExpandButton.setOnClickListener {
val isVisible = searchAdvanceFiltersContainer?.visibility == View.VISIBLE val isVisible = searchAdvanceFiltersContainer?.visibility == VISIBLE
if (isVisible) if (isVisible)
closeAdvancedFilters() closeAdvancedFilters()
else else
@@ -156,6 +162,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchParameters.searchInPasswords = isChecked searchParameters.searchInPasswords = isChecked
mOnParametersChangeListener?.invoke(searchParameters) mOnParametersChangeListener?.invoke(searchParameters)
} }
searchApplicationId.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInAppIds = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchURL.setOnCheckedChangeListener { _, isChecked -> searchURL.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInUrls = isChecked searchParameters.searchInUrls = isChecked
mOnParametersChangeListener?.invoke(searchParameters) mOnParametersChangeListener?.invoke(searchParameters)
@@ -200,10 +210,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchNumbers.text = SearchHelper.showNumberOfSearchResults(numbers) searchNumbers.text = SearchHelper.showNumberOfSearchResults(numbers)
} }
fun setCurrentGroupText(text: String) { fun setCurrentGroupText(text: String?) {
val maxChars = 12 val maxChars = 12
searchCurrentGroup.text = when { 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) + "" text.length > maxChars -> text.substring(0, maxChars) + ""
else -> text else -> text
} }
@@ -213,6 +223,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchOther.isVisible = available searchOther.isVisible = available
} }
fun availableApplicationIds(available: Boolean) {
searchApplicationId.isVisible = available
}
fun availableTags(available: Boolean) { fun availableTags(available: Boolean) {
searchTag.isVisible = available 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) { override fun setVisibility(visibility: Int) {
when (visibility) { when (visibility) {
View.VISIBLE -> { VISIBLE -> {
searchAdvanceFiltersContainer?.visibility = View.GONE searchAdvanceFiltersContainer?.visibility = GONE
searchContainer.showByFading() searchContainer.showByFading()
} }
else -> { else -> {
searchContainer.hideByFading() searchContainer.hideByFading()
if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) { if (searchAdvanceFiltersContainer?.visibility == VISIBLE) {
searchAdvanceFiltersContainer?.visibility = View.INVISIBLE searchAdvanceFiltersContainer?.visibility = INVISIBLE
searchAdvanceFiltersContainer?.collapse() searchAdvanceFiltersContainer?.collapse()
} }
} }

View File

@@ -24,7 +24,6 @@ import android.animation.AnimatorSet
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.PorterDuff 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.database.helper.getLocalizedMessage
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import java.util.EnumSet
/** /**
@@ -317,9 +317,7 @@ fun CollapsingToolbarLayout.changeTitleColor(color: Int) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, applyWindowInsets: () -> Unit) { fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, applyWindowInsets: () -> Unit) {
// Only in portrait if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1
&& resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector) window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector)
if (applyToStatusBar) { if (applyToStatusBar) {
@@ -335,7 +333,7 @@ fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, appl
/** /**
* Apply a margin to a view to fix the window inset * 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 -> ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
var consumed = false var consumed = false
@@ -351,52 +349,78 @@ fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.B
} }
} }
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()
when (position) { or WindowInsetsCompat.Type.displayCutout()
WindowInsetPosition.TOP -> { or WindowInsetsCompat.Type.ime())
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> { val isRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL
topMargin = insets.top
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
} }
} }
} if (wantEndMargins) {
WindowInsetPosition.LEGIT_TOP -> { if (isRtl) {
if (view.layoutParams is ViewGroup.MarginLayoutParams) { leftMargin = insets.left
view.updateLayoutParams<ViewGroup.MarginLayoutParams> { } else {
topMargin = 0 rightMargin = insets.right
}
}
}
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
} }
} }
} }
} }
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 any of the children consumed the insets, return an appropriate value
if (consumed) WindowInsetsCompat.CONSUMED else windowInsets if (consumed) WindowInsetsCompat.CONSUMED else windowInsets
} }
} }
enum class WindowInsetPosition { 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 package com.kunzisoft.keepass.viewmodels
import androidx.lifecycle.LiveData import android.app.Application
import androidx.lifecycle.MutableLiveData import android.net.Uri
import androidx.lifecycle.ViewModel import android.os.Bundle
import androidx.lifecycle.AndroidViewModel
import com.kunzisoft.keepass.database.ContextualDatabase 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.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine 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.Group
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm 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 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 mDatabaseState = MutableStateFlow<ContextualDatabase?>(null)
private val _database = MutableLiveData<ContextualDatabase?>() val databaseState: StateFlow<ContextualDatabase?> = mDatabaseState
val actionFinished : LiveData<ActionResult> get() = _actionFinished val database: ContextualDatabase?
private val _actionFinished = SingleLiveEvent<ActionResult>() get() = databaseState.value
val saveDatabase : LiveData<Boolean> get() = _saveDatabase private val mActionState = MutableStateFlow<ActionState>(ActionState.Wait)
private val _saveDatabase = SingleLiveEvent<Boolean>() val actionState: StateFlow<ActionState> = mActionState
val mergeDatabase : LiveData<Boolean> get() = _mergeDatabase private var mDatabaseTaskProvider: DatabaseTaskProvider = DatabaseTaskProvider(
private val _mergeDatabase = SingleLiveEvent<Boolean>() context = application
)
val reloadDatabase : LiveData<Boolean> get() = _reloadDatabase init {
private val _reloadDatabase = SingleLiveEvent<Boolean>() 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 override fun onActionUpdated(
private val _saveName = SingleLiveEvent<SuperString>() database: ContextualDatabase,
progressMessage: ProgressMessage
) {
mActionState.value = ActionState.OnDatabaseActionUpdated(database, progressMessage)
}
val saveDescription : LiveData<SuperString> get() = _saveDescription override fun onActionStopped(database: ContextualDatabase?) {
private val _saveDescription = SingleLiveEvent<SuperString>() mActionState.value = ActionState.OnDatabaseActionStopped(database)
}
val saveDefaultUsername : LiveData<SuperString> get() = _saveDefaultUsername override fun onActionFinished(
private val _saveDefaultUsername = SingleLiveEvent<SuperString>() database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
mActionState.value = ActionState.OnDatabaseActionFinished(database, actionTask, result)
}
}
val saveColor : LiveData<SuperString> get() = _saveColor mDatabaseTaskProvider.registerProgressTask()
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
} }
fun onActionFinished(database: ContextualDatabase, /*
actionTask: String, * Main database actions
result: ActionRunnable.Result) { */
this._actionFinished.value = ActionResult(database, actionTask, result)
fun loadDatabase(
databaseUri: Uri,
mainCredential: MainCredential,
readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean
) {
mDatabaseTaskProvider.startDatabaseLoad(
databaseUri,
mainCredential,
readOnly,
cipherEncryptDatabase,
fixDuplicateUuid
)
} }
fun saveDatabase(save: Boolean) { fun createDatabase(
_saveDatabase.value = save databaseUri: Uri,
mainCredential: MainCredential
) {
mDatabaseTaskProvider.startDatabaseCreate(databaseUri, mainCredential)
} }
fun mergeDatabase(save: Boolean) { fun assignMainCredential(
_mergeDatabase.value = save 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) { fun reloadDatabase(fixDuplicateUuid: Boolean) {
_reloadDatabase.value = fixDuplicateUuid mDatabaseTaskProvider.askToStartDatabaseReload(
conditionToAsk = database?.dataModifiedSinceLastLoading != false
) {
mDatabaseTaskProvider.startDatabaseReload(fixDuplicateUuid)
}
} }
fun saveName(oldValue: String, fun onDatabaseChangeValidated() {
newValue: String, mDatabaseTaskProvider.onDatabaseChangeValidated()
save: Boolean) {
_saveName.value = SuperString(oldValue, newValue, save)
} }
fun saveDescription(oldValue: String, /*
newValue: String, * Nodes actions
save: Boolean) { */
_saveDescription.value = SuperString(oldValue, newValue, save)
fun createEntry(
newEntry: Entry,
parent: Group,
save: Boolean
) {
mDatabaseTaskProvider.startDatabaseCreateEntry(
newEntry,
parent,
save
)
} }
fun saveDefaultUsername(oldValue: String, fun updateEntry(
newValue: String, oldEntry: Entry,
save: Boolean) { entryToUpdate: Entry,
_saveDefaultUsername.value = SuperString(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseUpdateEntry(
oldEntry,
entryToUpdate,
save
)
} }
fun saveColor(oldValue: String, fun restoreEntryHistory(
newValue: String, mainEntryId: NodeId<UUID>,
save: Boolean) { entryHistoryPosition: Int,
_saveColor.value = SuperString(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseRestoreEntryHistory(
mainEntryId,
entryHistoryPosition,
save
)
} }
fun saveCompression(oldValue: CompressionAlgorithm, fun deleteEntryHistory(
newValue: CompressionAlgorithm, mainEntryId: NodeId<UUID>,
save: Boolean) { entryHistoryPosition: Int,
_saveCompression.value = SuperCompression(oldValue, newValue, save) 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) { fun removeUnlinkedData(save: Boolean) {
_removeUnlinkData.value = save mDatabaseTaskProvider.startDatabaseRemoveUnlinkedData(save)
} }
fun saveRecycleBin(oldValue: Group?, fun saveRecycleBin(
newValue: Group?, oldValue: Group?,
save: Boolean) { newValue: Group?,
_saveRecycleBin.value = SuperGroup(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveRecycleBin(
oldValue,
newValue,
save
)
} }
fun saveTemplatesGroup(oldValue: Group?, fun saveTemplatesGroup(
newValue: Group?, oldValue: Group?,
save: Boolean) { newValue: Group?,
_saveTemplatesGroup.value = SuperGroup(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveTemplatesGroup(
oldValue,
newValue,
save
)
} }
fun saveMaxHistoryItems(oldValue: Int, fun saveMaxHistoryItems(
newValue: Int, oldValue: Int,
save: Boolean) { newValue: Int,
_saveMaxHistoryItems.value = SuperInt(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveMaxHistoryItems(
oldValue,
newValue,
save
)
} }
fun saveMaxHistorySize(oldValue: Long, fun saveMaxHistorySize(
newValue: Long, oldValue: Long,
save: Boolean) { newValue: Long,
_saveMaxHistorySize.value = SuperLong(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveMaxHistorySize(
oldValue,
newValue,
save
)
} }
fun saveEncryption(oldValue: EncryptionAlgorithm, fun saveEncryption(
newValue: EncryptionAlgorithm, oldValue: EncryptionAlgorithm,
save: Boolean) { newValue: EncryptionAlgorithm,
_saveEncryption.value = SuperEncryption(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveEncryption(
oldValue,
newValue,
save
)
} }
fun saveKeyDerivation(oldValue: KdfEngine, fun saveKeyDerivation(
newValue: KdfEngine, oldValue: KdfEngine,
save: Boolean) { newValue: KdfEngine,
_saveKeyDerivation.value = SuperKeyDerivation(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveKeyDerivation(
oldValue,
newValue,
save
)
} }
fun saveIterations(oldValue: Long, fun saveIterations(
newValue: Long, oldValue: Long,
save: Boolean) { newValue: Long,
_saveIterations.value = SuperLong(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveIterations(
oldValue,
newValue,
save
)
} }
fun saveMemoryUsage(oldValue: Long, fun saveMemoryUsage(
newValue: Long, oldValue: Long,
save: Boolean) { newValue: Long,
_saveMemoryUsage.value = SuperLong(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveMemoryUsage(
oldValue,
newValue,
save
)
} }
fun saveParallelism(oldValue: Long, fun saveParallelism(
newValue: Long, oldValue: Long,
save: Boolean) { newValue: Long,
_saveParallelism.value = SuperLong(oldValue, newValue, save) save: Boolean
) {
mDatabaseTaskProvider.startDatabaseSaveParallelism(
oldValue,
newValue,
save
)
} }
data class ActionResult(val database: ContextualDatabase, /*
val actionTask: String, * Hardware Key
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)
fun onChallengeResponded(challengeResponse: ByteArray?) {
mDatabaseTaskProvider.startChallengeResponded(
challengeResponse ?: ByteArray(0)
)
}
override fun onCleared() {
super.onCleared()
mDatabaseTaskProvider.unregisterProgressTask()
mDatabaseTaskProvider.destroy()
}
sealed class ActionState {
object Wait: 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 android.net.Uri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Entry 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.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.model.StreamDirection import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.utils.IOActionTask import com.kunzisoft.keepass.utils.IOActionTask
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.UUID import java.util.UUID
@@ -28,12 +30,18 @@ class EntryEditViewModel: NodeEditViewModel() {
private var mEntryId: NodeId<UUID>? = null private var mEntryId: NodeId<UUID>? = null
private var mParentId: NodeId<*>? = null private var mParentId: NodeId<*>? = null
private var mRegisterInfo: RegisterInfo? = null private var mRegisterInfo: RegisterInfo? = null
private var mSearchInfo: SearchInfo? = null
private var mParent: Group? = null private var mParent: Group? = null
private var mEntry: Entry? = null private var mEntry: Entry? = null
private var mIsTemplate: Boolean = false private var mIsTemplate: Boolean = false
private val mTempAttachments = mutableListOf<EntryAttachmentState>() 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 val templatesEntry : LiveData<TemplatesEntry?> get() = _templatesEntry
private val _templatesEntry = MutableLiveData<TemplatesEntry?>() private val _templatesEntry = MutableLiveData<TemplatesEntry?>()
@@ -73,24 +81,28 @@ class EntryEditViewModel: NodeEditViewModel() {
val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded
private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>() private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>()
fun loadDatabase(database: ContextualDatabase?) { private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo, mSearchInfo) val uiState: StateFlow<UIState> = mUiState
fun loadTemplateEntry(database: ContextualDatabase?) {
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo)
} }
fun loadTemplateEntry(database: ContextualDatabase?, fun loadTemplateEntry(
entryId: NodeId<UUID>?, database: ContextualDatabase?,
parentId: NodeId<*>?, entryId: NodeId<UUID>?,
registerInfo: RegisterInfo?, parentId: NodeId<*>?,
searchInfo: SearchInfo?) { registerInfo: RegisterInfo?
) {
this.mEntryId = entryId this.mEntryId = entryId
this.mParentId = parentId this.mParentId = parentId
this.mRegisterInfo = registerInfo this.mRegisterInfo = registerInfo
this.mSearchInfo = searchInfo
database?.let { database?.let {
mEntryId?.let { mEntryId?.let {
IOActionTask( IOActionTask(
{ scope = viewModelScope,
action = {
// Create an Entry copy to modify from the database entry // Create an Entry copy to modify from the database entry
mEntry = database.getEntryById(it) mEntry = database.getEntryById(it)
// Retrieve the parent // Retrieve the parent
@@ -105,21 +117,24 @@ class EntryEditViewModel: NodeEditViewModel() {
database, database,
entry, entry,
mIsTemplate, mIsTemplate,
registerInfo, registerInfo
searchInfo
) )
} }
}, },
{ templatesEntry -> onActionComplete = { templatesEntry ->
mEntryId = null mEntryId = null
_templatesEntry.value = templatesEntry _templatesEntry.value = templatesEntry
if (templatesEntry?.overwrittenData == true) {
mUiState.value = UIState.ShowOverwriteMessage
}
} }
).execute() ).execute()
} }
mParentId?.let { mParentId?.let {
IOActionTask( IOActionTask(
{ scope = viewModelScope,
action = {
mParent = database.getGroupById(it) mParent = database.getGroupById(it)
mParent?.let { parentGroup -> mParent?.let { parentGroup ->
mEntry = database.createEntry()?.apply { mEntry = database.createEntry()?.apply {
@@ -145,12 +160,11 @@ class EntryEditViewModel: NodeEditViewModel() {
database, database,
mEntry, mEntry,
mIsTemplate, mIsTemplate,
registerInfo, registerInfo
searchInfo
) )
} }
}, },
{ templatesEntry -> onActionComplete = { templatesEntry ->
mParentId = null mParentId = null
_templatesEntry.value = templatesEntry _templatesEntry.value = templatesEntry
} }
@@ -159,33 +173,37 @@ class EntryEditViewModel: NodeEditViewModel() {
} }
} }
private fun decodeTemplateEntry(database: ContextualDatabase, private fun decodeTemplateEntry(
entry: Entry?, database: ContextualDatabase,
isTemplate: Boolean, entry: Entry?,
registerInfo: RegisterInfo?, isTemplate: Boolean,
searchInfo: SearchInfo?): TemplatesEntry { registerInfo: RegisterInfo?
): TemplatesEntry {
val templates = database.getTemplates(isTemplate) val templates = database.getTemplates(isTemplate)
val entryTemplate = entry?.let { database.getTemplate(it) } val entryTemplate = entry?.let { database.getTemplate(it) }
?: Template.STANDARD ?: Template.STANDARD
var entryInfo: EntryInfo? = null var entryInfo: EntryInfo? = null
var overwrittenData = false
// Decode the entry / load entry info // Decode the entry / load entry info
entry?.let { entry?.let {
database.decodeEntryWithTemplateConfiguration(it).let { entry -> database.decodeEntryWithTemplateConfiguration(it).let { entry ->
// Load entry info // Load entry info
entry.getEntryInfo(database, true).let { tempEntryInfo -> entry.getEntryInfo(database, true).let { tempEntryInfo ->
// Retrieve data from registration // Retrieve data from registration
// TODO only save registration
searchInfo?.let { tempSearchInfo ->
tempEntryInfo.saveSearchInfo(database, tempSearchInfo)
}
registerInfo?.let { regInfo -> registerInfo?.let { regInfo ->
tempEntryInfo.saveRegisterInfo(database, regInfo) overwrittenData = tempEntryInfo.saveRegisterInfo(database, regInfo)
} }
entryInfo = tempEntryInfo entryInfo = tempEntryInfo
} }
} }
} }
return TemplatesEntry(isTemplate, templates, entryTemplate, entryInfo) return TemplatesEntry(
isTemplate,
templates,
entryTemplate,
entryInfo,
overwrittenData
)
} }
fun changeTemplate(template: Template) { fun changeTemplate(template: Template) {
@@ -198,44 +216,52 @@ class EntryEditViewModel: NodeEditViewModel() {
_requestEntryInfoUpdate.value = EntryUpdate(database, mEntry, mParent) _requestEntryInfoUpdate.value = EntryUpdate(database, mEntry, mParent)
} }
fun unlockAction() {
actionLocked = false
}
fun saveEntryInfo(database: ContextualDatabase?, entry: Entry?, parent: Group?, entryInfo: EntryInfo) { fun saveEntryInfo(database: ContextualDatabase?, entry: Entry?, parent: Group?, entryInfo: EntryInfo) {
IOActionTask( if (actionLocked.not()) {
{ actionLocked = true
removeTempAttachmentsNotCompleted(entryInfo) IOActionTask(
entry?.let { oldEntry -> scope = viewModelScope,
// Create a clone action = {
var newEntry = Entry(oldEntry) removeTempAttachmentsNotCompleted(entryInfo)
entry?.let { oldEntry ->
// Create a clone
var newEntry = Entry(oldEntry)
// Build info // Build info
newEntry.setEntryInfo(database, entryInfo) newEntry.setEntryInfo(database, entryInfo)
// Encode entry properties for template // Encode entry properties for template
_onTemplateChanged.value?.let { template -> _onTemplateChanged.value?.let { template ->
newEntry = newEntry =
database?.encodeEntryWithTemplateConfiguration(newEntry, template) database?.encodeEntryWithTemplateConfiguration(newEntry, template)
?: newEntry ?: newEntry
} }
// Delete temp attachment if not used // Delete temp attachment if not used
mTempAttachments.forEach { tempAttachmentState -> mTempAttachments.forEach { tempAttachmentState ->
val tempAttachment = tempAttachmentState.attachment val tempAttachment = tempAttachmentState.attachment
database?.attachmentPool?.let { binaryPool -> database?.attachmentPool?.let { binaryPool ->
if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) { if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) {
database.removeAttachmentIfNotUsed(tempAttachment) database.removeAttachmentIfNotUsed(tempAttachment)
}
} }
} }
}
// Return entry to save // Return entry to save
EntrySave(oldEntry, newEntry, parent) EntrySave(oldEntry, newEntry, parent)
}
},
onActionComplete = { entrySave ->
entrySave?.let {
_onEntrySaved.value = it
}
} }
}, ).execute()
{ entrySave -> }
entrySave?.let {
_onEntrySaved.value = it
}
}
).execute()
} }
private fun removeTempAttachmentsNotCompleted(entryInfo: EntryInfo) { private fun removeTempAttachmentsNotCompleted(entryInfo: EntryInfo) {
@@ -322,10 +348,13 @@ class EntryEditViewModel: NodeEditViewModel() {
_onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition) _onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition)
} }
data class TemplatesEntry(val isTemplate: Boolean, data class TemplatesEntry(
val templates: List<Template>, val isTemplate: Boolean,
val defaultTemplate: Template, val templates: List<Template>,
val entryInfo: EntryInfo?) val defaultTemplate: Template,
val entryInfo: EntryInfo?,
val overwrittenData: Boolean = false
)
data class EntryUpdate(val database: ContextualDatabase?, val entry: Entry?, val parent: Group?) 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 EntrySave(val oldEntry: Entry, val newEntry: Entry, val parent: Group?)
data class FieldEdition(val oldField: Field?, val newField: Field?) 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 AttachmentUpload(val attachmentToUploadUri: Uri, val attachment: Attachment)
data class AttachmentPosition(val entryAttachmentState: EntryAttachmentState, val viewPosition: Float) data class AttachmentPosition(val entryAttachmentState: EntryAttachmentState, val viewPosition: Float)
sealed class UIState {
object Loading: UIState()
object ShowOverwriteMessage: UIState()
}
companion object { companion object {
private val TAG = EntryEditViewModel::class.java.name 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" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_entry_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:filterTouchesWhenObscured="true"> android:filterTouchesWhenObscured="true">

View File

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

View File

@@ -335,7 +335,7 @@
<string name="device_unlock_explanation_summary">استخدم إلغاء القفل الجهاز لفتح قاعدة البيانات بسهولة</string> <string name="device_unlock_explanation_summary">استخدم إلغاء القفل الجهاز لفتح قاعدة البيانات بسهولة</string>
<string name="lock_database_show_button_summary">يعرض زر القَفل في الواجهة</string> <string name="lock_database_show_button_summary">يعرض زر القَفل في الواجهة</string>
<string name="lock_database_show_button_title">أظهر زر القفل</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="lock_database_back_root_title">اضغط على \"رجوع\" للإقفال</string>
<string name="clipboard_explanation_summary">انسخ حقول المدخل باستخدام الحافظة</string> <string name="clipboard_explanation_summary">انسخ حقول المدخل باستخدام الحافظة</string>
<string name="database_opened">قاعدة البيانات مفتوحة</string> <string name="database_opened">قاعدة البيانات مفتوحة</string>
@@ -452,7 +452,6 @@
<string name="menu_form_filling_settings">ملء النموذج</string> <string name="menu_form_filling_settings">ملء النموذج</string>
<string name="menu_reload_database">أعد تحميل البيانات</string> <string name="menu_reload_database">أعد تحميل البيانات</string>
<string name="menu_external_icon">أيقونة خارجية</string> <string name="menu_external_icon">أيقونة خارجية</string>
<string name="registration_mode">وضع التسجيل</string>
<string name="import_app_properties_title">استورد خصائص التطبيق</string> <string name="import_app_properties_title">استورد خصائص التطبيق</string>
<string name="import_app_properties_summary">اختر ملفًا لاستيراد إعدادات التطبيق</string> <string name="import_app_properties_summary">اختر ملفًا لاستيراد إعدادات التطبيق</string>
<string name="export_app_properties_title">صدّر إعدادات التطبيق</string> <string name="export_app_properties_title">صدّر إعدادات التطبيق</string>
@@ -643,8 +642,8 @@
<string name="show_uuid_title">أظهر \"المعرّف العام المميز\" UUID</string> <string name="show_uuid_title">أظهر \"المعرّف العام المميز\" UUID</string>
<string name="unlock_and_link_biometric">رابط فتح الجهاز</string> <string name="unlock_and_link_biometric">رابط فتح الجهاز</string>
<string name="device_unlock_invalid_key">لا يمكن قراءة مفتاح فتح الجهاز. يرجى حذفه وتكرار إجراء التعرف على الفتح.</string> <string name="device_unlock_invalid_key">لا يمكن قراءة مفتاح فتح الجهاز. يرجى حذفه وتكرار إجراء التعرف على الفتح.</string>
<string name="menu_appearance_settings_summary">المظاهر والألوان والسمات</string> <string name="menu_appearance_settings_summary">المظاهر والألوان والأيقونات والخطوط والسمات</string>
<string name="autofill_explanation_summary">تمكين الملء التلقائي لملء النماذج بسرعة في التطبيقات الأخرى</string> <string name="autofill_explanation_summary">اضبط الملء التلقائي لملء النماذج بسرعة في التطبيقات الأخرى</string>
<string name="device_credential_unlock_enable_summary">يتيح لك استخدام بيانات اعتماد جهازك لفتح قاعدة البيانات</string> <string name="device_credential_unlock_enable_summary">يتيح لك استخدام بيانات اعتماد جهازك لفتح قاعدة البيانات</string>
<string name="unlock">فتح</string> <string name="unlock">فتح</string>
<string name="menu_app_settings_summary">البحث، القفل، التاريخ، الخصائص</string> <string name="menu_app_settings_summary">البحث، القفل، التاريخ، الخصائص</string>
@@ -692,4 +691,45 @@
<string name="warning_large_keyfile">لا يُنصح بإضافة ملف مفتاحي كبير، فقد يؤدي هذا إلى منع فتح قاعدة البيانات.</string> <string name="warning_large_keyfile">لا يُنصح بإضافة ملف مفتاحي كبير، فقد يؤدي هذا إلى منع فتح قاعدة البيانات.</string>
<string name="hide_templates_title">أخفِ القوالب</string> <string name="hide_templates_title">أخفِ القوالب</string>
<string name="error_otp_secret_length">يجب أن يتكوّن المفتاح السري من %1$d أحرف على الأقل.</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> </resources>

View File

@@ -485,7 +485,6 @@
<string name="search_mode">Axtarış modu</string> <string name="search_mode">Axtarış modu</string>
<string name="save_mode">Yadda saxlama modu</string> <string name="save_mode">Yadda saxlama modu</string>
<string name="selection_mode">Seçim 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_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_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> <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="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="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="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_database_locations_title">Zapamtite lokacije baza podataka</string>
<string name="remember_hardware_key_title">Zapamtite hardverske ključeve</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> <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="search_mode">Рэжым пошуку</string>
<string name="save_mode">Рэжым захавання</string> <string name="save_mode">Рэжым захавання</string>
<string name="selection_mode">Рэжым выбару</string> <string name="selection_mode">Рэжым выбару</string>
<string name="registration_mode">Рэжым рэгістрацыі</string>
<string name="remember_database_locations_title">Запамінаць размяшчэнне баз дадзеных</string> <string name="remember_database_locations_title">Запамінаць размяшчэнне баз дадзеных</string>
<string name="remember_database_locations_summary">Адсочвае, дзе захоўваюцца базы дадзеных</string> <string name="remember_database_locations_summary">Адсочвае, дзе захоўваюцца базы дадзеных</string>
<string name="remember_keyfile_locations_title">Запамінаць размяшчэнне файлаў ключоў</string> <string name="remember_keyfile_locations_title">Запамінаць размяшчэнне файлаў ключоў</string>

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