Compare commits

...

171 Commits
3.0.0 ... 3.0.4

Author SHA1 Message Date
J-Jamet
045049243c Merge branch 'release/3.0.4' 2021-12-09 20:10:57 +01:00
J-Jamet
b9813a3494 Upgrade version code 2021-12-09 19:54:12 +01:00
J-Jamet
9b42a93ce1 Change lock button 2021-12-09 19:49:38 +01:00
J-Jamet
8502bceef1 Fix search 2021-12-09 18:38:38 +01:00
J-Jamet
663387476f Change select entry min height 2021-12-09 14:33:07 +01:00
J-Jamet
daafd83df9 Better extra key implementation 2021-12-09 14:29:41 +01:00
J-Jamet
f780f2725b Fix compat inline suggestions request 2021-12-09 14:15:40 +01:00
J-Jamet
483aca871a Upgrade to version 3.0.4 and fix inline autofill suggestion #1165 2021-12-09 13:38:42 +01:00
J-Jamet
352e709c3b Merge branch 'master' into develop 2021-12-09 11:53:40 +01:00
J-Jamet
629057b2c1 Fix autofill exception #1173 2021-12-09 11:46:39 +01:00
J-Jamet
0e5f53596d Merge tag '3.0.3' into develop
3.0.3
2021-12-08 17:58:49 +01:00
J-Jamet
0d91f07646 Merge branch 'release/3.0.3' 2021-12-08 17:58:37 +01:00
J-Jamet
db882a26ab Upgrade constraint layout lib 2021-12-08 11:37:02 +01:00
J-Jamet
7f01619358 Fix notification in older android versions 2021-12-07 18:14:36 +01:00
J-Jamet
ee109b4ceb Merge branch 'feature/StartActivityResult' into develop 2021-12-07 16:36:52 +01:00
J-Jamet
7a398e5453 Fix activity result for advanced unlocking 2021-12-07 16:20:57 +01:00
J-Jamet
d4655d7034 Fix search in activity 2021-12-02 13:31:41 +01:00
J-Jamet
9feb96b541 Fix start autofill service 2021-12-02 13:30:02 +01:00
J-Jamet
e939278193 Suppress deprecation with setTargetFragment 2021-12-02 13:21:25 +01:00
J-Jamet
d4ef1a2617 Fix small warnings 2021-12-02 11:39:42 +01:00
J-Jamet
5f8746ced3 Fix result with entry edit 2021-12-01 17:16:19 +01:00
J-Jamet
40a063e94f Fix result exit lock 2021-11-30 11:50:07 +01:00
J-Jamet
8f5439b958 Icon selection with activity result launcher 2021-11-30 11:20:09 +01:00
J-Jamet
e347f57d8b Refactor FileHelper and fix key file selection 2021-11-30 10:47:31 +01:00
J-Jamet
2efb8e8b8c Merge branch 'develop' into feature/StartActivityResult 2021-11-23 18:28:08 +01:00
J-Jamet
e5bb69ea5f Fix startActivityResult for Autofill 2021-11-23 18:28:01 +01:00
J-Jamet
6ae186b2af Fix exported and pending intent 2021-11-23 12:10:57 +01:00
J-Jamet
71fdd2d92d Fix lowercase and uppercase 2021-11-22 13:22:18 +01:00
J-Jamet
3656689ff3 Fix kotlin code warning 2021-11-22 13:10:42 +01:00
J-Jamet
7d78406db6 Fix pending intent 2021-11-22 12:24:22 +01:00
J-Jamet
ac47748e41 Fix pending intent 2021-11-22 11:51:30 +01:00
J-Jamet
80f9b46479 New lock icon in notification 2021-11-20 13:30:26 +01:00
J-Jamet
999f1bf47a New lock icon in notification 2021-11-20 13:10:23 +01:00
J-Jamet
9e114eb2b8 Add lock button in database notification 2021-11-20 12:30:58 +01:00
J-Jamet
d89b6529ef Upgrade kotlin and fragment versions 2021-11-16 16:51:06 +01:00
J-Jamet
5caf11556a Remove unused translation 2021-11-16 16:15:08 +01:00
J-Jamet
78cc6f0f40 Merge branch 'translations' into develop 2021-11-16 16:10:58 +01:00
J-Jamet
0007cd4668 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2021-11-16 16:09:26 +01:00
J-Jamet
05195e41de Upgrade appcompat and material libs 2021-11-16 14:53:54 +01:00
J-Jamet
66f44ef87d Encapsulate lib version through modules 2021-11-16 12:48:38 +01:00
J-Jamet
a0585d9b11 Upgrade repo and libs 2021-11-16 12:07:48 +01:00
J-Jamet
5067946b13 Change backup configuration #1144 2021-11-16 11:12:07 +01:00
J-Jamet
f52241d5a8 Change backup configuration #1144 2021-11-16 11:09:14 +01:00
J-Jamet
04ccb25fa3 Catch key file out of memory exception 2021-11-15 12:25:27 +01:00
J-Jamet
5a3f4b60b8 Catch style exception 2021-11-15 12:19:36 +01:00
J-Jamet
4408b2e488 Catch exception in run action 2021-11-15 12:04:46 +01:00
J-Jamet
9a26acee35 Change version to 3.0.3 and add issue tag 2021-11-15 11:35:08 +01:00
Rsec
6ac377348b Translated using Weblate (Romanian)
Currently translated at 63.2% (359 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2021-11-10 18:49:56 +01:00
I. Musthafa
daeb88d4f4 Translated using Weblate (Indonesian)
Currently translated at 79.0% (449 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-11-08 07:50:09 +01:00
J-Jamet
47bf199f52 Merge branch 'feature/Biometric_Refactor' into develop 2021-11-03 14:46:55 +01:00
J-Jamet
91540b022d Use strong box to store in security chip #1145 2021-11-02 17:55:30 +01:00
J-Jamet
505a51b6b5 Update CHANGELOG 2021-11-02 17:31:09 +01:00
J-Jamet
28400488aa Fix advanced unlock menu button 2021-11-02 17:22:23 +01:00
J-Jamet
45ae600289 Change biometric views 2021-11-02 17:10:06 +01:00
J-Jamet
8be6874651 Auto remove all biometric keys when invalidated 2021-11-02 16:27:29 +01:00
Milo Ivir
f694a500e0 Translated using Weblate (Croatian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-10-30 00:37:53 +02:00
J-Jamet
c415fa01fc Update Autofill compatibility list #725 2021-10-29 17:40:16 +02:00
J-Jamet
5225a9459c Fix template chars limit 2021-10-27 20:01:47 +02:00
J-Jamet
2974b150af Fix template spinner 2021-10-27 19:55:14 +02:00
J-Jamet
cf353c8067 Fix template icons 2021-10-27 19:27:21 +02:00
Serdar Sağlam
3aacb5d8b3 Translated using Weblate (Turkish)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-10-27 01:38:42 +02:00
abidin toumi
114fbdbe01 Translated using Weblate (Arabic)
Currently translated at 77.4% (440 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-10-27 01:38:42 +02:00
abidin toumi
f1f83cbec4 Translated using Weblate (Arabic)
Currently translated at 76.5% (435 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-10-25 20:38:00 +02:00
J-Jamet
335c28c2c9 Fix save default username 2021-10-25 15:57:01 +02:00
J-Jamet
daf12cbcce Change Wifi to Wi-Fi 2021-10-25 15:44:18 +02:00
J-Jamet
bf56eca003 Fix templates #1128 2021-10-25 15:41:20 +02:00
J-Jamet
a12b7fd58a Merge branch 'SUPERYAO541-feature/Complete_zh-rTW' into develop 2021-10-25 11:30:33 +02:00
J-Jamet
98d004edbf Merge branch 'feature/Complete_zh-rTW' of git://github.com/SUPERYAO541/KeePassDX into SUPERYAO541-feature/Complete_zh-rTW 2021-10-25 11:30:18 +02:00
random r
67586a98b3 Translated using Weblate (Italian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-10-23 11:41:24 +02:00
Milo Ivir
c6d8911883 Translated using Weblate (Croatian)
Currently translated at 99.8% (567 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-10-21 21:36:24 +02:00
Neko Nekowazarashi
12e398ce9b Translated using Weblate (Indonesian)
Currently translated at 78.1% (444 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-10-16 13:40:45 +02:00
abidin toumi
5c4a202616 Translated using Weblate (Arabic)
Currently translated at 76.4% (434 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-10-14 23:34:38 +02:00
solokot
adc1ec8444 Translated using Weblate (Russian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-10-14 23:34:36 +02:00
Darin Avdeyeva
0227d2fcb4 Translated using Weblate (Russian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-10-13 19:02:33 +02:00
abidin toumi
95abe3b5ac Translated using Weblate (Arabic)
Currently translated at 67.6% (384 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-10-10 18:02:20 +02:00
abidin toumi
133a902c54 Translated using Weblate (Arabic)
Currently translated at 61.4% (349 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-10-09 17:05:15 +02:00
Stephan Paternotte
aacb03d9ef Translated using Weblate (Dutch)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2021-10-07 20:03:28 +02:00
J-Jamet
fbeaa1781f Add no internet connection required in description 2021-10-06 18:45:14 +02:00
J-Jamet
ca73aad538 Add dynamic templates in description 2021-10-06 18:09:54 +02:00
André Marcelo Alvarenga
4a4bfefd17 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-10-05 10:08:58 +02:00
J-Jamet
6796b0cd2a Upgrade gradle 2021-10-04 16:49:15 +02:00
abidin toumi
29e1f824b0 Translated using Weblate (Arabic)
Currently translated at 61.2% (348 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-10-03 17:19:42 +02:00
hokonch
51263a2911 Translated using Weblate (Japanese)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-09-30 06:36:59 +02:00
Kunzisoft
dd7f857475 Translated using Weblate (French)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2021-09-30 06:36:59 +02:00
J-Jamet
f0fdd4a537 Add & edit custom icon name #976 2021-09-28 18:12:49 +02:00
J-Jamet
9b847a0561 Change default Argon2 parameters #1098
and upgrade to 3.1.0
2021-09-28 12:57:14 +02:00
Hosted Weblate
469e76b80a Merge branch 'origin/develop' into Weblate. 2021-09-28 09:56:49 +02:00
SC
9c6994b476 Translated using Weblate (Portuguese)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2021-09-27 19:36:47 +02:00
Oğuz Ersen
52ba487617 Translated using Weblate (Turkish)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-09-27 19:36:47 +02:00
Eric
21c57c9484 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-09-27 19:36:47 +02:00
Ihor Hordiichuk
9b1ea6a07a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-09-27 19:36:47 +02:00
solokot
f7d7bb0ea3 Translated using Weblate (Russian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-09-27 19:36:46 +02:00
SC
dd96b9ef53 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2021-09-27 19:36:46 +02:00
Matthaiks
b6b01893ba Translated using Weblate (Polish)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-09-27 19:36:46 +02:00
Óscar Fernández Díaz
ad531d793d Translated using Weblate (Spanish)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-09-27 19:36:45 +02:00
Retrial
a9dd11e24a Translated using Weblate (Greek)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-09-27 19:36:45 +02:00
VfBFan
115983830b Translated using Weblate (German)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-09-27 19:36:45 +02:00
zeritti
442cece081 Translated using Weblate (Czech)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-09-27 19:36:45 +02:00
SUPERYAO
14e08457b9 Complete and improve zh-rTW 2021-09-25 21:59:59 +08:00
J-Jamet
fabcc08cd5 Merge tag '3.0.2' into develop
3.0.2
2021-09-24 19:35:07 +02:00
J-Jamet
7750843b04 Merge branch 'release/3.0.2' 2021-09-24 19:34:58 +02:00
Hosted Weblate
c03188e976 Merge branch 'origin/develop' into Weblate. 2021-09-24 13:54:09 +02:00
J-Jamet
d7da1ce333 Upgrade to 3.0.2 and update CHANGELOG 2021-09-24 12:55:08 +02:00
J-Jamet
dd9ee8c3f8 Merge branch 'chenxiaolong-samsung_dex' into develop 2021-09-24 12:50:40 +02:00
Braja Yudhistira
34bbd8f439 Translated using Weblate (Indonesian)
Currently translated at 77.8% (442 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-09-23 21:34:44 +02:00
Milo Ivir
053f57cff5 Translated using Weblate (Croatian)
Currently translated at 99.8% (567 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-09-23 21:34:44 +02:00
Balázs Meskó
365d2e2844 Translated using Weblate (Hungarian)
Currently translated at 82.5% (469 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2021-09-23 21:34:43 +02:00
Andrew Gunnerson
c0ac01a34a Add workaround to support Samsung DeX
This commit changes the Magikeyboard service behavior so that KeePassDX
is able to run in Samsung DeX mode. Currently, the app cannot run in
DeX mode because apps which have services using `BIND_INPUT_METHOD` are
blocked.

A new broadcast receiver has been added to listen for DeX's enter/leave
events [1] and disable/enable the `Magikeyboard` service appropriately.
The enabled state of a service lives in the Android framework's
`PackageManager` and survives app crashes and device reboots (though it
does get reset when app data is cleared).

Additionally, an extra check is added to `FileDatabaseSelectActivity` to
ensure the service's enabled state is correct. This is necessary if the
app crashes or is force quit within DeX mode and then the user exits DeX
mode. Otherwise, the service would stay disabled until the user entered
and exited DeX again.

With the new behavior, KeePassDX will generally just work with DeX,
though there's one caveat: after the initial installation, the user must
open the app once outside of DeX. Otherwise, Android will not trigger
the broadcast receiver. This could be fixed by making the service
intially disabled in the manifest with `android:enabled="false"`, but
Android's Settings app in SDK 15 through 25 does not correctly refresh
the keyboard list when changing the service from disabled to enabled.
I opted *not* to introduce different behavior based on the API version.

[1] https://developer.samsung.com/sdp/blog/en-us/2017/07/27/samsung-dex-how-to-detect-the-samsung-dex-mode

Fixes: #245
Signed-off-by: Andrew Gunnerson <chillermillerlong@hotmail.com>
2021-09-18 23:30:37 -04:00
Stephan Paternotte
22c0bc0adb Translated using Weblate (Dutch)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2021-09-17 18:36:29 +02:00
J-Jamet
1b88f2ddf0 Merge tag '3.0.1' into develop
3.0.1
2021-09-15 13:25:03 +02:00
J-Jamet
b4f2a1eb89 Merge branch 'release/3.0.1' 2021-09-15 13:24:53 +02:00
J-Jamet
e9fc9cbc2a Capture cast exception 2021-09-15 12:21:44 +02:00
J-Jamet
b809180a1b Small changes 2021-09-15 11:25:15 +02:00
J-Jamet
ecc75df3a1 Fix search actions #1091 #1092 2021-09-15 11:16:32 +02:00
J-Jamet
d1b6863143 Update CHANGELOG 2021-09-15 10:36:41 +02:00
J-Jamet
faf27143aa Fix timeout reset #1107 2021-09-15 10:33:37 +02:00
J-Jamet
aee58a4475 Fix exception after group name change and save #1112 2021-09-15 09:48:48 +02:00
VfBFan
c4cbf07d78 Translated using Weblate (German)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-09-15 07:34:50 +02:00
J-Jamet
c5e07f643f Fix Magikeyboard URL auto action #1100 2021-09-14 20:40:22 +02:00
J-Jamet
818f5820d5 Update CHANGELOG 2021-09-14 20:13:35 +02:00
J-Jamet
77c6e28876 Fix max lines #1073 2021-09-14 20:06:49 +02:00
J-Jamet
fb7f66012d Upgrade to 3.0.1 2021-09-14 19:35:30 +02:00
J-Jamet
0f7f7bbe6c Min height to 48dp 2021-09-14 19:30:50 +02:00
J-Jamet
8dedb8deb4 Fix text dimension 2021-09-14 19:10:48 +02:00
Long Nguyễn Khánh
dbb2c10bba Translated using Weblate (Vietnamese)
Currently translated at 16.1% (92 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2021-09-13 12:46:09 +02:00
Ihor Hordiichuk
67a5eef7d6 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-09-12 00:23:20 +02:00
HARADA Hiroyuki
979f651251 Translated using Weblate (Japanese)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-09-12 00:23:19 +02:00
SC
3b93cbb009 Translated using Weblate (Portuguese)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2021-09-11 04:18:42 +02:00
SC
30c63bfc4b Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2021-09-11 04:18:42 +02:00
SC
bed40324a1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-11 04:18:37 +02:00
Wilker Santana da Silva
bc035de377 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-11 04:18:37 +02:00
SC
4db3cb6936 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.8% (567 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-11 01:07:20 +02:00
Wilker Santana da Silva
ed4b91f4bd Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.8% (567 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-11 01:07:20 +02:00
SC
24c7151276 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.6% (566 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-11 01:00:00 +02:00
Wilker Santana da Silva
804a9c07b8 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.6% (566 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-11 00:59:58 +02:00
SC
528ea56821 Translated using Weblate (Portuguese)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2021-09-11 00:01:53 +02:00
SC
7ba9c69ff8 Translated using Weblate (Portuguese (Brazil))
Currently translated at 88.7% (504 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-10 20:50:55 +02:00
Wilker Santana da Silva
0fc34da08a Translated using Weblate (Portuguese (Brazil))
Currently translated at 88.7% (504 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-10 20:50:54 +02:00
SC
3fbf8cdbc8 Translated using Weblate (Portuguese (Brazil))
Currently translated at 83.0% (472 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-10 20:36:00 +02:00
Wilker Santana da Silva
e21f20d818 Translated using Weblate (Portuguese (Brazil))
Currently translated at 83.0% (472 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-09-10 20:35:59 +02:00
Sina bagheri
9fd9a60ca3 Translated using Weblate (Persian)
Currently translated at 56.6% (322 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fa/
2021-09-09 17:32:51 +02:00
Hosted Weblate
78b683d724 Merge branch 'origin/develop' into Weblate. 2021-09-08 07:26:45 +02:00
vachan-maker
ad2f5036e1 Translated using Weblate (Malayalam)
Currently translated at 70.2% (399 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2021-09-08 07:26:45 +02:00
random r
4afbad8faa Translated using Weblate (Italian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-09-08 07:26:44 +02:00
J-Jamet
d284db4d3c Merge tag '3.0.0' into develop
3.0.0
2021-09-07 18:43:40 +02:00
Oliver Cervera
ae8b1c0c29 Translated using Weblate (Italian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-09-06 21:34:18 +02:00
Óscar Fernández Díaz
27978c459c Translated using Weblate (Spanish)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-09-06 21:34:17 +02:00
Martin
1dc7f5c666 Translated using Weblate (Czech)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-09-06 21:34:17 +02:00
zeritti
12ac870d3a Translated using Weblate (Czech)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-09-06 21:34:17 +02:00
Aman Kirely
dd1baa0224 Translated using Weblate (Czech)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-09-06 21:34:17 +02:00
Oğuz Ersen
bb27ef41cc Translated using Weblate (Turkish)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-09-04 17:34:43 +02:00
Allan Nordhøy
2d35ac1df8 Translated using Weblate (Norwegian Bokmål)
Currently translated at 74.8% (425 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2021-09-04 17:34:43 +02:00
Eric
589ffc0c06 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-09-04 17:34:42 +02:00
Ihor Hordiichuk
1f7f38c7d3 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-09-04 17:34:41 +02:00
solokot
83817a2dc0 Translated using Weblate (Russian)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-09-04 17:34:41 +02:00
Matthaiks
11af9da66f Translated using Weblate (Polish)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-09-04 17:34:40 +02:00
Retrial
af3926acf3 Translated using Weblate (Greek)
Currently translated at 100.0% (568 of 568 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-09-04 17:34:40 +02:00
Hosted Weblate
ab40c2b3fd Merge branch 'origin/develop' into Weblate. 2021-09-03 16:07:31 +02:00
Oliver Cervera
fd05670dbc Translated using Weblate (Italian)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-09-03 00:32:28 +02:00
Óscar Fernández Díaz
1ac094bfae Translated using Weblate (Spanish)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-09-03 00:32:27 +02:00
Oğuz Ersen
fdf052cddb Translated using Weblate (Turkish)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-09-01 10:34:02 +02:00
Eric
9a8d50ba6f Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-09-01 10:34:02 +02:00
Ihor Hordiichuk
136c97c312 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-09-01 10:34:02 +02:00
solokot
bf00b88ef3 Translated using Weblate (Russian)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-09-01 10:34:02 +02:00
Matthaiks
bafd1ea549 Translated using Weblate (Polish)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-09-01 10:34:01 +02:00
Hisikawa Mizuki
982618511b Translated using Weblate (Japanese)
Currently translated at 99.2% (561 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-09-01 10:34:01 +02:00
Oliver Cervera
a4ad7ca3b1 Translated using Weblate (Italian)
Currently translated at 99.4% (562 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-09-01 10:34:01 +02:00
Éfrit
99d71b57a4 Translated using Weblate (French)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2021-09-01 10:34:00 +02:00
Retrial
1b2d8502e0 Translated using Weblate (Greek)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-09-01 10:34:00 +02:00
VfBFan
53e4ea9334 Translated using Weblate (German)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-09-01 10:34:00 +02:00
zeritti
3ce704155c Translated using Weblate (Czech)
Currently translated at 100.0% (565 of 565 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-09-01 10:33:59 +02:00
147 changed files with 3747 additions and 2015 deletions

View File

@@ -1,3 +1,28 @@
KeePassDX(3.0.4)
* Fix autofill inline bugs #1173 #1165
* Small UI change
KeePassDX(3.0.3)
* Change default Argon2 parameters #1098
* Add & edit custom icon name #976
* Fix templates #1128 #1133 #1138
* Update Autofill compatibility list #725 #1154
* Improve fingerprint usage #1137 #1145
* Change backup configuration #1144
* Add lock button in database notification
KeePassDX(3.0.2)
* Samsung DeX mode #1114 #245 (Thx @chenxiaolong)
KeePassDX(3.0.1)
* Fix text size and smallest margin #1085
* Fix number of lines during an edition #1073
* Fix Magikeyboard URL auto action #1100
* Fix exception after group name change and save #1112
* Fix timeout reset #1107
* Fix search actions #1091 #1092
* Small changes #1106 #1085
KeePassDX(3.0.0) KeePassDX(3.0.0)
* Add / Manage dynamic templates #191 * Add / Manage dynamic templates #191
* Manually select RecycleBin group and Templates group #191 * Manually select RecycleBin group and Templates group #191

View File

@@ -15,6 +15,7 @@
- Material design with **themes**. - Material design with **themes**.
- **Auto-Fill** and Integration. - **Auto-Fill** and Integration.
- Field filling **keyboard**. - 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)*. - Code written in **native languages** *(Kotlin / Java / JNI / C)*.

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 30 targetSdkVersion 30
versionCode = 87 versionCode = 91
versionName = "3.0.0" versionName = "3.0.4"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"
@@ -99,33 +99,33 @@ android {
} }
} }
def room_version = "2.2.6" def room_version = "2.3.0"
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0' implementation "androidx.appcompat:appcompat:$android_appcompat_version"
implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.1.0' implementation 'androidx.biometric:biometric:1.1.0'
implementation 'androidx.media:media:1.4.3'
// Lifecycle - LiveData - ViewModel - Coroutines // Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:1.3.2" implementation "androidx.core:core-ktx:$android_core_version"
implementation 'androidx.fragment:fragment-ktx:1.2.5' implementation 'androidx.fragment:fragment-ktx:1.3.6'
// WARNING: Don't upgrade because slowdown https://github.com/Kunzisoft/KeePassDX/issues/923 implementation "com.google.android.material:material:$android_material_version"
implementation 'com.google.android.material:material:1.1.0'
// Database // Database
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
// Autofill // Autofill
implementation "androidx.autofill:autofill:1.1.0" implementation "androidx.autofill:autofill:1.1.0"
// Time // Time
implementation 'joda-time:joda-time:2.10.6' implementation 'joda-time:joda-time:2.10.13'
// Color // Color
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4' implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
// Education // Education
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0' implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3'
// Apache Commons // Apache Commons
implementation 'commons-io:commons-io:2.8.0' implementation 'commons-io:commons-io:2.8.0'
implementation 'commons-codec:commons-codec:1.15' implementation 'commons-codec:commons-codec:1.15'
@@ -136,6 +136,6 @@ dependencies {
implementation project(path: ':icon-pack-material') implementation project(path: ':icon-pack-material')
// Tests // Tests
androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation "androidx.test:runner:$android_test_version"
androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation "androidx.test:rules:$android_test_version"
} }

View File

@@ -44,6 +44,7 @@
android:theme="@style/KeepassDXStyle.SplashScreen" android:theme="@style/KeepassDXStyle.SplashScreen"
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTop" android:launchMode="singleTop"
android:exported="true"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:windowSoftInputMode="stateHidden|stateAlwaysHidden" > android:windowSoftInputMode="stateHidden|stateAlwaysHidden" >
<intent-filter> <intent-filter>
@@ -53,6 +54,7 @@
</activity> </activity>
<activity <activity
android:name="com.kunzisoft.keepass.activities.PasswordActivity" android:name="com.kunzisoft.keepass.activities.PasswordActivity"
android:exported="true"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustResize|stateUnchanged"> android:windowSoftInputMode="adjustResize|stateUnchanged">
<intent-filter> <intent-filter>
@@ -111,6 +113,7 @@
<!-- Main Activity --> <!-- Main Activity -->
<activity <activity
android:name="com.kunzisoft.keepass.activities.GroupActivity" android:name="com.kunzisoft.keepass.activities.GroupActivity"
android:exported="false"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustPan"> android:windowSoftInputMode="adjustPan">
<meta-data <meta-data
@@ -154,7 +157,8 @@
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" /> android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
<activity <activity
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity" android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
android:theme="@style/Theme.Transparent"> android:theme="@style/Theme.Transparent"
android:exported="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" />
@@ -173,7 +177,8 @@
android:theme="@style/Theme.Transparent" /> android:theme="@style/Theme.Transparent" />
<activity <activity
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity" android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
android:label="@string/keyboard_setting_label"> android:label="@string/keyboard_setting_label"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
</intent-filter> </intent-filter>
@@ -199,6 +204,7 @@
<service <service
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService" android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
android:label="@string/autofill_service_name" android:label="@string/autofill_service_name"
android:exported="true"
android:permission="android.permission.BIND_AUTOFILL_SERVICE"> android:permission="android.permission.BIND_AUTOFILL_SERVICE">
<meta-data <meta-data
android:name="android.autofill" android:name="android.autofill"
@@ -210,6 +216,7 @@
<service <service
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService" android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
android:label="@string/keyboard_label" android:label="@string/keyboard_label"
android:exported="true"
android:permission="android.permission.BIND_INPUT_METHOD" > android:permission="android.permission.BIND_INPUT_METHOD" >
<meta-data android:name="android.view.im" <meta-data android:name="android.view.im"
android:resource="@xml/keyboard_method"/> android:resource="@xml/keyboard_method"/>
@@ -221,6 +228,14 @@
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService" android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
<receiver
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.app.action.ENTER_KNOX_DESKTOP_MODE" />
<action android:name="android.app.action.EXIT_KNOX_DESKTOP_MODE" />
</intent-filter>
</receiver>
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" /> <meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
</application> </application>

View File

@@ -23,28 +23,33 @@ import android.app.Activity
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentSender
import android.os.Build import android.os.Build
import android.view.inputmethod.InlineSuggestionsRequest import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
import com.kunzisoft.keepass.autofill.KeeAutofillService import com.kunzisoft.keepass.autofill.KeeAutofillService
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.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.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : DatabaseModeActivity() { class AutofillLauncherActivity : DatabaseModeActivity() {
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this, true)
else null
override fun applyCustomStyle(): Boolean { override fun applyCustomStyle(): Boolean {
return false return false
} }
@@ -60,17 +65,37 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode -> EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
when (specialMode) { when (specialMode) {
SpecialMode.SELECTION -> { SpecialMode.SELECTION -> {
// Build search param intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle ->
val searchInfo = SearchInfo().apply { // To pass extra inline request
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID) var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME) compatInlineSuggestionsRequest = bundle.getParcelable(KEY_INLINE_SUGGESTION)
manualSelection = intent.getBooleanExtra(KEY_MANUAL_SELECTION, false) }
} // Build search param
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain -> bundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
searchInfo.webDomain = concreteWebDomain SearchInfo.getConcreteWebDomain(
launchSelection(database, searchInfo) this,
searchInfo.webDomain
) { concreteWebDomain ->
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val assistStructure = AutofillHelper
.retrieveAutofillComponent(intent)
?.assistStructure
val newAutofillComponent = if (assistStructure != null) {
AutofillComponent(
assistStructure,
compatInlineSuggestionsRequest
)
} else {
null
}
searchInfo.webDomain = concreteWebDomain
launchSelection(database, newAutofillComponent, searchInfo)
}
}
} }
// Remove bundle
intent.removeExtra(KEY_SELECTION_BUNDLE)
} }
SpecialMode.REGISTRATION -> { SpecialMode.REGISTRATION -> {
// To register info // To register info
@@ -91,10 +116,8 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
} }
private fun launchSelection(database: Database?, private fun launchSelection(database: Database?,
autofillComponent: AutofillComponent?,
searchInfo: SearchInfo) { searchInfo: SearchInfo) {
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
if (autofillComponent == null) { if (autofillComponent == null) {
setResult(Activity.RESULT_CANCELED) setResult(Activity.RESULT_CANCELED)
finish() finish()
@@ -119,6 +142,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
// Show the database UI to select the entry // Show the database UI to select the entry
GroupActivity.launchForAutofillResult(this, GroupActivity.launchForAutofillResult(this,
openedDatabase, openedDatabase,
mAutofillActivityResultLauncher,
autofillComponent, autofillComponent,
searchInfo, searchInfo,
false) false)
@@ -126,6 +150,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
{ {
// If database not open // If database not open
FileDatabaseSelectActivity.launchForAutofillResult(this, FileDatabaseSelectActivity.launchForAutofillResult(this,
mAutofillActivityResultLauncher,
autofillComponent, autofillComponent,
searchInfo) searchInfo)
} }
@@ -186,55 +211,49 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show() Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
if (PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
// Close the database
sendBroadcast(Intent(LOCK_ACTION))
}
super.onActivityResult(requestCode, resultCode, data)
}
companion object { companion object {
private const val KEY_MANUAL_SELECTION = "KEY_MANUAL_SELECTION" private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID" private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN" private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO" private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
fun getPendingIntentForSelection(context: Context, fun getPendingIntentForSelection(context: Context,
searchInfo: SearchInfo? = null, searchInfo: SearchInfo? = null,
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): PendingIntent { compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent {
return PendingIntent.getActivity(context, 0, return PendingIntent.getActivity(context, 0,
// Doesn't work with Parcelable (don't know why?) // Doesn't work with direct extra Parcelable (don't know why?)
Intent(context, AutofillLauncherActivity::class.java).apply { // Wrap into a bundle to bypass the problem
searchInfo?.let { Intent(context, AutofillLauncherActivity::class.java).apply {
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId) putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
putExtra(KEY_SEARCH_DOMAIN, it.webDomain) putParcelable(KEY_SEARCH_INFO, searchInfo)
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
putExtra(KEY_MANUAL_SELECTION, it.manualSelection)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.let { putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
}
} }
}, })
PendingIntent.FLAG_CANCEL_CURRENT) },
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// TODO Mutable
PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
})
} }
fun getPendingIntentForRegistration(context: Context, fun getPendingIntentForRegistration(context: Context,
registerInfo: RegisterInfo): PendingIntent { registerInfo: RegisterInfo): PendingIntent {
return PendingIntent.getActivity(context, 0, return PendingIntent.getActivity(context, 0,
Intent(context, AutofillLauncherActivity::class.java).apply { Intent(context, AutofillLauncherActivity::class.java).apply {
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION) EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
putExtra(KEY_REGISTER_INFO, registerInfo) putExtra(KEY_REGISTER_INFO, registerInfo)
}, },
PendingIntent.FLAG_CANCEL_CURRENT) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// TODO Mutable
PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
})
} }
fun launchForRegistration(context: Context, fun launchForRegistration(context: Context,

View File

@@ -32,6 +32,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar import android.widget.ProgressBar
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
@@ -62,7 +63,6 @@ import com.kunzisoft.keepass.view.hideByFading
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.* import java.util.*
import kotlin.collections.HashMap
class EntryActivity : DatabaseLockActivity() { class EntryActivity : DatabaseLockActivity() {
@@ -84,8 +84,13 @@ class EntryActivity : DatabaseLockActivity() {
private var mEntryLoaded = false private var mEntryLoaded = false
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var mAttachmentSelected: Attachment? = null
private var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) {
// Reload the current id from database
mEntryViewModel.loadDatabase(mDatabase)
}
private var mIcon: IconImage? = null private var mIcon: IconImage? = null
private var mIconColor: Int = 0 private var mIconColor: Int = 0
@@ -133,6 +138,15 @@ class EntryActivity : DatabaseLockActivity() {
// Init SAF manager // Init SAF manager
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
mAttachmentSelected?.let { attachment ->
if (createdFileUri != null) {
mAttachmentFileBinderManager
?.startDownloadAttachment(createdFileUri, attachment)
}
mAttachmentSelected = null
}
}
// Init attachment service binder manager // Init attachment service binder manager
mAttachmentFileBinderManager = AttachmentFileBinderManager(this) mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
@@ -209,9 +223,8 @@ class EntryActivity : DatabaseLockActivity() {
} }
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected -> mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
mExternalFileHelper?.createDocument(attachmentSelected.name)?.let { requestCode -> mAttachmentSelected = attachmentSelected
mAttachmentsToDownload[requestCode] = attachmentSelected mExternalFileHelper?.createDocument(attachmentSelected.name)
}
} }
mEntryViewModel.historySelected.observe(this) { historySelected -> mEntryViewModel.historySelected.observe(this) { historySelected ->
@@ -220,7 +233,8 @@ class EntryActivity : DatabaseLockActivity() {
this, this,
database, database,
historySelected.nodeId, historySelected.nodeId,
historySelected.historyPosition historySelected.historyPosition,
mEntryActivityResultLauncher
) )
} }
} }
@@ -290,26 +304,6 @@ class EntryActivity : DatabaseLockActivity() {
super.onPause() super.onPause()
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
// Reload the current id from database
mEntryViewModel.loadDatabase(mDatabase)
}
}
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
if (createdFileUri != null) {
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
mAttachmentFileBinderManager
?.startDownloadAttachment(createdFileUri, attachmentToDownload)
}
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
if (mEntryLoaded) { if (mEntryLoaded) {
@@ -391,7 +385,8 @@ class EntryActivity : DatabaseLockActivity() {
EntryEditActivity.launchToUpdate( EntryEditActivity.launchToUpdate(
this, this,
database, database,
entryId entryId,
mEntryActivityResultLauncher
) )
} }
} }
@@ -432,7 +427,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(EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE, this) setResult(Activity.RESULT_OK, this)
} }
super.finish() super.finish()
} }
@@ -450,15 +445,13 @@ class EntryActivity : DatabaseLockActivity() {
*/ */
fun launch(activity: Activity, fun launch(activity: Activity,
database: Database, database: Database,
entryId: NodeId<UUID>) { entryId: NodeId<UUID>,
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)
activity.startActivityForResult( activityResultLauncher.launch(intent)
intent,
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
)
} }
} }
} }
@@ -469,16 +462,14 @@ class EntryActivity : DatabaseLockActivity() {
fun launch(activity: Activity, fun launch(activity: Activity,
database: Database, database: Database,
entryId: NodeId<UUID>, entryId: NodeId<UUID>,
historyPosition: Int) { historyPosition: Int,
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)
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition) intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
activity.startActivityForResult( activityResultLauncher.launch(intent)
intent,
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
)
} }
} }
} }

View File

@@ -33,12 +33,17 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi 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 com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.* import com.kunzisoft.keepass.activities.dialogs.*
@@ -96,6 +101,7 @@ class EntryEditActivity : DatabaseLockActivity(),
private var mTemplate: Template? = null private var mTemplate: Template? = null
private var mIsTemplate: Boolean = false private var mIsTemplate: Boolean = false
private var mEntryLoaded: Boolean = false private var mEntryLoaded: Boolean = false
private var mTemplatesSelectorAdapter: TemplatesSelectorAdapter? = null
private var mAllowCustomFields = false private var mAllowCustomFields = false
private var mAllowOTP = false private var mAllowOTP = false
@@ -106,6 +112,10 @@ class EntryEditActivity : DatabaseLockActivity(),
// Education // Education
private var entryEditActivityEducation: EntryEditActivityEducation? = null private var entryEditActivityEducation: EntryEditActivityEducation? = null
private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon ->
mEntryEditViewModel.selectIcon(icon)
}
// To ask data lost only one time // To ask data lost only one time
private var backPressedAlreadyApproved = false private var backPressedAlreadyApproved = false
@@ -154,6 +164,21 @@ class EntryEditActivity : DatabaseLockActivity(),
// To retrieve attachment // To retrieve attachment
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let { attachmentToUploadUri ->
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
documentFile.name?.let { fileName ->
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
.show(supportFragmentManager, "fileTooBigFragment")
} else {
mEntryEditViewModel.buildNewAttachment(attachmentToUploadUri, fileName)
}
}
}
}
}
mAttachmentFileBinderManager = AttachmentFileBinderManager(this) mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
// Verify the education views // Verify the education views
entryEditActivityEducation = EntryEditActivityEducation(this) entryEditActivityEducation = EntryEditActivityEducation(this)
@@ -175,11 +200,13 @@ class EntryEditActivity : DatabaseLockActivity(),
templateSelectorSpinner?.apply { templateSelectorSpinner?.apply {
// Build template selector // Build template selector
if (templates.isNotEmpty()) { if (templates.isNotEmpty()) {
adapter = TemplatesSelectorAdapter( mTemplatesSelectorAdapter = TemplatesSelectorAdapter(
this@EntryEditActivity, this@EntryEditActivity,
mIconDrawableFactory,
templates templates
) ).apply {
iconDrawableFactory = mIconDrawableFactory
}
adapter = mTemplatesSelectorAdapter
val selectedTemplate = if (mTemplate != null) val selectedTemplate = if (mTemplate != null)
mTemplate mTemplate
else else
@@ -213,7 +240,7 @@ class EntryEditActivity : DatabaseLockActivity(),
// View model listeners // View model listeners
mEntryEditViewModel.requestIconSelection.observe(this) { iconImage -> mEntryEditViewModel.requestIconSelection.observe(this) { iconImage ->
IconPickerActivity.launch(this@EntryEditActivity, iconImage) IconPickerActivity.launch(this@EntryEditActivity, iconImage, mIconSelectionActivityResultLauncher)
} }
mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant -> mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
@@ -321,6 +348,10 @@ class EntryEditActivity : DatabaseLockActivity(),
mAllowCustomFields = database?.allowEntryCustomFields() == true mAllowCustomFields = database?.allowEntryCustomFields() == true
mAllowOTP = database?.allowOTP == true mAllowOTP = database?.allowOTP == true
mEntryEditViewModel.loadDatabase(database) mEntryEditViewModel.loadDatabase(database)
mTemplatesSelectorAdapter?.apply {
iconDrawableFactory = mIconDrawableFactory
notifyDataSetChanged()
}
} }
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
@@ -472,29 +503,6 @@ class EntryEditActivity : DatabaseLockActivity(),
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon ->
mEntryEditViewModel.selectIcon(icon)
}
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
uri?.let { attachmentToUploadUri ->
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
documentFile.name?.let { fileName ->
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
.show(supportFragmentManager, "fileTooBigFragment")
} else {
mEntryEditViewModel.buildNewAttachment(attachmentToUploadUri, fileName)
}
}
}
}
}
}
/** /**
* Set up OTP (HOTP or TOTP) and add it as extra field * Set up OTP (HOTP or TOTP) and add it as extra field
*/ */
@@ -585,7 +593,7 @@ class EntryEditActivity : DatabaseLockActivity(),
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation( && entryEditActivityEducation.checkAndPerformedAttachmentEducation(
attachmentView, attachmentView,
{ {
mExternalFileHelper?.openDocument() addNewAttachment()
}, },
{ {
performedNextEducation(entryEditActivityEducation) performedNextEducation(entryEditActivityEducation)
@@ -686,7 +694,7 @@ class EntryEditActivity : DatabaseLockActivity(),
val intentEntry = Intent() val intentEntry = Intent()
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId) bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
intentEntry.putExtras(bundle) intentEntry.putExtras(bundle)
setResult(ADD_OR_UPDATE_ENTRY_RESULT_CODE, intentEntry) setResult(Activity.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
@@ -701,23 +709,46 @@ class EntryEditActivity : DatabaseLockActivity(),
// Keys for current Activity // Keys for current Activity
const val KEY_ENTRY = "entry" const val KEY_ENTRY = "entry"
const val KEY_PARENT = "parent" const val KEY_PARENT = "parent"
// Keys for callback
const val ADD_OR_UPDATE_ENTRY_RESULT_CODE = 31
const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129
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,
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
entryAddedOrUpdatedListener.invoke(
result.data?.getParcelableExtra(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 ->
if (result.resultCode == Activity.RESULT_OK) {
entryAddedOrUpdatedListener.invoke(
result.data?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY)
)
} else {
entryAddedOrUpdatedListener.invoke(null)
}
}
}
/** /**
* Launch EntryEditActivity to update an existing entry by his [entryId] * Launch EntryEditActivity to update an existing entry by his [entryId]
*/ */
fun launchToUpdate(activity: Activity, fun launchToUpdate(activity: Activity,
database: Database, database: Database,
entryId: NodeId<UUID>) { entryId: NodeId<UUID>,
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) intent.putExtra(KEY_ENTRY, entryId)
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE) activityResultLauncher.launch(intent)
} }
} }
} }
@@ -727,12 +758,13 @@ class EntryEditActivity : DatabaseLockActivity(),
*/ */
fun launchToCreate(activity: Activity, fun launchToCreate(activity: Activity,
database: Database, database: Database,
groupId: NodeId<*>) { groupId: 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_PARENT, groupId) intent.putExtra(KEY_PARENT, groupId)
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE) activityResultLauncher.launch(intent)
} }
} }
} }
@@ -795,8 +827,9 @@ class EntryEditActivity : DatabaseLockActivity(),
* Launch EntryEditActivity to add a new entry in autofill selection * Launch EntryEditActivity to add a new entry in autofill selection
*/ */
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity, fun launchForAutofillResult(activity: AppCompatActivity,
database: Database, database: Database,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent, autofillComponent: AutofillComponent,
groupId: NodeId<*>, groupId: NodeId<*>,
searchInfo: SearchInfo? = null) { searchInfo: SearchInfo? = null) {
@@ -807,6 +840,7 @@ class EntryEditActivity : DatabaseLockActivity(),
AutofillHelper.startActivityForAutofillResult( AutofillHelper.startActivityForAutofillResult(
activity, activity,
intent, intent,
activityResultLauncher,
autofillComponent, autofillComponent,
searchInfo searchInfo
) )

View File

@@ -31,8 +31,10 @@ import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi 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.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -85,9 +87,20 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this)
else null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Enabling/disabling MagikeyboardService is normally done by DexModeReceiver, but this
// additional check will allow the keyboard to be reenabled more easily if the app crashes
// or is force quit within DeX mode and then the user leaves DeX mode. Without this, the
// user would need to enter and exit DeX mode once to reenable the service.
MagikeyboardUtil.setEnabled(this, !DexUtil.isDexMode(resources.configuration))
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext) mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
setContentView(R.layout.activity_file_selection) setContentView(R.layout.activity_file_selection)
@@ -103,6 +116,22 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
// Open database button // Open database button
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let {
launchPasswordActivityWithPath(uri)
}
}
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
mDatabaseFileUri = databaseFileCreatedUri
if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment.getInstance(true)
.show(supportFragmentManager, "passwordDialog")
} else {
val error = getString(R.string.error_create_database)
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
Log.e(TAG, error)
}
}
openDatabaseButtonView = findViewById(R.id.open_keyfile_button) openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper) openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
@@ -250,8 +279,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
* Create a new file by calling the content provider * Create a new file by calling the content provider
*/ */
private fun createNewFile() { private fun createNewFile() {
mExternalFileHelper?.createDocument( getString(R.string.database_file_name_default) + mExternalFileHelper?.createDocument(
getString(R.string.database_file_extension_default), "application/x-keepass") getString(R.string.database_file_name_default) +
getString(R.string.database_file_extension_default))
} }
private fun fileNoFoundAction(e: FileNotFoundException) { private fun fileNoFoundAction(e: FileNotFoundException) {
@@ -268,7 +298,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
fileNoFoundAction(exception) fileNoFoundAction(exception)
}, },
{ onCancelSpecialMode() }, { onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() }) { onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher)
} }
private fun launchGroupActivityIfLoaded(database: Database) { private fun launchGroupActivityIfLoaded(database: Database) {
@@ -277,7 +308,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
database, database,
{ onValidateSpecialMode() }, { onValidateSpecialMode() },
{ onCancelSpecialMode() }, { onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() }) { onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher)
} }
} }
@@ -353,33 +385,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {} override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
if (uri != null) {
launchPasswordActivityWithPath(uri)
}
}
// Retrieve the created URI from the file manager
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
mDatabaseFileUri = databaseFileCreatedUri
if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment.getInstance(true)
.show(supportFragmentManager, "passwordDialog")
} else {
val error = getString(R.string.error_create_database)
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
Log.e(TAG, error)
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
@@ -493,11 +498,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
*/ */
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity, fun launchForAutofillResult(activity: AppCompatActivity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent, autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null) { searchInfo: SearchInfo? = null) {
AutofillHelper.startActivityForAutofillResult(activity, AutofillHelper.startActivityForAutofillResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java), Intent(activity, FileDatabaseSelectActivity::class.java),
activityResultLauncher,
autofillComponent, autofillComponent,
searchInfo) searchInfo)
} }

View File

@@ -33,8 +33,10 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.*
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
@@ -111,6 +113,16 @@ class GroupActivity : DatabaseLockActivity(),
private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null
private var mOnSuggestionListener: SearchView.OnSuggestionListener? = null private var mOnSuggestionListener: SearchView.OnSuggestionListener? = null
private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon ->
// To create tree dialog for icon
mGroupEditViewModel.selectIcon(icon)
}
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this)
else null
private var mIconColor: Int = 0 private var mIconColor: Int = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -211,11 +223,14 @@ class GroupActivity : DatabaseLockActivity(),
mDatabase?.let { database -> mDatabase?.let { database ->
EntrySelectionHelper.doSpecialAction(intent, EntrySelectionHelper.doSpecialAction(intent,
{ {
EntryEditActivity.launchToCreate( mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
this@GroupActivity, EntryEditActivity.launchToCreate(
database, this@GroupActivity,
currentGroup.nodeId database,
) currentGroup.nodeId,
resultLauncher
)
}
}, },
{ {
// Search not used // Search not used
@@ -243,6 +258,7 @@ class GroupActivity : DatabaseLockActivity(),
EntryEditActivity.launchForAutofillResult( EntryEditActivity.launchForAutofillResult(
this@GroupActivity, this@GroupActivity,
database, database,
mAutofillActivityResultLauncher,
autofillComponent, autofillComponent,
currentGroup.nodeId, currentGroup.nodeId,
searchInfo searchInfo
@@ -277,7 +293,7 @@ class GroupActivity : DatabaseLockActivity(),
} }
mGroupEditViewModel.requestIconSelection.observe(this) { iconImage -> mGroupEditViewModel.requestIconSelection.observe(this) { iconImage ->
IconPickerActivity.launch(this@GroupActivity, iconImage) IconPickerActivity.launch(this@GroupActivity, iconImage, mIconSelectionActivityResultLauncher)
} }
mGroupEditViewModel.requestDateTimeSelection.observe(this) { dateInstant -> mGroupEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
@@ -319,6 +335,29 @@ class GroupActivity : DatabaseLockActivity(),
return rootContainerView return rootContainerView
} }
private fun loadGroup(database: Database?) {
when {
Intent.ACTION_SEARCH == intent.action -> {
finishNodeAction()
val searchString =
intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
mGroupViewModel.loadGroupFromSearch(
database,
searchString,
PreferencesUtil.omitBackup(this)
)
}
mCurrentGroupState == null -> {
mRootGroup?.let { rootGroup ->
mGroupViewModel.loadGroup(database, rootGroup, 0)
}
}
else -> {
mGroupViewModel.loadGroup(database, mCurrentGroupState)
}
}
}
override fun onDatabaseRetrieved(database: Database?) { override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
@@ -328,13 +367,7 @@ class GroupActivity : DatabaseLockActivity(),
&& database?.isRecycleBinEnabled == true && database?.isRecycleBinEnabled == true
mRootGroup = database?.rootGroup mRootGroup = database?.rootGroup
if (mCurrentGroupState == null) { loadGroup(database)
mRootGroup?.let { rootGroup ->
mGroupViewModel.loadGroup(database, rootGroup, 0)
}
} else {
mGroupViewModel.loadGroup(database, mCurrentGroupState)
}
// Search suggestion // Search suggestion
database?.let { database?.let {
@@ -447,16 +480,7 @@ class GroupActivity : DatabaseLockActivity(),
} }
// To transform KEY_SEARCH_INFO in ACTION_SEARCH // To transform KEY_SEARCH_INFO in ACTION_SEARCH
transformSearchInfoIntent(intent) transformSearchInfoIntent(intent)
if (Intent.ACTION_SEARCH == intent.action) { loadGroup(mDatabase)
finishNodeAction()
val searchString =
intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
mGroupViewModel.loadGroupFromSearch(
mDatabase,
searchString,
PreferencesUtil.omitBackup(this)
)
}
} }
} }
@@ -594,11 +618,14 @@ class GroupActivity : DatabaseLockActivity(),
val entryVersioned = node as Entry val entryVersioned = node as Entry
EntrySelectionHelper.doSpecialAction(intent, EntrySelectionHelper.doSpecialAction(intent,
{ {
EntryActivity.launch( mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
this@GroupActivity, EntryActivity.launch(
database, this@GroupActivity,
entryVersioned.nodeId database,
) entryVersioned.nodeId,
resultLauncher
)
}
}, },
{ {
// Nothing here, a search is simply performed // Nothing here, a search is simply performed
@@ -653,6 +680,8 @@ class GroupActivity : DatabaseLockActivity(),
Log.e(TAG, "Node can't be cast in Entry") Log.e(TAG, "Node can't be cast in Entry")
} }
} }
reloadGroupIfSearch()
} }
private fun entrySelectedForSave(database: Database, entry: Entry, searchInfo: SearchInfo) { private fun entrySelectedForSave(database: Database, entry: Entry, searchInfo: SearchInfo) {
@@ -738,6 +767,12 @@ class GroupActivity : DatabaseLockActivity(),
actionNodeMode?.finish() actionNodeMode?.finish()
} }
private fun reloadGroupIfSearch() {
if (Intent.ACTION_SEARCH == intent.action) {
reloadCurrentGroup()
}
}
override fun onNodeSelected( override fun onNodeSelected(
database: Database, database: Database,
nodes: List<Node> nodes: List<Node>
@@ -787,12 +822,18 @@ class GroupActivity : DatabaseLockActivity(),
GroupEditDialogFragment.TAG_CREATE_GROUP GroupEditDialogFragment.TAG_CREATE_GROUP
) )
} }
Type.ENTRY -> EntryEditActivity.launchToUpdate( Type.ENTRY -> {
this@GroupActivity, mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
database, EntryEditActivity.launchToUpdate(
(node as Entry).nodeId this@GroupActivity,
) database,
(node as Entry).nodeId,
resultLauncher
)
}
}
} }
reloadGroupIfSearch()
return true return true
} }
@@ -847,6 +888,7 @@ class GroupActivity : DatabaseLockActivity(),
): Boolean { ): Boolean {
deleteNodes(nodes) deleteNodes(nodes)
finishNodeAction() finishNodeAction()
reloadGroupIfSearch()
return true return true
} }
@@ -974,7 +1016,7 @@ class GroupActivity : DatabaseLockActivity(),
if (!sortMenuEducationPerformed) { if (!sortMenuEducationPerformed) {
// lockMenuEducationPerformed // lockMenuEducationPerformed
val lockButtonView = findViewById<View>(R.id.lock_button_icon) val lockButtonView = findViewById<View>(R.id.lock_button)
lockButtonView != null lockButtonView != null
&& groupActivityEducation.checkAndPerformedLockMenuEducation( && groupActivityEducation.checkAndPerformedLockMenuEducation(
lockButtonView, lockButtonView,
@@ -1047,37 +1089,6 @@ class GroupActivity : DatabaseLockActivity(),
} }
} }
override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) {
/*
* ACTION_SEARCH automatically forces a new task. This occurs when you open a kdb file in
* another app such as Files or GoogleDrive and then Search for an entry. Here we remove the
* FLAG_ACTIVITY_NEW_TASK flag bit allowing search to open it's activity in the current task.
*/
if (Intent.ACTION_SEARCH == intent.action) {
var flags = intent.flags
flags = flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv()
intent.flags = flags
}
super.startActivityForResult(intent, requestCode, options)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// To create tree dialog for icon
IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon ->
mGroupEditViewModel.selectIcon(icon)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
// Directly used the onActivityResult in fragment
mGroupFragment?.onActivityResult(requestCode, resultCode, data)
}
private fun removeSearch() { private fun removeSearch() {
intent.removeExtra(AUTO_SEARCH_KEY) intent.removeExtra(AUTO_SEARCH_KEY)
if (Intent.ACTION_SEARCH == intent.action) { if (Intent.ACTION_SEARCH == intent.action) {
@@ -1093,7 +1104,7 @@ class GroupActivity : DatabaseLockActivity(),
try { try {
mGroupViewModel.loadGroup(mDatabase, mCurrentGroupState) mGroupViewModel.loadGroup(mDatabase, mCurrentGroupState)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to rebuild the list after deletion", e) Log.e(TAG, "Unable to rebuild the group", e)
} }
} }
@@ -1282,8 +1293,9 @@ class GroupActivity : DatabaseLockActivity(),
* ------------------------- * -------------------------
*/ */
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity, fun launchForAutofillResult(activity: AppCompatActivity,
database: Database, database: Database,
activityResultLaunch: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent, autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null, searchInfo: SearchInfo? = null,
autoSearch: Boolean = false) { autoSearch: Boolean = false) {
@@ -1293,6 +1305,7 @@ class GroupActivity : DatabaseLockActivity(),
AutofillHelper.startActivityForAutofillResult( AutofillHelper.startActivityForAutofillResult(
activity, activity,
intent, intent,
activityResultLaunch,
autofillComponent, autofillComponent,
searchInfo searchInfo
) )
@@ -1325,11 +1338,12 @@ class GroupActivity : DatabaseLockActivity(),
* Global Launch * Global Launch
* ------------------------- * -------------------------
*/ */
fun launch(activity: Activity, fun launch(activity: AppCompatActivity,
database: Database, database: Database,
onValidateSpecialMode: () -> Unit, onValidateSpecialMode: () -> Unit,
onCancelSpecialMode: () -> Unit, onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit) { onLaunchActivitySpecialMode: () -> Unit,
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
EntrySelectionHelper.doSpecialAction(activity.intent, EntrySelectionHelper.doSpecialAction(activity.intent,
{ {
GroupActivity.launch( GroupActivity.launch(
@@ -1441,6 +1455,7 @@ class GroupActivity : DatabaseLockActivity(),
// Here no search info found, disable auto search // Here no search info found, disable auto search
GroupActivity.launchForAutofillResult(activity, GroupActivity.launchForAutofillResult(activity,
database, database,
autofillActivityResultLauncher,
autofillComponent, autofillComponent,
searchInfo, searchInfo,
false) false)

View File

@@ -27,12 +27,16 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.IconEditDialogFragment
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
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
@@ -81,6 +85,9 @@ class IconPickerActivity : DatabaseLockActivity() {
coordinatorLayout = findViewById(R.id.icon_picker_coordinator) coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
addCustomIcon(uri)
}
uploadButton = findViewById(R.id.icon_picker_upload) uploadButton = findViewById(R.id.icon_picker_upload)
@@ -139,6 +146,16 @@ class IconPickerActivity : DatabaseLockActivity() {
} }
uploadButton.isEnabled = true uploadButton.isEnabled = true
} }
iconPickerViewModel.customIconUpdated.observe(this) { iconCustomUpdated ->
if (iconCustomUpdated.error && !iconCustomUpdated.errorConsumed) {
Snackbar.make(coordinatorLayout, iconCustomUpdated.errorStringId, Snackbar.LENGTH_LONG).asError().show()
iconCustomUpdated.errorConsumed = true
}
iconCustomUpdated.iconCustom?.let {
mDatabase?.updateCustomIcon(it)
}
iconPickerViewModel.deselectAllCustomIcons()
}
} }
override fun viewToInvalidateTimeout(): View? { override fun viewToInvalidateTimeout(): View? {
@@ -197,6 +214,10 @@ class IconPickerActivity : DatabaseLockActivity() {
} }
override fun onPrepareOptionsMenu(menu: Menu?): Boolean { override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
menu?.findItem(R.id.menu_edit)?.apply {
isEnabled = mIconsSelected.size == 1
isVisible = isEnabled
}
menu?.findItem(R.id.menu_delete)?.apply { menu?.findItem(R.id.menu_delete)?.apply {
isEnabled = mCustomIconsSelectionMode isEnabled = mCustomIconsSelectionMode
isVisible = isEnabled isVisible = isEnabled
@@ -213,6 +234,9 @@ class IconPickerActivity : DatabaseLockActivity() {
onBackPressed() onBackPressed()
} }
} }
R.id.menu_edit -> {
updateCustomIcon(mIconsSelected[0])
}
R.id.menu_delete -> { R.id.menu_delete -> {
mIconsSelected.forEach { iconToRemove -> mIconsSelected.forEach { iconToRemove ->
removeCustomIcon(iconToRemove) removeCustomIcon(iconToRemove)
@@ -277,6 +301,11 @@ class IconPickerActivity : DatabaseLockActivity() {
} }
} }
private fun updateCustomIcon(iconImageCustom: IconImageCustom) {
IconEditDialogFragment.update(iconImageCustom)
.show(supportFragmentManager, IconEditDialogFragment.TAG_UPDATE_ICON)
}
private fun removeCustomIcon(iconImageCustom: IconImageCustom) { private fun removeCustomIcon(iconImageCustom: IconImageCustom) {
uploadButton.isEnabled = false uploadButton.isEnabled = false
iconPickerViewModel.deselectAllCustomIcons() iconPickerViewModel.deselectAllCustomIcons()
@@ -286,14 +315,6 @@ class IconPickerActivity : DatabaseLockActivity() {
) )
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
addCustomIcon(uri)
}
}
private fun setResult() { private fun setResult() {
setResult(Activity.RESULT_OK, Intent().apply { setResult(Activity.RESULT_OK, Intent().apply {
putExtra(EXTRA_ICON, mIconImage) putExtra(EXTRA_ICON, mIconImage)
@@ -308,30 +329,28 @@ class IconPickerActivity : DatabaseLockActivity() {
companion object { companion object {
private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG" private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG"
private const val ICON_SELECTED_REQUEST = 15861
private const val EXTRA_ICON = "EXTRA_ICON" private const val EXTRA_ICON = "EXTRA_ICON"
private const val MAX_ICON_SIZE = 5242880 private const val MAX_ICON_SIZE = 5242880
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, listener: (icon: IconImage) -> Unit) { fun registerIconSelectionForResult(context: FragmentActivity,
if (requestCode == ICON_SELECTED_REQUEST) { listener: (icon: IconImage) -> Unit): ActivityResultLauncher<Intent> {
if (resultCode == Activity.RESULT_OK) { return context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
listener.invoke(data?.getParcelableExtra(EXTRA_ICON) ?: IconImage()) if (result.resultCode == Activity.RESULT_OK) {
listener.invoke(result.data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
} }
} }
} }
fun launch(context: Activity, fun launch(context: FragmentActivity,
previousIcon: IconImage?) { previousIcon: IconImage?,
resultLauncher: ActivityResultLauncher<Intent>) {
// Create an instance to return the picker icon // Create an instance to return the picker icon
context.startActivityForResult( resultLauncher.launch(
Intent(context, Intent(context, IconPickerActivity::class.java).apply {
IconPickerActivity::class.java).apply {
if (previousIcon != null) if (previousIcon != null)
putExtra(EXTRA_ICON, previousIcon) putExtra(EXTRA_ICON, previousIcon)
}, }
ICON_SELECTED_REQUEST) )
} }
} }
} }

View File

@@ -35,9 +35,10 @@ import android.view.KeyEvent.KEYCODE_ENTER
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.* import android.widget.*
import android.widget.TextView.OnEditorActionListener import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi 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.app.ActivityCompat import androidx.core.app.ActivityCompat
@@ -71,11 +72,12 @@ import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import java.io.FileNotFoundException import java.io.FileNotFoundException
open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener { class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
// Views // Views
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
@@ -89,7 +91,8 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
private lateinit var coordinatorLayout: CoordinatorLayout private lateinit var coordinatorLayout: CoordinatorLayout
private var advancedUnlockFragment: AdvancedUnlockFragment? = null private var advancedUnlockFragment: AdvancedUnlockFragment? = null
private val databaseFileViewModel: DatabaseFileViewModel by viewModels() private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels()
private var mDefaultDatabase: Boolean = false private var mDefaultDatabase: Boolean = false
private var mDatabaseFileUri: Uri? = null private var mDatabaseFileUri: Uri? = null
@@ -111,7 +114,10 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
field = value field = value
} }
private var mAllowAutoOpenBiometricPrompt: Boolean = true private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this)
else null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -142,6 +148,12 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this) mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity) mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) {
mDatabaseKeyFileUri = uri
populateKeyFileTextView(uri)
}
}
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper) keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
passwordView?.setOnEditorActionListener(onEditorActionListener) passwordView?.setOnEditorActionListener(onEditorActionListener)
@@ -170,9 +182,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) { if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE)) mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
} }
if (savedInstanceState?.containsKey(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) == true) {
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
}
// Init Biometric elements // Init Biometric elements
advancedUnlockFragment = supportFragmentManager advancedUnlockFragment = supportFragmentManager
@@ -188,17 +197,17 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
// Listen password checkbox to init advanced unlock and confirmation button // Listen password checkbox to init advanced unlock and confirmation button
checkboxPasswordView?.setOnCheckedChangeListener { _, _ -> checkboxPasswordView?.setOnCheckedChangeListener { _, _ ->
advancedUnlockFragment?.checkUnlockAvailability() mAdvancedUnlockViewModel.checkUnlockAvailability()
enableOrNotTheConfirmationButton() enableOrNotTheConfirmationButton()
} }
// Observe if default database // Observe if default database
databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase -> mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
mDefaultDatabase = isDefaultDatabase mDefaultDatabase = isDefaultDatabase
} }
// Observe database file change // Observe database file change
databaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile -> mDatabaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
// Force read only if the file does not exists // Force read only if the file does not exists
mForceReadOnly = databaseFile?.let { mForceReadOnly = databaseFile?.let {
!it.databaseFileExists !it.databaseFileExists
@@ -232,12 +241,12 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
} }
// Don't allow auto open prompt if lock become when UI visible // Don't allow auto open prompt if lock become when UI visible
mAllowAutoOpenBiometricPrompt = if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) {
false mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
else }
mAllowAutoOpenBiometricPrompt
mDatabaseFileUri?.let { databaseFileUri -> mDatabaseFileUri?.let { databaseFileUri ->
databaseFileViewModel.loadDatabaseFile(databaseFileUri) mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
} }
checkPermission() checkPermission()
@@ -263,7 +272,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
when (actionTask) { when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> { ACTION_DATABASE_LOAD_TASK -> {
// Recheck advanced unlock if error // Recheck advanced unlock if error
advancedUnlockFragment?.initAdvancedUnlockMode() mAdvancedUnlockViewModel.initAdvancedUnlockMode()
if (result.isSuccess) { if (result.isSuccess) {
launchGroupActivityIfLoaded(database) launchGroupActivityIfLoaded(database)
@@ -311,7 +320,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
is FileNotFoundDatabaseException -> { is FileNotFoundDatabaseException -> {
// Remove this default database inaccessible // Remove this default database inaccessible
if (mDefaultDatabase) { if (mDefaultDatabase) {
databaseFileViewModel.removeDefaultDatabase() mDatabaseFileViewModel.removeDefaultDatabase()
} }
} }
} }
@@ -344,7 +353,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE) mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
} }
mDatabaseFileUri?.let { mDatabaseFileUri?.let {
databaseFileViewModel.checkIfIsDefaultDatabase(it) mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
} }
} }
@@ -361,7 +370,8 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
database, database,
{ onValidateSpecialMode() }, { onValidateSpecialMode() },
{ onCancelSpecialMode() }, { onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() } { onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher
) )
} }
} }
@@ -435,8 +445,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
verifyCheckboxesAndLoadDatabase(password, keyFileUri) verifyCheckboxesAndLoadDatabase(password, keyFileUri)
} else { } else {
// Init Biometric elements // Init Biometric elements
advancedUnlockFragment?.loadDatabase(databaseFileUri, mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
mAllowAutoOpenBiometricPrompt)
} }
enableOrNotTheConfirmationButton() enableOrNotTheConfirmationButton()
@@ -496,7 +505,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
override fun onPause() { override fun onPause() {
// Reinit locking activity UI variable // Reinit locking activity UI variable
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
mAllowAutoOpenBiometricPrompt = true
super.onPause() super.onPause()
} }
@@ -507,7 +515,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
outState.putString(KEY_KEYFILE, it.toString()) outState.putString(KEY_KEYFILE, it.toString())
} }
outState.putBoolean(KEY_READ_ONLY, mReadOnly) outState.putBoolean(KEY_READ_ONLY, mReadOnly)
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
@@ -709,45 +716,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mAllowAutoOpenBiometricPrompt = false
// To get device credential unlock result
advancedUnlockFragment?.onActivityResult(requestCode, resultCode, data)
// To get entry in result
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
var keyFileResult = false
mExternalFileHelper?.let {
keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
if (uri != null) {
mDatabaseKeyFileUri = uri
populateKeyFileTextView(uri)
}
}
}
if (!keyFileResult) {
// this block if not a key file response
when (resultCode) {
DatabaseLockActivity.RESULT_EXIT_LOCK -> {
clearCredentialsViews()
closeDatabase()
}
Activity.RESULT_CANCELED -> {
clearCredentialsViews()
}
}
}
}
companion object { companion object {
private val TAG = PasswordActivity::class.java.name private val TAG = PasswordActivity::class.java.name
@@ -764,8 +732,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED" private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647 private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
private const val ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT = "ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT"
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?, private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
intentBuildLauncher: (Intent) -> Unit) { intentBuildLauncher: (Intent) -> Unit) {
val intent = Intent(activity, PasswordActivity::class.java) val intent = Intent(activity, PasswordActivity::class.java)
@@ -855,15 +821,17 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
@Throws(FileNotFoundException::class) @Throws(FileNotFoundException::class)
fun launchForAutofillResult(activity: Activity, fun launchForAutofillResult(activity: AppCompatActivity,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?, keyFile: Uri?,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent, autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) { searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
AutofillHelper.startActivityForAutofillResult( AutofillHelper.startActivityForAutofillResult(
activity, activity,
intent, intent,
activityResultLauncher,
autofillComponent, autofillComponent,
searchInfo) searchInfo)
} }
@@ -891,12 +859,13 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
* Global Launch * Global Launch
* ------------------------- * -------------------------
*/ */
fun launch(activity: Activity, fun launch(activity: AppCompatActivity,
databaseUri: Uri, databaseUri: Uri,
keyFile: Uri?, keyFile: Uri?,
fileNoFoundAction: (exception: FileNotFoundException) -> Unit, fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
onCancelSpecialMode: () -> Unit, onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit) { onLaunchActivitySpecialMode: () -> Unit,
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
try { try {
EntrySelectionHelper.doSpecialAction(activity.intent, EntrySelectionHelper.doSpecialAction(activity.intent,
@@ -926,6 +895,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PasswordActivity.launchForAutofillResult(activity, PasswordActivity.launchForAutofillResult(activity,
databaseUri, keyFile, databaseUri, keyFile,
autofillActivityResultLauncher,
autofillComponent, autofillComponent,
searchInfo) searchInfo)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()

View File

@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
@@ -133,6 +132,18 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection) keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let { pathUri ->
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
keyFileSelectionView?.error = null
keyFileCheckBox?.isChecked = true
keyFileSelectionView?.uri = pathUri
if (lengthFile <= 0L) {
showEmptyKeyFileConfirmationDialog()
}
}
}
}
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper) keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
val dialog = builder.create() val dialog = builder.create()
@@ -208,7 +219,11 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match) passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
} }
if (mMasterPassword == null || mMasterPassword!!.isEmpty()) { if ((mMasterPassword == null
|| mMasterPassword!!.isEmpty())
&& (keyFileCheckBox == null
|| !keyFileCheckBox!!.isChecked
|| keyFileSelectionView?.uri == null)) {
error = true error = true
showEmptyPasswordConfirmationDialog() showEmptyPasswordConfirmationDialog()
} }
@@ -282,23 +297,6 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
uri?.let { pathUri ->
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
keyFileSelectionView?.error = null
keyFileCheckBox?.isChecked = true
keyFileSelectionView?.uri = pathUri
if (lengthFile <= 0L) {
showEmptyKeyFileConfirmationDialog()
}
}
}
}
}
companion object { companion object {
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG" private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"

View File

@@ -29,6 +29,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
} }
} }
@Suppress("DEPRECATION")
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.os.Bundle
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
class IconEditDialogFragment : DatabaseDialogFragment() {
private val mIconPickerViewModel: IconPickerViewModel by activityViewModels()
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
private lateinit var iconView: ImageView
private lateinit var nameTextLayoutView: TextInputLayout
private lateinit var nameTextView: TextView
private var mCustomIcon: IconImageCustom? = null
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
}
mCustomIcon?.let { customIcon ->
populateViewsWithCustomIcon(customIcon)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_edit, null)
iconView = root.findViewById(R.id.icon_edit_image)
nameTextLayoutView = root.findViewById(R.id.icon_edit_name_container)
nameTextView = root.findViewById(R.id.icon_edit_name)
if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_CUSTOM_ICON_ID)) {
mCustomIcon = savedInstanceState.getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
} else {
arguments?.apply {
if (containsKey(KEY_CUSTOM_ICON_ID)) {
mCustomIcon = getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
}
}
}
val builder = AlertDialog.Builder(activity)
builder.setView(root)
.setPositiveButton(android.R.string.ok) { _, _ ->
retrieveIconInfoFromViews()
mCustomIcon?.let { customIcon ->
mIconPickerViewModel.updateCustomIcon(
IconPickerViewModel.IconCustomState(customIcon, false)
)
}
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
// Do nothing
mIconPickerViewModel.updateCustomIcon(
IconPickerViewModel.IconCustomState(null, false)
)
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun populateViewsWithCustomIcon(customIcon: IconImageCustom) {
mPopulateIconMethod?.invoke(iconView, customIcon.getIconImageToDraw())
nameTextView.text = customIcon.name
}
private fun retrieveIconInfoFromViews() {
mCustomIcon?.name = nameTextView.text.toString()
mCustomIcon?.lastModificationTime = DateInstant()
}
override fun onSaveInstanceState(outState: Bundle) {
retrieveIconInfoFromViews()
outState.putParcelable(KEY_CUSTOM_ICON_ID, mCustomIcon)
super.onSaveInstanceState(outState)
}
companion object {
const val TAG_UPDATE_ICON = "TAG_UPDATE_ICON"
const val KEY_CUSTOM_ICON_ID = "KEY_CUSTOM_ICON_ID"
fun update(customIcon: IconImageCustom): IconEditDialogFragment {
val bundle = Bundle()
bundle.putParcelable(KEY_CUSTOM_ICON_ID, IconImageCustom(customIcon))
val fragment = IconEditDialogFragment()
fragment.arguments = bundle
return fragment
}
}
}

View File

@@ -309,7 +309,7 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
s?.toString()?.let { userString -> s?.toString()?.let { userString ->
try { try {
mOtpElement.setBase32Secret(userString.toUpperCase(Locale.ENGLISH)) mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
otpSecretContainer?.error = null otpSecretContainer?.error = null
} catch (exception: Exception) { } catch (exception: Exception) {
otpSecretContainer?.error = getString(R.string.error_otp_secret_key) otpSecretContainer?.error = getString(R.string.error_otp_secret_key)

View File

@@ -74,6 +74,19 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
private var mRecycleBinEnable: Boolean = false private var mRecycleBinEnable: Boolean = false
private var mRecycleBin: Group? = null private var mRecycleBin: Group? = null
var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) { entryId ->
entryId?.let {
// Simply refresh the list
rebuildList()
// Scroll to the new entry
mDatabase?.getEntryById(it)?.let { entry ->
mAdapter?.indexOf(entry)?.let { position ->
mNodesRecyclerView?.scrollToPosition(position)
}
}
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
}
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() { private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState) super.onScrollStateChanged(recyclerView, newState)
@@ -399,27 +412,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
if (resultCode == EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE) {
data?.getParcelableExtra<NodeId<UUID>>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let {
// Simply refresh the list
rebuildList()
// Scroll to the new entry
mDatabase?.getEntryById(it)?.let { entry ->
mAdapter?.indexOf(entry)?.let { position ->
mNodesRecyclerView?.scrollToPosition(position)
}
}
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
}
}
}
}
/** /**
* Callback listener to redefine to do an action when a node is click * Callback listener to redefine to do an action when a node is click
*/ */

View File

@@ -55,8 +55,10 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
iconCustomAdded?.iconCustom?.let { icon -> iconCustomAdded?.iconCustom?.let { icon ->
iconPickerAdapter.addIcon(icon) iconPickerAdapter.addIcon(icon)
iconCustomAdded.iconCustom = null iconCustomAdded.iconCustom = null
try {
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
} catch (ignore: Exception) {}
} }
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
} }
} }
iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved -> iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved ->
@@ -67,6 +69,14 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
} }
} }
} }
iconPickerViewModel.customIconUpdated.observe(viewLifecycleOwner) { iconCustomUpdated ->
if (!iconCustomUpdated.error) {
iconCustomUpdated?.iconCustom?.let { icon ->
iconPickerAdapter.updateIcon(icon)
iconCustomUpdated.iconCustom = null
}
}
}
} }
override fun onIconClickListener(icon: IconImageCustom) { override fun onIconClickListener(icon: IconImageCustom) {

View File

@@ -20,14 +20,16 @@
package com.kunzisoft.keepass.activities.helpers package com.kunzisoft.keepass.activities.helpers
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import androidx.annotation.RequiresApi import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
@@ -38,6 +40,10 @@ class ExternalFileHelper {
private var activity: FragmentActivity? = null private var activity: FragmentActivity? = null
private var fragment: Fragment? = null private var fragment: Fragment? = null
private var getContentResultLauncher: ActivityResultLauncher<String>? = null
private var openDocumentResultLauncher: ActivityResultLauncher<Array<String>>? = null
private var createDocumentResultLauncher: ActivityResultLauncher<String>? = null
constructor(context: FragmentActivity) { constructor(context: FragmentActivity) {
this.activity = context this.activity = context
this.fragment = null this.fragment = null
@@ -48,94 +54,81 @@ class ExternalFileHelper {
this.fragment = context this.fragment = context
} }
fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) {
val resultCallback = ActivityResultCallback<Uri> { result ->
result?.let { uri ->
UriUtil.takeUriPermission(activity?.contentResolver, uri)
onFileSelected?.invoke(uri)
}
}
getContentResultLauncher = if (fragment != null) {
fragment?.registerForActivityResult(
GetContent(),
resultCallback
)
} else {
activity?.registerForActivityResult(
GetContent(),
resultCallback
)
}
openDocumentResultLauncher = if (fragment != null) {
fragment?.registerForActivityResult(
OpenDocument(),
resultCallback
)
} else {
activity?.registerForActivityResult(
OpenDocument(),
resultCallback
)
}
}
fun buildCreateDocument(typeString: String = "application/octet-stream",
onFileCreated: (fileCreated: Uri?)->Unit) {
val resultCallback = ActivityResultCallback<Uri> { result ->
onFileCreated.invoke(result)
}
createDocumentResultLauncher = if (fragment != null) {
fragment?.registerForActivityResult(
CreateDocument(typeString),
resultCallback
)
} else {
activity?.registerForActivityResult(
CreateDocument(typeString),
resultCallback
)
}
}
fun openDocument(getContent: Boolean = false, fun openDocument(getContent: Boolean = false,
typeString: String = "*/*") { typeString: String = "*/*") {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { try {
try { if (getContent) {
if (getContent) { getContentResultLauncher?.launch(typeString)
openActivityWithActionGetContent(typeString) } else {
} else { openDocumentResultLauncher?.launch(arrayOf(typeString))
openActivityWithActionOpenDocument(typeString)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to open document", e)
showFileManagerDialogFragment()
} }
} else { } catch (e: Exception) {
Log.e(TAG, "Unable to open document", e)
showFileManagerDialogFragment() showFileManagerDialogFragment()
} }
} }
@RequiresApi(Build.VERSION_CODES.KITKAT) fun createDocument(titleString: String) {
private fun openActivityWithActionOpenDocument(typeString: String) { try {
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { createDocumentResultLauncher?.launch(titleString)
addCategory(Intent.CATEGORY_OPENABLE) } catch (e: Exception) {
type = typeString Log.e(TAG, "Unable to create document", e)
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) showFileManagerDialogFragment()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
}
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
} }
if (fragment != null)
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
else
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
}
@RequiresApi(Build.VERSION_CODES.KITKAT)
private fun openActivityWithActionGetContent(typeString: String) {
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
}
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
if (fragment != null)
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
else
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
}
/**
* To use in onActivityResultCallback in Fragment or Activity
* @param onFileSelected Callback retrieve from data
* @return true if requestCode was captured, false elsewhere
*/
fun onOpenDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
onFileSelected: ((uri: Uri?) -> Unit)?): Boolean {
when (requestCode) {
FILE_BROWSE -> {
if (resultCode == RESULT_OK) {
val filename = data?.dataString
var keyUri: Uri? = null
if (filename != null) {
keyUri = UriUtil.parse(filename)
}
onFileSelected?.invoke(keyUri)
}
return true
}
GET_CONTENT, OPEN_DOC -> {
if (resultCode == RESULT_OK) {
if (data != null) {
val uri = data.data
if (uri != null) {
UriUtil.takeUriPermission(activity?.contentResolver, uri)
onFileSelected?.invoke(uri)
}
}
}
return true
}
}
return false
} }
/** /**
@@ -155,62 +148,50 @@ class ExternalFileHelper {
} }
} }
fun createDocument(titleString: String, class OpenDocument : ActivityResultContracts.OpenDocument() {
typeString: String = "application/octet-stream"): Int? { @SuppressLint("InlinedApi")
val idCode = getUnusedCreateFileRequestCode() override fun createIntent(context: Context, input: Array<out String>): Intent {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { return super.createIntent(context, input).apply {
try { addCategory(Intent.CATEGORY_OPENABLE)
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
addCategory(Intent.CATEGORY_OPENABLE) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
type = typeString addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
putExtra(Intent.EXTRA_TITLE, titleString)
} }
if (fragment != null) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
fragment?.startActivityForResult(intent, idCode) addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
else
activity?.startActivityForResult(intent, idCode)
return idCode
} catch (e: Exception) {
Log.e(TAG, "Unable to create document", e)
showFileManagerDialogFragment()
} }
} else {
showFileManagerDialogFragment()
} }
return null
} }
/** class GetContent : ActivityResultContracts.GetContent() {
* To use in onActivityResultCallback in Fragment or Activity @SuppressLint("InlinedApi")
* @param onFileCreated Callback retrieve from data override fun createIntent(context: Context, input: String): Intent {
* @return true if requestCode was captured, false elsewhere return super.createIntent(context, input).apply {
*/ addCategory(Intent.CATEGORY_OPENABLE)
fun onCreateDocumentResult(requestCode: Int, resultCode: Int, data: Intent?, addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
onFileCreated: (fileCreated: Uri?)->Unit) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Retrieve the created URI from the file manager addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
if (fileRequestCodes.contains(requestCode) && resultCode == RESULT_OK) { }
onFileCreated.invoke(data?.data) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
fileRequestCodes.remove(requestCode) addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
} }
} }
class CreateDocument(private val typeString: String) : ActivityResultContracts.CreateDocument() {
override fun createIntent(context: Context, input: String): Intent {
return super.createIntent(context, input).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
}
}
}
companion object { companion object {
private const val TAG = "OpenFileHelper" private const val TAG = "OpenFileHelper"
private const val GET_CONTENT = 25745
private const val OPEN_DOC = 25845
private const val FILE_BROWSE = 25645
private var CREATE_FILE_REQUEST_CODE_DEFAULT = 3853
private var fileRequestCodes = ArrayList<Int>()
private fun getUnusedCreateFileRequestCode(): Int {
val newCreateFileRequestCode = CREATE_FILE_REQUEST_CODE_DEFAULT++
fileRequestCodes.add(newCreateFileRequestCode)
return newCreateFileRequestCode
}
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager, fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager,
typeString: String = "application/octet-stream"): Boolean { typeString: String = "application/octet-stream"): Boolean {
@@ -231,7 +212,7 @@ class ExternalFileHelper {
fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) { fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) {
externalFileHelper?.let { fileHelper -> externalFileHelper?.let { fileHelper ->
setOnClickListener { setOnClickListener {
fileHelper.openDocument() fileHelper.openDocument(false)
} }
setOnLongClickListener { setOnLongClickListener {
fileHelper.openDocument(true) fileHelper.openDocument(true)

View File

@@ -100,7 +100,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
} }
mDatabaseViewModel.saveDefaultUsername.observe(this) { mDatabaseViewModel.saveDefaultUsername.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save) mDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(it.oldValue, it.newValue, it.save)
} }
mDatabaseViewModel.saveColor.observe(this) { mDatabaseViewModel.saveColor.observe(this) {
@@ -180,8 +180,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
closeDatabase(database) closeDatabase(database)
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null) if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
// Add onActivityForResult response mExitLock = true
setResult(RESULT_EXIT_LOCK)
closeOptionsMenu() closeOptionsMenu()
finish() finish()
} }
@@ -353,14 +352,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable) mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_EXIT_LOCK) {
mExitLock = true
lockAndExit()
}
}
private fun checkRegister() { private fun checkRegister() {
// If in ave or registration mode, don't allow read only // If in ave or registration mode, don't allow read only
if ((mSpecialMode == SpecialMode.SAVE if ((mSpecialMode == SpecialMode.SAVE
@@ -440,8 +431,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
const val TAG = "LockingActivity" const val TAG = "LockingActivity"
const val RESULT_EXIT_LOCK = 1450
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY" const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
const val TIMEOUT_ENABLE_KEY_DEFAULT = true const val TIMEOUT_ENABLE_KEY_DEFAULT = true

View File

@@ -39,7 +39,11 @@ object Stylish {
*/ */
fun load(context: Context) { fun load(context: Context) {
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName) Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
themeString = PreferencesUtil.getStyle(context) try {
themeString = PreferencesUtil.getStyle(context)
} catch (e: Exception) {
Log.e("Stylish", "Unable to get preference style", e)
}
} }
fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String { fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {

View File

@@ -23,12 +23,12 @@ import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.annotation.StyleRes
import androidx.fragment.app.Fragment
import androidx.appcompat.view.ContextThemeWrapper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.StyleRes
import androidx.appcompat.view.ContextThemeWrapper
import androidx.fragment.app.Fragment
abstract class StylishFragment : Fragment() { abstract class StylishFragment : Fragment() {
@@ -42,7 +42,6 @@ abstract class StylishFragment : Fragment() {
contextThemed = ContextThemeWrapper(context, themeId) contextThemed = ContextThemeWrapper(context, themeId)
} }
@Suppress("DEPRECATION")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// To fix status bar color // To fix status bar color
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@@ -58,6 +57,7 @@ abstract class StylishFragment : Fragment() {
try { try {
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar)) val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
if (taWindowStatusLight?.getBoolean(0, false) == true) { if (taWindowStatusLight?.getBoolean(0, false) == true) {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} }
taWindowStatusLight?.recycle() taWindowStatusLight?.recycle()

View File

@@ -67,8 +67,10 @@ class NodeAdapter (private val context: Context,
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
private var mPrefSizeMultiplier: Float = 0F private var mPrefSizeMultiplier: Float = 0F
private var mSubtextDefaultDimension: Float = 0F private var mTextDefaultDimension: Float = 0F
private var mInfoTextDefaultDimension: Float = 0F private var mSubTextDefaultDimension: Float = 0F
private var mMetaTextDefaultDimension: Float = 0F
private var mOtpTokenTextDefaultDimension: Float = 0F
private var mNumberChildrenTextDefaultDimension: Float = 0F private var mNumberChildrenTextDefaultDimension: Float = 0F
private var mIconDefaultDimension: Float = 0F private var mIconDefaultDimension: Float = 0F
@@ -303,8 +305,10 @@ class NodeAdapter (private val context: Context,
mInflater.inflate(R.layout.item_list_nodes_entry, parent, false) mInflater.inflate(R.layout.item_list_nodes_entry, parent, false)
} }
val nodeViewHolder = NodeViewHolder(view) val nodeViewHolder = NodeViewHolder(view)
mInfoTextDefaultDimension = nodeViewHolder.text.textSize mTextDefaultDimension = nodeViewHolder.text.textSize
mSubtextDefaultDimension = nodeViewHolder.subText.textSize mSubTextDefaultDimension = nodeViewHolder.subText?.textSize ?: mSubTextDefaultDimension
mMetaTextDefaultDimension = nodeViewHolder.meta.textSize
mOtpTokenTextDefaultDimension = nodeViewHolder.otpToken?.textSize ?: mOtpTokenTextDefaultDimension
nodeViewHolder.numberChildren?.let { nodeViewHolder.numberChildren?.let {
mNumberChildrenTextDefaultDimension = it.textSize mNumberChildrenTextDefaultDimension = it.textSize
} }
@@ -315,7 +319,9 @@ class NodeAdapter (private val context: Context,
val subNode = mNodeSortedList.get(position) val subNode = mNodeSortedList.get(position)
// Node selection // Node selection
holder.container.isSelected = mActionNodesList.contains(subNode) holder.container.apply {
isSelected = mActionNodesList.contains(subNode)
}
// Assign image // Assign image
val iconColor = if (holder.container.isSelected) val iconColor = if (holder.container.isSelected)
@@ -337,19 +343,18 @@ class NodeAdapter (private val context: Context,
// Assign text // Assign text
holder.text.apply { holder.text.apply {
text = subNode.title text = subNode.title
setTextSize(mTextSizeUnit, mInfoTextDefaultDimension, mPrefSizeMultiplier) setTextSize(mTextSizeUnit, mTextDefaultDimension, mPrefSizeMultiplier)
strikeOut(subNode.isCurrentlyExpires) strikeOut(subNode.isCurrentlyExpires)
} }
// Add subText with username
holder.subText.apply {
text = ""
strikeOut(subNode.isCurrentlyExpires)
visibility = View.GONE
}
// Add meta text to show UUID // Add meta text to show UUID
holder.meta.apply { holder.meta.apply {
text = subNode.nodeId.toString() if (mShowUUID) {
visibility = if (mShowUUID) View.VISIBLE else View.GONE text = subNode.nodeId.toString()
setTextSize(mTextSizeUnit, mMetaTextDefaultDimension, mPrefSizeMultiplier)
visibility = View.VISIBLE
} else {
visibility = View.GONE
}
} }
// Specific elements for entry // Specific elements for entry
@@ -358,12 +363,16 @@ class NodeAdapter (private val context: Context,
database.startManageEntry(entry) database.startManageEntry(entry)
holder.text.text = entry.getVisualTitle() holder.text.text = entry.getVisualTitle()
holder.subText.apply { // Add subText with username
holder.subText?.apply {
val username = entry.username val username = entry.username
if (mShowUserNames && username.isNotEmpty()) { if (mShowUserNames && username.isNotEmpty()) {
visibility = View.VISIBLE visibility = View.VISIBLE
text = username text = username
setTextSize(mTextSizeUnit, mSubtextDefaultDimension, mPrefSizeMultiplier) setTextSize(mTextSizeUnit, mSubTextDefaultDimension, mPrefSizeMultiplier)
strikeOut(subNode.isCurrentlyExpires)
} else {
visibility = View.GONE
} }
} }
@@ -431,7 +440,10 @@ class NodeAdapter (private val context: Context,
} }
} }
} }
holder?.otpToken?.text = otpElement?.token holder?.otpToken?.apply {
text = otpElement?.token
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
}
holder?.otpContainer?.setOnClickListener { holder?.otpContainer?.setOnClickListener {
otpElement?.token?.let { token -> otpElement?.token?.let { token ->
Toast.makeText( Toast.makeText(
@@ -483,7 +495,7 @@ class NodeAdapter (private val context: Context,
var imageIdentifier: ImageView? = itemView.findViewById(R.id.node_image_identifier) var imageIdentifier: ImageView? = itemView.findViewById(R.id.node_image_identifier)
var icon: ImageView = itemView.findViewById(R.id.node_icon) var icon: ImageView = itemView.findViewById(R.id.node_icon)
var text: TextView = itemView.findViewById(R.id.node_text) var text: TextView = itemView.findViewById(R.id.node_text)
var subText: TextView = itemView.findViewById(R.id.node_subtext) var subText: TextView? = itemView.findViewById(R.id.node_subtext)
var meta: TextView = itemView.findViewById(R.id.node_meta) var meta: TextView = itemView.findViewById(R.id.node_meta)
var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container) var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container)
var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress) var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress)

View File

@@ -16,9 +16,9 @@ import com.kunzisoft.keepass.icons.IconDrawableFactory
class TemplatesSelectorAdapter(private val context: Context, class TemplatesSelectorAdapter(private val context: Context,
private val iconDrawableFactory: IconDrawableFactory?,
private var templates: List<Template>): BaseAdapter() { private var templates: List<Template>): BaseAdapter() {
var iconDrawableFactory: IconDrawableFactory? = null
private val inflater: LayoutInflater = LayoutInflater.from(context) private val inflater: LayoutInflater = LayoutInflater.from(context)
private var mIconColor = Color.BLACK private var mIconColor = Color.BLACK

View File

@@ -4,4 +4,4 @@ import android.app.assist.AssistStructure
import android.view.inputmethod.InlineSuggestionsRequest import android.view.inputmethod.InlineSuggestionsRequest
data class AutofillComponent(val assistStructure: AssistStructure, data class AutofillComponent(val assistStructure: AssistStructure,
val inlineSuggestionsRequest: InlineSuggestionsRequest?) val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)

View File

@@ -25,7 +25,6 @@ import android.app.PendingIntent
import android.app.assist.AssistStructure import android.app.assist.AssistStructure
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentSender
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
@@ -35,11 +34,13 @@ import android.service.autofill.InlinePresentation
import android.util.Log import android.util.Log
import android.view.autofill.AutofillManager import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue import android.view.autofill.AutofillValue
import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.RemoteViews import android.widget.RemoteViews
import android.widget.Toast import android.widget.Toast
import android.widget.inline.InlinePresentationSpec import android.widget.inline.InlinePresentationSpec
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.autofill.inline.UiVersions import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi import androidx.autofill.inline.v1.InlineSuggestionUi
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -49,21 +50,19 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.template.TemplateField
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.database.element.template.TemplateField
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 kotlin.collections.ArrayList import com.kunzisoft.keepass.utils.LOCK_ACTION
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
object AutofillHelper { object AutofillHelper {
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
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 retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure -> intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
@@ -112,7 +111,7 @@ object AutofillHelper {
database: Database, database: Database,
entryInfo: EntryInfo, entryInfo: EntryInfo,
struct: StructureParser.Result, struct: StructureParser.Result,
inlinePresentation: InlinePresentation?): Dataset? { additionalBuild: ((build: Dataset.Builder) -> Unit)? = null): Dataset? {
val title = makeEntryTitle(entryInfo) val title = makeEntryTitle(entryInfo)
val views = newRemoteViews(context, database, title, entryInfo.icon) val views = newRemoteViews(context, database, title, entryInfo.icon)
val builder = Dataset.Builder(views) val builder = Dataset.Builder(views)
@@ -201,11 +200,7 @@ object AutofillHelper {
} }
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { additionalBuild?.invoke(builder)
inlinePresentation?.let {
builder.setInlinePresentation(it)
}
}
return try { return try {
builder.build() builder.build()
@@ -236,40 +231,51 @@ object AutofillHelper {
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private fun buildInlinePresentationForEntry(context: Context, private fun buildInlinePresentationForEntry(context: Context,
database: Database, database: Database,
inlineSuggestionsRequest: InlineSuggestionsRequest, compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
positionItem: Int, positionItem: Int,
entryInfo: EntryInfo): InlinePresentation? { entryInfo: EntryInfo): InlinePresentation? {
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
if (positionItem <= maxSuggestion - 1 if (positionItem <= maxSuggestion - 1
&& inlinePresentationSpecs.size > positionItem) { && inlinePresentationSpecs.size > positionItem
val inlinePresentationSpec = inlinePresentationSpecs[positionItem] ) {
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
// 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))
return null return null
// Build the content for IME UI // Build the content for IME UI
val pendingIntent = PendingIntent.getActivity(context, val pendingIntent = PendingIntent.getActivity(
context,
0, 0,
Intent(context, AutofillSettingsActivity::class.java), Intent(context, AutofillSettingsActivity::class.java),
0) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return InlinePresentation( PendingIntent.FLAG_IMMUTABLE
} else {
0
}
)
return InlinePresentation(
InlineSuggestionUi.newContentBuilder(pendingIntent).apply { InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
setContentDescription(context.getString(R.string.autofill_sign_in_prompt)) setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
setTitle(entryInfo.title) setTitle(entryInfo.title)
setSubtitle(entryInfo.username) setSubtitle(entryInfo.username)
setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply { setStartIcon(
setTintBlendMode(BlendMode.DST) Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
}) setTintBlendMode(BlendMode.DST)
})
buildIconFromEntry(context, database, entryInfo)?.let { icon -> buildIconFromEntry(context, database, entryInfo)?.let { icon ->
setEndIcon(icon.apply { setEndIcon(icon.apply {
setTintBlendMode(BlendMode.DST) setTintBlendMode(BlendMode.DST)
}) })
} }
}.build().slice, inlinePresentationSpec, false) }.build().slice, inlinePresentationSpec, false
)
}
} }
return null return null
} }
@@ -299,7 +305,7 @@ object AutofillHelper {
database: Database, database: Database,
entriesInfo: List<EntryInfo>, entriesInfo: List<EntryInfo>,
parseResult: StructureParser.Result, parseResult: StructureParser.Result,
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse? { compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): 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) {
@@ -320,7 +326,7 @@ 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) {
inlineSuggestionsRequest?.let { 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) {
@@ -332,14 +338,19 @@ object AutofillHelper {
} }
entriesInfo.forEachIndexed { _, entry -> entriesInfo.forEachIndexed { _, entry ->
val inlinePresentation = if (numberInlineSuggestions > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (numberInlineSuggestions > 0
inlineSuggestionsRequest?.let { && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
buildInlinePresentationForEntry(context, database, inlineSuggestionsRequest, numberInlineSuggestions--, entry) && compatInlineSuggestionsRequest != null) {
} responseBuilder.addDataset(buildDataset(context, database, entry, parseResult) { builder ->
buildInlinePresentationForEntry(context, database,
compatInlineSuggestionsRequest, numberInlineSuggestions--, entry
)?.let { inlinePresentation ->
builder.setInlinePresentation(inlinePresentation)
}
})
} else { } else {
null responseBuilder.addDataset(buildDataset(context, database, entry, parseResult))
} }
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult, inlinePresentation))
} }
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) { if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
@@ -351,14 +362,14 @@ object AutofillHelper {
} }
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry) val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context, val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
searchInfo, inlineSuggestionsRequest) searchInfo, compatInlineSuggestionsRequest)
parseResult.allAutofillIds().let { autofillIds -> parseResult.allAutofillIds().let { autofillIds ->
autofillIds.forEach { id -> autofillIds.forEach { id ->
val builder = Dataset.Builder(manualSelectionView) val builder = Dataset.Builder(manualSelectionView)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.let { compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0] val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent) val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
inlinePresentation?.let { inlinePresentation?.let {
@@ -403,11 +414,11 @@ object AutofillHelper {
StructureParser(structure).parse()?.let { result -> StructureParser(structure).parse()?.let { result ->
// New Response // New Response
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val inlineSuggestionsRequest = activity.intent?.getParcelableExtra<InlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST) val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtra<CompatInlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
if (inlineSuggestionsRequest != null) { if (compatInlineSuggestionsRequest != null) {
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show() Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
} }
buildResponse(activity, database, entriesInfo, result, inlineSuggestionsRequest) buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest)
} else { } else {
buildResponse(activity, database, entriesInfo, result, null) buildResponse(activity, database, entriesInfo, result, null)
} }
@@ -427,37 +438,44 @@ object AutofillHelper {
} }
} }
fun buildActivityResultLauncher(activity: AppCompatActivity,
lockDatabase: Boolean = false): ActivityResultLauncher<Intent> {
return activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
// Utility method to loop and close each activity with return data
if (it.resultCode == Activity.RESULT_OK) {
activity.setResult(it.resultCode, it.data)
}
if (it.resultCode == Activity.RESULT_CANCELED) {
activity.setResult(Activity.RESULT_CANCELED)
}
activity.finish()
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(activity)) {
// Close the database
activity.sendBroadcast(Intent(LOCK_ACTION))
}
}
}
/** /**
* Utility method to start an activity with an Autofill for result * Utility method to start an activity with an Autofill for result
*/ */
fun startActivityForAutofillResult(activity: Activity, fun startActivityForAutofillResult(activity: AppCompatActivity,
intent: Intent, intent: Intent,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent, autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) { searchInfo: SearchInfo?) {
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION) EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure) intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) { && PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
autofillComponent.inlineSuggestionsRequest?.let { autofillComponent.compatInlineSuggestionsRequest?.let {
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it) intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
} }
} }
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo) EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE) activityResultLauncher?.launch(intent)
}
/**
* Utility method to loop and close each activity with return data
*/
fun onActivityResultSetResultAndFinish(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == AUTOFILL_RESPONSE_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
activity.setResult(resultCode, data)
}
if (resultCode == Activity.RESULT_CANCELED) {
activity.setResult(Activity.RESULT_CANCELED)
}
activity.finish()
}
} }
} }

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.autofill
import android.annotation.TargetApi
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.service.autofill.FillRequest
import android.view.inputmethod.InlineSuggestionsRequest
import androidx.annotation.RequiresApi
/**
* Utility class only to prevent java.lang.NoClassDefFoundError for old Android version and new lib compilation
*/
@RequiresApi(Build.VERSION_CODES.O)
class CompatInlineSuggestionsRequest : Parcelable {
@TargetApi(Build.VERSION_CODES.R)
var inlineSuggestionsRequest: InlineSuggestionsRequest? = null
private set
constructor(fillRequest: FillRequest) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
this.inlineSuggestionsRequest = fillRequest.inlineSuggestionsRequest
} else {
this.inlineSuggestionsRequest = null
}
}
@RequiresApi(Build.VERSION_CODES.R)
constructor(inlineSuggestionsRequest: InlineSuggestionsRequest?) {
this.inlineSuggestionsRequest = inlineSuggestionsRequest
}
constructor(parcel: Parcel) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
this.inlineSuggestionsRequest =
parcel.readParcelable(FillRequest::class.java.classLoader)
}
else {
this.inlineSuggestionsRequest = null
}
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
parcel.writeParcelable(inlineSuggestionsRequest, flags)
}
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<CompatInlineSuggestionsRequest> {
override fun createFromParcel(parcel: Parcel): CompatInlineSuggestionsRequest {
return CompatInlineSuggestionsRequest(parcel)
}
override fun newArray(size: Int): Array<CompatInlineSuggestionsRequest?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -109,7 +109,7 @@ class KeeAutofillService : AutofillService() {
searchInfo.webDomain = webDomainWithoutSubDomain searchInfo.webDomain = webDomainWithoutSubDomain
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) { && autofillInlineSuggestionsEnabled) {
request.inlineSuggestionsRequest CompatInlineSuggestionsRequest(request)
} else { } else {
null null
} }
@@ -127,7 +127,7 @@ class KeeAutofillService : AutofillService() {
private fun launchSelection(database: Database?, private fun launchSelection(database: Database?,
searchInfo: SearchInfo, searchInfo: SearchInfo,
parseResult: StructureParser.Result, parseResult: StructureParser.Result,
inlineSuggestionsRequest: InlineSuggestionsRequest?, inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
callback: FillCallback) { callback: FillCallback) {
SearchHelper.checkAutoSearchInfo(this, SearchHelper.checkAutoSearchInfo(this,
database, database,
@@ -155,7 +155,7 @@ class KeeAutofillService : AutofillService() {
private fun showUIForEntrySelection(parseResult: StructureParser.Result, private fun showUIForEntrySelection(parseResult: StructureParser.Result,
database: Database?, database: Database?,
searchInfo: SearchInfo, searchInfo: SearchInfo,
inlineSuggestionsRequest: InlineSuggestionsRequest?, inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
callback: FillCallback) { callback: FillCallback) {
parseResult.allAutofillIds().let { autofillIds -> parseResult.allAutofillIds().let { autofillIds ->
if (autofillIds.isNotEmpty()) { if (autofillIds.isNotEmpty()) {
@@ -249,7 +249,7 @@ class KeeAutofillService : AutofillService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) { && autofillInlineSuggestionsEnabled) {
var inlinePresentation: InlinePresentation? = null var inlinePresentation: InlinePresentation? = null
inlineSuggestionsRequest?.let { inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
if (inlineSuggestionsRequest.maxSuggestionCount > 0 if (inlineSuggestionsRequest.maxSuggestionCount > 0
&& inlinePresentationSpecs.size > 0) { && inlinePresentationSpecs.size > 0) {
@@ -262,9 +262,13 @@ class KeeAutofillService : AutofillService() {
inlinePresentation = InlinePresentation( inlinePresentation = InlinePresentation(
InlineSuggestionUi.newContentBuilder( InlineSuggestionUi.newContentBuilder(
PendingIntent.getActivity(this, PendingIntent.getActivity(this,
0, 0,
Intent(this, AutofillSettingsActivity::class.java), Intent(this, AutofillSettingsActivity::class.java),
0) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
})
).apply { ).apply {
setContentDescription(getString(R.string.autofill_sign_in_prompt)) setContentDescription(getString(R.string.autofill_sign_in_prompt))
setTitle(getString(R.string.autofill_sign_in_prompt)) setTitle(getString(R.string.autofill_sign_in_prompt))
@@ -277,8 +281,9 @@ class KeeAutofillService : AutofillService() {
} }
// Build response // Build response
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation) responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
} else {
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
} }
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
callback.onSuccess(responseBuilder.build()) callback.onSuccess(responseBuilder.build())
} }
} }

View File

@@ -272,12 +272,12 @@ class StructureParser(private val structure: AssistStructure) {
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean { private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
val autofillId = node.autofillId val autofillId = node.autofillId
val nodHtml = node.htmlInfo val nodHtml = node.htmlInfo
when (nodHtml?.tag?.toLowerCase(Locale.ENGLISH)) { when (nodHtml?.tag?.lowercase(Locale.ENGLISH)) {
"input" -> { "input" -> {
nodHtml.attributes?.forEach { pairAttribute -> nodHtml.attributes?.forEach { pairAttribute ->
when (pairAttribute.first.toLowerCase(Locale.ENGLISH)) { when (pairAttribute.first.lowercase(Locale.ENGLISH)) {
"type" -> { "type" -> {
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) { when (pairAttribute.second.lowercase(Locale.ENGLISH)) {
"tel", "email" -> { "tel", "email" -> {
result?.usernameId = autofillId result?.usernameId = autofillId
result?.usernameValue = node.autofillValue result?.usernameValue = node.autofillValue

View File

@@ -19,6 +19,7 @@
*/ */
package com.kunzisoft.keepass.biometric package com.kunzisoft.keepass.biometric
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
@@ -27,9 +28,11 @@ import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.view.* import android.view.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.getkeepsafe.taptargetview.TapTargetView import com.getkeepsafe.taptargetview.TapTargetView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
@@ -39,6 +42,7 @@ import com.kunzisoft.keepass.database.exception.IODatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -59,9 +63,12 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
/** /**
* Manage setting to auto open biometric prompt * Manage setting to auto open biometric prompt
*/ */
private var mAutoOpenPrompt: Boolean = false private var mAutoOpenPrompt: Boolean
get() { get() {
return field && mAutoOpenPromptEnabled return mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt && mAutoOpenPromptEnabled
}
set(value) {
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = value
} }
// Variable to check if the prompt can be open (if the right activity is currently shown) // Variable to check if the prompt can be open (if the right activity is currently shown)
@@ -72,6 +79,8 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by activityViewModels()
// Only to fix multiple fingerprint menu #332 // Only to fix multiple fingerprint menu #332
private var mAllowAdvancedUnlockMenu = false private var mAllowAdvancedUnlockMenu = false
private var mAddBiometricMenuInProgress = false private var mAddBiometricMenuInProgress = false
@@ -79,6 +88,15 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
// Only keep connection when we request a device credential activity // Only keep connection when we request a device credential activity
private var keepConnection = false private var keepConnection = false
private var mDeviceCredentialResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
// To wait resume
if (keepConnection) {
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = result.resultCode == Activity.RESULT_OK
}
keepConnection = false
}
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
@@ -97,10 +115,21 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
retainInstance = true
setHasOptionsMenu(true) setHasOptionsMenu(true)
cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext) cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
mAdvancedUnlockViewModel.onInitAdvancedUnlockModeRequested.observe(this) {
initAdvancedUnlockMode()
}
mAdvancedUnlockViewModel.onUnlockAvailabilityCheckRequested.observe(this) {
checkUnlockAvailability()
}
mAdvancedUnlockViewModel.onDatabaseFileLoaded.observe(this) {
onDatabaseLoaded(it)
}
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@@ -114,17 +143,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
return rootView return rootView
} }
private data class ActivityResult(var requestCode: Int, var resultCode: Int, var data: Intent?)
private var activityResult: ActivityResult? = null
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// To wait resume
if (keepConnection) {
activityResult = ActivityResult(requestCode, resultCode, data)
}
keepConnection = false
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
context?.let { context?.let {
@@ -154,32 +172,38 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
fun loadDatabase(databaseUri: Uri?, autoOpenPrompt: Boolean) { private fun onDatabaseLoaded(databaseUri: Uri?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// To get device credential unlock result, only if same database uri // To get device credential unlock result, only if same database uri
if (databaseUri != null if (databaseUri != null
&& mAdvancedUnlockEnabled) { && mAdvancedUnlockEnabled) {
activityResult?.let { val deviceCredentialAuthSucceeded = mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded
deviceCredentialAuthSucceeded?.let {
if (databaseUri == databaseFileUri) { if (databaseUri == databaseFileUri) {
advancedUnlockManager?.onActivityResult(it.requestCode, it.resultCode) if (deviceCredentialAuthSucceeded == true) {
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationSucceeded()
} else {
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationFailed()
}
} else { } else {
disconnect() disconnect()
} }
} ?: run { } ?: run {
this.mAutoOpenPrompt = autoOpenPrompt if (databaseUri != databaseFileUri) {
connect(databaseUri) connect(databaseUri)
}
} }
} else { } else {
disconnect() disconnect()
} }
activityResult = null mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = null
} }
} }
/** /**
* Check unlock availability and change the current mode depending of device's state * Check unlock availability and change the current mode depending of device's state
*/ */
fun checkUnlockAvailability() { private fun checkUnlockAvailability() {
context?.let { context -> context?.let { context ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
allowOpenBiometricPrompt = true allowOpenBiometricPrompt = true
@@ -317,7 +341,8 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
if (cryptoPrompt.isDeviceCredentialOperation) if (cryptoPrompt.isDeviceCredentialOperation)
keepConnection = true keepConnection = true
try { try {
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt) advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt,
mDeviceCredentialResultLauncher)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to open advanced unlock prompt", e) Log.e(TAG, "Unable to open advanced unlock prompt", e)
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized) setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
@@ -369,8 +394,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} ?: throw Exception("AdvancedUnlockManager not initialized") } ?: throw Exception("AdvancedUnlockManager not initialized")
} }
@Synchronized private fun initAdvancedUnlockMode() {
fun initAdvancedUnlockMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mAllowAdvancedUnlockMenu = false mAllowAdvancedUnlockMenu = false
try { try {
@@ -444,6 +468,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
fun deleteEncryptedDatabaseKey() { fun deleteEncryptedDatabaseKey() {
mAllowAdvancedUnlockMenu = false
advancedUnlockManager?.closeBiometricPrompt() advancedUnlockManager?.closeBiometricPrompt()
databaseFileUri?.let { databaseUri -> databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) { cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
@@ -516,6 +541,11 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} }
} }
@RequiresApi(Build.VERSION_CODES.M)
override fun onUnrecoverableKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
}
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
override fun onInvalidKeyException(e: Exception) { override fun onInvalidKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key) setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)

View File

@@ -19,15 +19,18 @@
*/ */
package com.kunzisoft.keepass.biometric package com.kunzisoft.keepass.biometric
import android.app.Activity
import android.app.KeyguardManager import android.app.KeyguardManager
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.* import androidx.biometric.BiometricManager.Authenticators.*
@@ -35,6 +38,7 @@ import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import java.security.KeyStore import java.security.KeyStore
import java.security.UnrecoverableKeyException import java.security.UnrecoverableKeyException
@@ -136,18 +140,24 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
// and the constrains (purposes) in the constructor of the Builder // and the constrains (purposes) in the constructor of the Builder
keyGenerator?.init( keyGenerator?.init(
KeyGenParameterSpec.Builder( KeyGenParameterSpec.Builder(
ADVANCED_UNLOCK_KEYSTORE_KEY, ADVANCED_UNLOCK_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.apply {
// Require the user to authenticate with a fingerprint to authorize every use // Require the user to authenticate with a fingerprint to authorize every use
// of the key, don't use it for device credential because it's the user authentication // of the key, don't use it for device credential because it's the user authentication
.apply { if (biometricUnlockEnable) {
if (biometricUnlockEnable) { setUserAuthenticationRequired(true)
setUserAuthenticationRequired(true)
}
} }
.build()) // To store in the security chip
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
&& retrieveContext().packageManager.hasSystemFeature(
PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
setIsStrongBoxBacked(true)
}
}
.build())
keyGenerator?.generateKey() keyGenerator?.generateKey()
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -164,8 +174,12 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
return null return null
} }
fun initEncryptData(actionIfCypherInit fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) {
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) { initEncryptData(actionIfCypherInit, true)
}
private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
firstLaunch: Boolean) {
if (!isKeyManagerInitialized) { if (!isKeyManagerInitialized) {
return return
} }
@@ -185,10 +199,15 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} catch (unrecoverableKeyException: UnrecoverableKeyException) { } catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException) Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
advancedUnlockCallback?.onInvalidKeyException(unrecoverableKeyException) advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) { } catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException) Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException) if (firstLaunch) {
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
initEncryptData(actionIfCypherInit, false)
} else {
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to initialize encrypt data", e) Log.e(TAG, "Unable to initialize encrypt data", e)
advancedUnlockCallback?.onGenericException(e) advancedUnlockCallback?.onGenericException(e)
@@ -214,8 +233,14 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} }
fun initDecryptData(ivSpecValue: String, actionIfCypherInit fun initDecryptData(ivSpecValue: String,
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) { actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
initDecryptData(ivSpecValue, actionIfCypherInit, true)
}
private fun initDecryptData(ivSpecValue: String,
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
firstLaunch: Boolean = true) {
if (!isKeyManagerInitialized) { if (!isKeyManagerInitialized) {
return return
} }
@@ -239,10 +264,20 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} catch (unrecoverableKeyException: UnrecoverableKeyException) { } catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException) Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
deleteKeystoreKey() if (firstLaunch) {
deleteKeystoreKey()
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
} else {
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
}
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) { } catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException) Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException) if (firstLaunch) {
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
} else {
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to initialize decrypt data", e) Log.e(TAG, "Unable to initialize decrypt data", e)
advancedUnlockCallback?.onGenericException(e) advancedUnlockCallback?.onGenericException(e)
@@ -278,9 +313,9 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} }
@Suppress("DEPRECATION") fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt,
@Synchronized deviceCredentialResultLauncher: ActivityResultLauncher<Intent>
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) { ) {
// Init advanced unlock prompt // Init advanced unlock prompt
if (biometricPrompt == null) { if (biometricPrompt == null) {
biometricPrompt = BiometricPrompt(retrieveContext(), biometricPrompt = BiometricPrompt(retrieveContext(),
@@ -311,20 +346,10 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
else if (cryptoPrompt.isDeviceCredentialOperation) { else if (cryptoPrompt.isDeviceCredentialOperation) {
val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java) val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java)
retrieveContext().startActivityForResult( @Suppress("DEPRECATION")
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription), deviceCredentialResultLauncher.launch(
REQUEST_DEVICE_CREDENTIAL) keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription)
} )
}
@Synchronized
fun onActivityResult(requestCode: Int, resultCode: Int) {
if (requestCode == REQUEST_DEVICE_CREDENTIAL) {
if (resultCode == Activity.RESULT_OK) {
advancedUnlockCallback?.onAuthenticationSucceeded()
} else {
advancedUnlockCallback?.onAuthenticationFailed()
}
} }
} }
@@ -333,6 +358,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
interface AdvancedUnlockErrorCallback { interface AdvancedUnlockErrorCallback {
fun onUnrecoverableKeyException(e: Exception)
fun onInvalidKeyException(e: Exception) fun onInvalidKeyException(e: Exception)
fun onGenericException(e: Exception) fun onGenericException(e: Exception)
} }
@@ -355,8 +381,6 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7 private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
private const val REQUEST_DEVICE_CREDENTIAL = 556
@RequiresApi(api = Build.VERSION_CODES.M) @RequiresApi(api = Build.VERSION_CODES.M)
fun canAuthenticate(context: Context): Int { fun canAuthenticate(context: Context): Int {
return try { return try {
@@ -449,6 +473,10 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
override fun handleDecryptedResult(decryptedValue: String) {} override fun handleDecryptedResult(decryptedValue: String) {}
override fun onUnrecoverableKeyException(e: Exception) {
advancedCallback.onUnrecoverableKeyException(e)
}
override fun onInvalidKeyException(e: Exception) { override fun onInvalidKeyException(e: Exception) {
advancedCallback.onInvalidKeyException(e) advancedCallback.onInvalidKeyException(e)
} }
@@ -460,6 +488,33 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
deleteKeystoreKey() deleteKeystoreKey()
} }
} }
fun deleteAllEntryKeysInKeystoreForBiometric(activity: FragmentActivity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
deleteEntryKeyInKeystoreForBiometric(
activity,
object : AdvancedUnlockErrorCallback {
fun showException(e: Exception) {
Toast.makeText(activity,
activity.getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
Toast.LENGTH_SHORT).show()
}
override fun onUnrecoverableKeyException(e: Exception) {
showException(e)
}
override fun onInvalidKeyException(e: Exception) {
showException(e)
}
override fun onGenericException(e: Exception) {
showException(e)
}
})
}
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
}
} }
} }

View File

@@ -192,17 +192,17 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
private val MIN_VERSION = UnsignedInt(0x10) private val MIN_VERSION = UnsignedInt(0x10)
private val MAX_VERSION = UnsignedInt(0x13) private val MAX_VERSION = UnsignedInt(0x13)
private val DEFAULT_ITERATIONS = UnsignedLong(2L) private val DEFAULT_ITERATIONS = UnsignedLong(3L)
private val MIN_ITERATIONS = UnsignedLong(1L) private val MIN_ITERATIONS = UnsignedLong(1L)
private val MAX_ITERATIONS = UnsignedLong(4294967295L) private val MAX_ITERATIONS = UnsignedLong(4294967295L)
private val DEFAULT_MEMORY = UnsignedLong((1024L * 1024L)) private val DEFAULT_MEMORY = UnsignedLong((1024L * 1024L * 16L))
private val MIN_MEMORY = UnsignedLong(1024L * 8L) private val MIN_MEMORY = UnsignedLong(1024L * 8L)
private val MAX_MEMORY = UnsignedInt.MAX_VALUE.toKotlinLong() private val MAX_MEMORY = UnsignedInt.MAX_VALUE.toKotlinLong()
private const val MEMORY_BLOCK_SIZE: Long = 1024L private const val MEMORY_BLOCK_SIZE: Long = 1024L
private val DEFAULT_PARALLELISM = UnsignedInt(2) private val DEFAULT_PARALLELISM = UnsignedInt(4)
private val MIN_PARALLELISM = UnsignedInt.fromKotlinLong(1L) private val MIN_PARALLELISM = UnsignedInt.fromKotlinLong(1L)
private val MAX_PARALLELISM = UnsignedInt.fromKotlinLong(((1 shl 24) - 1)) private val MAX_PARALLELISM = UnsignedInt.fromKotlinLong(((1 shl 24) - 1).toLong())
} }
} }

View File

@@ -68,7 +68,7 @@ abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>>
pwEntry.notes = getString(getColumnIndex(COLUMN_INDEX_NOTES)) pwEntry.notes = getString(getColumnIndex(COLUMN_INDEX_NOTES))
pwEntry.expiryTime = DateInstant(getString(getColumnIndex(COLUMN_INDEX_EXPIRY_TIME))) pwEntry.expiryTime = DateInstant(getString(getColumnIndex(COLUMN_INDEX_EXPIRY_TIME)))
pwEntry.expires = getString(getColumnIndex(COLUMN_INDEX_EXPIRES)) pwEntry.expires = getString(getColumnIndex(COLUMN_INDEX_EXPIRES))
.toLowerCase(Locale.ENGLISH) != "false" .lowercase(Locale.ENGLISH) != "false"
} }
companion object { companion object {

View File

@@ -147,6 +147,10 @@ class Database {
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid) iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
} }
fun updateCustomIcon(customIcon: IconImageCustom) {
iconsManager.getIcon(customIcon.uuid).updateWith(customIcon)
}
fun getTemplates(templateCreation: Boolean): List<Template> { fun getTemplates(templateCreation: Boolean): List<Template> {
return mDatabaseKDBX?.getTemplates(templateCreation) ?: listOf() return mDatabaseKDBX?.getTemplates(templateCreation) ?: listOf()
} }

View File

@@ -177,16 +177,20 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
fun addChildrenFrom(group: Group) { fun addChildrenFrom(group: Group) {
group.groupKDB?.getChildEntries()?.forEach { entryToAdd -> group.groupKDB?.getChildEntries()?.forEach { entryToAdd ->
groupKDB?.addChildEntry(entryToAdd) groupKDB?.addChildEntry(entryToAdd)
entryToAdd.parent = groupKDB
} }
group.groupKDB?.getChildGroups()?.forEach { groupToAdd -> group.groupKDB?.getChildGroups()?.forEach { groupToAdd ->
groupKDB?.addChildGroup(groupToAdd) groupKDB?.addChildGroup(groupToAdd)
groupToAdd.parent = groupKDB
} }
group.groupKDBX?.getChildEntries()?.forEach { entryToAdd -> group.groupKDBX?.getChildEntries()?.forEach { entryToAdd ->
groupKDBX?.addChildEntry(entryToAdd) groupKDBX?.addChildEntry(entryToAdd)
entryToAdd.parent = groupKDBX
} }
group.groupKDBX?.getChildGroups()?.forEach { groupToAdd -> group.groupKDBX?.getChildGroups()?.forEach { groupToAdd ->
groupKDBX?.addChildGroup(groupToAdd) groupKDBX?.addChildGroup(groupToAdd)
groupToAdd.parent = groupKDBX
} }
} }

View File

@@ -31,6 +31,10 @@ class CustomIconPool(private val binaryCache: BinaryCache) : BinaryPool<UUID>(bi
return newUUID return newUUID
} }
fun getCustomIcon(key: UUID): IconImageCustom? {
return customIcons[key]
}
fun any(predicate: (IconImageCustom)-> Boolean): Boolean { fun any(predicate: (IconImageCustom)-> Boolean): Boolean {
return customIcons.any { predicate(it.value) } return customIcons.any { predicate(it.value) }
} }

View File

@@ -41,7 +41,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
private var kdfListV3: MutableList<KdfEngine> = ArrayList() private var kdfListV3: MutableList<KdfEngine> = ArrayList()
override val version: String override val version: String
get() = "KeePass 1" get() = "V1"
init { init {
kdfListV3.add(KdfFactory.aesKdf) kdfListV3.add(KdfFactory.aesKdf)

View File

@@ -156,7 +156,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
FILE_VERSION_41 -> "4.1" FILE_VERSION_41 -> "4.1"
else -> "UNKNOWN" else -> "UNKNOWN"
} }
return "KeePass 2 - KDBX$kdbxStringVersion" return "V2 - KDBX$kdbxStringVersion"
} }
override val kdfEngine: KdfEngine? override val kdfEngine: KdfEngine?

View File

@@ -124,25 +124,29 @@ abstract class DatabaseVersioned<
@Throws(IOException::class) @Throws(IOException::class)
protected fun getFileKey(keyInputStream: InputStream): ByteArray { protected fun getFileKey(keyInputStream: InputStream): ByteArray {
val keyData = keyInputStream.readBytes() try {
val keyData = keyInputStream.readBytes()
// Check XML key file // Check XML key file
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData)) val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
if (xmlKeyByteArray != null) { if (xmlKeyByteArray != null) {
return xmlKeyByteArray return xmlKeyByteArray
}
// Check 32 bytes key file
when (keyData.size) {
32 -> return keyData
64 -> try {
return Hex.decodeHex(String(keyData).toCharArray())
} catch (ignoredException: Exception) {
// Key is not base 64, treat it as binary data
} }
// Check 32 bytes key file
when (keyData.size) {
32 -> return keyData
64 -> try {
return Hex.decodeHex(String(keyData).toCharArray())
} catch (ignoredException: Exception) {
// Key is not base 64, treat it as binary data
}
}
// Hash file as binary data
return HashManager.hashSha256(keyData)
} catch (outOfMemoryError: OutOfMemoryError) {
throw IOException("Keyfile data is too large", outOfMemoryError)
} }
// Hash file as binary data
return HashManager.hashSha256(keyData)
} }
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? { protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {

View File

@@ -32,6 +32,16 @@ class IconImageCustom : IconImageDraw {
var name: String = "" var name: String = ""
var lastModificationTime: DateInstant? = null var lastModificationTime: DateInstant? = null
fun updateWith(icon: IconImageCustom) {
this.name = icon.name
this.lastModificationTime = icon.lastModificationTime
}
constructor(copy: IconImageCustom) {
this.uuid = copy.uuid
updateWith(copy)
}
constructor(name: String = "", lastModificationTime: DateInstant? = null) { constructor(name: String = "", lastModificationTime: DateInstant? = null) {
this.uuid = DatabaseVersioned.UUID_ZERO this.uuid = DatabaseVersioned.UUID_ZERO
this.name = name this.name = name

View File

@@ -65,7 +65,7 @@ class IconsManager(binaryCache: BinaryCache) {
} }
fun getIcon(iconUuid: UUID): IconImageCustom { fun getIcon(iconUuid: UUID): IconImageCustom {
return IconImageCustom(iconUuid) return customCache.getCustomIcon(iconUuid) ?: IconImageCustom(iconUuid)
} }
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean { fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {

View File

@@ -208,16 +208,8 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
when (attribute.type) { when (attribute.type) {
TemplateAttributeType.TEXT -> { TemplateAttributeType.TEXT -> {
try { try {
when (attribute.options.getNumberLines()) { // It's always a number of lines...
1 -> { attribute.options.setNumberLines(defaultOption.toInt())
// If one line, default attribute option is number of chars
attribute.options.setNumberChars(defaultOption.toInt())
}
else -> {
// else it's number of lines
attribute.options.setNumberLines(defaultOption.toInt())
}
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to transform default text option", e) Log.e(TAG, "Unable to transform default text option", e)
} }

View File

@@ -27,7 +27,7 @@ object TemplateField {
const val LABEL_DATE_OF_ISSUE = "Date of issue" const val LABEL_DATE_OF_ISSUE = "Date of issue"
const val LABEL_EMAIL = "Email" const val LABEL_EMAIL = "Email"
const val LABEL_EMAIL_ADDRESS = "Email address" const val LABEL_EMAIL_ADDRESS = "Email address"
const val LABEL_WIRELESS = "Wifi" const val LABEL_WIRELESS = "Wi-Fi"
const val LABEL_SSID = "SSID" const val LABEL_SSID = "SSID"
const val LABEL_TYPE = "Type" const val LABEL_TYPE = "Type"
const val LABEL_CRYPTOCURRENCY = "Cryptocurrency wallet" const val LABEL_CRYPTOCURRENCY = "Cryptocurrency wallet"

View File

@@ -130,6 +130,6 @@ constructor(private val databaseKDBX: DatabaseKDBX,
} }
companion object { companion object {
private val EndHeaderValue = byteArrayOf('\r'.toByte(), '\n'.toByte(), '\r'.toByte(), '\n'.toByte()) private val EndHeaderValue = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte(), '\r'.code.toByte(), '\n'.code.toByte())
} }
} }

View File

@@ -765,7 +765,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
var character: Char var character: Char
for (element in text) { for (element in text) {
character = element character = element
val hexChar = character.toInt() val hexChar = character.code
if ( if (
hexChar in 0x20..0xD7FF || hexChar in 0x20..0xD7FF ||
hexChar == 0x9 || hexChar == 0x9 ||

View File

@@ -119,7 +119,7 @@ class GroupActivityEducation(activity: Activity)
.outerCircleColorInt(getCircleColor()) .outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha()) .outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor()) .textColorInt(getTextColor())
.tintTarget(true) .tintTarget(false)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {
override fun onTargetClick(view: TapTargetView) { override fun onTargetClick(view: TapTargetView) {

View File

@@ -272,7 +272,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
if (entryInfoKey != null) { if (entryInfoKey != null) {
currentInputConnection.commitText(entryInfoKey!!.url, 1) currentInputConnection.commitText(entryInfoKey!!.url, 1)
} }
actionTabAutomatically() actionGoAutomatically()
} }
KEY_FIELDS -> { KEY_FIELDS -> {
if (entryInfoKey != null) { if (entryInfoKey != null) {

View File

@@ -234,7 +234,7 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
fun replaceBase32Chars(parameter: String): String { fun replaceBase32Chars(parameter: String): String {
// Add padding '=' at end if not Base32 length // Add padding '=' at end if not Base32 length
var parameterNewSize = parameter.toUpperCase(Locale.ENGLISH).removeSpaceChars() var parameterNewSize = parameter.uppercase(Locale.ENGLISH).removeSpaceChars()
while (parameterNewSize.length % 8 != 0) { while (parameterNewSize.length % 8 != 0) {
parameterNewSize += '=' parameterNewSize += '='
} }
@@ -264,7 +264,7 @@ enum class OtpTokenType {
companion object { companion object {
fun getFromString(tokenType: String): OtpTokenType { fun getFromString(tokenType: String): OtpTokenType {
return when (tokenType.toLowerCase(Locale.ENGLISH)) { return when (tokenType.lowercase(Locale.ENGLISH)) {
"s", "steam" -> STEAM "s", "steam" -> STEAM
"hotp" -> RFC4226 "hotp" -> RFC4226
else -> RFC6238 else -> RFC6238

View File

@@ -143,7 +143,7 @@ object OtpEntryFields {
if (otpPlainText != null && otpPlainText.isNotEmpty() && isOTPUri(otpPlainText)) { if (otpPlainText != null && otpPlainText.isNotEmpty() && isOTPUri(otpPlainText)) {
val uri = Uri.parse(otpPlainText.removeSpaceChars()) val uri = Uri.parse(otpPlainText.removeSpaceChars())
if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase(Locale.ENGLISH)) { if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.lowercase(Locale.ENGLISH)) {
Log.e(TAG, "Invalid or missing scheme in uri") Log.e(TAG, "Invalid or missing scheme in uri")
return false return false
} }
@@ -309,7 +309,7 @@ object OtpEntryFields {
} }
if (algorithmField != null) { if (algorithmField != null) {
otpElement.algorithm = otpElement.algorithm =
when (algorithmField.toUpperCase(Locale.ENGLISH)) { when (algorithmField.uppercase(Locale.ENGLISH)) {
TIMEOTP_ALGORITHM_SHA1_VALUE -> HashAlgorithm.SHA1 TIMEOTP_ALGORITHM_SHA1_VALUE -> HashAlgorithm.SHA1
TIMEOTP_ALGORITHM_SHA256_VALUE -> HashAlgorithm.SHA256 TIMEOTP_ALGORITHM_SHA256_VALUE -> HashAlgorithm.SHA256
TIMEOTP_ALGORITHM_SHA512_VALUE -> HashAlgorithm.SHA512 TIMEOTP_ALGORITHM_SHA512_VALUE -> HashAlgorithm.SHA512
@@ -417,7 +417,7 @@ object OtpEntryFields {
val output = HashMap<String, String>() val output = HashMap<String, String>()
for (element in elements) { for (element in elements) {
val pair = element.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() val pair = element.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
output[pair[0].toLowerCase(Locale.ENGLISH)] = pair[1] output[pair[0].lowercase(Locale.ENGLISH)] = pair[1]
} }
return output return output
} }

View File

@@ -0,0 +1,33 @@
package com.kunzisoft.keepass.receivers
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.util.Log
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.utils.DexUtil
import com.kunzisoft.keepass.utils.MagikeyboardUtil
class DexModeReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val enabled = when (intent?.action) {
"android.app.action.ENTER_KNOX_DESKTOP_MODE" -> {
Log.i(TAG, "Entered DeX mode")
false
}
"android.app.action.EXIT_KNOX_DESKTOP_MODE" -> {
Log.i(TAG, "Left DeX mode")
true
}
else -> return
}
MagikeyboardUtil.setEnabled(context!!, enabled)
}
companion object {
private val TAG = DexModeReceiver::class.java.name
}
}

View File

@@ -1,9 +1,11 @@
package com.kunzisoft.keepass.services package com.kunzisoft.keepass.services
import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
import android.content.* import android.content.*
import android.net.Uri import android.net.Uri
import android.os.Binder import android.os.Binder
import android.os.Build
import android.os.IBinder import android.os.IBinder
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
@@ -50,11 +52,20 @@ class AdvancedUnlockNotificationService : NotificationService() {
mTempCipherDao = ArrayList() mTempCipherDao = ArrayList()
} }
// It's simpler to use pendingIntent to perform REMOVE_ADVANCED_UNLOCK_KEY_ACTION
// because can be directly broadcast to another module or app
@SuppressLint("LaunchActivityFromNotification")
override fun onBind(intent: Intent): IBinder { override fun onBind(intent: Intent): IBinder {
super.onBind(intent) super.onBind(intent)
val pendingDeleteIntent = PendingIntent.getBroadcast(this, val pendingDeleteIntent = PendingIntent.getBroadcast(this,
4577, Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION), 0) 4577,
Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
})
val biometricUnlockEnabled = PreferencesUtil.isBiometricUnlockEnable(this) val biometricUnlockEnabled = PreferencesUtil.isBiometricUnlockEnable(this)
val notificationBuilder = buildNewNotification().apply { val notificationBuilder = buildNewNotification().apply {
setSmallIcon(if (biometricUnlockEnabled) { setSmallIcon(if (biometricUnlockEnabled) {

View File

@@ -24,6 +24,7 @@ import android.content.ContentResolver
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Binder import android.os.Binder
import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
@@ -188,20 +189,30 @@ class AttachmentFileNotificationService: LockNotificationService() {
private fun newNotification(attachmentNotification: AttachmentNotification) { private fun newNotification(attachmentNotification: AttachmentNotification) {
val pendingContentIntent = PendingIntent.getActivity(this, val pendingContentIntent = PendingIntent.getActivity(this,
0, 0,
Intent().apply { Intent().apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
setDataAndType(attachmentNotification.uri, setDataAndType(attachmentNotification.uri,
contentResolver.getType(attachmentNotification.uri)) contentResolver.getType(attachmentNotification.uri))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}, PendingIntent.FLAG_CANCEL_CURRENT) }, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
}
)
val pendingDeleteIntent = PendingIntent.getService(this, val pendingDeleteIntent = PendingIntent.getService(this,
0, 0,
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)
}, PendingIntent.FLAG_CANCEL_CURRENT) }, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
}
)
val fileName = UriUtil.getFileData(this, attachmentNotification.uri)?.name val fileName = UriUtil.getFileData(this, attachmentNotification.uri)?.name
?: attachmentNotification.uri.path ?: attachmentNotification.uri.path

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.services
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
@@ -112,7 +113,13 @@ class ClipboardEntryNotificationService : LockNotificationService() {
putParcelableArrayListExtra(EXTRA_CLIPBOARD_FIELDS, fieldsToAdd) putParcelableArrayListExtra(EXTRA_CLIPBOARD_FIELDS, fieldsToAdd)
} }
return PendingIntent.getService( return PendingIntent.getService(
this, 0, copyIntent, PendingIntent.FLAG_UPDATE_CURRENT) this, 0, copyIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
} }
private fun newNotification(title: String?, fieldsToAdd: ArrayList<ClipboardEntryNotificationField>) { private fun newNotification(title: String?, fieldsToAdd: ArrayList<ClipboardEntryNotificationField>) {
@@ -162,7 +169,13 @@ class ClipboardEntryNotificationService : LockNotificationService() {
val cleanIntent = Intent(this, ClipboardEntryNotificationService::class.java) val cleanIntent = Intent(this, ClipboardEntryNotificationService::class.java)
cleanIntent.action = ACTION_CLEAN_CLIPBOARD cleanIntent.action = ACTION_CLEAN_CLIPBOARD
val cleanPendingIntent = PendingIntent.getService( val cleanPendingIntent = PendingIntent.getService(
this, 0, cleanIntent, PendingIntent.FLAG_UPDATE_CURRENT) this, 0, cleanIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
builder.setDeleteIntent(cleanPendingIntent) builder.setDeleteIntent(cleanPendingIntent)
//Get settings //Get settings

View File

@@ -24,6 +24,7 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.* import android.os.*
import android.util.Log import android.util.Log
import androidx.media.app.NotificationCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
@@ -407,11 +408,21 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
this, this,
0, 0,
Intent(this, GroupActivity::class.java), Intent(this, GroupActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
) )
val pendingDeleteIntent = PendingIntent.getBroadcast( val pendingDeleteIntent = PendingIntent.getBroadcast(
this, this,
4576, Intent(LOCK_ACTION), 0 4576,
Intent(LOCK_ACTION),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
) )
// Add actions in notifications // Add actions in notifications
notificationBuilder.apply { notificationBuilder.apply {
@@ -420,9 +431,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
// Unfortunately swipe is disabled in lollipop+ // Unfortunately swipe is disabled in lollipop+
setDeleteIntent(pendingDeleteIntent) setDeleteIntent(pendingDeleteIntent)
addAction( addAction(
R.drawable.ic_lock_white_24dp, getString(R.string.lock), R.drawable.ic_lock_database_white_32dp, getString(R.string.lock),
pendingDeleteIntent pendingDeleteIntent
) )
// Won't work with Xiaomi and Kitkat
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
setStyle(
NotificationCompat.MediaStyle()
.setShowActionsInCompactView(0)
)
}
} }
} }
} }

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.services
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@@ -93,7 +94,13 @@ class KeyboardEntryNotificationService : LockNotificationService() {
val deleteIntent = Intent(this, KeyboardEntryNotificationService::class.java).apply { val deleteIntent = Intent(this, KeyboardEntryNotificationService::class.java).apply {
action = ACTION_CLEAN_KEYBOARD_ENTRY action = ACTION_CLEAN_KEYBOARD_ENTRY
} }
pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT) pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
val builder = buildNewNotification() val builder = buildNewNotification()
.setSmallIcon(R.drawable.notification_ic_keyboard_key_24dp) .setSmallIcon(R.drawable.notification_ic_keyboard_key_24dp)

View File

@@ -57,6 +57,7 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
} }
if (dialogFragment != null) { if (dialogFragment != null) {
@Suppress("DEPRECATION")
dialogFragment.setTargetFragment(this, 0) dialogFragment.setTargetFragment(this, 0)
dialogFragment.show(parentFragmentManager, TAG_AUTOFILL_PREF_FRAGMENT) dialogFragment.show(parentFragmentManager, TAG_AUTOFILL_PREF_FRAGMENT)
} }

View File

@@ -48,6 +48,7 @@ class MagikeyboardSettingsFragment : PreferenceFragmentCompat() {
} }
if (dialogFragment != null) { if (dialogFragment != null) {
@Suppress("DEPRECATION")
dialogFragment.setTargetFragment(this, 0) dialogFragment.setTargetFragment(this, 0)
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT) dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
} }

View File

@@ -40,7 +40,6 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.ProFeatureDialogFragment import com.kunzisoft.keepass.activities.dialogs.ProFeatureDialogFragment
import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment
import com.kunzisoft.keepass.activities.stylish.Stylish import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.education.Education import com.kunzisoft.keepass.education.Education
@@ -157,7 +156,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE) val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
intent.data = Uri.parse("package:com.kunzisoft.keepass.autofill.KeeAutofillService") intent.data = Uri.parse("package:com.kunzisoft.keepass.autofill.KeeAutofillService")
Log.d(javaClass.name, "Autofill enable service: intent=$intent") Log.d(javaClass.name, "Autofill enable service: intent=$intent")
startActivityForResult(intent, REQUEST_CODE_AUTOFILL) startActivity(intent)
} else { } else {
Log.d(javaClass.name, "Autofill service already enabled.") Log.d(javaClass.name, "Autofill service already enabled.")
} }
@@ -366,26 +365,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
) { _, _ -> ) { _, _ ->
validate?.invoke() validate?.invoke()
deleteKeysAlertDialog?.setOnDismissListener(null) deleteKeysAlertDialog?.setOnDismissListener(null)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
AdvancedUnlockManager.deleteEntryKeyInKeystoreForBiometric(
activity,
object : AdvancedUnlockManager.AdvancedUnlockErrorCallback {
fun showException(e: Exception) {
Toast.makeText(context,
getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
Toast.LENGTH_SHORT).show()
}
override fun onInvalidKeyException(e: Exception) {
showException(e)
}
override fun onGenericException(e: Exception) {
showException(e)
}
})
}
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
} }
.setNegativeButton(resources.getString(android.R.string.cancel) .setNegativeButton(resources.getString(android.R.string.cancel)
) { _, _ ->} ) { _, _ ->}
@@ -494,6 +474,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
} }
if (dialogFragment != null) { if (dialogFragment != null) {
@Suppress("DEPRECATION")
dialogFragment.setTargetFragment(this, 0) dialogFragment.setTargetFragment(this, 0)
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT) dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
} }
@@ -533,7 +514,6 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
} }
companion object { companion object {
private const val REQUEST_CODE_AUTOFILL = 5201
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT" private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
var DATABASE_APPEARANCE_PREFERENCE_CHANGED = false var DATABASE_APPEARANCE_PREFERENCE_CHANGED = false

View File

@@ -57,7 +57,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
private var dbNamePref: InputTextPreference? = null private var dbNamePref: InputTextPreference? = null
private var dbDescriptionPref: InputTextPreference? = null private var dbDescriptionPref: InputTextPreference? = null
private var dbDefaultUsername: InputTextPreference? = null private var dbDefaultUsernamePref: InputTextPreference? = null
private var dbCustomColorPref: DialogColorPreference? = null private var dbCustomColorPref: DialogColorPreference? = null
private var dbDataCompressionPref: Preference? = null private var dbDataCompressionPref: Preference? = null
private var recycleBinGroupPref: DialogListExplanationPreference? = null private var recycleBinGroupPref: DialogListExplanationPreference? = null
@@ -164,11 +164,11 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} }
// Database default username // Database default username
dbDefaultUsername = findPreference(getString(R.string.database_default_username_key)) dbDefaultUsernamePref = findPreference(getString(R.string.database_default_username_key))
if (database.allowDefaultUsername) { if (database.allowDefaultUsername) {
dbDefaultUsername?.summary = database.defaultUsername dbDefaultUsernamePref?.summary = database.defaultUsername
} else { } else {
dbDefaultUsername?.isEnabled = false dbDefaultUsernamePref?.isEnabled = false
// TODO dbGeneralPrefCategory?.removePreference(dbDefaultUsername) // TODO dbGeneralPrefCategory?.removePreference(dbDefaultUsername)
} }
@@ -416,7 +416,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
mDatabase?.defaultUsername = oldDefaultUsername mDatabase?.defaultUsername = oldDefaultUsername
oldDefaultUsername oldDefaultUsername
} }
dbDefaultUsername?.summary = defaultUsernameToShow dbDefaultUsernamePref?.summary = defaultUsernameToShow
} }
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_COLOR_TASK -> { DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_COLOR_TASK -> {
val oldColor = data.getString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY)!! val oldColor = data.getString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY)!!
@@ -632,6 +632,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} }
if (dialogFragment != null && !mDatabaseReadOnly) { if (dialogFragment != null && !mDatabaseReadOnly) {
@Suppress("DEPRECATION")
dialogFragment.setTargetFragment(this, 0) dialogFragment.setTargetFragment(this, 0)
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT) dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
} }

View File

@@ -49,7 +49,6 @@ open class SettingsActivity
private var backupManager: BackupManager? = null private var backupManager: BackupManager? = null
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var appPropertiesFileCreationRequestCode: Int? = null
private var coordinatorLayout: CoordinatorLayout? = null private var coordinatorLayout: CoordinatorLayout? = null
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
@@ -64,6 +63,41 @@ open class SettingsActivity
toolbar = findViewById(R.id.toolbar) toolbar = findViewById(R.id.toolbar)
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { selectedFileUri ->
// Import app properties result
try {
selectedFileUri?.let { uri ->
val appProperties = Properties()
contentResolver?.openInputStream(uri)?.use { inputStream ->
appProperties.load(inputStream)
}
PreferencesUtil.setAppProperties(this, appProperties)
// Restart the current activity
reloadActivity()
Toast.makeText(this, R.string.success_import_app_properties, Toast.LENGTH_LONG).show()
}
} catch (e: Exception) {
Toast.makeText(this, R.string.error_import_app_properties, Toast.LENGTH_LONG).show()
Log.e(TAG, "Unable to import app properties", e)
}
}
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
// Export app properties result
try {
createdFileUri?.let { uri ->
contentResolver?.openOutputStream(uri)?.use { outputStream ->
PreferencesUtil
.getAppProperties(this)
.store(outputStream, getString(R.string.description_app_properties))
}
Toast.makeText(this, R.string.success_export_app_properties, Toast.LENGTH_LONG).show()
}
} catch (e: Exception) {
Toast.makeText(this, R.string.error_export_app_properties, Toast.LENGTH_LONG).show()
Log.e(DatabaseLockActivity.TAG, "Unable to export app properties", e)
}
}
if (savedInstanceState?.getString(TITLE_KEY).isNullOrEmpty()) if (savedInstanceState?.getString(TITLE_KEY).isNullOrEmpty())
toolbar?.setTitle(R.string.settings) toolbar?.setTitle(R.string.settings)
@@ -217,54 +251,10 @@ open class SettingsActivity
} }
fun exportAppProperties() { fun exportAppProperties() {
appPropertiesFileCreationRequestCode = mExternalFileHelper?.createDocument(getString(R.string.app_properties_file_name, mExternalFileHelper?.createDocument(getString(R.string.app_properties_file_name,
DateTime.now().toLocalDateTime().toString("yyyy-MM-dd'_'HH-mm"))) DateTime.now().toLocalDateTime().toString("yyyy-MM-dd'_'HH-mm")))
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// Import app properties result
try {
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { selectedFileUri ->
selectedFileUri?.let { uri ->
val appProperties = Properties()
contentResolver?.openInputStream(uri)?.use { inputStream ->
appProperties.load(inputStream)
}
PreferencesUtil.setAppProperties(this, appProperties)
// Restart the current activity
reloadActivity()
Toast.makeText(this, R.string.success_import_app_properties, Toast.LENGTH_LONG).show()
}
}
} catch (e: Exception) {
Toast.makeText(this, R.string.error_import_app_properties, Toast.LENGTH_LONG).show()
Log.e(TAG, "Unable to import app properties", e)
}
// Export app properties result
try {
if (requestCode == appPropertiesFileCreationRequestCode) {
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
createdFileUri?.let { uri ->
contentResolver?.openOutputStream(uri)?.use { outputStream ->
PreferencesUtil
.getAppProperties(this)
.store(outputStream, getString(R.string.description_app_properties))
}
Toast.makeText(this, R.string.success_export_app_properties, Toast.LENGTH_LONG).show()
}
}
appPropertiesFileCreationRequestCode = null
}
} catch (e: Exception) {
Toast.makeText(this, R.string.error_export_app_properties, Toast.LENGTH_LONG).show()
Log.e(DatabaseLockActivity.TAG, "Unable to export app properties", e)
}
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)

View File

@@ -64,11 +64,13 @@ class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
private fun durationToDaysHoursMinutesSeconds(duration: Long) { private fun durationToDaysHoursMinutesSeconds(duration: Long) {
if (duration < 0) { if (duration < 0) {
mEnabled = false
mDays = 0 mDays = 0
mHours = 0 mHours = 0
mMinutes = 0 mMinutes = 0
mSeconds = 0 mSeconds = 0
} else { } else {
mEnabled = true
mDays = (duration / (24L * 60L * 60L * 1000L)).toInt() mDays = (duration / (24L * 60L * 60L * 1000L)).toInt()
val daysMilliseconds = mDays * 24L * 60L * 60L * 1000L val daysMilliseconds = mDays * 24L * 60L * 60L * 1000L
mHours = ((duration - daysMilliseconds) / (60L * 60L * 1000L)).toInt() mHours = ((duration - daysMilliseconds) / (60L * 60L * 1000L)).toInt()
@@ -125,10 +127,9 @@ class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
} }
} }
mEnabled = isSwitchActivated()
setSwitchAction({ isChecked -> setSwitchAction({ isChecked ->
mEnabled = isChecked mEnabled = isChecked
}, mDays + mHours + mMinutes + mSeconds > 0) }, mEnabled)
assignValuesInViews() assignValuesInViews()
} }

View File

@@ -31,9 +31,13 @@ abstract class ActionRunnable: Runnable {
var result: Result = Result() var result: Result = Result()
override fun run() { override fun run() {
onStartRun() try {
onActionRun() onStartRun()
onFinishRun() onActionRun()
onFinishRun()
} catch (runException: Exception) {
setError(runException)
}
} }
abstract fun onStartRun() abstract fun onStartRun()

View File

@@ -43,9 +43,14 @@ object TimeoutHelper {
private fun getLockPendingIntent(context: Context): PendingIntent { private fun getLockPendingIntent(context: Context): PendingIntent {
return PendingIntent.getBroadcast(context.applicationContext, return PendingIntent.getBroadcast(context.applicationContext,
REQUEST_ID, REQUEST_ID,
Intent(LOCK_ACTION), Intent(LOCK_ACTION),
PendingIntent.FLAG_CANCEL_CURRENT) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
}
)
} }
/** /**

View File

@@ -60,11 +60,16 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
Intent.ACTION_SCREEN_OFF -> { Intent.ACTION_SCREEN_OFF -> {
if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(context)) { if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(context)) {
mLockPendingIntent = PendingIntent.getBroadcast(context, mLockPendingIntent = PendingIntent.getBroadcast(context,
4575, 4575,
Intent(intent).apply { Intent(intent).apply {
action = LOCK_ACTION action = LOCK_ACTION
}, },
0) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
)
// Launch the effective action after a small time // Launch the effective action after a small time
val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong() val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong()
val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager? val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager?

View File

@@ -0,0 +1,27 @@
package com.kunzisoft.keepass.utils
import android.content.res.Configuration
import android.util.Log
object DexUtil {
private val TAG = DexUtil::class.java.name
// Determine if the current environment is in DeX mode. Always returns false on non-Samsung
// devices.
fun isDexMode(config: Configuration): Boolean {
// This is the documented way to check this: https://developer.samsung.com/samsung-dex/modify-optimizing.html
return try {
val configClass = config.javaClass
val enabledConstant = configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass)
val enabledField = configClass.getField("semDesktopModeEnabled").getInt(config)
val isEnabled = enabledConstant == enabledField
Log.d(TAG, "DeX currently enabled: $isEnabled")
isEnabled
} catch (e: Exception) {
Log.d(TAG, "Failed to check for DeX mode; likely not Samsung device: $e")
false
}
}
}

View File

@@ -0,0 +1,27 @@
package com.kunzisoft.keepass.utils
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.util.Log
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
object MagikeyboardUtil {
private val TAG = MagikeyboardUtil::class.java.name
// Set whether MagikeyboardService is enabled. This change is persistent and survives app
// crashes and device restarts. The state is changed immediately and does not require an app
// restart.
fun setEnabled(context: Context, enabled: Boolean) {
val componentState = if (enabled) {
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
} else {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
}
Log.d(TAG, "Setting service state: $enabled")
val component = ComponentName(context, MagikeyboardService::class.java)
context.packageManager.setComponentEnabledSetting(component, componentState, PackageManager.DONT_KILL_APP)
}
}

View File

@@ -86,7 +86,7 @@ object UriUtil {
private fun isFileScheme(fileUri: Uri): Boolean { private fun isFileScheme(fileUri: Uri): Boolean {
val scheme = fileUri.scheme val scheme = fileUri.scheme
if (scheme == null || scheme.isEmpty() || scheme.toLowerCase(Locale.ENGLISH) == "file") { if (scheme == null || scheme.isEmpty() || scheme.lowercase(Locale.ENGLISH) == "file") {
return true return true
} }
return false return false
@@ -94,7 +94,7 @@ object UriUtil {
private fun isContentScheme(fileUri: Uri): Boolean { private fun isContentScheme(fileUri: Uri): Boolean {
val scheme = fileUri.scheme val scheme = fileUri.scheme
if (scheme != null && scheme.toLowerCase(Locale.ENGLISH) == "content") { if (scheme != null && scheme.lowercase(Locale.ENGLISH) == "content") {
return true return true
} }
return false return false

View File

@@ -104,10 +104,12 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
return unlockMessageTextView?.text?.toString() ?: "" return unlockMessageTextView?.text?.toString() ?: ""
} }
set(value) { set(value) {
if (value == null || value.isEmpty()) if (value == null || value.isEmpty()) {
unlockMessageTextView?.visibility = GONE unlockMessageTextView?.visibility = GONE
else } else {
unlockMessageTextView?.visibility = VISIBLE unlockMessageTextView?.visibility = VISIBLE
stopIconViewAnimation()
}
unlockMessageTextView?.text = value ?: "" unlockMessageTextView?.text = value ?: ""
} }

View File

@@ -2,6 +2,7 @@ package com.kunzisoft.keepass.view
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Parcelable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.TextView import android.widget.TextView
@@ -9,6 +10,9 @@ import androidx.constraintlayout.widget.ConstraintLayout
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import android.os.Parcel
import android.os.Parcelable.Creator
class KeyFileSelectionView @JvmOverloads constructor(context: Context, class KeyFileSelectionView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
@@ -54,4 +58,45 @@ class KeyFileSelectionView @JvmOverloads constructor(context: Context,
UriUtil.getFileData(context, value)?.name ?: value.path UriUtil.getFileData(context, value)?.name ?: value.path
} ?: "" } ?: ""
} }
override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState()
val saveState = SavedState(superState)
saveState.mUri = this.mUri
return saveState
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state !is SavedState) {
super.onRestoreInstanceState(state)
return
}
super.onRestoreInstanceState(state.superState)
this.mUri = state.mUri
}
internal class SavedState : BaseSavedState {
var mUri: Uri? = null
constructor(superState: Parcelable?) : super(superState) {}
private constructor(parcel: Parcel) : super(parcel) {
mUri = parcel.readParcelable(Uri::class.java.classLoader)
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeParcelable(mUri, flags)
}
companion object CREATOR : Creator<SavedState> {
override fun createFromParcel(parcel: Parcel): SavedState {
return SavedState(parcel)
}
override fun newArray(size: Int): Array<SavedState?> {
return arrayOfNulls(size)
}
}
}
} }

View File

@@ -86,7 +86,8 @@ abstract class TemplateAbstractView<
if (mTemplate != template) { if (mTemplate != template) {
mTemplate = template mTemplate = template
if (mEntryInfo != null) { if (mEntryInfo != null) {
populateEntryInfoWithViews(true) populateEntryInfoWithViews(templateFieldNotEmpty = true,
retrieveDefaultValues = false)
} }
buildTemplateAndPopulateInfo() buildTemplateAndPopulateInfo()
clearFocus() clearFocus()
@@ -203,9 +204,7 @@ abstract class TemplateAbstractView<
setNumberLines(20) setNumberLines(20)
}, },
TemplateAttributeAction.CUSTOM_EDITION TemplateAttributeAction.CUSTOM_EDITION
).apply { )
default = field.protectedValue.stringValue
}
return buildViewForTemplateField(customFieldTemplateAttribute, field, FIELD_CUSTOM_TAG) return buildViewForTemplateField(customFieldTemplateAttribute, field, FIELD_CUSTOM_TAG)
} }
@@ -275,22 +274,26 @@ abstract class TemplateAbstractView<
templateAttribute: TemplateAttribute, templateAttribute: TemplateAttribute,
entryInfoValue: String, entryInfoValue: String,
showEmptyFields: Boolean) { showEmptyFields: Boolean) {
var fieldView: TEntryFieldView? = findViewWithTag(fieldTag) try {
if (!showEmptyFields && entryInfoValue.isEmpty()) { var fieldView: TEntryFieldView? = findViewWithTag(fieldTag)
fieldView?.isFieldVisible = false if (!showEmptyFields && entryInfoValue.isEmpty()) {
} else if (fieldView == null && entryInfoValue.isNotEmpty()) { fieldView?.isFieldVisible = false
// Add new not referenced view if standard field not in template } else if (fieldView == null && entryInfoValue.isNotEmpty()) {
fieldView = buildViewForNotReferencedField( // Add new not referenced view if standard field not in template
Field(templateAttribute.label, fieldView = buildViewForNotReferencedField(
ProtectedString(templateAttribute.protected, "")), Field(templateAttribute.label,
templateAttribute ProtectedString(templateAttribute.protected, "")),
) as? TEntryFieldView? templateAttribute
fieldView?.let { ) as? TEntryFieldView?
addNotReferencedView(it as View) fieldView?.let {
addNotReferencedView(it as View)
}
} }
fieldView?.value = entryInfoValue
fieldView?.applyFontVisibility(mFontInVisibility)
} catch(e: Exception) {
Log.e(TAG, "Unable to populate entry field view", e)
} }
fieldView?.value = entryInfoValue
fieldView?.applyFontVisibility(mFontInVisibility)
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@@ -299,22 +302,25 @@ abstract class TemplateAbstractView<
expires: Boolean, expires: Boolean,
expiryTime: DateInstant, expiryTime: DateInstant,
showEmptyFields: Boolean) { showEmptyFields: Boolean) {
try {
var fieldView: TDateTimeView? = findViewWithTag(fieldTag) var fieldView: TDateTimeView? = findViewWithTag(fieldTag)
if (!showEmptyFields && !expires) { if (!showEmptyFields && !expires) {
fieldView?.isFieldVisible = false fieldView?.isFieldVisible = false
} else if (fieldView == null && expires) { } else if (fieldView == null && expires) {
fieldView = buildViewForNotReferencedField( fieldView = buildViewForNotReferencedField(
Field(templateAttribute.label, Field(templateAttribute.label,
ProtectedString(templateAttribute.protected, "")), ProtectedString(templateAttribute.protected, "")),
templateAttribute templateAttribute
) as? TDateTimeView? ) as? TDateTimeView?
fieldView?.let { fieldView?.let {
addNotReferencedView(it as View) addNotReferencedView(it as View)
}
} }
fieldView?.activation = expires
fieldView?.dateTime = expiryTime
} catch(e: Exception) {
Log.e(TAG, "Unable to populate date time view", e)
} }
fieldView?.activation = expires
fieldView?.dateTime = expiryTime
} }
/** /**
@@ -383,7 +389,8 @@ abstract class TemplateAbstractView<
return emptyList() return emptyList()
} }
protected open fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean) { protected open fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean,
retrieveDefaultValues: Boolean) {
if (mEntryInfo == null) if (mEntryInfo == null)
mEntryInfo = EntryInfo() mEntryInfo = EntryInfo()
@@ -422,11 +429,12 @@ abstract class TemplateAbstractView<
mEntryInfo?.notes = it mEntryInfo?.notes = it
} }
retrieveCustomFieldsFromView(templateFieldNotEmpty) retrieveCustomFieldsFromView(templateFieldNotEmpty, retrieveDefaultValues)
} }
fun getEntryInfo(): EntryInfo { fun getEntryInfo(): EntryInfo {
populateEntryInfoWithViews(true) populateEntryInfoWithViews(templateFieldNotEmpty = true,
retrieveDefaultValues = true)
return mEntryInfo ?: EntryInfo() return mEntryInfo ?: EntryInfo()
} }
@@ -472,23 +480,31 @@ abstract class TemplateAbstractView<
return mViewFields.indexOfFirst { it.field.name.equals(name, true) } return mViewFields.indexOfFirst { it.field.name.equals(name, true) }
} }
private fun retrieveCustomFieldsFromView(templateFieldNotEmpty: Boolean = false) { private fun retrieveCustomFieldsFromView(templateFieldNotEmpty: Boolean = false,
retrieveDefaultValues: Boolean = false) {
mEntryInfo?.customFields = mViewFields.mapNotNull { mEntryInfo?.customFields = mViewFields.mapNotNull {
getCustomField(it.field.name, templateFieldNotEmpty) getCustomField(it.field.name, templateFieldNotEmpty, retrieveDefaultValues)
}.toMutableList() }.toMutableList()
} }
protected fun getCustomField(fieldName: String): Field { protected fun getCustomField(fieldName: String): Field {
return getCustomField(fieldName, false) return getCustomField(fieldName,
?: Field(fieldName, ProtectedString(false)) templateFieldNotEmpty = false,
retrieveDefaultValues = false
) ?: Field(fieldName, ProtectedString(false))
} }
private fun getCustomField(fieldName: String, templateFieldNotEmpty: Boolean): Field? { private fun getCustomField(fieldName: String,
templateFieldNotEmpty: Boolean,
retrieveDefaultValues: Boolean): Field? {
getViewFieldByName(fieldName)?.let { fieldId -> getViewFieldByName(fieldName)?.let { fieldId ->
val editView: View? = fieldId.view val editView: View = fieldId.view
if (editView is GenericFieldView) { if (editView is GenericFieldView) {
// Do not return field with a default value // Do not return field with a default value
val defaultViewValue = if (editView.value == editView.default) "" else editView.value val defaultViewValue =
if (retrieveDefaultValues || editView.value != editView.default) {
editView.value
} else ""
if (!templateFieldNotEmpty if (!templateFieldNotEmpty
|| (editView.tag == FIELD_CUSTOM_TAG && defaultViewValue.isNotEmpty())) { || (editView.tag == FIELD_CUSTOM_TAG && defaultViewValue.isNotEmpty())) {
return Field( return Field(
@@ -634,7 +650,8 @@ abstract class TemplateAbstractView<
override fun onSaveInstanceState(): Parcelable { override fun onSaveInstanceState(): Parcelable {
val superSave = super.onSaveInstanceState() val superSave = super.onSaveInstanceState()
val saveState = SavedState(superSave) val saveState = SavedState(superSave)
populateEntryInfoWithViews(false) populateEntryInfoWithViews(templateFieldNotEmpty = false,
retrieveDefaultValues = false)
saveState.template = this.mTemplate saveState.template = this.mTemplate
saveState.entryInfo = this.mEntryInfo saveState.entryInfo = this.mEntryInfo
onSaveEntryInstanceState(saveState) onSaveEntryInstanceState(saveState)

View File

@@ -64,6 +64,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
TextEditFieldView(it).apply { TextEditFieldView(it).apply {
// hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout // hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout
setProtection(field.protectedValue.isProtected) setProtection(field.protectedValue.isProtected)
default = templateAttribute.default
setMaxChars(templateAttribute.options.getNumberChars()) setMaxChars(templateAttribute.options.getNumberChars())
setMaxLines(templateAttribute.options.getNumberLines()) setMaxLines(templateAttribute.options.getNumberLines())
setActionClick(templateAttribute, field, this) setActionClick(templateAttribute, field, this)
@@ -79,7 +80,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
return context?.let { return context?.let {
TextSelectFieldView(it).apply { TextSelectFieldView(it).apply {
setItems(templateAttribute.options.getListItems()) setItems(templateAttribute.options.getListItems())
default = field.protectedValue.stringValue default = templateAttribute.default
setActionClick(templateAttribute, field, this) setActionClick(templateAttribute, field, this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO
@@ -198,8 +199,9 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
return super.populateViewsWithEntryInfo(showEmptyFields) return super.populateViewsWithEntryInfo(showEmptyFields)
} }
override fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean) { override fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean,
super.populateEntryInfoWithViews(templateFieldNotEmpty) retrieveDefaultValues: Boolean) {
super.populateEntryInfoWithViews(templateFieldNotEmpty, retrieveDefaultValues)
mEntryInfo?.otpModel = OtpEntryFields.parseFields { key -> mEntryInfo?.otpModel = OtpEntryFields.parseFields { key ->
getCustomField(key).protectedValue.toString() getCustomField(key).protectedValue.toString()
}?.otpModel }?.otpModel

View File

@@ -187,6 +187,6 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
companion object { companion object {
const val MAX_CHARS_LIMIT = Integer.MAX_VALUE const val MAX_CHARS_LIMIT = Integer.MAX_VALUE
const val MAX_LINES_LIMIT = 40 const val MAX_LINES_LIMIT = Integer.MAX_VALUE
} }
} }

View File

@@ -194,6 +194,7 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context,
get() = valueSpinnerAdapter.getItem(mDefaultPosition) get() = valueSpinnerAdapter.getItem(mDefaultPosition)
set(value) { set(value) {
mDefaultPosition = valueSpinnerAdapter.getPosition(value) mDefaultPosition = valueSpinnerAdapter.getPosition(value)
valueSpinnerAdapter.notifyDataSetChanged()
} }
override fun setOnActionClickListener(onActionClickListener: OnClickListener?, override fun setOnActionClickListener(onActionClickListener: OnClickListener?,

View File

@@ -0,0 +1,32 @@
package com.kunzisoft.keepass.viewmodels
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
class AdvancedUnlockViewModel : ViewModel() {
var allowAutoOpenBiometricPrompt : Boolean = true
var deviceCredentialAuthSucceeded: Boolean? = null
val onInitAdvancedUnlockModeRequested : LiveData<Void?> get() = _onInitAdvancedUnlockModeRequested
private val _onInitAdvancedUnlockModeRequested = SingleLiveEvent<Void?>()
val onUnlockAvailabilityCheckRequested : LiveData<Void?> get() = _onUnlockAvailabilityCheckRequested
private val _onUnlockAvailabilityCheckRequested = SingleLiveEvent<Void?>()
val onDatabaseFileLoaded : LiveData<Uri?> get() = _onDatabaseFileLoaded
private val _onDatabaseFileLoaded = SingleLiveEvent<Uri?>()
fun initAdvancedUnlockMode() {
_onInitAdvancedUnlockModeRequested.call()
}
fun checkUnlockAvailability() {
_onUnlockAvailabilityCheckRequested.call()
}
fun databaseFileLoaded(databaseUri: Uri?) {
_onDatabaseFileLoaded.value = databaseUri
}
}

View File

@@ -2,6 +2,7 @@ package com.kunzisoft.keepass.viewmodels
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
@@ -30,6 +31,10 @@ class IconPickerViewModel: ViewModel() {
MutableLiveData<IconCustomState>() MutableLiveData<IconCustomState>()
} }
val customIconUpdated : MutableLiveData<IconCustomState> by lazy {
MutableLiveData<IconCustomState>()
}
fun pickStandardIcon(icon: IconImageStandard) { fun pickStandardIcon(icon: IconImageStandard) {
standardIconPicked.value = icon standardIconPicked.value = icon
} }
@@ -54,6 +59,10 @@ class IconPickerViewModel: ViewModel() {
customIconRemoved.value = customIcon customIconRemoved.value = customIcon
} }
fun updateCustomIcon(customIcon: IconCustomState) {
customIconUpdated.value = customIcon
}
data class IconCustomState(var iconCustom: IconImageCustom? = null, data class IconCustomState(var iconCustom: IconImageCustom? = null,
var error: Boolean = true, var error: Boolean = true,
var errorStringId: Int = -1, var errorStringId: Int = -1,

View File

@@ -144,7 +144,7 @@ internal class PublicSuffixListData(
} }
companion object { companion object {
val WILDCARD_LABEL = byteArrayOf('*'.toByte()) val WILDCARD_LABEL = byteArrayOf('*'.code.toByte())
val PREVAILING_RULE = listOf("*") val PREVAILING_RULE = listOf("*")
val EMPTY_RULE = listOf<String>() val EMPTY_RULE = listOf<String>()
const val EXCEPTION_MARKER = '!' const val EXCEPTION_MARKER = '!'

View File

@@ -36,7 +36,7 @@ internal fun ByteArray.binarySearch(labels: List<ByteArray>, labelIndex: Int): S
while (true) { while (true) {
val byte0 = if (expectDot) { val byte0 = if (expectDot) {
expectDot = false expectDot = false
'.'.toByte() '.'.code.toByte()
} else { } else {
labels[currentLabelIndex][currentLabelByteIndex] and BITMASK labels[currentLabelIndex][currentLabelByteIndex] and BITMASK
} }
@@ -103,7 +103,7 @@ internal fun ByteArray.binarySearch(labels: List<ByteArray>, labelIndex: Int): S
*/ */
private fun ByteArray.findStartOfLineFromIndex(start: Int): Int { private fun ByteArray.findStartOfLineFromIndex(start: Int): Int {
var index = start var index = start
while (index > -1 && this[index] != '\n'.toByte()) { while (index > -1 && this[index] != '\n'.code.toByte()) {
index-- index--
} }
index++ index++
@@ -115,7 +115,7 @@ private fun ByteArray.findStartOfLineFromIndex(start: Int): Int {
*/ */
private fun ByteArray.findEndOfLineFromIndex(start: Int): Int { private fun ByteArray.findEndOfLineFromIndex(start: Int): Int {
var end = 1 var end = 1
while (this[start + end] != '\n'.toByte()) { while (this[start + end] != '\n'.code.toByte()) {
end++ end++
} }
return end return end

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorAccentLight" android:state_pressed="true" />
<item android:color="@color/white_grey_darker" android:state_enabled="false" />
<item android:color="?attr/colorAccent" android:state_enabled="true" />
</selector>

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:color="@color/white"
tools:targetApi="lollipop">
<item>
<shape>
<corners
android:topLeftRadius="0dp"
android:topRightRadius="40dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"/>
<solid android:color="?attr/colorAccent"/>
</shape>
</item>
</ripple>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:color="@color/white"
tools:targetApi="lollipop">
<item>
<shape>
<stroke
android:width="1dp"
android:color="?attr/textColorInverse" />
<corners
android:topLeftRadius="0dp"
android:topRightRadius="40dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"/>
<solid
android:color="@color/transparent"/>
</shape>
</item>
</ripple>

View File

@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_pressed="true">
<shape>
<corners
android:topLeftRadius="0dp"
android:topRightRadius="40dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"/>
<padding
android:left="4dp"
android:right="12dp"
android:top="18dp"
android:bottom="8dp"/>
<solid android:color="@color/orange_light"/>
</shape>
</item>
<item>
<shape>
<corners
android:topLeftRadius="0dp"
android:topRightRadius="40dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"/>
<padding
android:left="4dp"
android:right="12dp"
android:top="18dp"
android:bottom="8dp"/>
<solid android:color="@color/orange"/>
</shape>
</item>
</selector>

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/white">
<item>
<shape>
<stroke
android:width="1dp"
android:color="@color/white_grey" />
<corners
android:topLeftRadius="0dp"
android:topRightRadius="40dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"/>
<solid
android:color="@color/transparent"/>
</shape>
</item>
</selector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M 11 2.0566406 C 6.762335 2.4220229 3.0067094 5.7987155 2.203125 9.9785156 C 1.3601753 13.960549 3.1781148 18.394742 6.7089844 20.480469 C 9.6237318 22.368157 13.514425 22.492178 16.582031 20.892578 C 17.959775 20.180473 19.316015 19.099467 20.087891 17.808594 L 18.402344 16.791016 C 16.277892 19.724364 12.039121 20.844605 8.7519531 19.306641 C 5.481064 17.911182 3.4461934 14.150571 4.109375 10.648438 C 4.6649664 7.2806969 7.5784749 4.4226117 11 4.0839844 L 11 2.0566406 z M 13 2.0644531 L 13 4.09375 C 16.367309 4.4801387 19.308002 7.2166099 19.861328 10.574219 C 20.123352 12.069186 19.935398 13.632674 19.367188 15.037109 C 19.94644 15.387646 20.527063 15.73602 21.105469 16.087891 C 22.671737 12.714066 22.120988 8.4920871 19.708984 5.6542969 C 18.063396 3.6246553 15.604973 2.2995704 13 2.0644531 z M 12 6.7148438 C 10.737143 6.7148437 9.7148438 7.737143 9.7148438 9 L 9.7148438 10.142578 L 9.1425781 10.142578 C 8.5140068 10.142578 8 10.656585 8 11.285156 L 8 15.857422 C 8 16.491709 8.5140068 17 9.1425781 17 L 14.857422 17 C 15.491709 17 16 16.491709 16 15.857422 L 16 11.285156 C 16 10.656585 15.491709 10.142578 14.857422 10.142578 L 14.285156 10.142578 L 14.285156 9 C 14.285156 7.737143 13.262857 6.7148438 12 6.7148438 z M 12 7.8574219 C 12.634286 7.8574219 13.142578 8.3714294 13.142578 9 L 13.142578 10.142578 L 10.857422 10.142578 L 10.857422 9 C 10.857422 8.3714294 11.371429 7.8574219 12 7.8574219 z M 12 12.427734 C 12.634286 12.427734 13.142578 12.943693 13.142578 13.572266 C 13.142578 14.20655 12.634286 14.714844 12 14.714844 C 11.371429 14.714844 10.857422 14.20655 10.857422 13.572266 C 10.857422 12.943693 11.371429 12.427734 12 12.427734 z" />
</vector>

View File

@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<group
android:scaleX="0.8"
android:scaleY="0.8"
android:pivotX="12"
android:pivotY="12">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
</group>
</vector>

View File

@@ -52,8 +52,7 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@+id/biometric_message" app:layout_constraintBottom_toTopOf="@+id/biometric_message"
tools:text="@string/advanced_unlock_prompt_store_credential_title" tools:text="@string/advanced_unlock_prompt_store_credential_title"
style="@style/KeepassDXStyle.TextAppearance.Default.TextOnPrimary" style="@style/KeepassDXStyle.TextAppearance.Secondary.TextOnPrimary"
android:textSize="14sp"
android:gravity="center" /> android:gravity="center" />
<TextView <TextView
@@ -67,7 +66,6 @@
app:layout_constraintTop_toBottomOf="@+id/biometric_title" app:layout_constraintTop_toBottomOf="@+id/biometric_title"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
tools:text="Sample error" tools:text="Sample error"
style="@style/KeepassDXStyle.TextAppearance.Secondary.TextOnPrimary" style="@style/KeepassDXStyle.TextAppearance.Warning.TextOnPrimary"
android:textSize="12sp"
android:gravity="center" /> android:gravity="center" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -138,8 +138,8 @@
<include <include
layout="@layout/view_button_lock" layout="@layout/view_button_lock"
android:layout_width="@dimen/lock_button_size" android:layout_width="wrap_content"
android:layout_height="@dimen/lock_button_size" android:layout_height="wrap_content"
android:layout_gravity="start|bottom" /> android:layout_gravity="start|bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -100,8 +100,8 @@
<include <include
layout="@layout/view_button_lock" layout="@layout/view_button_lock"
android:layout_width="@dimen/lock_button_size" android:layout_width="wrap_content"
android:layout_height="@dimen/lock_button_size" android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" /> app:layout_constraintBottom_toBottomOf="parent" />

View File

@@ -132,6 +132,7 @@
android:orientation="vertical" android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_below="@+id/toolbar"> android:layout_below="@+id/toolbar">
<FrameLayout <FrameLayout
android:id="@+id/nodes_list_fragment_container" android:id="@+id/nodes_list_fragment_container"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -168,8 +169,8 @@
<include <include
layout="@layout/view_button_lock" layout="@layout/view_button_lock"
android:layout_width="@dimen/lock_button_size" android:layout_width="wrap_content"
android:layout_height="@dimen/lock_button_size" android:layout_height="wrap_content"
android:layout_alignParentBottom="true"/> android:layout_alignParentBottom="true"/>
</RelativeLayout> </RelativeLayout>

View File

@@ -59,8 +59,8 @@
<include <include
layout="@layout/view_button_lock" layout="@layout/view_button_lock"
android:layout_width="@dimen/lock_button_size" android:layout_width="wrap_content"
android:layout_height="@dimen/lock_button_size" android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" /> app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -23,6 +23,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?android:attr/windowBackground"
tools:targetApi="o"> tools:targetApi="o">
<com.kunzisoft.keepass.view.SpecialModeView <com.kunzisoft.keepass.view.SpecialModeView

View File

@@ -42,8 +42,8 @@
<include <include
layout="@layout/view_button_lock" layout="@layout/view_button_lock"
android:layout_width="@dimen/lock_button_size" android:layout_width="wrap_content"
android:layout_height="@dimen/lock_button_size" android:layout_height="wrap_content"
android:visibility="gone" android:visibility="gone"
android:layout_gravity="start|bottom" /> android:layout_gravity="start|bottom" />

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2021 Jeremy Jamet / Kunzisoft.
This file is part of KeePassDX.
KeePassDX is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
KeePassDX is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/default_margin"
android:importantForAutofill="noExcludeDescendants"
tools:targetApi="o">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon_edit_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginRight="@dimen/default_margin"
android:layout_marginEnd="@dimen/default_margin"
android:src="@drawable/ic_blank_32dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/icon_edit_name_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/icon_edit_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
android:inputType="text"
android:maxLines="1"
android:singleLine="true"
android:hint="@string/hint_icon_name"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -56,7 +56,7 @@
android:layout_marginRight="20dp" android:layout_marginRight="20dp"
android:layout_marginStart="20dp" android:layout_marginStart="20dp"
android:layout_marginEnd="20dp" android:layout_marginEnd="20dp"
style="@style/KeepassDXStyle.TextAppearance.WarningTextStyle"/> style="@style/KeepassDXStyle.TextAppearance.Warning"/>
<com.google.android.material.progressindicator.LinearProgressIndicator <com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_dialog_bar" android:id="@+id/progress_dialog_bar"

View File

@@ -63,7 +63,7 @@
android:layout_marginEnd="@dimen/card_view_margin_horizontal" android:layout_marginEnd="@dimen/card_view_margin_horizontal"
android:layout_marginRight="@dimen/card_view_margin_horizontal" android:layout_marginRight="@dimen/card_view_margin_horizontal"
android:text="@string/error_otp_type" android:text="@string/error_otp_type"
style="@style/KeepassDXStyle.TextAppearance.WarningTextStyle"/> style="@style/KeepassDXStyle.TextAppearance.Warning"/>
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:id="@+id/card_view_otp_selection" android:id="@+id/card_view_otp_selection"

View File

@@ -18,7 +18,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?android:attr/windowBackground" android:background="?android:attr/windowBackground"
android:minHeight="36dp" android:minHeight="48dp"
android:orientation="horizontal"> android:orientation="horizontal">
<ImageView <ImageView
android:layout_width="24dp" android:layout_width="24dp"

View File

@@ -30,8 +30,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:minHeight="56dp" android:minHeight="48dp"
android:maxHeight="72dp"
app:layout_constraintWidth_percent="@dimen/content_percent" app:layout_constraintWidth_percent="@dimen/content_percent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@@ -104,10 +103,12 @@
tools:text="7543A7EAB2EA7CFD1394F1615EBEB08C" /> tools:text="7543A7EAB2EA7CFD1394F1615EBEB08C" />
</LinearLayout> </LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout <LinearLayout
android:id="@+id/node_options" android:id="@+id/node_options"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="end"
android:layout_marginStart="12dp" android:layout_marginStart="12dp"
android:layout_marginLeft="12dp" android:layout_marginLeft="12dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@@ -164,7 +165,7 @@
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/node_otp_container" /> app:layout_constraintTop_toBottomOf="@+id/node_otp_container" />
</androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout>
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -30,8 +30,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:minHeight="56dp" android:minHeight="48dp"
android:maxHeight="72dp"
app:layout_constraintWidth_percent="@dimen/content_percent" app:layout_constraintWidth_percent="@dimen/content_percent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@@ -89,18 +88,6 @@
android:maxLines="2" android:maxLines="2"
tools:text="Node Title" /> tools:text="Node Title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/node_subtext"
style="@style/KeepassDXStyle.TextAppearance.Group.SubTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-4dp"
android:gravity="center_vertical"
android:lines="1"
android:singleLine="true"
android:visibility="gone"
tools:text="Node SubTitle" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/node_meta" android:id="@+id/node_meta"
style="@style/KeepassDXStyle.TextAppearance.Group.Meta" style="@style/KeepassDXStyle.TextAppearance.Group.Meta"

View File

@@ -1,48 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<FrameLayout <FrameLayout
android:id="@+id/lock_button"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/lock_button_background" android:id="@+id/lock_button"
android:layout_width="@dimen/lock_button_size" style="@style/KeepassDXStyle.Fab.Special"
android:layout_height="@dimen/lock_button_size" android:layout_width="wrap_content"
android:layout_marginStart="-2dp" android:layout_height="wrap_content"
android:layout_marginLeft="-2dp" app:fabSize="mini"
android:layout_marginBottom="-2dp" android:layout_margin="8dp"
tools:targetApi="lollipop" android:contentDescription="@string/lock"
android:elevation="4dp" android:src="@drawable/ic_lock_white_padding_24dp"
style="@style/KeepassDXStyle.Special.Button.Background" xmlns:app="http://schemas.android.com/apk/res-auto" />
android:layout_gravity="bottom|start"
android:contentDescription="@string/menu_lock" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/lock_button_stroke"
android:layout_width="@dimen/lock_button_size"
android:layout_height="@dimen/lock_button_size"
android:layout_marginStart="-2dp"
android:layout_marginLeft="-2dp"
android:layout_marginBottom="-2dp"
tools:targetApi="lollipop"
android:elevation="4dp"
style="@style/KeepassDXStyle.Special.Button.Stroke"
android:layout_gravity="bottom|start"
android:contentDescription="@string/menu_lock" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/lock_button_icon"
android:layout_width="@dimen/lock_button_size"
android:layout_height="@dimen/lock_button_size"
android:paddingBottom="6dp"
android:paddingTop="16dp"
android:paddingStart="4dp"
android:paddingLeft="4dp"
android:paddingEnd="12dp"
android:paddingRight="12dp"
tools:targetApi="lollipop"
android:elevation="4dp"
android:src="@drawable/ic_lock_white_24dp"
android:tint="@color/white"
android:layout_gravity="bottom|start"
android:contentDescription="@string/menu_lock" />
</FrameLayout> </FrameLayout>

View File

@@ -19,6 +19,12 @@
--> -->
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu 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">
<item android:id="@+id/menu_edit"
android:icon="@drawable/ic_mode_edit_white_24dp"
android:title="@string/menu_edit"
android:orderInCategory="5"
app:iconTint="?attr/colorControlNormal"
app:showAsAction="ifRoom" />
<item android:id="@+id/menu_delete" <item android:id="@+id/menu_delete"
android:icon="@drawable/ic_delete_forever_white_24dp" android:icon="@drawable/ic_delete_forever_white_24dp"
android:title="@string/menu_delete" android:title="@string/menu_delete"

View File

@@ -79,7 +79,7 @@
<string name="no_url_handler">ثبت متصفح لزيارة هذا الرابط.</string> <string name="no_url_handler">ثبت متصفح لزيارة هذا الرابط.</string>
<string name="progress_create">إنشاء قاعدة بيانات جديدة …</string> <string name="progress_create">إنشاء قاعدة بيانات جديدة …</string>
<string name="protection">الحماية</string> <string name="protection">الحماية</string>
<string name="read_only">للقراءة فقط</string> <string name="read_only">محمي من التعديل</string>
<string name="content_description_remove_from_list">حذف</string> <string name="content_description_remove_from_list">حذف</string>
<string name="root">الجذر</string> <string name="root">الجذر</string>
<string name="memory_usage">استخدام الذاكرة</string> <string name="memory_usage">استخدام الذاكرة</string>
@@ -99,10 +99,10 @@
<string name="underline">تسطير</string> <string name="underline">تسطير</string>
<string name="uppercase">حروف كبيرة</string> <string name="uppercase">حروف كبيرة</string>
<string name="warning">تحذير</string> <string name="warning">تحذير</string>
<string name="warning_empty_password">هل تريد حقاً استخدام سلسلة فارغة ككلمة سرية ؟</string> <string name="warning_empty_password">هل تريد المتابعة دون حماية قاعدة البيانات بكلمة سر ؟</string>
<string name="warning_no_encryption_key">هل أنت متأكد من أنك لا تريد استخدام أي مفتاح تشفير ؟</string> <string name="warning_no_encryption_key">أمتأكد أنك لا تريد استخدام أي مفتاح لتشفير ؟</string>
<string name="version_label">الإصدار %1$s</string> <string name="version_label">الإصدار %1$s</string>
<string name="education_new_node_title">أضف عناصر جديدة إلى قاعدتك</string> <string name="education_new_node_title">أضف عناصر إلى قاعدة البيانات</string>
<string name="education_entry_new_field_title">إضافة حقول مخصصة</string> <string name="education_entry_new_field_title">إضافة حقول مخصصة</string>
<string name="education_field_copy_title">نسخ حقل</string> <string name="education_field_copy_title">نسخ حقل</string>
<string name="education_lock_title">تأمين قاعدة البيانات</string> <string name="education_lock_title">تأمين قاعدة البيانات</string>
@@ -111,7 +111,7 @@
<string name="add_entry">إضافة مدخلة</string> <string name="add_entry">إضافة مدخلة</string>
<string name="edit_entry">تحرير مدخلة</string> <string name="edit_entry">تحرير مدخلة</string>
<string name="key_derivation_function">وظيفة اشتقاق المفتاح</string> <string name="key_derivation_function">وظيفة اشتقاق المفتاح</string>
<string name="app_timeout">مهلة التطبيق</string> <string name="app_timeout">انتهت المهلة</string>
<string name="app_timeout_summary">مدة الانتظار قبل إقفال قاعدة البيانات</string> <string name="app_timeout_summary">مدة الانتظار قبل إقفال قاعدة البيانات</string>
<string name="file_manager_install_description">المحرر الذي يمتلك صلاحتي ACTION_CREATE_DOCUMENT و ACTION_OPEN_DOCUMENT ضروري لانشاء, وفتح وحفض قواعد البيانات.</string> <string name="file_manager_install_description">المحرر الذي يمتلك صلاحتي ACTION_CREATE_DOCUMENT و ACTION_OPEN_DOCUMENT ضروري لانشاء, وفتح وحفض قواعد البيانات.</string>
<string name="clipboard_error">بعض الأجهزة لا تسمح للتطبيقات باستعمال الحافظة.</string> <string name="clipboard_error">بعض الأجهزة لا تسمح للتطبيقات باستعمال الحافظة.</string>
@@ -120,8 +120,8 @@
<string name="select_to_copy">اختر لنسخ %1$s إلى الحافظة</string> <string name="select_to_copy">اختر لنسخ %1$s إلى الحافظة</string>
<string name="retrieving_db_key">يجلب مفتاح قاعدة البيانات…</string> <string name="retrieving_db_key">يجلب مفتاح قاعدة البيانات…</string>
<string name="default_checkbox">استخدامها كقاعدة بيانات افتراضية</string> <string name="default_checkbox">استخدامها كقاعدة بيانات افتراضية</string>
<string name="html_about_licence">KeePassDX © %1$d كونزيسوفت &lt;strong&gt;مفتوح المصدر&lt;/strong&gt; و &lt;strong&gt;بدون اعلانات&lt;/strong&gt;. <string name="html_about_licence">KeePassDX © %1$d كونزيسوفت <strong>مفتوح المصدر</strong> و <strong>بدون اعلانات</strong>.
\n يوزع كما هو، بدون ضمان, تحت ترخيص &lt;strong&gt;GPLv3&lt;/strong&gt;</string> \n يوزع كما هو، بدون ضمان, تحت ترخيص <strong>GPLv3</strong>.</string>
<string name="entry_accessed">نُفذ إليه</string> <string name="entry_accessed">نُفذ إليه</string>
<string name="entry_expires">تنتهي صلاحيته في</string> <string name="entry_expires">تنتهي صلاحيته في</string>
<string name="entry_keyfile">ملف المفتاح</string> <string name="entry_keyfile">ملف المفتاح</string>
@@ -133,7 +133,7 @@
<string name="error_load_database">تعذر تحميل قاعدة البيانات.</string> <string name="error_load_database">تعذر تحميل قاعدة البيانات.</string>
<string name="error_load_database_KDF_memory">لا يمكن تحميل المفتاح، حاول تقليل \"الذاكرة المستخدمة\" من قبل KDF.</string> <string name="error_load_database_KDF_memory">لا يمكن تحميل المفتاح، حاول تقليل \"الذاكرة المستخدمة\" من قبل KDF.</string>
<string name="error_pass_gen_type">يجب تحديد نوع واحد على الأقل لتوليد كلمة السر.</string> <string name="error_pass_gen_type">يجب تحديد نوع واحد على الأقل لتوليد كلمة السر.</string>
<string name="error_rounds_too_large">\"جولات\" كبيرة جداً. الإعداد إلى 2147483648.</string> <string name="error_rounds_too_large">\"جولات التحويل\" كثيرة جداً. الإعداد إلى 2147483648.</string>
<string name="error_string_key">يجب أن يكون لكل سلسلة اسم حقل.</string> <string name="error_string_key">يجب أن يكون لكل سلسلة اسم حقل.</string>
<string name="error_wrong_length">أدخل عددًا صحيحًا موجبًا في حقل «الطول».</string> <string name="error_wrong_length">أدخل عددًا صحيحًا موجبًا في حقل «الطول».</string>
<string name="error_autofill_enable_service">تعذر تمكين خدمة الملء التلقائي.</string> <string name="error_autofill_enable_service">تعذر تمكين خدمة الملء التلقائي.</string>
@@ -147,7 +147,7 @@
<string name="hint_generated_password">كلمة السر الموَلدة</string> <string name="hint_generated_password">كلمة السر الموَلدة</string>
<string name="hint_keyfile">الملف المفتاحي</string> <string name="hint_keyfile">الملف المفتاحي</string>
<string name="hide_password_title">اخفاء كلمات السر</string> <string name="hide_password_title">اخفاء كلمات السر</string>
<string name="copy_field">نُسخ %1$s</string> <string name="copy_field">نُسخة من %1$s</string>
<string name="menu_copy">نسخ</string> <string name="menu_copy">نسخ</string>
<string name="menu_move">نقل</string> <string name="menu_move">نقل</string>
<string name="menu_paste">لصق</string> <string name="menu_paste">لصق</string>
@@ -166,8 +166,8 @@
<string name="unsupported_db_version">قاعدة بيانات غير مدعومة.</string> <string name="unsupported_db_version">قاعدة بيانات غير مدعومة.</string>
<string name="build_label">بناء %1$s</string> <string name="build_label">بناء %1$s</string>
<string name="encrypted_value_stored">تم حفظ كلمة السر المشفرة</string> <string name="encrypted_value_stored">تم حفظ كلمة السر المشفرة</string>
<string name="no_credentials_stored">قاعدة البيانات لا تمتلك كلمة سر.</string> <string name="no_credentials_stored">قاعدة البيانات لا تمتلك بيانات اعتماد.</string>
<string name="menu_appearance_settings">مظهر</string> <string name="menu_appearance_settings">المظهر</string>
<string name="general">عام</string> <string name="general">عام</string>
<string name="autofill">ملأ تلقائي</string> <string name="autofill">ملأ تلقائي</string>
<string name="autofill_sign_in_prompt">سجل باستخدام KeePassDX</string> <string name="autofill_sign_in_prompt">سجل باستخدام KeePassDX</string>
@@ -187,7 +187,7 @@
<string name="file_name">اسم الملف</string> <string name="file_name">اسم الملف</string>
<string name="path">مسار</string> <string name="path">مسار</string>
<string name="database_history">تأريخ</string> <string name="database_history">تأريخ</string>
<string name="clipboard_notifications_summary">مكن اشعارات الحافظة لنسخ الحقول</string> <string name="clipboard_notifications_summary">أظهر اشعارات الحافظة لنسخ الحقول عند عرض مدخل</string>
<string name="advanced_unlock">البصمة</string> <string name="advanced_unlock">البصمة</string>
<string name="biometric_unlock_enable_title">فحص البصمة</string> <string name="biometric_unlock_enable_title">فحص البصمة</string>
<string name="biometric_unlock_enable_summary">يسمح بفحص البصمة لفتح قاعدة البيانات</string> <string name="biometric_unlock_enable_summary">يسمح بفحص البصمة لفتح قاعدة البيانات</string>
@@ -210,23 +210,23 @@
<string name="keyboard_notification_entry_summary">أظهر إشعار عند توفر مدخل</string> <string name="keyboard_notification_entry_summary">أظهر إشعار عند توفر مدخل</string>
<string name="keyboard_notification_entry_content_title_text">مدخل</string> <string name="keyboard_notification_entry_content_title_text">مدخل</string>
<string name="keyboard_notification_entry_clear_close_title">إمسح عند الخروج</string> <string name="keyboard_notification_entry_clear_close_title">إمسح عند الخروج</string>
<string name="keyboard_notification_entry_clear_close_summary">إمسح مدخل الحافظة عند إغلاق الإشعار</string> <string name="keyboard_notification_entry_clear_close_summary">أغلق قاعدة البيانات عند إغلاق الإشعار</string>
<string name="keyboard_appearance_category">مظهر</string> <string name="keyboard_appearance_category">مظهر</string>
<string name="keyboard_theme_title">سمة لوحة المفاتيح</string> <string name="keyboard_theme_title">سمة لوحة المفاتيح</string>
<string name="keyboard_keys_category">مفاتيح</string> <string name="keyboard_keys_category">مفاتيح</string>
<string name="keyboard_key_vibrate_title">إهتز عند اللمس</string> <string name="keyboard_key_vibrate_title">إهتزاز عند اللمس</string>
<string name="keyboard_key_sound_title">صوت عند اللمس</string> <string name="keyboard_key_sound_title">صوت عند اللمس</string>
<string name="allow_no_password_title">"إسمح بالفتح دون كلمة سر "</string> <string name="allow_no_password_title">"إسمح بالفتح دون كلمة سر "</string>
<string name="enable_read_only_title">محمي من التعديل</string> <string name="enable_read_only_title">محمي من التعديل</string>
<string name="enable_read_only_summary">افتح قاعدة البيانات للقراءة فقط افتراضيا</string> <string name="enable_read_only_summary">افتح قاعدة البيانات للقراءة فقط افتراضيا</string>
<string name="enable_education_screens_title">شاشات تعليمية</string> <string name="enable_education_screens_title">شاشات تعليمية</string>
<string name="reset_education_screens_summary">أعد عرض كل العناصر التعليمية</string> <string name="reset_education_screens_summary">أعد عرض كل المعلومات التعليمية</string>
<string name="reset_education_screens_text">إعادة تعيين الشاشات التعليمية</string> <string name="reset_education_screens_text">إعادة تعيين الشاشات التلميحات</string>
<string name="education_create_database_title">أنشئ قاعدة بيانات</string> <string name="education_create_database_title">أنشئ قاعدة بيانات</string>
<string name="education_create_database_summary">أنشئ ملف إدارة كلمات السر.</string> <string name="education_create_database_summary">أنشئ ملف إدارة كلمات السر.</string>
<string name="education_select_database_title">إفتح قاعدة بيانات</string> <string name="education_select_database_title">إفتح قاعدة بيانات</string>
<string name="sort_recycle_bin_bottom">سلة المحذوفات في الأسفل</string> <string name="sort_recycle_bin_bottom">سلة المحذوفات في الأسفل</string>
<string name="sort_db">قاعده بيانات طبيعية</string> <string name="sort_db">ترتيب طبيعي</string>
<string name="sort_last_access_time">الوصول</string> <string name="sort_last_access_time">الوصول</string>
<string name="lock">إقفال</string> <string name="lock">إقفال</string>
<string name="assign_master_key">تعيين مفتاح رئيسي</string> <string name="assign_master_key">تعيين مفتاح رئيسي</string>
@@ -246,9 +246,9 @@
<string name="content_description_background">الخلفية</string> <string name="content_description_background">الخلفية</string>
<string name="rounds">دورات التحويل</string> <string name="rounds">دورات التحويل</string>
<string name="rounds_explanation">توفر الدورات الاضافية ضد هجوم توليد التركيبات ،لكنها تبطئ التحميل والحفظ.</string> <string name="rounds_explanation">توفر الدورات الاضافية ضد هجوم توليد التركيبات ،لكنها تبطئ التحميل والحفظ.</string>
<string name="memory_usage_explanation">مقدار الذاكرة لاستخدامها في دالة اشتقاق المفتاح.</string> <string name="memory_usage_explanation">مقدار الذاكرة المستخدمة في دالة اشتقاق المفتاح.</string>
<string name="parallelism_explanation">درجة التوازي (عدد العمليات) لدالة اشتقاق المفتاح.</string> <string name="parallelism_explanation">درجة التوازي (عدد العمليات) لدالة اشتقاق المفتاح.</string>
<string name="sort_groups_before">مجموعات قبل</string> <string name="sort_groups_before">المجموعات أولًا</string>
<string name="selection_mode">نمط التحديد</string> <string name="selection_mode">نمط التحديد</string>
<string name="do_not_kill_app">لا تقتل التطبيق…</string> <string name="do_not_kill_app">لا تقتل التطبيق…</string>
<string name="content_description_node_children">العقد الفرعية</string> <string name="content_description_node_children">العقد الفرعية</string>
@@ -264,7 +264,7 @@
<string name="content_description_update_from_list">تحديث</string> <string name="content_description_update_from_list">تحديث</string>
<string name="content_description_keyboard_close_fields">أغلق الحقول</string> <string name="content_description_keyboard_close_fields">أغلق الحقول</string>
<string name="error_create_database_file">لا يمكن انشاء قاعدة بيانات بكلمة السر وملف المفتاح الحاليين.</string> <string name="error_create_database_file">لا يمكن انشاء قاعدة بيانات بكلمة السر وملف المفتاح الحاليين.</string>
<string name="menu_advanced_unlock_settings">إلغاء القفل المتقدم</string> <string name="menu_advanced_unlock_settings">فك القفل المتقدم</string>
<string name="entry_attachments">مرفقات</string> <string name="entry_attachments">مرفقات</string>
<string name="entry_history">السجل</string> <string name="entry_history">السجل</string>
<string name="entry_add_attachment">أضف مرفقا</string> <string name="entry_add_attachment">أضف مرفقا</string>
@@ -283,7 +283,7 @@
<string name="otp_algorithm">الخوارزمية</string> <string name="otp_algorithm">الخوارزمية</string>
<string name="otp_digits">أرقام</string> <string name="otp_digits">أرقام</string>
<string name="otp_counter">العداد</string> <string name="otp_counter">العداد</string>
<string name="entry_setup_otp">كلمة المرور للمرة الواحدة</string> <string name="entry_setup_otp">عيّن كلمة مرور لمرة واحدة</string>
<string name="entry_UUID">UUID</string> <string name="entry_UUID">UUID</string>
<string name="html_about_contribution">من أجل &lt;strong&gt;حماية خصوصيتا&lt;/strong&gt;٫&lt;strong&gt; إصلاح العلل&lt;/strong&gt;٫ &lt;strong&gt;إضافة مميزات&lt;/strong&gt; &lt;strong&gt;وجعلنا نشطاء دائما&lt;/strong&gt;٫ نحن نعتمد على &lt;strong&gt;مساهمتك&lt;/strong&gt;.</string> <string name="html_about_contribution">من أجل &lt;strong&gt;حماية خصوصيتا&lt;/strong&gt;٫&lt;strong&gt; إصلاح العلل&lt;/strong&gt;٫ &lt;strong&gt;إضافة مميزات&lt;/strong&gt; &lt;strong&gt;وجعلنا نشطاء دائما&lt;/strong&gt;٫ نحن نعتمد على &lt;strong&gt;مساهمتك&lt;/strong&gt;.</string>
<string name="content_description_keyfile_checkbox">خانة تأشير الملف المفتاحي</string> <string name="content_description_keyfile_checkbox">خانة تأشير الملف المفتاحي</string>
@@ -295,10 +295,10 @@
<string name="hide_broken_locations_title">اِخفي روابط قواعد البيانات المعطلة</string> <string name="hide_broken_locations_title">اِخفي روابط قواعد البيانات المعطلة</string>
<string name="show_recent_files_summary">أظهر موقع قواعد البيانات الأخيرة</string> <string name="show_recent_files_summary">أظهر موقع قواعد البيانات الأخيرة</string>
<string name="show_recent_files_title">أظهر الملفات الأخيرة</string> <string name="show_recent_files_title">أظهر الملفات الأخيرة</string>
<string name="remember_keyfile_locations_summary">تذكر موقع الملفات المفتاحية لقاعدة البيانات</string> <string name="remember_keyfile_locations_summary">تعقب موقع الملفات المفتاحية لقاعدة البيانات</string>
<string name="remember_keyfile_locations_title">احفظ موقع الملف المفتاحي</string> <string name="remember_keyfile_locations_title">تذكر موقع الملف المفتاحي</string>
<string name="remember_database_locations_summary">تذكر موقع قاعدة البيانات</string> <string name="remember_database_locations_summary">تعقب موقع قاعدة البيانات</string>
<string name="remember_database_locations_title">موقع تخزين قاعدة البيانات</string> <string name="remember_database_locations_title">تذكر موقع تخزين قاعدة البيانات</string>
<string name="contains_duplicate_uuid_procedure">للمتابعة هل تريد حل المشكلة بتوليد UUID للعناصر المكررة ؟</string> <string name="contains_duplicate_uuid_procedure">للمتابعة هل تريد حل المشكلة بتوليد UUID للعناصر المكررة ؟</string>
<string name="contains_duplicate_uuid">تحتوي قاعدة البيانات على UUID مكرر.</string> <string name="contains_duplicate_uuid">تحتوي قاعدة البيانات على UUID مكرر.</string>
<string name="auto_focus_search_title">البحث السريع</string> <string name="auto_focus_search_title">البحث السريع</string>
@@ -379,8 +379,8 @@
<string name="keyboard_selection_entry_title">اختيار المدخلة</string> <string name="keyboard_selection_entry_title">اختيار المدخلة</string>
<string name="device_keyboard_setting_title">إعدادات لوحة مفاتيح الجهاز</string> <string name="device_keyboard_setting_title">إعدادات لوحة مفاتيح الجهاز</string>
<string name="magic_keyboard_explanation_summary">نشِّط لوحة مفاتيح مخصصة لملأ كلمة السر وحقول معرّفك</string> <string name="magic_keyboard_explanation_summary">نشِّط لوحة مفاتيح مخصصة لملأ كلمة السر وحقول معرّفك</string>
<string name="biometric_auto_open_prompt_summary">اطلب فحص البصمة ان كانت قاعدة البيانات معدّة لذلك</string> <string name="biometric_auto_open_prompt_summary">اطلب فك القفل المتقدم ان كانت قاعدة البيانات معدّة لذلك</string>
<string name="biometric_auto_open_prompt_title">افتح محث البصمة تلقائيا</string> <string name="biometric_auto_open_prompt_title">افتح المحث تلقائيا</string>
<string name="keystore_not_accessible">لم يُهيأ مخزن المفاتيح بشكل صحيح.</string> <string name="keystore_not_accessible">لم يُهيأ مخزن المفاتيح بشكل صحيح.</string>
<string name="warning_remove_unlinked_attachment">حذف البيانات سيقلل من حجم قاعدة البيانات لكن احذر أن تكون إحدى هذه البيانات ملحقة لكي-باس.</string> <string name="warning_remove_unlinked_attachment">حذف البيانات سيقلل من حجم قاعدة البيانات لكن احذر أن تكون إحدى هذه البيانات ملحقة لكي-باس.</string>
<string name="subdomain_search_summary">البحث في نطاقات الويب التي فيها قيود النطاقات الفرعية</string> <string name="subdomain_search_summary">البحث في نطاقات الويب التي فيها قيود النطاقات الفرعية</string>
@@ -408,4 +408,79 @@
<string name="education_generate_password_title">أنشئ كلمة سر قوية</string> <string name="education_generate_password_title">أنشئ كلمة سر قوية</string>
<string name="save_mode">وضع الحفظ</string> <string name="save_mode">وضع الحفظ</string>
<string name="search_mode">وضع البحث</string> <string name="search_mode">وضع البحث</string>
<string name="version">النسخة</string>
<string name="template_group_name">النماذج</string>
<string name="holder">الحامل</string>
<string name="number">الرقم</string>
<string name="card_verification_value">CVV</string>
<string name="personal_identification_number">PIN</string>
<string name="id_card">بطاقة الهوية</string>
<string name="type">النوع</string>
<string name="cryptocurrency">محفظة عملات مشفرة</string>
<string name="public_key">المفتاح العمومي</string>
<string name="private_key">المفتاح الخاص</string>
<string name="account">الحساب</string>
<string name="bank">مصرف</string>
<string name="bank_name">اسم المصرف</string>
<string name="secure_note">ملاحظة آمنة</string>
<string name="error_word_reserved">هذه الكلمة محجوزة ولا يمكن استخدامها.</string>
<string name="error_field_name_already_exists">اسم الحقل موجود سلفًا.</string>
<string name="error_file_to_big">الملف الذي ترفعه كبير.</string>
<string name="error_upload_file">حدث خطأ أثناء رفع الملف.</string>
<string name="error_duplicate_file">بيانات الملف موجودة سلفًا.</string>
<string name="error_remove_file">حدث خطأ أثناء إزالة بيانات الملف.</string>
<string name="error_start_database_action">حدث خطأ أثناء تنفيذ إجراء على قاعدة البيانات.</string>
<string name="content_description_otp_information">معلومات السر لمرة واحدة</string>
<string name="membership">العضوية</string>
<string name="name">الاسم</string>
<string name="email">البريد الإلكتروني</string>
<string name="email_address">البريد الإلكتروني</string>
<string name="ssid">SSID</string>
<string name="debit_credit_card">بطاقة السحب الفوري / الإئتمان</string>
<string name="error_registration_read_only">لا يمكن حفظ عنصر في قاعدة بيانات مفتوحة للقراءة فقط</string>
<string name="otp_secret">الرمز السري</string>
<string name="place_of_issue">مكان المشكلة</string>
<string name="date_of_issue">تاريخ المشكلة</string>
<string name="standard">المعيار</string>
<string name="template">النموذج</string>
<string name="error_invalid_OTP">الرمز السري لـ OTP غير صالح.</string>
<string name="error_otp_digits">يجب أن الرمز محتوًا بين %1$d و %2$d رقمًا.</string>
<string name="autofill_select_entry">اختر مُدخلًا…</string>
<string name="content">المحتوى</string>
<string name="keyboard_save_search_info_title">احفظ المعلومات المشاركة</string>
<string name="custom_fields">حقول مخصصة</string>
<string name="back_to_previous_keyboard">عُد للوحة المفاتيح السابقة</string>
<string name="select_entry">اختر مدخلًا</string>
<string name="autofill_close_database_title">أغلق قاعدة البيانات</string>
<string name="success_import_app_properties">أّستوردت خصائص التطبيق</string>
<string name="success_export_app_properties">صُدرت خصائص التطبيق</string>
<string name="warning_database_revoked">أُجهض الوصول إلى الملف بواسطة مدير الملفات ، أغلق قاعدة البيانات ثم أعد فتحها.</string>
<string name="properties">الخصائص</string>
<string name="token">الرمز</string>
<string name="seed">البذرة</string>
<string name="error_database_uri_null">يتعذر استرداد مسار قاعدة البيانات.</string>
<string name="error_rebuild_list">يتعذر إعادة بناء القائمة بشكل صحيح.</string>
<string name="menu_keystore_remove_key">احذف رمز فك القفل المتقدم</string>
<string name="menu_form_filling_settings">تعبئة الحقول</string>
<string name="menu_reload_database">أعد تحميل قاعدة البيانات</string>
<string name="menu_external_icon">أيقونة خارجية</string>
<string name="registration_mode">وضع التسجيل</string>
<string name="import_app_properties_title">استورد خصائص التطبيق</string>
<string name="import_app_properties_summary">اختر ملفًا لاستيراد خصائص التطبيق</string>
<string name="export_app_properties_title">صدّر خصائص التطبيق</string>
<string name="export_app_properties_summary">أنشئ ملفًا لتصدير خصائص التطبيق</string>
<string name="error_import_app_properties">خطأ أثناء استيراد خصائص التطبيق</string>
<string name="error_export_app_properties">خطأ أثناء تصدير خصائص التطبيق</string>
<string name="warning_database_info_changed">غُيِّرت معلومات قاعدة البيانات من خارج هذا التطبيق.</string>
<string name="warning_database_info_changed_options">اكتب فوق التعديلات الخارجية عن طريق حفظ قاعدة البيانات أو أعد تحميلها لتضمين هذه التغييرات.</string>
<string name="open_advanced_unlock_prompt_store_credential">افتح محث فك القفل المتقدم لتخزين بيانات الاعتماد</string>
<string name="open_advanced_unlock_prompt_unlock_database">افتح محث فك القفل المتقدم لفتح قاعدة البيانات</string>
<string name="credential_before_click_advanced_unlock_button">اكتب كلمة السر، وأنقر هذا الزر.</string>
<string name="device_credential">بيانات الاعتماد للجهاز</string>
<string name="advanced_unlock_tap_delete">انفر لحذف مفاتيح فك القفل المتقدم</string>
<string name="keyboard_search_share_title">ابحث في المعلومات المشاركة</string>
<string name="keyboard_auto_go_action_title">إجراء اللمس التلقائي</string>
<string name="keyboard_previous_fill_in_title">إجراء لمس تلقائي</string>
<string name="keyboard_previous_lock_title">اقفل قاعدة البيانات</string>
<string name="education_advanced_unlock_title">فك القفل المتقدم لقاعدة البيانات</string>
</resources> </resources>

View File

@@ -197,7 +197,7 @@
<string name="clipboard_warning">Vymazat historii schránky manuálně, pokud automatické vymazání schránky selže.</string> <string name="clipboard_warning">Vymazat historii schránky manuálně, pokud automatické vymazání schránky selže.</string>
<string name="lock">Zamknout</string> <string name="lock">Zamknout</string>
<string name="lock_database_screen_off_title">Zámek obrazovky</string> <string name="lock_database_screen_off_title">Zámek obrazovky</string>
<string name="lock_database_screen_off_summary">Při zhasnutí obrazovky uzamknout databázi</string> <string name="lock_database_screen_off_summary">Několik vteřin po zhasnutí obrazovky uzamknout databázi</string>
<string name="advanced_unlock">Rozšířené odemknutí</string> <string name="advanced_unlock">Rozšířené odemknutí</string>
<string name="biometric_unlock_enable_title">Biometrické odemknutí</string> <string name="biometric_unlock_enable_title">Biometrické odemknutí</string>
<string name="biometric_unlock_enable_summary">Nechá otevřít databázi snímáním biometrického údaje</string> <string name="biometric_unlock_enable_summary">Nechá otevřít databázi snímáním biometrického údaje</string>
@@ -224,7 +224,7 @@
<string name="application_appearance">Rozhraní</string> <string name="application_appearance">Rozhraní</string>
<string name="other">Ostatní</string> <string name="other">Ostatní</string>
<string name="keyboard">Klávesnice</string> <string name="keyboard">Klávesnice</string>
<string name="magic_keyboard_title">Magikeyboard</string> <string name="magic_keyboard_title">Klávesnice Magikeyboard</string>
<string name="magic_keyboard_explanation_summary">Aktivovat vlastní klávesnici, která snadno vyplní hesla a další položky identity</string> <string name="magic_keyboard_explanation_summary">Aktivovat vlastní klávesnici, která snadno vyplní hesla a další položky identity</string>
<string name="allow_no_password_title">Umožnit bez hlavního klíče</string> <string name="allow_no_password_title">Umožnit bez hlavního klíče</string>
<string name="allow_no_password_summary">Povolit klepnutí na \"Otevřít\", i když není vybráno žádné heslo</string> <string name="allow_no_password_summary">Povolit klepnutí na \"Otevřít\", i když není vybráno žádné heslo</string>
@@ -236,7 +236,7 @@
<string name="reset_education_screens_summary">Opět zobrazit všechny vzdělávací informace</string> <string name="reset_education_screens_summary">Opět zobrazit všechny vzdělávací informace</string>
<string name="reset_education_screens_text">Nastavit vzdělávací nápovědy do výchozího stavu</string> <string name="reset_education_screens_text">Nastavit vzdělávací nápovědy do výchozího stavu</string>
<string name="education_create_database_title">Vytvořit databázový soubor</string> <string name="education_create_database_title">Vytvořit databázový soubor</string>
<string name="education_create_database_summary">Založte svůj první soubor pro správu hesel.</string> <string name="education_create_database_summary">Vytvořte svůj první soubor pro správu hesel.</string>
<string name="education_select_database_title">Otevřít existující databázi</string> <string name="education_select_database_title">Otevřít existující databázi</string>
<string name="education_select_database_summary">Otevřete svou dříve používanou databázi ze správce souborů a pokračujte v jejím používání.</string> <string name="education_select_database_summary">Otevřete svou dříve používanou databázi ze správce souborů a pokračujte v jejím používání.</string>
<string name="education_new_node_title">Přidejte záznamy do databáze</string> <string name="education_new_node_title">Přidejte záznamy do databáze</string>
@@ -267,7 +267,7 @@
<string name="education_sort_summary">Vyberte řazení položek a skupin.</string> <string name="education_sort_summary">Vyberte řazení položek a skupin.</string>
<string name="education_donation_title">Zapojit se</string> <string name="education_donation_title">Zapojit se</string>
<string name="education_donation_summary">Zapojte se a pomozte zvýšit stabilitu, bezpečnost a doplnění dalších funkcí.</string> <string name="education_donation_summary">Zapojte se a pomozte zvýšit stabilitu, bezpečnost a doplnění dalších funkcí.</string>
<string name="html_text_ad_free">Na rozdíl od mnoha aplikací pro správu hesel je tato &lt;strong&gt;bez reklam&lt;/strong&gt;, je \u0020&lt;strong&gt;svobodný software pod copyleft licencí&lt;/strong&gt; a nesbírá žádné osobní údaje na svých serverech bez ohledu na to, jakou verzi používáte.</string> <string name="html_text_ad_free">Na rozdíl od mnoha aplikací pro správu hesel je tato <strong>bez reklam</strong>, je <strong>svobodný software pod copyleft licencí</strong> a nesbírá žádné osobní údaje na svých serverech bez ohledu na to, jakou verzi používáte.</string>
<string name="html_text_buy_pro">Zakoupením varianty \"pro\" získáte přístup k tomuto &lt;strong&gt;vizuálnímu stylu&lt;/strong&gt; a hlavně pomůžete &lt;strong&gt;uskutečnění komunitních projektů.&lt;/strong&gt;</string> <string name="html_text_buy_pro">Zakoupením varianty \"pro\" získáte přístup k tomuto &lt;strong&gt;vizuálnímu stylu&lt;/strong&gt; a hlavně pomůžete &lt;strong&gt;uskutečnění komunitních projektů.&lt;/strong&gt;</string>
<string name="html_text_feature_generosity">Tento &lt;strong&gt;vizuální styl&lt;/strong&gt; je k dispozici díky vaší štědrosti.</string> <string name="html_text_feature_generosity">Tento &lt;strong&gt;vizuální styl&lt;/strong&gt; je k dispozici díky vaší štědrosti.</string>
<string name="html_text_donation">Pro zajištění svobody nás všech a pokračování aktivity počítáme s Vaším &lt;strong&gt;přispěním.&lt;/strong&gt;</string> <string name="html_text_donation">Pro zajištění svobody nás všech a pokračování aktivity počítáme s Vaším &lt;strong&gt;přispěním.&lt;/strong&gt;</string>
@@ -285,8 +285,8 @@
<string name="icon_pack_choose_title">Sada ikon</string> <string name="icon_pack_choose_title">Sada ikon</string>
<string name="icon_pack_choose_summary">Sada ikon používaných v aplikaci</string> <string name="icon_pack_choose_summary">Sada ikon používaných v aplikaci</string>
<string name="build_label">Sestavení %1$s</string> <string name="build_label">Sestavení %1$s</string>
<string name="keyboard_name">Magikeyboard</string> <string name="keyboard_name">Klávesnice Magikeyboard</string>
<string name="keyboard_label">Magikeyboard (KeePassDX)</string> <string name="keyboard_label">Klávesnice Magikeyboard (KeePassDX)</string>
<string name="keyboard_setting_label">Magikeyboard nastavení</string> <string name="keyboard_setting_label">Magikeyboard nastavení</string>
<string name="keyboard_entry_category">Záznam</string> <string name="keyboard_entry_category">Záznam</string>
<string name="keyboard_entry_timeout_title">Časový limit</string> <string name="keyboard_entry_timeout_title">Časový limit</string>
@@ -315,7 +315,7 @@
<string name="delete_entered_password_title">Smazat heslo</string> <string name="delete_entered_password_title">Smazat heslo</string>
<string name="delete_entered_password_summary">Smaže heslo zadané po pokusu o připojení k databázi</string> <string name="delete_entered_password_summary">Smaže heslo zadané po pokusu o připojení k databázi</string>
<string name="content_description_open_file">Otevřít soubor</string> <string name="content_description_open_file">Otevřít soubor</string>
<string name="content_description_node_children">Podřazené prvky uzlu</string> <string name="content_description_node_children">Podřazení uzlu</string>
<string name="content_description_add_node">Přidat uzel</string> <string name="content_description_add_node">Přidat uzel</string>
<string name="content_description_add_entry">Přidat záznam</string> <string name="content_description_add_entry">Přidat záznam</string>
<string name="content_description_add_group">Přidat skupinu</string> <string name="content_description_add_group">Přidat skupinu</string>
@@ -411,7 +411,7 @@
<string name="hide_expired_entries_summary">Propadlé záznamy nebudou ukázány</string> <string name="hide_expired_entries_summary">Propadlé záznamy nebudou ukázány</string>
<string name="contact">Kontakt</string> <string name="contact">Kontakt</string>
<string name="contribution">Příspěvky</string> <string name="contribution">Příspěvky</string>
<string name="feedback">Feedback</string> <string name="feedback">Zpětná vazba</string>
<string name="auto_focus_search_title">Snadné hledání</string> <string name="auto_focus_search_title">Snadné hledání</string>
<string name="auto_focus_search_summary">Při otevření databáze žádat hledání</string> <string name="auto_focus_search_summary">Při otevření databáze žádat hledání</string>
<string name="remember_database_locations_title">Pamatovat si umístění databází</string> <string name="remember_database_locations_title">Pamatovat si umístění databází</string>
@@ -474,13 +474,13 @@
<string name="database_data_remove_unlinked_attachments_summary">Odstraní přílohy obsažené v databázi, ale nikoli přílohy propojené se záznamem</string> <string name="database_data_remove_unlinked_attachments_summary">Odstraní přílohy obsažené v databázi, ale nikoli přílohy propojené se záznamem</string>
<string name="education_add_attachment_title">Přidat přílohu</string> <string name="education_add_attachment_title">Přidat přílohu</string>
<string name="education_add_attachment_summary">Nahrát přílohu k záznamu pro uložení důležitých externích dat.</string> <string name="education_add_attachment_summary">Nahrát přílohu k záznamu pro uložení důležitých externích dat.</string>
<string name="show_uuid_summary">Ukáže UUID propojené se záznamem</string> <string name="show_uuid_summary">Ukáže UUID propojené se záznamem nebo skupinou</string>
<string name="show_uuid_title">Ukázat UUID</string> <string name="show_uuid_title">Ukázat UUID</string>
<string name="autofill_read_only_save">Uložení dat není povoleno, je-li databáze v režimu pouze pro čtení.</string> <string name="autofill_read_only_save">Uložení dat není povoleno, je-li databáze v režimu pouze pro čtení.</string>
<string name="autofill_ask_to_save_data_summary">Zeptat se na uložení dat, jakmile byl formulář přezkoušen</string> <string name="autofill_ask_to_save_data_summary">Zeptat se na uložení dat, jakmile byl formulář přezkoušen</string>
<string name="autofill_ask_to_save_data_title">Zeptat se před uložením</string> <string name="autofill_ask_to_save_data_title">Zeptat se před uložením</string>
<string name="autofill_save_search_info_summary">Pokuste se uložit údaje hledání, když manuálně vybíráte položku</string> <string name="autofill_save_search_info_summary">Pokuste se uložit údaje hledání, když manuálně vybíráte položku</string>
<string name="autofill_save_search_info_title">Uložit info hledání</string> <string name="autofill_save_search_info_title">Uložit výsledky vyhledá</string>
<string name="autofill_close_database_summary">Zavřít databázi po samovyplnění polí</string> <string name="autofill_close_database_summary">Zavřít databázi po samovyplnění polí</string>
<string name="autofill_close_database_title">Zavřít databázi</string> <string name="autofill_close_database_title">Zavřít databázi</string>
<string name="keyboard_previous_lock_summary">Po uzamknutí databáze automaticky přepnout zpět na předchozí klávesnici</string> <string name="keyboard_previous_lock_summary">Po uzamknutí databáze automaticky přepnout zpět na předchozí klávesnici</string>
@@ -491,9 +491,9 @@
<string name="biometric_security_update_required">Vyžadována aktualizace biometrického zabezpečení.</string> <string name="biometric_security_update_required">Vyžadována aktualizace biometrického zabezpečení.</string>
<string name="configure_biometric">Žádné přihlašovací ani biometrické údaje nejsou registrovány.</string> <string name="configure_biometric">Žádné přihlašovací ani biometrické údaje nejsou registrovány.</string>
<string name="warning_empty_recycle_bin">Trvale odstranit všechny uzly z koše\?</string> <string name="warning_empty_recycle_bin">Trvale odstranit všechny uzly z koše\?</string>
<string name="registration_mode">Režim registrace</string> <string name="registration_mode">Registrace</string>
<string name="save_mode">Režim ukládání</string> <string name="save_mode">Uložit</string>
<string name="search_mode">Režim vyhledávání</string> <string name="search_mode">Vyhledávání</string>
<string name="error_field_name_already_exists">Jméno kolonky již existuje.</string> <string name="error_field_name_already_exists">Jméno kolonky již existuje.</string>
<string name="error_registration_read_only">Uložení nové položky v režimu databáze pouze pro čtení není povoleno</string> <string name="error_registration_read_only">Uložení nové položky v režimu databáze pouze pro čtení není povoleno</string>
<string name="enter">Enter</string> <string name="enter">Enter</string>
@@ -596,4 +596,10 @@
<string name="holder">Majitel</string> <string name="holder">Majitel</string>
<string name="debit_credit_card">Debitní / Kreditní karta</string> <string name="debit_credit_card">Debitní / Kreditní karta</string>
<string name="template_group_name">Předlohy</string> <string name="template_group_name">Předlohy</string>
<string name="show_otp_token_summary">Ukáže OTP tokeny v seznamu záznamů</string>
<string name="show_otp_token_title">Ukázat OTP token</string>
<string name="menu_external_icon">Externí ikona</string>
<string name="autofill_select_entry">Vyberte položku…</string>
<string name="autofill_manual_selection_summary">Zobrazit možnosti umožňující uživateli si vybrat položku z databáze</string>
<string name="autofill_manual_selection_title">Ruční výběr</string>
</resources> </resources>

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