Compare commits

..

1612 Commits
3.3.3 ... 4.1.8

Author SHA1 Message Date
J-Jamet
84a62a32ff Merge branch 'release/4.1.8' 2025-09-12 16:03:59 +02:00
J-Jamet
da8ef9340c fix: Loading ViewModel 2025-09-12 15:23:32 +02:00
J-Jamet
af068349e4 fix: Upgrade to 4.1.8 2025-09-12 14:14:06 +02:00
J-Jamet
56cb5953dd fix: Deletable recycle bin #2163 2025-09-12 13:00:56 +02:00
J-Jamet
2fc2a9c7c1 fix: Delete algo during merge #1516 2025-09-11 21:19:40 +02:00
J-Jamet
69e7cdbc47 fix: Search with space #175 2025-09-11 16:43:40 +02:00
J-Jamet
39d9a74a73 fix: Warnings 2025-09-11 16:36:35 +02:00
J-Jamet
7212c73481 fix: Warnings 2025-09-11 14:55:37 +02:00
J-Jamet
3ee4caa153 fix: Warnings 2025-09-11 14:53:41 +02:00
J-Jamet
28e4d929bb fix: Warnings 2025-09-11 14:51:35 +02:00
J-Jamet
a46251be7b fix: Better biometric exception implementation 2025-09-09 13:37:50 +02:00
J-Jamet
e8ec27dc38 fix: Upgrade to API 35 2025-09-09 11:45:30 +02:00
J-Jamet
30dd7c567c fix: Autofill compatibility package 2025-09-08 15:17:09 +02:00
J-Jamet
b196145578 Merge branch 'milotype-croatian-translation-20250907' into translations
# Conflicts:
#	fastlane/metadata/android/hr/full_description.txt
#	fastlane/metadata/android/hr/short_description.txt
#	fastlane/metadata/android/hr/title.txt
2025-09-08 12:45:09 +02:00
J-Jamet
ac347db2d1 fix: Translations 2025-09-08 12:43:06 +02:00
J-Jamet
013c437cf7 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-09-08 12:38:51 +02:00
Milo Ivir
1f600d60e3 Translated using Weblate (Croatian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/hr/
2025-09-08 12:33:27 +02:00
Milo Ivir
d5ecaeb331 Add Croatian translation 2025-09-07 16:29:42 +02:00
Milo Ivir
db8b0100de Translated using Weblate (Croatian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2025-09-07 15:51:05 +02:00
Ihor Hordiichuk
5f41177a1f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2025-09-06 12:01:59 +00:00
Artyom Rybakov
0db2b7023e Translated using Weblate (Russian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ru/
2025-09-02 14:02:02 +00:00
Artyom Rybakov
a2c2a21dde Translated using Weblate (Russian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-09-02 14:02:00 +00:00
scollovati
d7a3e7fedd Translated using Weblate (Italian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2025-09-02 14:01:58 +00:00
J-Jamet
5e4ee167fc Merge branch 'rmacklin-remember-last-read-only-state-of-each-database' into develop 2025-09-01 19:02:59 +02:00
J-Jamet
c911b7c511 fix: Import DatabaseFile 2025-09-01 19:01:35 +02:00
J-Jamet
c79d1f1b81 Merge branch 'Dev-ClayP-master' into develop 2025-09-01 18:19:11 +02:00
J-Jamet
daf717becd fix: Remove JELLY_BEAN_MR1 conditions and unused PRNGFixes 2025-09-01 18:15:00 +02:00
J-Jamet
48d4483484 Merge branch 'master' of github.com:Dev-ClayP/KeePassDX into Dev-ClayP-master 2025-09-01 17:34:46 +02:00
leap123
c6b0ee27df Translated using Weblate (Indonesian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/id/
2025-08-31 17:01:54 +00:00
Priit Jõerüüt
0053726d0b Translated using Weblate (Estonian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-08-30 18:16:53 +02:00
leap123
1395af88d1 Translated using Weblate (Indonesian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2025-08-30 18:16:52 +02:00
Fjuro
2e3ade1b4a Translated using Weblate (Czech)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2025-08-30 18:16:52 +02:00
XiveZ
90c43acfbf Translated using Weblate (Belarusian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/be/
2025-08-30 07:03:02 +02:00
Ghost of Sparta
90b68fd972 Translated using Weblate (Hungarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2025-08-30 07:03:02 +02:00
XiveZ
ef6aeceb20 Translated using Weblate (Belarusian)
Currently translated at 5.2% (35 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/be/
2025-08-29 07:25:41 +02:00
jonnysemon
ef8685f0e7 Translated using Weblate (Arabic)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-08-29 07:25:41 +02:00
XiveZ
3021ed158b Added translation using Weblate (Belarusian) 2025-08-29 07:02:51 +02:00
weblator
a57043f496 Translated using Weblate (Azerbaijani)
Currently translated at 96.8% (645 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2025-08-29 07:02:51 +02:00
weblator
fdfd124fee Translated using Weblate (Serbian)
Currently translated at 50.3% (335 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sr/
2025-08-29 07:02:50 +02:00
weblator
71739de91a Translated using Weblate (Cantonese (Traditional Han script))
Currently translated at 19.9% (133 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/yue_Hant/
2025-08-29 07:02:50 +02:00
weblator
041b1fbf53 Translated using Weblate (Burmese)
Currently translated at 6.4% (43 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/my/
2025-08-29 07:02:50 +02:00
weblator
3a72b32b4a Translated using Weblate (Slovenian)
Currently translated at 9.7% (65 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sl/
2025-08-29 07:02:50 +02:00
weblator
994f174300 Translated using Weblate (Marathi)
Currently translated at 1.2% (8 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/mr/
2025-08-29 07:02:49 +02:00
weblator
c0f32254bb Translated using Weblate (Esperanto)
Currently translated at 21.1% (141 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eo/
2025-08-29 07:02:49 +02:00
109247019824
fd98dbeebe Translated using Weblate (Bulgarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-08-29 07:02:49 +02:00
weblator
69ac6e6698 Translated using Weblate (Serbian (Latin script))
Currently translated at 59.0% (393 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sr_Latn/
2025-08-29 07:02:48 +02:00
weblator
35e224d227 Translated using Weblate (Portuguese)
Currently translated at 99.6% (664 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2025-08-29 07:02:47 +02:00
weblator
2da8552a53 Translated using Weblate (Javanese)
Currently translated at 3.6% (24 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/jv/
2025-08-29 07:02:47 +02:00
weblator
a9a5047949 Translated using Weblate (Persian)
Currently translated at 43.8% (292 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fa/
2025-08-29 07:02:47 +02:00
weblator
17c98f7fea Translated using Weblate (Indonesian)
Currently translated at 99.6% (664 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2025-08-29 07:02:47 +02:00
weblator
c3bc890665 Translated using Weblate (Malayalam)
Currently translated at 56.4% (376 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2025-08-29 07:02:47 +02:00
weblator
7a295c2541 Translated using Weblate (Punjabi)
Currently translated at 48.4% (323 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pa/
2025-08-29 07:02:46 +02:00
weblator
01b1b74c6a Translated using Weblate (Bengali (Bangladesh))
Currently translated at 13.3% (89 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bn_BD/
2025-08-29 07:02:46 +02:00
weblator
fd25d21c72 Translated using Weblate (Hindi)
Currently translated at 21.9% (146 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hi/
2025-08-29 07:02:46 +02:00
Telaneo
6b1d8d24dd Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2025-08-29 07:02:46 +02:00
大王叫我来巡山
5d002f5128 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2025-08-29 07:02:45 +02:00
weblator
98314c466f Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.6% (664 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2025-08-29 07:02:44 +02:00
Matthaiks
4f7afd7c97 Translated using Weblate (Polish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-08-29 07:02:44 +02:00
weblator
a9e139ff7e Translated using Weblate (Norwegian Nynorsk)
Currently translated at 42.6% (284 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nn/
2025-08-29 07:02:44 +02:00
Stephan Paternotte
4ff483a8d2 Translated using Weblate (Dutch)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-08-29 07:02:44 +02:00
weblator
1916b79df1 Translated using Weblate (Lithuanian)
Currently translated at 60.2% (401 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2025-08-29 07:02:44 +02:00
Liner Seven
98e15a7717 Translated using Weblate (Japanese)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-08-29 07:02:43 +02:00
Masowick
dfd18e3c7f Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-08-29 07:02:42 +02:00
weblator
8fbbaae05b Translated using Weblate (English)
Currently translated at 99.8% (665 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2025-08-29 07:02:41 +02:00
Richard Macklin
8a60056866 Remove no-longer-needed enable_read_only_* string resources
(We just removed the usages of these strings)
2025-08-26 20:10:46 -07:00
Richard Macklin
e9d20a51a5 Remove global "Write-protected" setting
This addresses J-Jamet's feedback that keeping the global setting would
potentially lead to confusion, so it should be removed now that we are
remembering each database's read-only state.
2025-08-26 20:10:42 -07:00
Richard Macklin
a28d77ba32 feat: Remember the last read-only state of each database
The app has supported a global setting for opening (all) databases in
read-only mode. But that's not particularly flexible for the use case
where you have one database that should be read-only and one that should
be read-write.

Previously, to handle this use case you could open one database in
read-only mode, but the next time you attempted to open the same
database, it would "forget" that, so you would have to toggle it to
read-only mode again manually. This commit changes that behavior so that
if you toggle a database to read-only mode, it'll be remembered the next
time you open the database. (You can still toggle it back to read-write
if you change your mind, and that, too, will be remembered the next time
you open the database.)
2025-08-26 20:08:22 -07:00
Telaneo
d143605a40 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2025-08-25 17:28:06 +02:00
J-Jamet
15972efb4f Merge branch 'develop' 2025-08-23 00:40:23 +02:00
J-Jamet
dae5f65c0d Revert "fix: revert checkUnlock"
This reverts commit 564b5f10ea.
2025-08-23 00:20:48 +02:00
J-Jamet
564b5f10ea fix: revert checkUnlock 2025-08-23 00:03:59 +02:00
J-Jamet
e6e40f9bd4 fix: Update to 4.1.7 2025-08-22 23:54:03 +02:00
J-Jamet
bd15e36b52 fix: Multiple biometric prompt 2025-08-22 22:50:58 +02:00
J-Jamet
43faca3061 fix: Multiple call to check availability 2025-08-22 22:34:11 +02:00
J-Jamet
82af9bada2 fix: CipherDatabase listener and rename advanced unlock in device unlock 2025-08-22 22:05:51 +02:00
Liner Seven
2befa68c93 Translated using Weblate (Japanese)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-08-22 11:02:30 +02:00
Random
4034a2bfc4 Translated using Weblate (Italian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/it/
2025-08-20 13:02:22 +00:00
Nara Huang
0d93e867cf Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2025-08-20 13:02:18 +00:00
Hosted Weblate
8cac4eb51c Merge branch 'origin/develop' into Weblate. 2025-08-19 10:32:22 +02:00
Alex Ionescu
933d34ff1d Translated using Weblate (Romanian)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2025-08-19 10:32:21 +02:00
J-Jamet
d34f460b98 fix: Remove title in template issue 2025-08-19 10:12:11 +02:00
J-Jamet
7632face63 fix: Update issue template 2025-08-19 10:09:48 +02:00
J-Jamet
c4cbd2e587 fix: tags 2025-08-19 09:28:59 +02:00
J-Jamet
ad454c2e4a Merge branch 'translations' into develop 2025-08-19 09:20:35 +02:00
J-Jamet
fbb03c3ecf Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-08-19 09:19:55 +02:00
J-Jamet
1a4d963d53 fix: Background event 2025-08-18 20:57:38 +02:00
J-Jamet
0c58992c21 feat: New github templates 2025-08-18 19:47:25 +02:00
J-Jamet
0eaeb3b90b fix: Upgrade CHANGELOG 2025-08-18 18:55:37 +02:00
J-Jamet
fba8443d0a fix: logs 2025-08-18 18:45:11 +02:00
J-Jamet
601874442c fix: Better database loading 2025-08-18 18:29:48 +02:00
J-Jamet
fa34618d67 fix: Connect database 2025-08-18 16:55:39 +02:00
J-Jamet
a60fc83379 fix: old device code 2025-08-18 16:28:45 +02:00
J-Jamet
c2c9ebe4c7 fix: Orientatino change in old device 2025-08-18 16:08:14 +02:00
J-Jamet
fdd86e2b9e fix: cryptoPrompt private variable 2025-08-18 13:16:26 +02:00
J-Jamet
34f0f45862 fix: Add app in background event 2025-08-18 13:12:32 +02:00
J-Jamet
c0345d4dc4 Revert "fix: Smal fix"
This reverts commit 24e859c4ce.
2025-08-18 12:11:17 +02:00
J-Jamet
24e859c4ce fix: Smal fix 2025-08-18 12:09:31 +02:00
J-Jamet
1fff0c526c fix: Authentication error 2025-08-18 10:03:02 +02:00
J-Jamet
1ee1bb8d95 fix: Device credential operation 2025-08-18 09:55:46 +02:00
J-Jamet
b8e996a5af fix: Orientation change 2025-08-17 22:45:00 +02:00
J-Jamet
df999002af fix: DeviceUnlockManager init 2025-08-17 13:36:36 +02:00
J-Jamet
64f5a4152b fix: prompt null 2025-08-17 13:24:17 +02:00
J-Jamet
0af99d1830 fix: Orientation change 2025-08-17 13:16:21 +02:00
J-Jamet
733f337312 fix: Lock orientation change 2025-08-17 11:54:39 +02:00
J-Jamet
1f99b1f884 fix: Error Keystore #2114 2025-08-17 11:24:52 +02:00
J-Jamet
c3b5598a09 fix: Auto open biometric prompt from database list #2113 Update to 4.1.6 2025-08-17 10:09:04 +02:00
jonnysemon
26cb36ccd0 Translated using Weblate (Arabic)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-08-16 07:02:04 +02:00
Kaya Zeren
67e0496efc Translated using Weblate (Turkish)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2025-08-15 00:02:25 +00:00
Ihor Hordiichuk
6c4d040564 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2025-08-15 00:02:23 +00:00
J-Jamet
cb5a725d50 Merge tag '4.1.5' into develop
4.1.5
2025-08-14 17:18:34 +02:00
J-Jamet
1c30d533ad Merge branch 'release/4.1.5' 2025-08-14 17:18:23 +02:00
J-Jamet
845d1a581b fix: warnings 2025-08-14 17:14:01 +02:00
J-Jamet
23bebf9597 fix: Auto prompt #2111 2025-08-14 17:02:40 +02:00
J-Jamet
536e038306 Merge branch 'release/4.1.4' 2025-08-14 13:04:44 +02:00
J-Jamet
9e1f6d29a5 fix: update Gemfile.lock 2025-08-14 12:28:22 +02:00
J-Jamet
7a9469e59d fix: code improvement #2105 2025-08-13 19:21:42 +02:00
J-Jamet
c12eb3d643 fix: error code 28 #2105 2025-08-13 19:03:15 +02:00
J-Jamet
da0f02e536 fix: better prompt variable management #2105 2025-08-13 18:27:51 +02:00
J-Jamet
04bcc6631c fix: open auto prompt too often #2105 2025-08-13 17:59:56 +02:00
J-Jamet
698e3b7fb1 fix: auto open prompt #2105 2025-08-13 11:49:54 +02:00
J-Jamet
6de02384c1 fix: close prompt #2105 2025-08-13 11:25:36 +02:00
J-Jamet
df3bd7e0a1 fix: cipher call #2105 2025-08-13 11:03:50 +02:00
J-Jamet
c8c232639f fix: better cipher and prompt workflow #2105 2025-08-12 19:49:31 +02:00
J-Jamet
192d6eedd0 fix: autoprompt thread #2105 2025-08-12 15:51:41 +02:00
J-Jamet
9cae3f0794 fix: initDecryptData #2105 2025-08-12 15:33:35 +02:00
J-Jamet
a680db9707 fix: initDecryptData #2105 2025-08-12 15:20:32 +02:00
J-Jamet
fe526089d7 fix: auto prompt #2105 2025-08-12 14:54:52 +02:00
J-Jamet
dfd7ade416 fix: Regression #2105 2025-08-12 13:42:57 +02:00
J-Jamet
3cd65345c5 fix: Small biometric fixes 2025-08-12 13:04:52 +02:00
J-Jamet
2d398908de fix: refresh when activate setting 2025-08-12 11:11:12 +02:00
J-Jamet
756454abc3 fix: update CHANGELOG 2025-08-12 10:52:42 +02:00
J-Jamet
b7619b45b1 fix: Transition deprecation 2025-08-12 10:51:15 +02:00
J-Jamet
1369a3cad9 fix: test dependency and gradle version 2025-08-12 10:26:32 +02:00
J-Jamet
f46c062c4e fix: Auto biometric prompt #2105 2025-08-10 22:34:48 +02:00
J-Jamet
0a0abef4d4 fix: Keystore exception 2025-08-10 21:37:41 +02:00
J-Jamet
3a8245ee74 fix: Exception as Snackbar 2025-08-10 21:32:12 +02:00
J-Jamet
7be554a378 fix: unlock manager #2098 #2101 2025-08-10 12:24:23 +02:00
Masowick
7007efa627 Translated using Weblate (German)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-08-07 08:01:53 +02:00
J-Jamet
6c37f7b12c fix: Update to 4.1.4 2025-08-03 18:17:56 +02:00
Mark Bengtsson
05defff5ef Translated using Weblate (Swedish)
Currently translated at 72.2% (482 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2025-07-31 16:02:37 +02:00
ssantos
e4569662ba Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2025-07-31 16:02:34 +02:00
Xo
4727f7a761 Translated using Weblate (Hebrew)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-07-31 16:02:32 +02:00
scollovati
89b15e715d Translated using Weblate (Italian)
Currently translated at 33.3% (1 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/it/
2025-07-29 10:24:20 +02:00
Liner Seven
ef72df02e3 Translated using Weblate (Japanese)
Currently translated at 100.0% (667 of 667 strings)

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

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/pl/
2025-07-26 12:05:52 +00:00
Viktor Varvaruk
b99fa9ffcf Translated using Weblate (Ukrainian)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2025-07-26 12:05:50 +00:00
scollovati
1497ab85b2 Translated using Weblate (Italian)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2025-07-26 12:05:49 +00:00
Ghost of Sparta
2ade463974 Translated using Weblate (Hungarian)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2025-07-26 12:05:47 +00:00
J-Jamet
3a67ec09d5 fix: Update CHANGELOG 2025-07-25 16:04:02 +02:00
J-Jamet
dca800b1bb Merge tag '4.1.3' into develop
4.1.3
2025-07-25 15:56:49 +02:00
J-Jamet
70665f110d Merge branch 'release/4.1.3' 2025-07-25 15:56:17 +02:00
J-Jamet
3b39cafb99 fix: Warnings 2025-07-25 14:30:47 +02:00
Priit Jõerüüt
0c1b94468d Translated using Weblate (Estonian)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-07-25 12:58:50 +02:00
109247019824
7a841bbf57 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-07-25 12:58:50 +02:00
jonnysemon
0066f1c77a Translated using Weblate (Arabic)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-07-25 12:58:50 +02:00
大王叫我来巡山
14dbff603d Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2025-07-25 12:58:50 +02:00
solokot
42afa93293 Translated using Weblate (Russian)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-07-25 12:58:50 +02:00
Matthaiks
2fa25a51ad Translated using Weblate (Polish)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-07-25 12:58:50 +02:00
Stephan Paternotte
2b7f41477f Translated using Weblate (Dutch)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-07-25 12:58:49 +02:00
Sylvain Pichon
be0d5f80c3 Translated using Weblate (French)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2025-07-25 12:58:49 +02:00
Retrial
3844188fcc Translated using Weblate (Greek)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2025-07-25 12:58:49 +02:00
VfBFan
ef81ba5a8f Translated using Weblate (German)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-07-25 12:58:49 +02:00
Fjuro
2871668d8f Translated using Weblate (Czech)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2025-07-25 12:58:49 +02:00
J-Jamet
2b5ecb2f84 fix: Simplify query #2096 2025-07-24 22:19:39 +02:00
J-Jamet
e397b92c36 fix: Search and allow empty query #2096 2025-07-24 21:29:43 +02:00
J-Jamet
e273eb6e03 fix: Replace strong tag 2025-07-24 20:36:01 +02:00
J-Jamet
28b624afa3 fix: Descriptions 2025-07-24 20:30:14 +02:00
J-Jamet
fb1e6cdc3f Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-07-24 20:23:50 +02:00
jonnysemon
8b6499d040 Translated using Weblate (Arabic)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-07-24 20:16:45 +02:00
Kunzisoft
054af507ad Translated using Weblate (French)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2025-07-24 20:16:44 +02:00
J-Jamet
ac9bb9b666 fix: Biometric error #2081 2025-07-24 19:41:28 +02:00
J-Jamet
809e1929e5 fix: Biometric error #2081 2025-07-24 19:21:29 +02:00
J-Jamet
a1b1338d67 fix: Biometric after orientation change #2081 2025-07-24 19:12:25 +02:00
J-Jamet
bd4cacfab1 Merge branch 'IzzySoft-fastlane' into develop 2025-07-24 18:26:32 +02:00
J-Jamet
e0343bdc55 Merge branch 'IzzySoft-iod' into develop 2025-07-24 18:23:40 +02:00
J-Jamet
b743d004e2 fix: Template EMAIL #1986 2025-07-24 18:22:11 +02:00
J-Jamet
4b20e035b2 fix: Upgrade CHANGELOG 2025-07-24 17:12:09 +02:00
J-Jamet
afe5fddc50 Merge branch 'shauser88-fix-save-copy-localtime' into develop 2025-07-24 17:09:48 +02:00
J-Jamet
d68ca1b51f Merge branch 'fix-save-copy-localtime' of github.com:shauser88/KeePassDX into shauser88-fix-save-copy-localtime 2025-07-24 17:08:55 +02:00
Jérémy JAMET
061b087229 Merge pull request #2095 from d20n/fix-en_GB-strings
Fix several incomplete en_GB strings
2025-07-24 16:59:23 +02:00
J-Jamet
bb3a379965 fix: Upgrade CHANGELOG 2025-07-24 16:42:35 +02:00
J-Jamet
593b5c6338 fix: Biometric error prompts #2081 2025-07-24 16:39:42 +02:00
J-Jamet
56f8a1bf9f fix: Upgrade version and CHANGELOG 2025-07-22 18:45:44 +02:00
J-Jamet
962b547b36 fix: Registration #2089 2025-07-22 18:40:36 +02:00
Izzy
6df8ff4310 fastlane: slight formatting adjustments to full descriptions 2025-07-20 00:31:38 +02:00
Martin Milchevski
52f17140b8 Translated using Weblate (Macedonian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/mk/
2025-07-20 00:03:07 +02:00
ginger-co
75c2bb4a87 Translated using Weblate (Hebrew)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-07-20 00:03:05 +02:00
Martin Milchevski
f36f6c3155 Translated using Weblate (Macedonian)
Currently translated at 4.8% (32 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/mk/
2025-07-18 23:16:13 +02:00
Martin Milchevski
b88b92c5b0 Added translation using Weblate (Macedonian) 2025-07-18 23:00:51 +02:00
pigmentblue15
d2c569c4f0 Translated using Weblate (Japanese)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-07-16 17:01:50 +02:00
Izzy
cb1316564e IzzyOnDroid provides both, the Free and the Libre variants 2025-07-15 09:51:50 +02:00
VfBFan
245d3f7df2 Translated using Weblate (German)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/de/
2025-07-14 19:02:00 +02:00
Random
3729b3c5a0 Translated using Weblate (Italian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2025-07-14 19:01:52 +02:00
Random
7ce5eb3c27 Translated using Weblate (Italian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2025-07-14 19:01:50 +02:00
VfBFan
43defea85e Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-07-14 19:01:48 +02:00
d20n
8470c4e39b Fix several incomplete en_GB strings 2025-07-14 17:20:42 +02:00
Dev-ClayP
1f678fc975 Update Loupe.kt
removed old sdk checks
2025-07-09 11:24:30 -04:00
Dev-ClayP
082c839639 Update EntryEditActivity.kt
removed old sdk checks
2025-07-09 11:18:59 -04:00
Dev-ClayP
600d548fce Update BroadcastAction.kt
removed old SDK check
2025-07-09 11:15:46 -04:00
Dev-ClayP
3035f9b686 Update TimeoutHelper.kt
Removed old SDK check
2025-07-09 11:14:18 -04:00
Dev-ClayP
6eae0f02d3 Update FileDatabaseSelectActivity.kt
removed PackageManager.allowCreateDocumentByStorageAccessFramework()
2025-07-09 11:11:53 -04:00
Dev-ClayP
87be2f4b9e Update UriHelper.kt
removed PackageManager.allowCreateDocumentByStorageAccessFramework()  it will always eval to true with sdk update
2025-07-09 11:09:44 -04:00
Dev-ClayP
3b054504a1 Update UriUtil.kt
removed another kitkat check
2025-07-09 11:05:01 -04:00
Dev-ClayP
a88f6b968a Update UriUtil.kt
Removed KitKat sdk check
2025-07-09 11:04:12 -04:00
Dev-ClayP
1fc4f150bf Update item_breadcrumb.xml
Removed jelly_bean target check
2025-07-09 10:45:16 -04:00
Dev-ClayP
1f4e59cbdc Update fragment_set_otp.xml
Removed sdk target checks for jelly_bean
2025-07-09 10:44:21 -04:00
Dev-ClayP
b5dc8d9adf Update build.gradle
changed minsdk to 19
2025-07-09 10:40:38 -04:00
Dev-ClayP
43f7e08548 Update build.gradle
changed minsdk to 19
2025-07-09 10:40:13 -04:00
Dev-ClayP
05fc6f87ec Update build.gradle
changed minsdk to 19
2025-07-09 10:39:34 -04:00
Dev-ClayP
daae535fa1 Update TemplateView.kt
Removed old SDK checks
2025-07-09 10:28:27 -04:00
Dev-ClayP
90c8cb3455 Update ViewUtil.kt
Removed SDK check
2025-07-09 10:26:12 -04:00
Dev-ClayP
daeee10de9 Update TemplateEditView.kt
Removed old SDK check
2025-07-09 10:25:15 -04:00
Dev-ClayP
6c1c401a71 Update NodesAdapter.kt
Removed old SDK check
2025-07-09 10:23:44 -04:00
Dev-ClayP
fd7f0fceb2 Update UriUtil.kt
Removed Old SDK Check
2025-07-09 10:21:55 -04:00
Dev-ClayP
26b8a616be Update AboutActivity.kt
Removed old SDK Check
2025-07-09 10:19:54 -04:00
Dev-ClayP
d88882f439 Removed PRNGFixes App.kt 2025-07-09 10:15:36 -04:00
Dev-ClayP
09dc1d6baa Removed sdk checks on TextFieldView.kt
Removed SDK checks that will always resolve to true now. Since we are updating min sdk to 19, these checks are no longer necessary.
2025-07-09 09:57:23 -04:00
Clay Perry
f4f5e86979 Updated Minimum SDK to 16 2025-07-08 12:52:14 -04:00
Stephan Paternotte
a41afb7f1e Translated using Weblate (Dutch)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/nl/
2025-07-07 15:02:17 +02:00
Stephan Paternotte
32d9cfbe29 Translated using Weblate (Dutch)
Currently translated at 100.0% (3 of 3 strings)

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

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

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/de/
2025-07-07 15:02:11 +02:00
Stephan Paternotte
44df4ec181 Added translation using Weblate (Dutch) 2025-07-06 14:29:32 +02:00
Fjuro
7afe356082 Translated using Weblate (Czech)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/cs/
2025-07-06 14:29:32 +02:00
Fjuro
87597553b8 Translated using Weblate (Czech)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/cs/
2025-07-06 14:29:32 +02:00
Fjuro
27e5f58d5e Added translation using Weblate (Czech) 2025-07-05 23:12:59 +02:00
jonnysemon
762c946d35 Translated using Weblate (Arabic)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ar/
2025-07-05 23:12:58 +02:00
jonnysemon
21a927e3e9 Translated using Weblate (Arabic)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ar/
2025-07-05 23:12:58 +02:00
jonnysemon
f93bb7436a Translated using Weblate (Arabic)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ar/
2025-07-05 23:12:58 +02:00
jonnysemon
6294fddbba Translated using Weblate (Arabic)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-07-05 23:12:58 +02:00
jonnysemon
c5719dfaf2 Translated using Weblate (Arabic)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-07-05 23:12:58 +02:00
Matthaiks
673fd67f15 Translated using Weblate (Polish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-07-05 23:12:58 +02:00
Fjuro
25524c48e9 Translated using Weblate (Czech)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2025-07-05 23:12:58 +02:00
Fjuro
631b924c33 Translated using Weblate (Czech)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2025-07-05 23:12:58 +02:00
J-Jamet
fba12bc278 Merge tag '4.1.2' into develop
4.1.2
2025-07-05 17:30:44 +02:00
J-Jamet
e809109bb2 Merge branch 'release/4.1.2' 2025-07-05 17:30:06 +02:00
J-Jamet
0e31890624 fix: Compilation with fastlane 2025-07-05 17:11:17 +02:00
J-Jamet
0124021ce5 fix: Upgrade README.md 2025-07-05 14:18:27 +02:00
J-Jamet
74db6bf77f fix: Update CHANGE 2025-07-05 14:09:07 +02:00
J-Jamet
efde33182e fix: Digit count UI #1987 2025-07-05 13:55:06 +02:00
J-Jamet
ec68b22330 fix: Update CHANGELOG 2025-07-05 11:55:24 +02:00
Tomás
bf7e014f8c Translated using Weblate (Spanish)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/es/
2025-07-05 11:40:35 +02:00
Tomás
40e1607698 Translated using Weblate (Spanish)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/es/
2025-07-05 11:40:35 +02:00
Matthaiks
4a132f06fe Translated using Weblate (Polish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-07-05 11:40:35 +02:00
Tomás
0396dd975d Translated using Weblate (Spanish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2025-07-05 02:47:45 +02:00
J-Jamet
80a38c0c54 fix: Update CHANGELOG 2025-07-04 23:05:17 +02:00
J-Jamet
2aa6461094 Merge branch 'Dev-ClayP-master' into develop 2025-07-04 22:56:37 +02:00
Dev-ClayP
258433b3b8 Update SetOTPDialogFragment.kt
Tiny Whitespace fix
2025-07-04 10:53:19 -04:00
Clay Perry
79e723545c Expanded minimum secret length functionality so that the user is alerted if the length is not long enough. This fixed an issue where the user wasnt alerted about invalid secret length but was still able to click ok and submit the string, bypassing the base32 check altogether. 2025-07-04 10:50:25 -04:00
Yurt Page
a6b20455ef Translated using Weblate (Russian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ru/
2025-07-04 16:00:48 +02:00
Dev-ClayP
9659b55bf3 Update SetOTPDialogFragment.kt
Fixed UX bug for button in the way,
Added string length check.
2025-07-04 01:21:47 -04:00
Bora Atıcı
ca148ef546 Translated using Weblate (Turkish)
Currently translated at 66.6% (2 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/tr/
2025-07-02 19:01:48 +02:00
J-Jamet
322b21d645 fix: Update Gradle to 8.11.0 2025-07-01 20:40:42 +02:00
J-Jamet
ed2ba65ecf fix: Update CHANGELOG 2025-07-01 20:26:48 +02:00
J-Jamet
defc8b1c57 fix: Update CHANGELOG 2025-07-01 20:25:34 +02:00
J-Jamet
a90ecc56d8 Merge branch 'develop' into codokie-rtl 2025-07-01 20:16:24 +02:00
J-Jamet
2faa0ac320 fix: Note group modification #2053 2025-07-01 20:07:19 +02:00
J-Jamet
e391fd59fe fix: Autofill registration popup #2054 An CHANGELOG 2025-07-01 19:45:26 +02:00
Priit Jõerüüt
25df86606c Translated using Weblate (Estonian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/et/
2025-07-01 18:31:43 +02:00
Priit Jõerüüt
811f33eb3f Translated using Weblate (Estonian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/et/
2025-07-01 18:31:43 +02:00
solokot
ca7e2ed89d Translated using Weblate (Russian)
Currently translated at 66.6% (2 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ru/
2025-07-01 18:31:43 +02:00
solokot
6f4cd79e2c Translated using Weblate (Russian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-07-01 18:31:43 +02:00
solokot
da1caf4b8b Translated using Weblate (Russian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-07-01 18:31:43 +02:00
Stephan Paternotte
4a0a8e44ca Translated using Weblate (Dutch)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-07-01 18:31:43 +02:00
Stephan Paternotte
6bc2c3481b Translated using Weblate (Dutch)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-07-01 18:31:43 +02:00
J-Jamet
0aa89ea9ff fix: Same domain unit tests 2025-07-01 17:26:56 +02:00
J-Jamet
f31a30bf47 fix: Search #1946 #2003 2025-07-01 17:22:42 +02:00
Liner Seven
dc75837ac7 Translated using Weblate (Japanese)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ja/
2025-07-01 09:22:50 +02:00
Liner Seven
9849b0a1da Translated using Weblate (Japanese)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ja/
2025-07-01 09:22:50 +02:00
Priit Jõerüüt
2c15a1ddd6 Translated using Weblate (Estonian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-07-01 09:22:50 +02:00
Priit Jõerüüt
98eb9976cf Translated using Weblate (Estonian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-07-01 09:22:50 +02:00
Keterion
0d9a5810b1 Translated using Weblate (Filipino)
Currently translated at 46.2% (308 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fil/
2025-07-01 09:22:50 +02:00
Besnik Bleta
1adaa137a5 Translated using Weblate (Albanian)
Currently translated at 96.5% (643 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2025-07-01 09:22:50 +02:00
bowornsin
44a428d15a Translated using Weblate (Thai)
Currently translated at 89.3% (595 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2025-07-01 09:22:50 +02:00
Suman Garai
5416a7942a Translated using Weblate (Bengali)
Currently translated at 52.5% (350 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bn/
2025-07-01 09:22:49 +02:00
Keterion
9e0024baf5 Translated using Weblate (English (United Kingdom))
Currently translated at 33.9% (226 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en_GB/
2025-07-01 09:22:49 +02:00
Keterion
8d47ce38c2 Translated using Weblate (Vietnamese)
Currently translated at 98.6% (657 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2025-07-01 09:22:49 +02:00
109247019824
80af43c0ca Translated using Weblate (Bulgarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-07-01 09:22:49 +02:00
109247019824
225f8243c2 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-07-01 09:22:49 +02:00
தமிழ்நேரம்
68bc118add Translated using Weblate (Tamil)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ta/
2025-07-01 09:22:49 +02:00
தமிழ்நேரம்
abbc584402 Translated using Weblate (Tamil)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ta/
2025-07-01 09:22:49 +02:00
தமிழ்நேரம்
6635594639 Translated using Weblate (Tamil)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ta/
2025-07-01 09:22:49 +02:00
தமிழ்நேரம்
10db77d402 Translated using Weblate (Tamil)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ta/
2025-07-01 09:22:49 +02:00
Allan Nordhøy
000fd7e520 Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.4% (656 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2025-07-01 09:22:48 +02:00
Keterion
c8ced4ae59 Translated using Weblate (Galician)
Currently translated at 87.0% (580 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2025-07-01 09:22:48 +02:00
hugoalh
9209ca9af7 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 99.8% (665 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2025-07-01 09:22:48 +02:00
大王叫我来巡山
c7bd90c610 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2025-07-01 09:22:47 +02:00
aasami
fceb9c3547 Translated using Weblate (Slovak)
Currently translated at 99.8% (665 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2025-07-01 09:22:47 +02:00
Ash Ed
030c49b571 Translated using Weblate (Russian)
Currently translated at 99.8% (665 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-07-01 09:22:47 +02:00
Stephan Paternotte
f2d6a6a536 Translated using Weblate (Dutch)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-07-01 09:22:47 +02:00
Liner Seven
8f61521f05 Translated using Weblate (Japanese)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-07-01 09:22:47 +02:00
cc5efd7b0
89af7ec5d0 Translated using Weblate (Japanese)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-07-01 09:22:47 +02:00
Ghost of Sparta
362f1aebed Translated using Weblate (Hungarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2025-07-01 09:22:46 +02:00
summoner001
5226527cec Translated using Weblate (Hungarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2025-07-01 09:22:46 +02:00
Aitor Elorza
b8464cd0e5 Translated using Weblate (Basque)
Currently translated at 95.6% (637 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2025-07-01 09:22:46 +02:00
Óscar Fernández Díaz
46e7b04d66 Translated using Weblate (Spanish)
Currently translated at 99.6% (664 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2025-07-01 09:22:46 +02:00
VfBFan
73111b770f Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-07-01 09:22:46 +02:00
Keterion
995d485700 Translated using Weblate (Danish)
Currently translated at 98.6% (657 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2025-07-01 09:22:46 +02:00
pitroig
5ebbbef667 Translated using Weblate (Catalan)
Currently translated at 99.8% (665 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2025-07-01 09:22:45 +02:00
J-Jamet
c79144400f Merge branch 'translations' into develop 2025-06-30 17:17:13 +02:00
J-Jamet
b56556f5a2 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-06-30 17:11:30 +02:00
J-Jamet
35d5f01b8e Merge branch 'fuzzpart-Error-strings-mismatch-#1938' into develop 2025-06-30 15:02:44 +02:00
J-Jamet
501c647236 fix: Warnings 2025-06-05 19:47:35 +02:00
J-Jamet
e77c7b84a3 fix: Upgrade gradle 2025-06-05 19:38:32 +02:00
J-Jamet
d9f4e9b6ab fix: Upgrade JDK and Gradle 2025-06-05 19:27:41 +02:00
Kunzisoft
1d7f7d2a5b Translated using Weblate (French)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/fr/
2025-05-29 10:27:20 +02:00
Yurt Page
df408e862b Translated using Weblate (Russian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-05-29 10:24:23 +02:00
Yurt Page
66845926d5 Translated using Weblate (Russian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-05-28 21:30:16 +02:00
J-Jamet
0a06acbf1d Merge branch 'ymcx-master' into develop 2025-05-28 21:13:50 +02:00
J-Jamet
5d3c6798c0 Merge branch 'codokie-dialog' into develop 2025-05-28 21:04:16 +02:00
solokot
b9d2b9ddc9 Translated using Weblate (Russian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-05-22 07:01:52 +00:00
abdelbasset jabrane
8a6525f45e Translated using Weblate (Arabic)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-05-19 01:32:49 +02:00
Priit Jõerüüt
41d89b590d Translated using Weblate (Estonian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-05-14 17:03:07 +02:00
Ash Ed
8354d08ff5 Translated using Weblate (Russian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-05-10 18:03:02 +02:00
fuzzpart
16725b21f3 All error messages were ended with a full stop. The respective punctuation marks have been used in translations. 2025-05-08 10:31:39 +02:00
ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝)
93ba17792f Translated using Weblate (Latvian)
Currently translated at 22.3% (149 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lv/
2025-05-08 07:03:01 +02:00
Priit Jõerüüt
940b96cf21 Translated using Weblate (Estonian)
Currently translated at 94.4% (629 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-04-29 09:03:25 +02:00
Ihor Hordiichuk
157253ce24 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2025-04-21 23:01:42 +02:00
ssantos
46b3810f34 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2025-04-20 00:01:42 +02:00
VfBFan
05c030dbbb Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-04-18 08:26:32 +02:00
aasami
5b0fd99351 Translated using Weblate (Slovak)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2025-04-15 00:44:02 +02:00
Xo
2946a6e231 Translated using Weblate (Hebrew)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-04-07 09:03:37 +00:00
codokie
47896fcdc9 [RTL] Fix text direction of OTP token 2025-04-06 14:32:03 +03:00
codokie
bc51345f0d [RTL] Workaround for license text direction 2025-04-06 10:54:57 +03:00
codokie
750e1b6c43 [RTL] Force LTR text direction for protected fields 2025-04-06 10:44:36 +03:00
codokie
4a2106837c [RTL] Align text to the start of the view 2025-04-06 10:44:10 +03:00
codokie
39b9fc350a [RTL] Fix direction of arrow icons 2025-04-05 23:23:07 +03:00
codokie
53560dbe29 [RTL] Fix order of duration pickers 2025-04-05 23:22:52 +03:00
codokie
5172dbe114 [RTL] Fix padding of toolbar buttons 2025-04-05 23:22:25 +03:00
codokie
4f0ff67fdf [RTL] Fix DX logo 2025-04-05 23:20:19 +03:00
codokie
bac971ced8 [RTL] Enable support 2025-04-05 23:20:00 +03:00
Priit Jõerüüt
64bf5ba165 Translated using Weblate (Estonian)
Currently translated at 90.2% (601 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-03-27 20:25:08 +01:00
Tino
90c4a5e1b8 Fix OTP code alignment in the main screen 2025-03-19 14:26:00 +02:00
Xo
b08a5d9cda Translated using Weblate (Hebrew)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-03-15 18:52:30 +01:00
Alonso González Chaves
d5be79948d Translated using Weblate (Spanish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2025-03-14 14:06:23 +01:00
Alonso González Chaves
6b64be4925 Translated using Weblate (Spanish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2025-03-14 13:07:43 +01:00
Alonso González Chaves
e8e6eb6ca5 Translated using Weblate (Spanish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2025-03-14 09:49:22 +01:00
codokie
ee9383dd0b [Dialog] Fix EditText context menu background 2025-03-12 14:50:33 +00:00
Xo
aee0b82cff Translated using Weblate (Hebrew)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-03-12 13:25:53 +01:00
Xo
ba5913da57 Translated using Weblate (Hebrew)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-03-11 14:27:29 +01:00
Xo
3238b9b2ce Translated using Weblate (Hebrew)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-03-10 12:07:14 +01:00
Xo
5215181c0f Translated using Weblate (Hebrew)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-03-10 09:56:08 +01:00
Xo
d3676a1454 Translated using Weblate (Hebrew)
Currently translated at 99.8% (665 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-03-10 00:21:10 +01:00
Xo
792ce6f86e Translated using Weblate (Hebrew)
Currently translated at 99.0% (660 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-03-09 19:02:23 +01:00
Xo
ddbd0376fc Translated using Weblate (Hebrew)
Currently translated at 99.2% (661 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-03-09 15:35:57 +01:00
Xo
496655093c Translated using Weblate (Hebrew)
Currently translated at 99.2% (661 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-03-09 13:24:53 +01:00
109247019824
755293eff7 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-03-06 21:14:00 +01:00
Priit Jõerüüt
b6e51a1f32 Translated using Weblate (Estonian)
Currently translated at 89.0% (593 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-03-05 13:20:43 +01:00
Priit Jõerüüt
08f17f3f19 Translated using Weblate (Estonian)
Currently translated at 83.9% (559 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-03-05 11:20:13 +01:00
pitroig
14440725fc Translated using Weblate (Catalan)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2025-03-02 22:01:57 +01:00
pitroig
b4f84c5cd6 Translated using Weblate (Catalan)
Currently translated at 97.4% (649 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2025-03-02 11:57:49 +01:00
Xo
ad6b1cead1 Translated using Weblate (Hebrew)
Currently translated at 93.3% (622 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-02-25 18:07:20 +01:00
Priit Jõerüüt
e06398ff19 Translated using Weblate (Estonian)
Currently translated at 78.6% (524 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-02-25 11:01:38 +01:00
신태진
919ad5cfd4 Translated using Weblate (Korean)
Currently translated at 50.1% (334 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ko/
2025-02-24 16:49:09 +01:00
신태진
b3fb721588 Translated using Weblate (Korean)
Currently translated at 47.5% (317 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ko/
2025-02-24 15:34:19 +01:00
Xo
590497852d Translated using Weblate (Hebrew)
Currently translated at 92.7% (618 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-02-17 23:57:45 +01:00
Bora Atıcı
ebd7a9c7cf Translated using Weblate (Turkish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2025-02-15 13:56:30 +01:00
SHINJI.K
939fb2fa54 Translated using Weblate (Japanese)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-02-13 13:02:12 +00:00
cc5efd7b0
2245daffe9 Translated using Weblate (Japanese)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-02-13 13:02:11 +00:00
Priit Jõerüüt
784b25ada8 Translated using Weblate (Estonian)
Currently translated at 70.5% (470 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-02-11 11:02:10 +01:00
Milo Ivir
146d631794 Translated using Weblate (Croatian)
Currently translated at 99.5% (663 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2025-02-08 10:54:21 +01:00
Priit Jõerüüt
cc062d7f0e Translated using Weblate (Estonian)
Currently translated at 64.4% (429 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-02-06 23:02:18 +00:00
Francisco Serrador
298dd4af61 Translated using Weblate (Spanish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2025-02-02 23:32:33 +01:00
NicolaeFericitu
739ba3b14d Translated using Weblate (Romanian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2025-01-22 21:44:57 +01:00
Priit Jõerüüt
2864ea9868 Translated using Weblate (Estonian)
Currently translated at 62.7% (418 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-01-21 18:12:06 +01:00
efelantti
ed8d3247ca Translated using Weblate (Finnish)
Currently translated at 59.0% (393 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fi/
2025-01-20 16:15:45 +01:00
efelantti
ff5de7b327 Translated using Weblate (Finnish)
Currently translated at 53.1% (354 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fi/
2025-01-19 13:00:30 +01:00
Francisco Serrador
aa77552ff4 Translated using Weblate (Spanish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2025-01-19 13:00:29 +01:00
efelantti
12456c0ea2 Translated using Weblate (Finnish)
Currently translated at 43.3% (289 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fi/
2025-01-18 10:47:03 +01:00
Stefan Hauser
a7a93fa2a2 Use local date/time in filename for saving a copy of the database 2025-01-16 09:15:32 +01:00
Matthaiks
ff7cd29b77 Translated using Weblate (Polish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-01-14 11:05:42 +01:00
Ihor Hordiichuk
f7065acc40 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2025-01-13 04:30:56 +01:00
Bora Atıcı
e4e2e5c43c Translated using Weblate (Turkish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2025-01-11 22:42:13 +01:00
Stephan Paternotte
48d240b010 Translated using Weblate (Dutch)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-01-09 22:43:59 +01:00
Stephan Paternotte
9f2deb56b9 Translated using Weblate (Dutch)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-01-09 09:01:50 +01:00
Masowick
951257bed8 Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-01-09 09:01:50 +01:00
தமிழ்நேரம்
f27ce804fb Translated using Weblate (Tamil)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ta/
2025-01-05 18:01:22 +00:00
FX8350
1d896e83b3 Translated using Weblate (Japanese)
Currently translated at 96.8% (645 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-01-05 18:01:19 +00:00
Laurent FAVOLE
a2c2925610 Translated using Weblate (French)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2025-01-01 18:00:34 +01:00
hugoalh
97e5f90603 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2024-12-27 09:00:25 +01:00
qx100
593b7188dc Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2024-12-27 09:00:24 +01:00
bowornsin
4c4e61a711 Translated using Weblate (Thai)
Currently translated at 89.9% (599 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2024-12-24 02:07:49 +01:00
Besnik Bleta
140f09c77e Translated using Weblate (Albanian)
Currently translated at 97.1% (647 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2024-12-17 12:00:23 +01:00
Masowick
c0fe4faf8a Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-12-17 12:00:22 +01:00
Pixelcode
066fff7aca Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-12-15 15:02:18 +01:00
Masowick
6dcbdffed4 Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-12-15 15:02:16 +01:00
Priit Jõerüüt
3a07dea6d7 Translated using Weblate (Estonian)
Currently translated at 57.9% (386 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-12-14 07:02:47 +01:00
Sandyran
72ed07ef17 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 42.6% (284 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nn/
2024-12-14 07:02:46 +01:00
Sandyran
51512b4588 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 20.5% (137 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nn/
2024-12-10 22:00:22 +00:00
J-Jamet
aed49e19e8 fix: view null exception 2024-12-08 17:07:46 +01:00
J-Jamet
113601d09a fix: Remove unused tests 2024-12-08 17:07:05 +01:00
J-Jamet
b7d0b65715 fix: Add linebreak in description 2024-12-08 15:55:37 +01:00
Priit Jõerüüt
3075591885 Translated using Weblate (Estonian)
Currently translated at 57.9% (386 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-12-06 08:00:33 +01:00
Masowick
e9596f56db Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-12-06 08:00:31 +01:00
Priit Jõerüüt
3559830738 Translated using Weblate (Estonian)
Currently translated at 55.7% (371 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-12-04 16:06:01 +01:00
jonnysemon
e42beccb22 Translated using Weblate (Arabic)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2024-11-29 15:00:34 +01:00
Priit Jõerüüt
d05b7394e8 Translated using Weblate (Estonian)
Currently translated at 41.8% (279 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-11-27 21:00:44 +01:00
Besnik Bleta
00b11ea659 Translated using Weblate (Albanian)
Currently translated at 96.2% (641 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2024-11-27 21:00:44 +01:00
Cleverson Cândido
99f0f096d1 Translated using Weblate (Portuguese)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2024-11-27 21:00:43 +01:00
Priit Jõerüüt
416329d50d Translated using Weblate (Estonian)
Currently translated at 35.4% (236 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-11-26 19:05:32 +00:00
Besnik Bleta
cb0d1b05d7 Translated using Weblate (Albanian)
Currently translated at 94.4% (629 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2024-11-26 19:05:31 +00:00
Oliver Cervera
a2845c33f8 Translated using Weblate (Italian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2024-11-26 19:05:30 +00:00
Besnik Bleta
6d06265d94 Translated using Weblate (Albanian)
Currently translated at 88.5% (590 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2024-11-25 17:18:05 +01:00
109247019824
49d03efe56 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-11-25 17:18:04 +01:00
Wellington Terumi Uemura
7f13a3ca76 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2024-11-25 17:18:04 +01:00
Retrial
cec9d168e3 Translated using Weblate (Greek)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2024-11-25 17:18:04 +01:00
VfBFan
ecf253451d Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-11-25 17:18:04 +01:00
J-Jamet
89a6219659 fix: Remove unused tests 2024-11-25 14:23:35 +01:00
J-Jamet
9dec308caa fix: URL matching fails in presence of a path #1940 2024-11-25 14:21:39 +01:00
J-Jamet
fb1459de9b fix: Upgrade to 4.1.2 2024-11-25 14:18:18 +01:00
109247019824
f3bef64461 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-11-25 10:53:54 +01:00
Linerly
2509caff6b Translated using Weblate (Indonesian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2024-11-25 10:53:54 +01:00
hugoalh
232aafe2c0 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 99.3% (662 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2024-11-25 10:53:54 +01:00
大王叫我来巡山
ed26cb4891 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2024-11-25 10:53:54 +01:00
solokot
0b966a6cd1 Translated using Weblate (Russian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-11-25 10:53:54 +01:00
Stephan Paternotte
c84afd2281 Translated using Weblate (Dutch)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2024-11-25 10:53:54 +01:00
summoner001
1711f09547 Translated using Weblate (Hungarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2024-11-25 10:53:54 +01:00
Masowick
4460a44e99 Translated using Weblate (German)
Currently translated at 99.6% (664 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-11-25 10:53:54 +01:00
Fjuro
c8f7bcfb52 Translated using Weblate (Czech)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2024-11-25 10:53:53 +01:00
J-Jamet
bd2bd842af Merge tag '4.1.1' into develop
4.1.1
2024-11-24 15:46:07 +01:00
J-Jamet
2b0f4fe46b Merge branch 'release/4.1.1' 2024-11-24 15:46:00 +01:00
J-Jamet
55c2f41c71 fix: tags 2024-11-24 14:59:06 +01:00
109247019824
94985422ca Translated using Weblate (Bulgarian)
Currently translated at 99.0% (660 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-11-24 14:27:14 +01:00
Matthaiks
eaff1aa58f Translated using Weblate (Polish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2024-11-24 14:27:14 +01:00
Ghost of Sparta
c7cb9d0990 Translated using Weblate (Hungarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2024-11-24 14:27:14 +01:00
gallegonovato
a9fdf30421 Translated using Weblate (Spanish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-11-24 14:27:13 +01:00
Matthaiks
d0f45f6dfb Translated using Weblate (Polish)
Currently translated at 99.3% (662 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2024-11-24 12:07:11 +01:00
Hosted Weblate
8cdb6a3c9f Merge branch 'origin/develop' into Weblate. 2024-11-24 12:06:37 +01:00
Kunzisoft
b369a46431 Translated using Weblate (French)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2024-11-24 12:06:37 +01:00
gallegonovato
f40ca2d5e0 Translated using Weblate (Spanish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-11-24 12:06:37 +01:00
Anonymous
400393c677 Translated using Weblate (Azerbaijani)
Currently translated at 97.1% (647 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-11-24 12:05:41 +01:00
Anonymous
5710e3be55 Translated using Weblate (Serbian)
Currently translated at 50.4% (336 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sr/
2024-11-24 12:05:41 +01:00
Anonymous
af1312a92b Translated using Weblate (Estonian)
Currently translated at 32.4% (216 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-11-24 12:05:40 +01:00
Anonymous
d2a63c48b1 Translated using Weblate (Filipino)
Currently translated at 46.2% (308 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fil/
2024-11-24 12:05:40 +01:00
Anonymous
8f10ea7ed6 Translated using Weblate (Cantonese (Traditional Han script))
Currently translated at 20.1% (134 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/yue_Hant/
2024-11-24 12:05:40 +01:00
Anonymous
7bd701368a Translated using Weblate (Thai)
Currently translated at 89.3% (595 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2024-11-24 12:05:40 +01:00
Anonymous
894e846a62 Translated using Weblate (English (United Kingdom))
Currently translated at 33.9% (226 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en_GB/
2024-11-24 12:05:40 +01:00
Anonymous
504ef5a7ab Translated using Weblate (Vietnamese)
Currently translated at 98.6% (657 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2024-11-24 12:05:40 +01:00
Anonymous
af6436da77 Translated using Weblate (Bulgarian)
Currently translated at 98.6% (657 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-11-24 12:05:40 +01:00
Anonymous
d031420ed3 Translated using Weblate (Portuguese)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2024-11-24 12:05:40 +01:00
Anonymous
4b6b7478de Translated using Weblate (Indonesian)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2024-11-24 12:05:40 +01:00
Anonymous
bb1b5eab96 Translated using Weblate (Romanian)
Currently translated at 98.6% (657 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-11-24 12:05:40 +01:00
Anonymous
9486b50342 Translated using Weblate (Croatian)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2024-11-24 12:05:39 +01:00
Anonymous
fc9a5b3545 Translated using Weblate (Turkish)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2024-11-24 12:05:39 +01:00
Anonymous
8b4d0e2541 Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.6% (657 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2024-11-24 12:05:39 +01:00
Anonymous
0ef07f615c Translated using Weblate (Galician)
Currently translated at 86.7% (578 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2024-11-24 12:05:39 +01:00
Anonymous
836413cff2 Translated using Weblate (Arabic)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2024-11-24 12:05:39 +01:00
Anonymous
c71e34fee9 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2024-11-24 12:05:39 +01:00
Anonymous
b389a4db92 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2024-11-24 12:05:39 +01:00
Anonymous
0a1a54cb33 Translated using Weblate (Ukrainian)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2024-11-24 12:05:39 +01:00
Anonymous
630228675c Translated using Weblate (Swedish)
Currently translated at 62.4% (416 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2024-11-24 12:05:39 +01:00
Anonymous
f7955d00fc Translated using Weblate (Slovak)
Currently translated at 92.0% (613 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2024-11-24 12:05:39 +01:00
Anonymous
68b08f9b9a Translated using Weblate (Russian)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-11-24 12:05:39 +01:00
Anonymous
612f136ced Translated using Weblate (Portuguese (Portugal))
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2024-11-24 12:05:39 +01:00
Anonymous
8c313c3d48 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2024-11-24 12:05:38 +01:00
Anonymous
517a6c0062 Translated using Weblate (Polish)
Currently translated at 99.2% (661 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2024-11-24 12:05:38 +01:00
Matthaiks
46c61b10de Translated using Weblate (Polish)
Currently translated at 99.2% (661 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2024-11-24 12:05:38 +01:00
Anonymous
edc8d27577 Translated using Weblate (Dutch)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2024-11-24 12:05:38 +01:00
Anonymous
05354777fe Translated using Weblate (Japanese)
Currently translated at 96.8% (645 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2024-11-24 12:05:38 +01:00
Anonymous
98d3d2a39b Translated using Weblate (Italian)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2024-11-24 12:05:38 +01:00
Anonymous
f91f75912e Translated using Weblate (Hungarian)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2024-11-24 12:05:38 +01:00
Anonymous
9f21f67035 Translated using Weblate (French)
Currently translated at 99.5% (663 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2024-11-24 12:05:38 +01:00
Kunzisoft
699e4da112 Translated using Weblate (French)
Currently translated at 99.5% (663 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2024-11-24 12:05:38 +01:00
Anonymous
6458285e75 Translated using Weblate (Basque)
Currently translated at 95.7% (638 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2024-11-24 12:05:38 +01:00
Anonymous
e0ddf3711f Translated using Weblate (Spanish)
Currently translated at 99.8% (665 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-11-24 12:05:38 +01:00
gallegonovato
68d415375d Translated using Weblate (Spanish)
Currently translated at 99.8% (665 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-11-24 12:05:38 +01:00
Anonymous
0e11afdd8b Translated using Weblate (Greek)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2024-11-24 12:05:37 +01:00
Anonymous
ff2c01584f Translated using Weblate (German)
Currently translated at 98.6% (657 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-11-24 12:05:37 +01:00
Anonymous
a59e20b864 Translated using Weblate (Danish)
Currently translated at 98.6% (657 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2024-11-24 12:05:37 +01:00
Anonymous
d7d898896d Translated using Weblate (Czech)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2024-11-24 12:05:37 +01:00
Anonymous
2dde78d5e7 Translated using Weblate (Catalan)
Currently translated at 94.8% (632 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2024-11-24 12:05:37 +01:00
J-Jamet
2ca39ff399 fix: update string 2024-11-24 12:04:36 +01:00
Hosted Weblate
1f6fdaf9b3 Merge branch 'origin/develop' into Weblate. 2024-11-24 11:58:45 +01:00
J-Jamet
edaf58135f fix: Upgrade to 4.1.1 2024-11-24 11:56:41 +01:00
J-Jamet
724698fc51 fix: Date parser #1933 2024-11-24 11:52:13 +01:00
summoner001
6aabe9e12c Translated using Weblate (Hungarian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2024-11-23 19:39:32 +01:00
bowornsin
2b5a1bb893 Translated using Weblate (Thai)
Currently translated at 90.2% (595 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2024-11-23 04:00:27 +00:00
summoner001
7403305f3c Translated using Weblate (Hungarian)
Currently translated at 100.0% (659 of 659 strings)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-07-30 11:56:01 +02:00
Htet Oo Hlaing
cc593e6e1f Added translation using Weblate (Burmese) 2023-07-30 04:40:47 +02:00
J-Jamet
552684fd90 feat: Delete entry from search 2023-07-29 21:40:00 +02:00
J-Jamet
a260e1d4e3 fix: Encapsulate Keyboard code 2023-07-29 18:59:16 +02:00
J-Jamet
07bbf232b6 Merge tag '4.0.0beta01' into develop
4.0.0beta01
2023-07-29 12:45:50 +02:00
J-Jamet
64b4a2f317 Merge branch 'release/4.0.0beta01' 2023-07-29 12:45:31 +02:00
J-Jamet
f0455fbca6 fix: Upgrade CHANGELOG 2023-07-29 12:35:07 +02:00
J-Jamet
2fa973f34a fix: Upgrade Gemfile and version to 4.0.0 2023-07-29 12:02:50 +02:00
J-Jamet
470497b8ef fix: Small size bug 2023-07-28 21:02:05 +02:00
J-Jamet
215cc641c3 feat: Switch keyboard only if focus #1351 2023-07-28 20:52:35 +02:00
J-Jamet
a98fa0d7bb feat: add simple theme in free version 2023-07-28 18:48:35 +02:00
J-Jamet
c70841d3e5 feat: add simple theme in free version 2023-07-28 18:47:55 +02:00
J-Jamet
3fdcd3c43d feat: Build title from webDomain 2023-07-28 18:44:13 +02:00
J-Jamet
b32bb609de fix: Notification for missing notification permission 2023-07-28 17:30:32 +02:00
J-Jamet
4db03fe034 fix: Remove URL in menu 2023-07-28 17:26:00 +02:00
J-Jamet
68430ef62c fix: Null when unbind service 2023-07-28 17:02:57 +02:00
J-Jamet
d46f48f317 fix: Special toolbar style 2023-07-28 16:43:08 +02:00
J-Jamet
e68e4dac46 fix: Unchecked auto key action by default 2023-07-28 15:35:56 +02:00
J-Jamet
3a79f94698 fix: Add validate entry education 2023-07-28 13:06:48 +02:00
J-Jamet
b913913bf1 fix: Add validate entry education 2023-07-28 12:59:20 +02:00
J-Jamet
16f92fc895 fix: Change wording for screen on option 2023-07-28 12:27:44 +02:00
J-Jamet
189e98b3fe fix: Small biometric changes 2023-07-28 12:27:23 +02:00
J-Jamet
64fea078e8 fix: Update device unlock wording 2023-07-28 11:06:07 +02:00
J-Jamet
4ad5d35be7 fix: Theme in settings 2023-07-27 01:40:11 +02:00
J-Jamet
82015953b9 Merge branch 'feature/Material_You' into develop 2023-07-27 01:12:06 +02:00
J-Jamet
630318dbef fix: Remove context themed 2023-07-27 01:10:30 +02:00
J-Jamet
d289e8acaa feat: Add Material You with dynamic color 2023-07-27 00:07:41 +02:00
J-Jamet
4632a202a9 fix: Small color change 2023-07-25 13:04:41 +02:00
J-Jamet
4a9282ee53 fix: Titles 2023-07-25 01:07:56 +02:00
J-Jamet
45ec38ecab fix: Parcelable for action 2023-07-24 23:32:03 +02:00
J-Jamet
b82fc4659a Merge branch 'feature/Material_You_1469' into develop 2023-07-24 21:12:29 +02:00
J-Jamet
aade780f92 fix: Image database modified 2023-07-24 21:03:32 +02:00
J-Jamet
6001f68cce fix: Breadcrumb and colors 2023-07-24 20:32:40 +02:00
J-Jamet
d0a072554e fix: Refactoring text styles 2023-07-24 19:14:33 +02:00
J-Jamet
53ccac0e9c fix: Preference flickering 2023-07-24 14:39:07 +02:00
J-Jamet
14e2a3f79e fix: Titles 2023-07-24 14:17:26 +02:00
J-Jamet
9afe5aaaf4 fix: Title and small bugs 2023-07-24 13:51:29 +02:00
J-Jamet
b992c9eb65 fix: Better preference implementation and fix colors 2023-07-24 13:21:24 +02:00
J-Jamet
fa2a79bc6c fix: Breadcrumb and buttons 2023-07-24 09:47:17 +02:00
J-Jamet
b1d4310b93 fix: OTP clipboard as sensitive 2023-07-23 22:57:22 +02:00
J-Jamet
1811b02fc8 fix: colors Kitkat selection and progressCircular 2023-07-23 22:54:09 +02:00
J-Jamet
5bbf436e4e fix: color selection 2023-07-23 12:40:39 +02:00
J-Jamet
d47a12c3d9 fix: Collapsing title 2023-07-23 12:00:35 +02:00
J-Jamet
dd7f6b389c fix: Custom entry color 2023-07-23 11:49:17 +02:00
J-Jamet
e829e3eedf fix: Dialog implementation 2023-07-22 21:02:58 +02:00
J-Jamet
178de4dd99 fix: Colors in styles 2023-07-22 20:45:05 +02:00
J-Jamet
a4cfc8ff49 fix: Chips 2023-07-22 20:08:54 +02:00
J-Jamet
b607cce691 fix: Kunzite style 2023-07-22 19:49:11 +02:00
J-Jamet
5986f39041 fix: Date and Time Picker as Material 2023-07-22 19:09:12 +02:00
J-Jamet
8ab39aaea6 fix: Style Reply 2023-07-22 17:46:58 +02:00
J-Jamet
4114d5aeb1 fix: Styles moon and sun 2023-07-21 22:11:39 +02:00
J-Jamet
2a1a5d9bd4 fix: Styles 2023-07-21 18:17:00 +02:00
J-Jamet
72b347bc85 fix: Search view 2023-07-20 23:23:47 +02:00
J-Jamet
44628f6fc4 fix: Lock button position 2023-07-20 23:09:00 +02:00
J-Jamet
9191fd4a21 fix: colors 2023-07-20 22:54:32 +02:00
J-Jamet
02c4eba676 fix: Many style issues 2023-07-19 21:42:31 +02:00
J-Jamet
a45b5605a1 fix: group icon 2023-07-11 22:54:57 +02:00
J-Jamet
8570f47372 fix: Large pass to add Material 3 2023-07-10 20:11:39 +02:00
J-Jamet
71fd17c0ac fix: Add color for tabs 2023-07-09 13:28:22 +02:00
J-Jamet
eb4e45cb69 fix: Add tertiary color, change color accent to color secondary 2023-07-09 11:40:58 +02:00
J-Jamet
3891bb1c8f fix: Add configuration cache 2023-07-09 10:14:07 +02:00
J-Jamet
a3c5237c2b fix: Null 2023-07-08 17:40:08 +02:00
J-Jamet
e05b904e78 fix: Styles 2023-07-08 16:35:25 +02:00
J-Jamet
0423dd7295 fix: Settings and biometric button 2023-07-08 15:59:51 +02:00
J-Jamet
8fa8043ff4 feat: Fingerprint button as real button 2023-07-08 00:38:58 +02:00
J-Jamet
244174e494 fix: Fingerprint button 2023-07-07 23:53:01 +02:00
J-Jamet
db8920ae69 fix: move advanced unlock button to the bottom 2023-07-07 22:32:47 +02:00
J-Jamet
670e949e9f fix: Buttons and wording 2023-06-27 23:20:51 +02:00
J-Jamet
c4daafea6d fix: rollback AlertDialog 2023-06-26 20:20:15 +02:00
J-Jamet
c8f07ffa7c fix: red and blue styles 2023-06-26 20:05:55 +02:00
J-Jamet
7f721427b6 fix: switch and color 2023-06-26 20:01:46 +02:00
J-Jamet
ad670151c8 fix: radius and color 2023-06-26 19:26:01 +02:00
J-Jamet
800335e416 fix: DateTime dialog color and switch to the right 2023-06-26 18:44:38 +02:00
J-Jamet
2da58f5899 fix: Lot of styles bugs 2023-06-26 18:03:36 +02:00
J-Jamet
0850b19aec fix: Crash on kitkat 2023-06-26 13:38:59 +02:00
Marijn Suijten
b6d32999b9 Add JavaVersion compatible to icon-pack
This now also contains Kotlin code.
2023-06-22 01:11:06 +02:00
J-Jamet
4f96a300c2 fix: CardView Radius 2023-06-19 19:03:31 +02:00
J-Jamet
c630b9d0c0 fix: Button color 2023-06-19 18:00:42 +02:00
J-Jamet
7324f630ba fix: File selection screen 2023-06-19 17:08:37 +02:00
J-Jamet
519da9c4ef fix: First pass to Material3 2023-06-19 16:10:36 +02:00
J-Jamet
6fc26ef6e6 fix: update strings 2023-06-18 23:03:29 +02:00
J-Jamet
dfb7e6f3bd fix: check permission 2023-06-18 23:02:09 +02:00
J-Jamet
5a883b83a5 fix: bn string 2023-06-18 21:20:53 +02:00
J-Jamet
dfef51fd27 fix: replace strong tag 2023-06-18 21:11:31 +02:00
J-Jamet
8bb7c7c8af Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2023-06-18 21:09:20 +02:00
J-Jamet
98370a03b5 fix: better intent to ask fingerprint enrollment 2023-06-18 20:46:48 +02:00
J-Jamet
82875ff832 fix: ask for notification permission 2023-06-18 18:12:17 +02:00
J-Jamet
9b4b5914d6 fix: allowCreateDocumentByStorageAccessFramework method 2023-06-18 12:23:37 +02:00
J-Jamet
7b9576841d fix: Menu deprecation 2023-06-18 12:05:08 +02:00
J-Jamet
59794557b3 fix: PackageInfo compat 2023-06-18 11:00:22 +02:00
J-Jamet
c3e4504a1a fix: TokenAutoComplete as lib 2023-06-18 10:43:07 +02:00
J-Jamet
65d7fdbf7e fix: Resolve dependencies and small changes 2023-06-18 10:33:58 +02:00
J-Jamet
d5310e5c58 fix: Deprecated parcelable 2023-06-18 10:08:58 +02:00
J-Jamet
f02b7c16d2 fix: Upgrade to SDK 33 2023-06-17 19:01:35 +02:00
J-Jamet
7cd456ecbc fix: Manifest tag 2023-06-17 18:39:30 +02:00
J-Jamet
1aeff9684e fix: Upgrade gradle to 7.4.2 2023-06-17 18:20:05 +02:00
Jeanluc Henrot
4b0ad75e7b Translated using Weblate (French)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2023-06-06 14:50:16 +02:00
Masowick
055b3c6002 Translated using Weblate (German)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-05-19 10:48:28 +02:00
abidin toumi
9ad4858224 Translated using Weblate (Arabic)
Currently translated at 75.4% (477 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2023-05-18 09:47:41 +02:00
ctntt
33422f90ce Translated using Weblate (German)
Currently translated at 99.5% (629 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-05-18 09:47:41 +02:00
ctntt
b9f4f9380b Translated using Weblate (German)
Currently translated at 99.2% (627 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-05-15 16:52:04 +02:00
J-Jamet
56a1601d79 Merge branch 'feature/Module_Refactoring' into develop 2023-05-15 16:43:46 +02:00
J-Jamet
a7d04e573a fix: Better database action implementation 2023-05-15 16:21:49 +02:00
J-Jamet
0c4d827469 fix: Remove unused assign password runnable 2023-05-15 00:13:02 +02:00
J-Jamet
5656f6afb5 fix: Small changes 2023-05-14 23:43:45 +02:00
J-Jamet
4bd12b8a95 fix: Refactoring database actions 2023-05-14 23:38:08 +02:00
J-Jamet
939b319563 fix: Change ContextualDatabase 2023-05-14 22:21:34 +02:00
J-Jamet
05865fe8c3 fix: Encapsulate BASE64_FLAG 2023-05-14 22:05:08 +02:00
J-Jamet
09cf8aabf6 Revert "fix: Base64 encoder as external library"
This reverts commit 13f2003fed.
2023-05-14 21:56:11 +02:00
J-Jamet
111a77d984 Revert "fix: Base64 encoder as external library"
This reverts commit 1b96747187.
2023-05-14 21:56:11 +02:00
J-Jamet
c2cc0a77ab fix: NDKVersion in the right package 2023-05-14 21:54:52 +02:00
J-Jamet
5b052b02a2 fix: crypto module at the upper level 2023-05-14 21:33:37 +02:00
J-Jamet
3b3908dd7c fix: Better icon pack implementation 2023-05-14 21:25:24 +02:00
J-Jamet
1b96747187 fix: Base64 encoder as external library 2023-05-14 20:19:22 +02:00
J-Jamet
13f2003fed fix: Base64 encoder as external library 2023-05-14 20:06:51 +02:00
J-Jamet
4d6dbc9e30 fix: Create unit tests in right package 2023-05-14 19:40:43 +02:00
J-Jamet
a235b21fbf feat: Move modules 2023-05-14 18:40:45 +02:00
J-Jamet
158ed26f6f fix: Remove unused libs 2023-05-14 18:03:05 +02:00
J-Jamet
81930bf3a1 fix: Remove AndroidX dependency and format code 2023-05-14 17:52:40 +02:00
J-Jamet
e1ecebb4a0 fix: Remove unnecessary coroutine lib 2023-05-14 15:55:04 +02:00
J-Jamet
52fd576f55 fix: Delete color lib dependency 2023-05-14 15:51:32 +02:00
J-Jamet
6742a8893c fix: Separate Main credential, remove ContentProvider from Database 2023-05-14 15:33:39 +02:00
J-Jamet
7db0c4f7d3 fix: Locale warning 2023-05-14 13:26:23 +02:00
J-Jamet
372b2aebe0 fix: Smaller isSearchable implementation 2023-05-14 13:22:51 +02:00
J-Jamet
b713237ec3 fix: Small warning 2023-05-14 13:14:27 +02:00
J-Jamet
ee1a0a53a6 fix: Better icon builder implementation, add ContextualDatabase 2023-05-14 13:04:49 +02:00
VfBFan
45ece887a2 Translated using Weblate (German)
Currently translated at 99.2% (627 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-05-14 12:49:11 +02:00
ctntt
0f59aaf4f7 Translated using Weblate (German)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-05-13 02:50:29 +02:00
J-Jamet
0d2c814b3d fix: Small changes 2023-05-08 21:17:00 +02:00
J-Jamet
64bdcc2f32 fix: Remove unused interface 2023-05-08 20:50:59 +02:00
J-Jamet
dfb418c12a fix: better static utils implementation 2023-05-08 20:30:16 +02:00
J-Jamet
c8868f31e6 Refactoring code, better encapsulation 2023-05-08 18:49:42 +02:00
J-Jamet
48cc8b3f75 Merge branch 'extract-database' of github.com:GianpaMX/KeePassDX into GianpaMX-extract-database 2023-05-08 15:19:32 +02:00
Aitor Elorza
62777373a4 Translated using Weblate (Basque)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2023-05-05 16:48:19 +02:00
Aitor Elorza
ce987237ae Translated using Weblate (Basque)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2023-05-03 18:48:25 +02:00
Aitor Elorza
77b99f9ef4 Translated using Weblate (Bengali)
Currently translated at 57.1% (361 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bn/
2023-04-29 14:51:26 +02:00
Aitor Elorza
049ac4cb21 Translated using Weblate (Basque)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2023-04-29 14:51:25 +02:00
Éfrit
41e3c60632 Translated using Weblate (French)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2023-04-28 11:27:27 +02:00
Aitor Elorza
5b2c846f7d Translated using Weblate (Basque)
Currently translated at 94.3% (596 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2023-04-28 11:27:26 +02:00
C. Rüdinger
8a0192bcac Translated using Weblate (German)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-04-28 11:27:25 +02:00
Subham Jena
3d2fce78c6 Translated using Weblate (Odia)
Currently translated at 0.4% (3 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/or/
2023-04-24 00:51:47 +02:00
Ihor Hordiichuk
120bfd47e2 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2023-04-24 00:51:47 +02:00
Aitor Elorza
408ac08059 Translated using Weblate (Basque)
Currently translated at 69.7% (441 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2023-04-24 00:51:47 +02:00
Subham Jena
9ae2377bd9 Added translation using Weblate (Odia) 2023-04-23 00:13:42 +02:00
J-Jamet
f126a9ab09 Merge branch 'jkrasuk-jkrasuk-patch-1' into develop 2023-04-22 22:41:38 +02:00
J-Jamet
c2d4bf67d0 Merge branch 'jkrasuk-patch-1' of github.com:jkrasuk/KeePassDX into jkrasuk-jkrasuk-patch-1 2023-04-22 22:39:25 +02:00
J-Jamet
d0367ea7f8 fix: Replace strong tags 2023-04-22 22:34:50 +02:00
J-Jamet
73c6c97637 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2023-04-22 22:31:42 +02:00
J-Jamet
3f63fa9c30 fix: Upgrade to 3.5.2 and add new default configuration for protected memory 2023-04-22 22:15:49 +02:00
J-Jamet
2884d12b4b fix: Upgrade GemFile 2023-04-22 22:10:58 +02:00
Aitor Elorza
24dd966eab Translated using Weblate (Basque)
Currently translated at 35.4% (224 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2023-04-18 07:14:03 +02:00
ctntt
47b2464297 Translated using Weblate (German)
Currently translated at 97.9% (619 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-04-18 07:14:02 +02:00
Masowick
08bb11a4d7 Translated using Weblate (German)
Currently translated at 97.9% (619 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-04-18 07:14:02 +02:00
Martin
e0c2c14b98 Translated using Weblate (German)
Currently translated at 97.9% (619 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-04-16 19:27:55 +02:00
Alexandru
481b818619 Translated using Weblate (Romanian)
Currently translated at 65.5% (414 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2023-04-12 10:32:45 +02:00
Aitor Elorza
138efebd8c Translated using Weblate (Basque)
Currently translated at 28.0% (177 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2023-04-05 18:48:12 +02:00
Aitor Elorza
3251f02ab2 Translated using Weblate (Basque)
Currently translated at 22.7% (144 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2023-04-01 15:37:39 +02:00
Aitor Elorza
8f74cdfac0 Translated using Weblate (Basque)
Currently translated at 20.5% (130 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2023-03-29 18:39:08 +02:00
VfBFan
75f795b6ab Translated using Weblate (German)
Currently translated at 99.5% (629 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-03-29 18:39:07 +02:00
C. Rüdinger
dd901f3b16 Translated using Weblate (German)
Currently translated at 99.5% (629 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-03-29 18:39:07 +02:00
Masowick
cf2d641303 Translated using Weblate (German)
Currently translated at 99.5% (629 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-03-29 18:39:07 +02:00
Ettore Atalan
489c820ab9 Translated using Weblate (German)
Currently translated at 99.0% (626 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-03-28 09:37:14 +02:00
Masowick
dd21382b29 Translated using Weblate (German)
Currently translated at 99.0% (626 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-03-28 09:37:14 +02:00
VfBFan
05b2ccf0ed Translated using Weblate (German)
Currently translated at 99.0% (626 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-03-28 09:37:14 +02:00
J-Jamet
428efd2bb6 fix: CHANGELOG 2023-03-26 14:53:27 +02:00
J-Jamet
512b4cd16e Merge tag '3.5.1' into develop
3.5.1
2023-03-26 14:48:43 +02:00
J-Jamet
4d648b77b9 Merge branch 'release/3.5.1' 2023-03-26 14:48:32 +02:00
J-Jamet
02f6caf4dd fix: Loading dialog with Yubikey #1506 2023-03-26 14:46:11 +02:00
J-Jamet
2b23649674 feat: upgrade to 3.5.1 2023-03-26 14:44:20 +02:00
WB
1693be7776 Translated using Weblate (Galician)
Currently translated at 97.3% (615 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2023-03-25 16:40:20 +01:00
Aitor Elorza
c97be80a3a Translated using Weblate (Basque)
Currently translated at 13.2% (84 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2023-03-25 16:40:19 +01:00
Aitor Elorza
29d4d5232d Translated using Weblate (Basque)
Currently translated at 11.3% (72 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2023-03-22 16:38:00 +01:00
Tom Sawyer
90ee9d8c40 Translated using Weblate (Swedish)
Currently translated at 61.2% (387 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2023-03-20 08:40:54 +01:00
C. Rüdinger
5c0a82ba38 Translated using Weblate (German)
Currently translated at 98.8% (625 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-03-20 08:40:53 +01:00
Aitor Elorza
13aee1072f Translated using Weblate (Basque)
Currently translated at 11.3% (72 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2023-03-17 17:37:53 +01:00
VfBFan
b158cf1323 Translated using Weblate (German)
Currently translated at 98.7% (624 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-03-17 17:37:53 +01:00
C. Rüdinger
22be5ecbca Translated using Weblate (German)
Currently translated at 98.7% (624 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-03-16 08:07:05 +01:00
Aitor Elorza
4b16d9394a Translated using Weblate (Basque)
Currently translated at 10.7% (68 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2023-03-15 19:37:20 +01:00
Masowick
c0ad54219f Translated using Weblate (German)
Currently translated at 99.8% (631 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-03-15 19:37:20 +01:00
76c72cfe
4af6f776b0 Translated using Weblate (Swedish)
Currently translated at 60.9% (385 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2023-03-12 17:37:26 +01:00
P.O
76373658d8 Translated using Weblate (Swedish)
Currently translated at 60.9% (385 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2023-03-12 17:37:26 +01:00
Anonimas
f9f6e69a95 Translated using Weblate (Lithuanian)
Currently translated at 51.7% (327 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2023-03-08 20:37:37 +01:00
C. Rüdinger
61354df795 Translated using Weblate (German)
Currently translated at 99.8% (631 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-03-08 20:37:37 +01:00
Masowick
3a5c42b138 Translated using Weblate (German)
Currently translated at 99.8% (631 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-03-07 07:41:09 +01:00
jc
55e7544739 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2023-03-05 22:40:37 +01:00
Masowick
ad0dcdd54a Translated using Weblate (German)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-03-05 22:40:37 +01:00
Masowick
caa208c847 Translated using Weblate (German)
Currently translated at 99.5% (629 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-02-25 19:38:55 +01:00
VfBFan
067d00376d Translated using Weblate (German)
Currently translated at 99.5% (629 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-02-22 10:35:42 +01:00
Masowick
07f0521ef9 Translated using Weblate (German)
Currently translated at 99.5% (629 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-02-22 10:35:42 +01:00
Masowick
3be3593b1b Translated using Weblate (German)
Currently translated at 99.5% (629 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-02-21 07:53:12 +01:00
VfBFan
71db66d174 Translated using Weblate (German)
Currently translated at 99.5% (629 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-02-21 07:53:11 +01:00
Masowick
cd58a8e1b5 Translated using Weblate (German)
Currently translated at 99.6% (630 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-02-20 22:05:26 +01:00
VfBFan
256a579b1c Translated using Weblate (German)
Currently translated at 99.6% (630 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-02-20 22:05:25 +01:00
Masowick
97a427ea24 Translated using Weblate (German)
Currently translated at 99.6% (630 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-02-20 19:37:59 +01:00
VfBFan
b848587901 Translated using Weblate (German)
Currently translated at 99.6% (630 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-02-20 19:37:58 +01:00
VfBFan
fd90af3757 Translated using Weblate (German)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-02-20 18:42:57 +01:00
Ivan
cb1c9e1103 Translated using Weblate (Slovak)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2023-02-20 13:13:00 +01:00
Masowick
c14092d52b Translated using Weblate (German)
Currently translated at 99.8% (631 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-02-20 13:12:59 +01:00
Deleted User
27c19bd708 Translated using Weblate (German)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-02-14 18:39:31 +01:00
VfBFan
13d5273314 Translated using Weblate (German)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-02-13 17:59:14 +01:00
HudobniVolk
a0f93b5441 Translated using Weblate (Slovenian)
Currently translated at 8.5% (54 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sl/
2023-02-13 07:12:36 +01:00
gallegonovato
4c4c92527e Translated using Weblate (Galician)
Currently translated at 97.3% (615 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2023-02-13 07:12:35 +01:00
Deleted User
ff9999f39c Translated using Weblate (German)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-02-13 07:12:35 +01:00
HudobniVolk
45d06f269c Added translation using Weblate (Slovenian) 2023-02-12 13:45:47 +01:00
Ivan
acf92c0f8e Translated using Weblate (Slovak)
Currently translated at 99.0% (626 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2023-02-11 19:32:18 +01:00
Joaquín Krasuk
4b7de3b85f Update es translation 2023-02-11 12:16:34 -03:00
Milo Ivir
0502bfb986 Translated using Weblate (Croatian)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2023-02-05 20:38:52 +01:00
bowornsin
eacecd9b0d Translated using Weblate (Thai)
Currently translated at 87.6% (554 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2023-02-02 11:38:08 +01:00
SC
38245b4807 Translated using Weblate (Portuguese)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2023-02-02 11:38:07 +01:00
Oğuz Ersen
95228e67c2 Translated using Weblate (Turkish)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2023-02-02 11:38:07 +01:00
Eric
33cb4a55f5 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2023-02-02 11:38:07 +01:00
Ihor Hordiichuk
7beb13d859 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2023-02-02 11:38:07 +01:00
solokot
f0e0d26923 Translated using Weblate (Russian)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2023-02-02 11:38:06 +01:00
SC
5326322356 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2023-02-02 11:38:06 +01:00
Sengeku Ferreira Lima
ebe1c0bcdd Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2023-02-02 11:38:06 +01:00
Matthaiks
42100d4cb9 Translated using Weblate (Polish)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2023-02-02 11:38:06 +01:00
Stephan Paternotte
369f3d240e Translated using Weblate (Dutch)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2023-02-02 11:38:05 +01:00
random r
d40e83fb52 Translated using Weblate (Italian)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2023-02-02 11:38:05 +01:00
gallegonovato
6776b0867f Translated using Weblate (Spanish)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2023-02-02 11:38:05 +01:00
Retrial
5b1e945883 Translated using Weblate (Greek)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2023-02-02 11:38:05 +01:00
VfBFan
2f2f5e6f43 Translated using Weblate (German)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2023-02-02 11:38:05 +01:00
Fjuro
dfc7654842 Translated using Weblate (Czech)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2023-02-02 11:38:04 +01:00
Linerly
183015707f Translated using Weblate (Indonesian)
Currently translated at 100.0% (632 of 632 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2023-01-29 15:10:28 +01:00
Hosted Weblate
2bc4908452 Merge branch 'origin/develop' into Weblate. 2023-01-29 14:36:53 +01:00
bowornsin
213e5c40d7 Translated using Weblate (Thai)
Currently translated at 84.7% (535 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2023-01-29 14:36:53 +01:00
J-Jamet
e7cce21c51 Merge tag '3.5.0' into develop
3.5.0
2023-01-26 23:25:50 +01:00
J-Jamet
128ce5657e Merge branch 'release/3.5.0' 2023-01-26 23:25:40 +01:00
J-Jamet
861911ad63 fix: update fastlane changelogs 2023-01-26 23:21:10 +01:00
J-Jamet
9093c65235 feat: upgrade to 3.5.0 2023-01-26 23:15:02 +01:00
J-Jamet
e6ab8f82ff fix: url to download driver 2023-01-26 23:01:54 +01:00
J-Jamet
77ff1850f3 fix: url to download driver 2023-01-26 23:01:26 +01:00
J-Jamet
e985bd2a20 fix: dialog to download driver 2023-01-26 22:29:01 +01:00
bowornsin
a1e91bb26f Translated using Weblate (Thai)
Currently translated at 8.8% (56 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2023-01-24 18:58:28 +01:00
J-Jamet
a6cd02d146 feat: new year 2023-01-22 21:30:37 +01:00
J-Jamet
eb51f6712b fix: changelog length 2023-01-22 21:12:43 +01:00
J-Jamet
0a19ecb715 fi: translation strong tags 2023-01-22 20:59:11 +01:00
J-Jamet
d7e7020244 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2023-01-22 20:54:48 +01:00
J-Jamet
ea3349eea4 fix: autofill with hardware key 2023-01-22 20:33:29 +01:00
J-Jamet
9cec64ded4 fix: better challenge channel implementation 2023-01-22 20:21:17 +01:00
J-Jamet
f9dc456032 fix: remove ChallengeResponseViewModel and add HardwareKeyActivity 2023-01-22 15:10:36 +01:00
Linerly
6416aad823 Translated using Weblate (Indonesian)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2023-01-12 22:48:40 +01:00
Kornelijus Tvarijanavičius
10b5c9c261 Translated using Weblate (Lithuanian)
Currently translated at 40.5% (256 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2023-01-12 22:48:40 +01:00
ERYpTION
bc0e364164 Translated using Weblate (Danish)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2023-01-12 22:48:40 +01:00
bowornsin
83ab5223a8 Translated using Weblate (Thai)
Currently translated at 8.2% (52 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2023-01-08 06:48:05 +01:00
Ahmad0a
94c6710f22 Translated using Weblate (Arabic)
Currently translated at 75.2% (475 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2023-01-06 02:49:44 +01:00
solokot
a1e5266161 Translated using Weblate (Russian)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2023-01-04 18:47:52 +01:00
J-Jamet
adbdb9a642 first code to manage merge with yubikey 2023-01-02 19:30:19 +01:00
bowornsin
527f734fcf Translated using Weblate (Thai)
Currently translated at 5.5% (35 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2023-01-02 08:50:00 +01:00
Stephan Paternotte
f1a29af0c6 Translated using Weblate (Dutch)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2022-12-29 16:51:03 +01:00
WB
b69a277769 Translated using Weblate (Galician)
Currently translated at 97.3% (614 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2022-12-26 22:49:03 +01:00
J-Jamet
d049ed39e8 fix: upgrade to 3.5.0Beta03 2022-12-18 23:58:10 +01:00
J-Jamet
ecfe767068 fix: replace error method 2022-12-18 23:46:33 +01:00
J-Jamet
8d827eb562 Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2022-12-18 12:24:19 +01:00
Jérémy JAMET
9048f618c5 Merge pull request #1447 from flawedworld/vanadium-rebrand
Add support for Vanadium in GrapheneOS
2022-12-18 12:21:47 +01:00
Salih Ail
66377ded62 Translated using Weblate (Arabic)
Currently translated at 75.1% (474 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2022-12-18 04:50:51 +01:00
VfBFan
04c4c82953 Translated using Weblate (German)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-12-10 09:49:31 +01:00
Besnik Bleta
ccd8467cba Translated using Weblate (Albanian)
Currently translated at 57.3% (362 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2022-12-08 18:48:14 +01:00
Besnik Bleta
4eee9e95c4 Translated using Weblate (Albanian)
Currently translated at 54.5% (344 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2022-12-07 17:48:59 +01:00
Besnik Bleta
c64b4b8d62 Translated using Weblate (Albanian)
Currently translated at 5.8% (37 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2022-12-07 12:44:47 +01:00
Besnik Bleta
72ef0f2f3f Added translation using Weblate (Albanian) 2022-12-07 12:34:56 +01:00
Michael
9b5a95a10f Translated using Weblate (German)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-12-06 07:43:25 +01:00
Yudong
1a3baa6523 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-12-04 16:48:49 +01:00
Matthaiks
a745ed1c28 Translated using Weblate (Polish)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-11-26 20:48:46 +01:00
WaldiS
39ff112b42 Translated using Weblate (Polish)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-11-25 20:13:07 +01:00
WB
b3aab27c9b Translated using Weblate (Galician)
Currently translated at 88.7% (560 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2022-11-24 20:38:26 +01:00
WB
9b3c751a49 Translated using Weblate (Galician)
Currently translated at 80.1% (506 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2022-11-19 21:46:56 +01:00
flawedworld
d29f4e0097 Add support for Vanadium in GrapheneOS 2022-11-19 02:11:56 +00:00
Abhi
0645fbe938 Translated using Weblate (Malayalam)
Currently translated at 60.6% (383 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2022-11-17 14:47:11 +01:00
Jacek
ee6c8fc041 Translated using Weblate (Polish)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-11-16 02:49:13 +01:00
bowornsin
d93e3a1c2d Translated using Weblate (Thai)
Currently translated at 4.1% (26 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2022-11-09 11:50:24 +01:00
bowornsin
d4e0c008b8 Added translation using Weblate (Thai) 2022-11-08 07:37:59 +01:00
ssantos
0ca392f312 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2022-11-07 09:05:08 +01:00
solokot
c6c14c2354 Translated using Weblate (Russian)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-11-06 09:03:25 +01:00
Darin Avdeyeva
652616226b Translated using Weblate (Russian)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-11-05 09:54:16 +01:00
Retrial
d2101fd3e5 Translated using Weblate (Greek)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-11-02 06:36:25 +01:00
GianpaMX
3b967dd4e3 clean up 2022-10-27 17:31:48 +01:00
GianpaMX
e5d58721dd icon size 2022-10-26 17:19:40 +01:00
GianpaMX
1495c442ac default template group name 2022-10-26 17:16:38 +01:00
GianpaMX
2206184bcb recycler bin title 2022-10-26 16:37:08 +01:00
GianpaMX
9e0b6fa800 named compression algorithm 2022-10-26 15:25:38 +01:00
GianpaMX
b11533f9fe exceptions refactor 2022-10-26 12:20:19 +01:00
GianpaMX
50343193e1 template field 2022-10-26 11:33:51 +01:00
GianpaMX
7d7e3f4ad6 extract database module 2022-10-25 18:00:31 +01:00
WB
e22d9f6bdf Translated using Weblate (Galician)
Currently translated at 66.4% (419 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2022-10-24 23:37:12 +02:00
magnum Choi
affcc28f13 Translated using Weblate (Korean)
Currently translated at 50.2% (317 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ko/
2022-10-22 15:02:26 +02:00
Jacek
7a151bc2fe Translated using Weblate (Polish)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-10-22 15:02:24 +02:00
Anonimas
5349c4783e Translated using Weblate (Lithuanian)
Currently translated at 39.7% (251 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2022-10-22 15:02:24 +02:00
Óscar Fernández Díaz
96a72d9842 Translated using Weblate (Spanish)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2022-10-22 15:02:23 +02:00
GianpaMX
04eae1ae3d new module 2022-10-22 07:57:40 +01:00
Gabriel Cardoso
67afa55f1d Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-10-17 02:53:24 +02:00
J-Jamet
efa8fd9f17 fix: replace strong tags 2022-10-14 21:21:27 +02:00
J-Jamet
64128991a6 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2022-10-14 21:16:09 +02:00
J-Jamet
3fa38c29f6 fix: fix channel closing 2022-10-14 21:06:52 +02:00
J-Jamet
86a335768c fix: update CHANGELOG 2022-10-14 20:49:26 +02:00
J-Jamet
2d29092c52 fix: update CHANGELOG 2022-10-14 20:43:49 +02:00
J-Jamet
2d7e76a279 Merge branch 'issue-1412' of github.com:ryg-git/KeePassDX into ryg-git-issue-1412 2022-10-14 20:39:26 +02:00
J-Jamet
1514dbb1de fix: comment FIDO2 code #304 2022-10-14 13:59:20 +02:00
J-Jamet
1707d3c3ba Revert "fix: Smaller advanced unlock UI"
This reverts commit 2882bb30d7.
2022-10-14 13:53:11 +02:00
J-Jamet
3fee162c4d fix: rollback mime-types */* #1211 #1408, add monochrome icon #1403 #1404 2022-10-14 13:40:01 +02:00
J-Jamet
78fd9d616b Merge branch 'Sandelinos-themed-icons' into release/3.5.0 2022-10-14 13:30:02 +02:00
J-Jamet
46828c3317 Revert "fix: remove application/octet-stream file recognition #1211"
This reverts commit e367051b80.
Mimetype */* #1408
2022-10-14 13:25:39 +02:00
ryg-git
5126bd4fb6 remove onRegularBackPressed method call as lockAndExit handles exiting app 2022-10-01 20:54:01 +05:30
Digger
6440e5e054 Translated using Weblate (Japanese)
Currently translated at 98.4% (621 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2022-10-01 04:18:30 +02:00
J-Jamet
0474e5b1fe Merge branch 'Sandelinos-themed-icons' into develop 2022-09-29 16:44:43 +02:00
Fjuro
15b84739e7 Translated using Weblate (Czech)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2022-09-28 19:19:26 +02:00
Fjuro
4bfe296e1a Translated using Weblate (Czech)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2022-09-27 08:57:42 +02:00
Stephan Paternotte
3fcc0db4f8 Translated using Weblate (Dutch)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2022-09-25 13:53:18 +02:00
Suman Garai
a8238565f3 Translated using Weblate (Bengali)
Currently translated at 57.2% (361 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bn/
2022-09-24 15:19:38 +02:00
Digger
53114462b3 Translated using Weblate (Japanese)
Currently translated at 98.2% (620 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2022-09-24 15:19:34 +02:00
Roee Hershberg
094da79cea Translated using Weblate (Hebrew)
Currently translated at 45.1% (285 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2022-09-24 15:19:33 +02:00
SC
f093206a1c Translated using Weblate (Portuguese)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2022-09-20 20:19:37 +02:00
Digger
5b4940d017 Translated using Weblate (Japanese)
Currently translated at 97.4% (615 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2022-09-20 20:19:36 +02:00
VfBFan
aee36eeec6 Translated using Weblate (German)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-09-20 20:19:36 +02:00
Digger
cc783f8be1 Translated using Weblate (Japanese)
Currently translated at 95.5% (603 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2022-09-18 06:24:10 +02:00
WB
a20f491cf1 Translated using Weblate (Galician)
Currently translated at 62.2% (393 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2022-09-17 18:16:24 +02:00
Anonimas
f5ff9bf263 Translated using Weblate (Lithuanian)
Currently translated at 30.4% (192 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2022-09-17 18:16:23 +02:00
random r
dcba54b499 Translated using Weblate (Italian)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2022-09-17 18:16:22 +02:00
zeritti
d1e103c1d7 Translated using Weblate (Czech)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2022-09-17 18:16:21 +02:00
Sandelinos
542daba206 Added monochrome icon 2022-09-17 00:42:47 +03:00
J-Jamet
94b61b1bbd Change KeePassX by KeeWeb 2022-09-16 09:19:43 +02:00
WB
718e590bfd Translated using Weblate (Galician)
Currently translated at 52.9% (334 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2022-09-15 21:22:54 +02:00
J-Jamet
9ce5e184c0 fix: try to not call BiometricManager methods if disabled #1400 2022-09-15 18:47:19 +02:00
J-Jamet
dbc477765f fix: upgrade to 3.5.0beta02 2022-09-15 15:05:38 +02:00
J-Jamet
92fcadf3f3 fix: attachment download button #1401 2022-09-15 15:04:24 +02:00
WB
c437fd96a8 Translated using Weblate (Galician)
Currently translated at 41.0% (259 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2022-09-14 05:15:57 +02:00
Vitor Henrique
10d33ecb82 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.6% (629 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-09-14 05:15:54 +02:00
Vitor Henrique
d506c0cb27 Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.0% (606 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-09-11 22:18:00 +02:00
Milo Ivir
acac7f7540 Translated using Weblate (Croatian)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2022-09-10 19:49:28 +02:00
Roee Hershberg
c853bd282a Translated using Weblate (Hebrew)
Currently translated at 37.7% (238 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2022-09-10 19:49:28 +02:00
J-Jamet
ecc4550261 Add Deutsh description 2022-09-07 22:42:18 +02:00
Linerly
c26ece7166 Translated using Weblate (Indonesian)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2022-09-07 02:15:21 +02:00
nautilusx
88b50c7902 Translated using Weblate (German)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-09-07 02:15:20 +02:00
J-Jamet
7ba2cdd6ff Revert "fix: Change key driver url"
This reverts commit 941f9bcd48.
2022-09-06 09:29:41 +02:00
J-Jamet
589d9a2f1d Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2022-09-04 18:24:06 +02:00
Oğuz Ersen
a92411b95b Translated using Weblate (Turkish)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-09-04 17:54:45 +02:00
Eric
472051bd24 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-09-04 17:54:44 +02:00
Ihor Hordiichuk
e5109a1f43 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-09-04 17:54:44 +02:00
solokot
f7ae9e3574 Translated using Weblate (Russian)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-09-04 17:54:43 +02:00
Matthaiks
170ec3c636 Translated using Weblate (Polish)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-09-04 17:54:43 +02:00
Retrial
1ab3fa8b3b Translated using Weblate (Greek)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-09-04 17:54:43 +02:00
J-Jamet
8b046512e3 fix: upgrade Gemfile.lock 2022-09-04 14:20:36 +02:00
J-Jamet
228a10c8e0 Merge branch 'translations' into develop 2022-09-04 12:24:32 +02:00
J-Jamet
9c53bea190 fix: replace <strong> tags 2022-09-04 12:23:49 +02:00
J-Jamet
11cf991498 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2022-09-04 12:15:55 +02:00
J-Jamet
a88c3721b2 Merge branch 'translations' into develop 2022-09-04 12:14:33 +02:00
J-Jamet
0b4b6d4d91 feat: upgrade to 3.5.0 2022-09-04 12:13:19 +02:00
J-Jamet
941f9bcd48 fix: Change key driver url 2022-09-03 23:03:11 +02:00
Jérémy JAMET
09988a858d Add badge in README.md 2022-09-03 22:12:17 +02:00
solokot
f1bf9fb25c Translated using Weblate (Russian)
Currently translated at 99.5% (628 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-09-03 19:24:05 +02:00
Matthaiks
1751fa49c0 Translated using Weblate (Polish)
Currently translated at 99.6% (629 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-09-03 19:24:05 +02:00
Kunzisoft
6b4fc9a4fa Translated using Weblate (French)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2022-09-03 19:24:04 +02:00
Retrial
7c8d85e428 Translated using Weblate (Greek)
Currently translated at 99.5% (628 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-09-03 19:24:04 +02:00
Allan Nordhøy
e335140f23 Translated using Weblate (English)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2022-09-03 19:24:03 +02:00
Kunzisoft
d85f398b5f Translated using Weblate (English)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2022-09-03 19:24:03 +02:00
Wilker Santana da Silva
a16082a59d Translated using Weblate (English)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2022-09-03 19:11:24 +02:00
Kunzisoft
456269a343 Translated using Weblate (English)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2022-09-03 19:11:23 +02:00
J-Jamet
eb8e1e20eb fix: Remove cancel button for development dialog 2022-09-03 17:30:31 +02:00
Hosted Weblate
ed3c84fec0 Merge branch 'origin/develop' into Weblate. 2022-09-03 17:27:29 +02:00
J-Jamet
be40416a2d feat: Add privacy text in About section 2022-09-03 17:25:20 +02:00
PiQuark6046
5b5476a513 Translated using Weblate (Korean)
Currently translated at 35.0% (221 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ko/
2022-09-01 19:19:15 +02:00
random r
dc64dd6400 Translated using Weblate (Italian)
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2022-09-01 19:19:14 +02:00
atilluF
eca02d3bde Translated using Weblate (Italian)
Currently translated at 97.4% (614 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2022-08-31 15:15:20 +02:00
SC
176b6c2936 Translated using Weblate (Portuguese)
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2022-08-26 20:21:24 +02:00
J-Jamet
5b22350bdf fix: Hide clipboard text when copy entry field #1386 2022-08-23 11:56:29 +02:00
J-Jamet
6e1e011234 fix: exec gradlew version 7.5.1 to update scripts 2022-08-23 11:32:10 +02:00
J-Jamet
ac65ef6a5c Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2022-08-23 11:26:35 +02:00
Jérémy JAMET
fc198dde74 Merge pull request #1387 from lberrymage/update-gradle
Upgrade Gradle to 7.5.1
2022-08-23 11:25:59 +02:00
lberrymage
15ac51b2fc Upgrade Gradle to 7.5.1
Generated by `./gradlew wrapper --gradle-version 7.5.1`
2022-08-22 18:09:13 -08:00
J-Jamet
34214432e1 fix: upgrade libs 2022-08-17 23:01:45 +02:00
J-Jamet
361ca92493 fix: upgrade gradle plugin to 7.2.2 2022-08-17 22:56:50 +02:00
J-Jamet
e367051b80 fix: remove application/octet-stream file recognition #1211 2022-08-17 22:25:47 +02:00
Linerly
a2a4a50c5e Translated using Weblate (Indonesian)
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2022-08-14 14:20:41 +02:00
Milo Ivir
afc74b2f2a Translated using Weblate (Croatian)
Currently translated at 99.2% (625 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2022-08-14 14:20:40 +02:00
devchung
fc756d1eaf Translated using Weblate (Chinese (Traditional))
Currently translated at 97.3% (613 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2022-08-14 14:20:39 +02:00
Eric
eb8a4b1e49 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-08-14 14:20:39 +02:00
Ihor Hordiichuk
8d258b3538 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-08-14 14:20:38 +02:00
solokot
a59cfa3477 Translated using Weblate (Russian)
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-08-14 14:20:38 +02:00
Matthaiks
f1e513006e Translated using Weblate (Polish)
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-08-14 14:20:37 +02:00
Retrial
9df5c8f439 Translated using Weblate (Greek)
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-08-14 14:20:36 +02:00
Deleted User
3ae099accf Translated using Weblate (German)
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-08-14 14:20:36 +02:00
VfBFan
bb3e9396f2 Translated using Weblate (German)
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-08-14 14:20:35 +02:00
Hosted Weblate
1628749bde Merge branch 'origin/develop' into Weblate. 2022-08-11 12:47:58 +02:00
J-Jamet
f2006b5e42 fix: show lock button hidden by screenshot mode banner #1377 2022-08-11 12:37:28 +02:00
GianpaMX
80d387d9e7 Add screenshot mode
* Add new screenshot mode entry under Settings -> App -> General
* Disable Screenshot mode  by default
* Add a screenshot mode indication at the bottom of the screen
* Set or clear window FLAG_SECURE accordingly
* Translate strings into Spanish
2022-08-10 15:19:09 +01:00
J-Jamet
4452b4d599 Merge branch 'feature/Hardware_Key' into develop 2022-08-08 14:00:21 +02:00
J-Jamet
dfeaeb9888 feature: todo open external app in f-droid 2022-08-08 13:57:51 +02:00
J-Jamet
7e45a20ee7 fix: Refactoring key driver app id 2022-08-07 23:27:59 +02:00
J-Jamet
f3fe92e4de Merge branch 'develop' into feature/Hardware_Key 2022-08-02 22:25:44 +02:00
J-Jamet
b606909c65 fix: Update libs and SDK to 32 2022-08-02 22:25:19 +02:00
J-Jamet
2882bb30d7 fix: Smaller advanced unlock UI 2022-08-02 21:47:33 +02:00
eamz8jpajok
5b62227e3f Translated using Weblate (German)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-07-28 23:21:09 +02:00
J-Jamet
8b6af6fd8a feat: Derive master key exception 2022-07-05 18:09:35 +02:00
J-Jamet
99e9a92953 fix: KDB opening 2022-07-05 17:55:12 +02:00
Noël Krähenbühl
9f626309c3 Translated using Weblate (English (United Kingdom))
Currently translated at 8.3% (51 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en_GB/
2022-06-25 17:16:13 +02:00
Anonimas
3fe7cf2bfd Translated using Weblate (Lithuanian)
Currently translated at 23.4% (143 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2022-06-19 16:16:36 +02:00
Matthaiks
9b5c274b49 Translated using Weblate (Polish)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-06-18 00:19:02 +02:00
WB
46b350e7ac Translated using Weblate (Galician)
Currently translated at 23.4% (143 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2022-06-16 00:19:34 +02:00
Óscar Fernández Díaz
22a4aeb108 Translated using Weblate (Spanish)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2022-06-14 00:19:14 +02:00
random r
332e116ba7 Translated using Weblate (Italian)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2022-06-04 11:15:25 +02:00
Anonimas
8b594a1a1f Translated using Weblate (Lithuanian)
Currently translated at 22.9% (140 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2022-06-02 18:17:01 +02:00
J-Jamet
ab23ec6d4d Merge tag '3.4.5' into develop
3.4.5
2022-06-02 11:29:08 +02:00
J-Jamet
0ef574d675 Merge branch 'release/3.4.5' 2022-06-02 11:29:00 +02:00
J-Jamet
6d15a2462d Merge branch 'translations' into develop 2022-06-02 11:02:57 +02:00
J-Jamet
24fcdeb7aa Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2022-06-02 11:01:11 +02:00
J-Jamet
13905db732 fix: searchable selection 2022-06-01 14:15:41 +02:00
J-Jamet
6e1c8e5bec fix: custom data in group (fix KeeShare) #1335 2022-06-01 12:41:01 +02:00
J-Jamet
9aa1d11b94 Change the order of the search filters 2022-05-31 18:46:46 +02:00
J-Jamet
6c9f359fae New clipboard manager #1343 2022-05-31 18:22:01 +02:00
J-Jamet
531ebcae85 Fix device credential unlocking #1344 2022-05-31 11:44:42 +02:00
J-Jamet
fe9601b510 Change to 3.4.5 2022-05-31 10:25:34 +02:00
Oymate
bdf7cc6ea0 Translated using Weblate (Bengali)
Currently translated at 15.7% (96 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bn/
2022-05-30 21:18:06 +02:00
Douglas Han
1cfe02af6f Translated using Weblate (Korean)
Currently translated at 28.0% (171 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ko/
2022-05-30 21:18:05 +02:00
J-Jamet
647e3f9383 Change intent challenge recognition 2022-05-30 18:19:01 +02:00
J-Jamet
597f52799d Cache to capture exception during database save 2022-05-30 16:59:33 +02:00
J-Jamet
a59e052ed8 Merge branch 'develop' into feature/Hardware_Key 2022-05-30 10:31:46 +02:00
J-Jamet
11da0a4500 Keep screen on by default when viewing an entry
Upgrade to 3.5.0
2022-05-30 10:31:18 +02:00
J-Jamet
fd736bd1c2 Keep screen on entry by default 2022-05-30 10:24:47 +02:00
Oymate
ca6a4bfeef Translated using Weblate (Bengali)
Currently translated at 7.2% (44 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bn/
2022-05-27 16:16:54 +02:00
Oymate
02e9debc42 Translated using Weblate (Bengali (Bangladesh))
Currently translated at 15.2% (93 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bn_BD/
2022-05-27 16:16:53 +02:00
Oymate
9bc9bd8b95 Added translation using Weblate (Bengali) 2022-05-26 15:10:24 +02:00
Park JM
d810f79b7a Translated using Weblate (Korean)
Currently translated at 20.5% (125 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ko/
2022-05-24 11:17:53 +02:00
Milo Ivir
f3468951f1 Translated using Weblate (Croatian)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2022-05-22 19:32:57 +02:00
Kunzisoft
7ec5badabb Translated using Weblate (French)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2022-05-20 22:17:36 +02:00
J-Jamet
1ff2f501ca Fix capture database exception 2022-05-19 21:22:13 +02:00
J-Jamet
cfcb49e233 Better management of exceptions 2022-05-19 21:16:29 +02:00
J-Jamet
467df2020e Fix merge 2022-05-19 19:48:43 +02:00
J-Jamet
a961b41de0 Fix file save outside of the app 2022-05-19 19:20:21 +02:00
J-Jamet
40e8d5225e Fix notification and save state 2022-05-19 15:54:34 +02:00
J-Jamet
bc755ae1df Fix progress message 2022-05-19 15:00:12 +02:00
J-Jamet
b1cb0c3786 Fix infinite loop 2022-05-19 13:41:38 +02:00
J-Jamet
090d0fa2db Encapsulate channels 2022-05-19 12:53:12 +02:00
J-Jamet
27918a12b0 Fix small bugs 2022-05-19 11:47:53 +02:00
J-Jamet
ba1498b0b2 Fix error message and better implementation 2022-05-19 11:15:28 +02:00
J-Jamet
cbde96dd82 Add waiting task message and cancellable 2022-05-18 19:49:18 +02:00
J-Jamet
344118a755 Better error management 2022-05-18 18:35:24 +02:00
J-Jamet
259c8a4bd9 Setting to remember hardware key 2022-05-18 16:39:35 +02:00
SHINJI.K
fe92e41e91 Translated using Weblate (Japanese)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2022-05-15 15:18:25 +02:00
zeritti
e58c2f2a99 Translated using Weblate (Czech)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2022-05-15 15:18:24 +02:00
J-Jamet
f4d5bd1bea Fix save and better write implementation 2022-05-11 15:26:49 +02:00
J-Jamet
20b352cabe Better code encapsulation 2022-05-11 14:19:32 +02:00
J-Jamet
20e35f1a69 Encapsulate database operations 2022-05-11 13:42:48 +02:00
J-Jamet
d963f56d0f Merge branch 'develop' into feature/Hardware_Key 2022-05-11 11:36:20 +02:00
J-Jamet
aecfbc7728 Upgrade gradle 2022-05-11 10:59:14 +02:00
J-Jamet
5734df89f0 Merge branch 'develop' into feature/Hardware_Key 2022-05-11 10:10:52 +02:00
J-Jamet
bdf9b864d4 Merge tag '3.4.4' into develop
3.4.4
2022-05-11 10:00:50 +02:00
J-Jamet
1c0f1a036b Merge branch 'release/3.4.4' 2022-05-11 10:00:42 +02:00
J-Jamet
327c9de464 Change main credential validation 2022-05-10 19:59:56 +02:00
J-Jamet
8b2f994769 Save database with challenge response 2022-05-10 15:02:22 +02:00
J-Jamet
a5e53d872b Open database with challenge response in service 2022-05-09 15:56:53 +02:00
Claudio
1868d90693 Translated using Weblate (Italian)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2022-05-08 16:00:54 +02:00
bondlxv
da0c19e068 Translated using Weblate (Italian)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2022-05-08 16:00:54 +02:00
jazzyjabroni
d1103d8db4 Translated using Weblate (Danish)
Currently translated at 84.8% (517 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2022-05-07 21:13:46 +02:00
solokot
b2e92646a1 Translated using Weblate (Russian)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-05-06 18:36:13 +02:00
J-Jamet
19bc2444bc Merge branch 'develop' into feature/Hardware_Key 2022-05-05 16:15:03 +02:00
J-Jamet
831b649cbb Merge branch 'translations' into develop 2022-05-05 15:59:37 +02:00
Kunzisoft
ded3c204b9 Translated using Weblate (French)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2022-05-05 15:58:56 +02:00
Hosted Weblate
23eec5f066 Merge branch 'origin/develop' into Weblate. 2022-05-05 15:54:24 +02:00
J-Jamet
6c167090e1 Small changes 2022-05-05 15:50:51 +02:00
J-Jamet
7d9eca0d46 Small changes 2022-05-05 15:29:56 +02:00
J-Jamet
c551aff474 Upgrade libs 2022-05-05 15:10:30 +02:00
J-Jamet
e627745358 Prevent Tapjacking #1318 2022-05-05 14:49:36 +02:00
J-Jamet
5a30d9d2b5 * Fix crash in New Android 13 #1321
* Better backstack management for selection mode
2022-05-05 12:45:07 +02:00
Santosh Anantwal
0a46817bbc Translated using Weblate (Marathi)
Currently translated at 1.9% (12 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/mr/
2022-05-04 20:13:11 +02:00
Hosted Weblate
a4134fa8c8 Merge branch 'origin/develop' into Weblate. 2022-05-03 19:23:33 +02:00
Santosh Anantwal
683535a5a6 Translated using Weblate (Marathi)
Currently translated at 1.4% (9 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/mr/
2022-05-03 19:23:33 +02:00
Santosh Anantwal
edb53112c2 Added translation using Weblate (Marathi) 2022-05-03 19:01:31 +02:00
SHINJI.K
83a77af520 Translated using Weblate (Japanese)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2022-05-03 15:13:15 +02:00
SC
df3ae17c7b Translated using Weblate (Portuguese)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2022-05-01 10:10:24 +02:00
abidin toumi
4a1624a443 Translated using Weblate (Arabic)
Currently translated at 78.3% (477 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2022-05-01 10:10:24 +02:00
wqk317
a8de9f9f9f Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-05-01 10:10:23 +02:00
hokonch
3aa5b40acd Translated using Weblate (Japanese)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2022-05-01 10:10:23 +02:00
wqk317
8400f3e874 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.6% (607 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-04-30 09:35:42 +02:00
wqk317
b40bca1913 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.8% (608 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-04-30 09:18:00 +02:00
wqk317
7100257f31 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.1% (604 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-04-30 09:17:06 +02:00
SHINJI.K
17df1a4d8a Translated using Weblate (Japanese)
Currently translated at 98.0% (597 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2022-04-30 08:53:45 +02:00
hokonch
d7a5209c68 Translated using Weblate (Japanese)
Currently translated at 98.0% (597 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2022-04-30 08:53:45 +02:00
wqk317
076220eacd Translated using Weblate (Chinese (Simplified))
Currently translated at 99.8% (608 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-04-30 08:43:03 +02:00
SHINJI.K
99a50f271a Translated using Weblate (Japanese)
Currently translated at 96.8% (590 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2022-04-30 08:43:03 +02:00
hokonch
63d265da06 Translated using Weblate (Japanese)
Currently translated at 96.8% (590 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2022-04-30 08:43:03 +02:00
wqk317
30e3624eb1 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.8% (608 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-04-30 08:25:13 +02:00
Eric
88f3713e28 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.8% (608 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-04-30 08:25:13 +02:00
wqk317
90f0c22545 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-04-30 08:23:43 +02:00
wqk317
8deed8468d Translated using Weblate (Chinese (Simplified))
Currently translated at 99.8% (608 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-04-30 08:21:23 +02:00
hokonch
923ad26b1b Translated using Weblate (Japanese)
Currently translated at 96.0% (585 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2022-04-30 08:17:37 +02:00
SHINJI.K
3bc858e4c2 Translated using Weblate (Japanese)
Currently translated at 96.0% (585 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2022-04-30 08:17:37 +02:00
SHINJI.K
f5a7fa41a7 Translated using Weblate (Japanese)
Currently translated at 95.5% (582 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2022-04-30 08:10:18 +02:00
hokonch
bf71d5508b Translated using Weblate (Japanese)
Currently translated at 95.5% (582 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2022-04-30 08:10:17 +02:00
J-Jamet
b44c9cfc51 Opening refactoring 2022-04-28 20:39:26 +02:00
J-Jamet
5b4338abae Better implementation for challenge response intent 2022-04-27 14:39:08 +02:00
John Veness
aa5adc28cb Translated using Weblate (English (United Kingdom))
Currently translated at 3.6% (22 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en_GB/
2022-04-26 20:07:59 +02:00
abidin toumi
2dad013cc0 Translated using Weblate (Arabic)
Currently translated at 75.8% (462 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2022-04-26 20:07:59 +02:00
solokot
7ade66f3ac Translated using Weblate (Russian)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-04-26 20:07:58 +02:00
Stephan Paternotte
ed75a64b46 Translated using Weblate (Dutch)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2022-04-26 20:07:58 +02:00
Kunzisoft
e156b80d91 Translated using Weblate (French)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2022-04-26 20:07:58 +02:00
J-Jamet
e8f79ae467 Fix to open challenge-response dynamically / refactoring methods #8 2022-04-25 21:47:43 +02:00
Kunzisoft
90e4862280 Added translation using Weblate (English (United Kingdom)) 2022-04-25 18:22:17 +02:00
André Marcelo Alvarenga
438080d3d6 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-04-23 13:08:01 +02:00
SHINJI.K
3c17605764 Translated using Weblate (Japanese)
Currently translated at 94.2% (574 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2022-04-23 13:08:00 +02:00
VfBFan
3f68bc0eda Translated using Weblate (German)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-04-23 13:08:00 +02:00
J-Jamet
ecbee73eae Add view and first implementation of hardware key #8 2022-04-21 18:03:32 +02:00
J-Jamet
3e4452da00 Fix inherited view after orientation change 2022-04-21 17:43:24 +02:00
Linerly
549c690b56 Translated using Weblate (Indonesian)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2022-04-21 01:11:05 +02:00
Oğuz Ersen
aabe06f29b Translated using Weblate (Turkish)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-04-21 01:11:05 +02:00
Eric
82693c5cd3 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-04-21 01:11:04 +02:00
Ihor Hordiichuk
37a4f26d2f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-04-21 01:11:04 +02:00
solokot
ca94063c7b Translated using Weblate (Russian)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-04-21 01:11:04 +02:00
Matthaiks
eadc4bf6c2 Translated using Weblate (Polish)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-04-21 01:11:03 +02:00
Retrial
b1c307c86b Translated using Weblate (Greek)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-04-21 01:11:03 +02:00
J-Jamet
1874a0056d Merge branch 'develop' into feature/Hardware_Key 2022-04-20 14:24:10 +02:00
J-Jamet
48331f9552 Merge tag '3.4.3' into develop
3.4.3
2022-04-19 21:17:37 +02:00
J-Jamet
f907aa578a Merge branch 'release/3.4.3' 2022-04-19 21:17:31 +02:00
J-Jamet
41e2620cc1 Generate auto description for github repo 2022-04-19 21:16:55 +02:00
J-Jamet
e7a82b167a fix Fastfile 2022-04-19 18:18:15 +02:00
J-Jamet
088c556b00 Update fastlane to copy the release file with name 2022-04-19 18:04:12 +02:00
J-Jamet
c80343b6d4 Remove unused import 2022-04-19 16:41:13 +02:00
J-Jamet
4e52a8cf60 Show visual title when entry is available in Magikeyboard 2022-04-19 16:10:40 +02:00
J-Jamet
1ed1d4233f Allow to add entry with no info in Magikeyboard 2022-04-19 16:06:16 +02:00
J-Jamet
6e4626bc02 Fix ask lock when database can be saved 2022-04-19 15:47:21 +02:00
J-Jamet
2608ae247f Fix quick search and better loadGroup implementation #1302 2022-04-19 14:35:28 +02:00
J-Jamet
785586bfe9 Remove "Select share info" setting for Magikeyboard #1304 2022-04-19 12:24:51 +02:00
J-Jamet
bdcbb177ae Upgrade to 3.4.3 2022-04-19 11:36:04 +02:00
J-Jamet
15ac365d79 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2022-04-15 19:29:56 +02:00
Braja Yudhistira
debbcb753b Translated using Weblate (Indonesian)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2022-04-15 18:27:39 +02:00
Linerly
69d73aeaa4 Translated using Weblate (Indonesian)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2022-04-15 18:27:39 +02:00
solokot
dffe53370f Translated using Weblate (Russian)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-04-15 18:27:37 +02:00
Darin Avdeyeva
4334e6dcdf Translated using Weblate (Russian)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-04-15 18:27:37 +02:00
Stephan Paternotte
c2c6c093d5 Translated using Weblate (Dutch)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2022-04-15 18:27:36 +02:00
Cow
77e539eec2 Translated using Weblate (Spanish)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2022-04-15 18:27:36 +02:00
J-Jamet
a57970210e Merge tag '3.4.2' into develop
3.4.2
2022-04-15 13:42:49 +02:00
J-Jamet
1b31a46fb7 Merge branch 'release/3.4.2' 2022-04-15 13:42:43 +02:00
J-Jamet
87f19c74fc Add clean in fastlane 2022-04-15 13:23:25 +02:00
J-Jamet
bd157a9724 Fix small UI color 2022-04-15 13:18:11 +02:00
J-Jamet
5a327eb0db Catch reset app timeout for unexpected exceptions 2022-04-15 13:00:41 +02:00
J-Jamet
4b9c0b0109 Fix navigation bar color in dark mode 2022-04-15 12:54:10 +02:00
J-Jamet
df6b75cdbb Fix color 2022-04-15 12:47:50 +02:00
J-Jamet
0b4f8c122b Fix color 2022-04-15 12:46:37 +02:00
J-Jamet
2a87eaf3e5 Upgrade to 3.4.2 and fix service parameter and workflow 2022-04-15 12:21:14 +02:00
J-Jamet
c52266f5cf Merge branch 'master' of github.com:Kunzisoft/KeePassDX 2022-04-14 19:28:49 +02:00
J-Jamet
3b21f8add2 Merge tag '3.4.1' into develop
3.4.1
2022-04-14 19:28:12 +02:00
J-Jamet
6574bd10a0 Merge branch 'release/3.4.1' 2022-04-14 19:28:05 +02:00
J-Jamet
23f3335988 Update version code for deployment 2022-04-14 19:17:49 +02:00
J-Jamet
a5d7f33c82 Fix unexpected lock of the app #1294 2022-04-14 19:11:33 +02:00
J-Jamet
3782c4dac0 Fix styles 2022-04-14 17:17:38 +02:00
J-Jamet
1fc02fd2fe Small UI changes 2022-04-14 15:16:32 +02:00
J-Jamet
cc347c1dbe Update strings 2022-04-14 15:07:32 +02:00
J-Jamet
79ff20eb18 Remove irrelevant Autofill autosearch setting 2022-04-14 14:55:00 +02:00
J-Jamet
e6e8a447da Clear focus in autosearch and update CHANGELOG 2022-04-14 14:32:34 +02:00
J-Jamet
233f0c5bdb Fix another entry in selection mode with Magikeyboard #1293 2022-04-14 14:12:35 +02:00
J-Jamet
9ed4271a14 Fix search mode with Magikeyboard #1292 2022-04-14 12:50:33 +02:00
J-Jamet
470c0b6b43 Update README description 2022-04-14 11:54:16 +02:00
J-Jamet
afa8ae42b9 Upgrade gradle to 3.4.1 2022-04-14 11:45:55 +02:00
Jérémy JAMET
63d426503f Update FUNDING.yml
Fix issuehunt
2022-04-12 20:43:48 +02:00
Jérémy JAMET
ffb7f80b26 Create FUNDING.yml 2022-04-12 20:42:35 +02:00
J-Jamet
63f8826fd8 Merge branch 'master' into develop 2022-04-12 20:08:21 +02:00
J-Jamet
ef836e8b84 Change screenshots 2022-04-12 20:08:10 +02:00
J-Jamet
abc1c43a51 Merge tag '3.4.0' into develop
3.4.0
2022-04-12 19:45:17 +02:00
J-Jamet
6b54dd9e0d Merge branch 'release/3.4.0' 2022-04-12 19:45:10 +02:00
J-Jamet
1f54e7752d Disable keyboard timeout by default 2022-04-12 15:04:12 +02:00
J-Jamet
6ac941f276 Upgrade to 3.4.0 2022-04-12 14:58:13 +02:00
Hosted Weblate
a4fe92562f Merge branch 'origin/develop' into Weblate. 2022-04-12 12:39:39 +02:00
SC
b9bd1d9d4b Translated using Weblate (Portuguese)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2022-04-12 12:39:39 +02:00
Milo Ivir
3b6c28488a Translated using Weblate (Croatian)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2022-04-12 12:39:38 +02:00
Eric
875eb3500d Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-04-12 12:39:38 +02:00
Ihor Hordiichuk
3a88a2451c Translated using Weblate (Ukrainian)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-04-12 12:39:37 +02:00
solokot
6800b73a4f Translated using Weblate (Russian)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-04-12 12:39:37 +02:00
Matthaiks
983404e6d8 Translated using Weblate (Polish)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-04-12 12:39:37 +02:00
Retrial
b95c0a18a7 Translated using Weblate (Greek)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-04-12 12:39:36 +02:00
VfBFan
36b317cad8 Translated using Weblate (German)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-04-12 12:39:36 +02:00
Sebastian
35d74888fb Translated using Weblate (Danish)
Currently translated at 84.3% (517 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2022-04-12 12:39:36 +02:00
J-Jamet
6c308483f7 Upgrade gradle and kotlin 2022-04-12 12:31:18 +02:00
J-Jamet
9d25fb74ec Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2022-04-12 12:14:23 +02:00
J-Jamet
d217b52744 Upgrade to 3.4.0_beta02
Fix #1282 with workaround
2022-04-12 12:12:05 +02:00
J-Jamet
319da4b174 Rollback openOutputStream in "rwt" 2022-04-12 11:32:49 +02:00
nautilusx
9bee467942 Translated using Weblate (German)
Currently translated at 99.6% (611 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-04-12 07:46:27 +02:00
VfBFan
44ac70fc97 Translated using Weblate (German)
Currently translated at 99.6% (611 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-04-12 07:46:26 +02:00
J-Jamet
d897611d62 Change main screenshot 2022-04-10 17:21:35 +02:00
J-Jamet
c2ae251e73 Change screenshots 2022-04-10 17:10:01 +02:00
J-Jamet
35ad285864 Fix education screen 2022-04-09 16:52:25 +02:00
J-Jamet
97bdae21eb Small change 2022-04-09 16:37:11 +02:00
J-Jamet
d6dc6e43c7 Update strings 2022-04-09 16:35:20 +02:00
Oğuz Ersen
01e6e530d5 Translated using Weblate (Turkish)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-04-09 16:28:35 +02:00
Kunzisoft
9ec0178beb Translated using Weblate (French)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2022-04-09 16:28:35 +02:00
Hosted Weblate
d66f2f6d24 Merge branch 'origin/develop' into Weblate. 2022-04-09 16:15:08 +02:00
Milo Ivir
79cd4004cc Translated using Weblate (Croatian)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2022-04-09 16:15:08 +02:00
VfBFan
991243e2df Translated using Weblate (German)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-04-09 16:15:07 +02:00
J-Jamet
b91cf11d86 Upgrade to 3.4 beta01 2022-04-09 16:14:15 +02:00
J-Jamet
d182ec09fa Upgrade CHANGELOG 2022-04-09 16:12:57 +02:00
J-Jamet
8641822358 Fix small bugs 2022-04-09 16:03:12 +02:00
J-Jamet
9665cbb428 Fix small visual bug 2022-04-08 18:37:06 +02:00
J-Jamet
a280dfaf3b Better magikeyboard views 2022-04-08 18:23:02 +02:00
J-Jamet
3e56521ea8 Empty Magikeyboard memory when the main service is killed #1261 2022-04-08 16:53:03 +02:00
SC
b205230ea9 Translated using Weblate (Portuguese)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2022-04-08 09:31:01 +02:00
Oğuz Ersen
51645ab126 Translated using Weblate (Turkish)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-04-08 09:31:00 +02:00
Eric
5d04897e75 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-04-08 09:30:59 +02:00
Ihor Hordiichuk
1ac0ea5cc6 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-04-08 09:30:59 +02:00
solokot
a07e8b51e5 Translated using Weblate (Russian)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-04-08 09:30:58 +02:00
Vitor Henrique
a81f0238f4 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-04-08 09:30:57 +02:00
Matthaiks
2b81eb8ec7 Translated using Weblate (Polish)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-04-08 09:30:56 +02:00
Retrial
e5eb642781 Translated using Weblate (Greek)
Currently translated at 100.0% (613 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-04-08 09:30:56 +02:00
VfBFan
a4cbe25733 Translated using Weblate (German)
Currently translated at 99.8% (612 of 613 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-04-08 09:30:55 +02:00
J-Jamet
2042c85b22 Ask confirmation to lock if changes without save #970 2022-04-07 18:06:47 +02:00
J-Jamet
3149f8745c Fix passkey view 2022-04-07 17:39:46 +02:00
J-Jamet
15b9f1616f Fix keyboard search and selection 2022-04-07 14:28:45 +02:00
J-Jamet
94c02b7288 Fix selection mode instance issue 2022-04-07 13:26:07 +02:00
J-Jamet
7e70b59a59 Fix selection mode instance issue 2022-04-07 13:22:19 +02:00
Hosted Weblate
2c7f5e41ed Merge branch 'origin/develop' into Weblate. 2022-04-05 18:00:37 +02:00
J-Jamet
108b8df280 Fix CHANGELOG issue number 2022-04-05 15:32:42 +02:00
J-Jamet
553098f9be Manage package name from Magikeyboard #1010 2022-04-05 15:28:15 +02:00
J-Jamet
131eb78407 Setting to change the keyboard during a search #1254 2022-04-05 13:38:36 +02:00
J-Jamet
f956a279a5 Save search parameters #1254 2022-04-05 13:07:36 +02:00
J-Jamet
7150686b92 Fix search parameter parcelable 2022-04-05 11:59:02 +02:00
J-Jamet
4b1fb2c173 Upgrade CHANGELOG 2022-04-05 11:28:58 +02:00
J-Jamet
94464bf608 Save pass generator options in app.properties 2022-04-04 15:30:57 +02:00
J-Jamet
2faa88784a Update CHANGELOG 2022-04-04 15:09:32 +02:00
J-Jamet
e6607b53d8 Better search implementation #175 2022-04-04 15:08:11 +02:00
J-Jamet
3f6a6c864a Show TOTP in 3-digit grouping #1270 2022-04-04 13:25:01 +02:00
J-Jamet
30a578257d Update CHANGELOG 2022-04-04 13:07:38 +02:00
J-Jamet
8411134adf Save files with "wt" #1282 2022-04-04 12:21:35 +02:00
J-Jamet
f86a5d1a19 Fix small bug 2022-04-01 19:19:47 +02:00
J-Jamet
be72492537 Merge branch 'feature/Mnemonics' into develop 2022-04-01 19:14:07 +02:00
J-Jamet
76f9e8ec6e Generate passphrase #218 2022-04-01 19:08:50 +02:00
Milo Ivir
8fb1c44e58 Translated using Weblate (Croatian)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2022-04-01 13:08:19 +02:00
J-Jamet
f607b35cf3 Change max slider to 64 2022-04-01 11:01:00 +02:00
J-Jamet
0e56bec35a Key generator as tabs 2022-04-01 10:51:38 +02:00
Hosted Weblate
c890d10114 Merge branch 'origin/develop' into Weblate. 2022-03-31 12:09:22 +02:00
Milo Ivir
dee2fe5ce7 Translated using Weblate (Croatian)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2022-03-31 12:09:21 +02:00
J-Jamet
4a4d767bce Manage setting to hide password #696 2022-03-29 17:18:49 +02:00
J-Jamet
d57e0cf601 Add setting to colorize password 2022-03-29 16:41:23 +02:00
J-Jamet
7fa141dd1b Merge branch 'develop' into feature/Mnemonics 2022-03-29 16:01:50 +02:00
J-Jamet
c261a0cbca Smaller tab buttons 2022-03-29 16:01:20 +02:00
J-Jamet
66661cbd49 Add bold and change password colors 2022-03-29 15:54:37 +02:00
J-Jamet
100c126c3d Update CHANGELOG 2022-03-29 15:32:51 +02:00
J-Jamet
d466e3077d Add color for special password chars #454 2022-03-29 15:32:29 +02:00
J-Jamet
24587dc34e Small refactorization 2022-03-29 12:42:53 +02:00
J-Jamet
32cc57dd03 Add editable chars fields #539 2022-03-28 22:14:24 +02:00
J-Jamet
a55488846b Add advanced password filters #1052 2022-03-28 21:26:26 +02:00
J-Jamet
dcf61fd4e2 Update CHANGELOG 2022-03-28 16:34:37 +02:00
J-Jamet
5bf998468a Save password preferences dynamically 2022-03-28 16:24:25 +02:00
J-Jamet
01c9625c59 Dynamic filter change and fix bugs 2022-03-28 15:08:56 +02:00
Dixon Huang
772c378922 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2022-03-28 13:09:14 +02:00
J-Jamet
ee50a91379 Move entropy to the top 2022-03-27 21:40:13 +02:00
J-Jamet
9cfda3bad8 Fix small bugs 2022-03-27 21:33:44 +02:00
J-Jamet
aa19b08bd9 Fix password entropy and add chips 2022-03-27 20:41:51 +02:00
J-Jamet
87f69bb7e2 Entropy #869 2022-03-27 18:56:12 +02:00
J-Jamet
41c0aeedbe Show visual password strength indicator #631 2022-03-27 17:22:52 +02:00
Eric
3cbe53d76f Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-03-27 00:27:21 +01:00
Ihor Hordiichuk
aed60d6c1e Translated using Weblate (Ukrainian)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-03-27 00:27:21 +01:00
Stephan Paternotte
be7d35490d Translated using Weblate (Dutch)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2022-03-27 00:27:20 +01:00
VfBFan
d0ea997c63 Translated using Weblate (German)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-03-27 00:27:20 +01:00
J-Jamet
1fecffeba2 Merge branch 'master' of github.com:Kunzisoft/KeePassDX 2022-03-26 16:28:39 +01:00
J-Jamet
76319a56a2 Merge tag '3.3.3' into develop
3.3.3
2022-03-26 16:27:23 +01:00
Oğuz Ersen
e9a1cfea11 Translated using Weblate (Turkish)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-03-26 04:15:30 +01:00
solokot
9115856d19 Translated using Weblate (Russian)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-03-26 04:15:30 +01:00
Matthaiks
8c45266c18 Translated using Weblate (Polish)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-03-26 04:15:29 +01:00
Retrial
6b4130df89 Translated using Weblate (Greek)
Currently translated at 100.0% (595 of 595 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-03-26 04:15:29 +01:00
Jérémy JAMET
7948358d85 Add spaces in issue template 2022-03-18 19:46:32 +01:00
Jérémy JAMET
96b82bb9b2 Remove "please report..." in issue template 2022-03-18 19:45:39 +01:00
J-Jamet
279bd16b74 Best autofill recognition #1250 2022-02-26 13:13:07 +01:00
J-Jamet
2e0081b66c Prepare hardware key in main credential 2022-02-26 12:51:00 +01:00
1143 changed files with 36120 additions and 19107 deletions

13
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
# These are supported funding model platforms
#github: [J-Jamet] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
#patreon: # Replace with a single Patreon username
#open_collective: # Replace with a single Open Collective username
#ko_fi: # Replace with a single Ko-fi username
#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: Kunzisoft # Replace with a single Liberapay username
issuehunt: Kunzisoft/KeePassDX # Replace with a single IssueHunt username
#otechie: # Replace with a single Otechie username
#lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: ['https://www.keepassdx.com/#donation'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -1,42 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**KeePass Database**
- Created with: [e.g Windows KeePass 2.42]
- Version: [e.g. 2]
- Location: [e.g. Remote file retrieved with GDrive app]
- File provider (`content://` URI): [e.g. `content://com.google.android.apps.docs.storage/5`]
- Size: [e.g. 150Mo]
- Contains attachment: [e.g. Yes]
**KeePassDX (please complete the following information):**
- Version: [e.g. 2.5.0.0beta23]
- Build: [e.g. Free]
- Language: [e.g. French]
**Android (please complete the following information):**
- Device: [e.g. GalaxyS8]
- Version: [e.g. 8.1]
**Additional context**
Add any other context about the problem here.
- Browser for Autofill: [e.g. Chrome version X]

62
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: Bug Report
description: Report a bug.
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Please check out the [Wiki](https://github.com/Kunzisoft/KeePassDX/wiki) and [existing issues](https://github.com/Kunzisoft/KeePassDX/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug) to see if your problem has already been reported.
- type: checkboxes
id: checks
attributes:
label: Checks
options:
- label: I have read the Wiki, searched the open issues, and still think this is a new bug.
required: true
- type: textarea
id: bug
attributes:
label: "Explain the problem clearly and succinctly:"
validations:
required: true
- type: textarea
id: expected
attributes:
label: "Describe what you expected to happen:"
- type: input
id: app-version
attributes:
label: "KeePassDX version:"
validations:
required: true
- type: dropdown
id: app-build
attributes:
label: "Build:"
multiple: true
options:
- Free
- Libre
validations:
required: true
- type: input
id: database-version
attributes:
label: "Database version:"
- type: input
id: file-provider
attributes:
label: "File provider (`content://` URI)"
- type: input
id: android-version
attributes:
label: "Android version:"
- type: input
id: android-device
attributes:
label: "Android device:"
- type: textarea
id: additional-context
attributes:
label: "Additional context:"

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,33 @@
name: Feature request
description: Suggest an idea.
labels: ["feature"]
body:
- type: markdown
attributes:
value: |
Please check out the [Wiki](https://github.com/Kunzisoft/KeePassDX/wiki) and [existing issues](https://github.com/Kunzisoft/KeePassDX/issues?q=is%3Aissue%20state%3Aopen%20label%3Afeature) to see if your feature has already been reported.
- type: checkboxes
id: checks
attributes:
label: Checks
options:
- label: I have read the Wiki, searched the open issues, and still think this is a new feature.
required: true
- type: textarea
id: problem
attributes:
label: "Explain the problem clearly and succinctly:"
- type: textarea
id: solution
attributes:
label: "Describe the solution you'd like:"
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: "Describe alternatives you've considered:"
- type: textarea
id: additional-context
attributes:
label: "Additional context:"

3
.gitignore vendored
View File

@@ -80,6 +80,9 @@ art/screen*.png
art/logo_512.png
art/store_screens/
# Release
releases/*
# Dir linux
.directory
*/.directory

156
CHANGELOG
View File

@@ -1,3 +1,159 @@
KeePassDX(4.1.8)
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)
* Remember last read-only state #2099 #2100 (Thx @rmacklin)
* Fix merge deletion #1516
* Fix space in search #175
* Fix deletable recycle bin #2163
* Small fixes
KeePassDX(4.1.7)
* Fix CipherDatabase for biometric states #2119
KeePassDX(4.1.6)
* Auto open biometric prompt from database list #2113
* Fix Keystore errors #2114 #2115
* Complete biometric refactoring for better compatibility
KeePassDX(4.1.5)
* Fix auto prompt #2111
KeePassDX(4.1.4)
* Fix UnlockManager #2098 #2101
* Auto device unlock prompt #2105
* Small fixes ##2066
KeePassDX(4.1.3)
* Fix Autofill Registration #2089
* Fix Biometric errors #2081
* Fixed timestamp in copy file #1981 #1983
* Fix Template Email #1986
* Fix Search #2096
KeePassDX(4.1.2)
* Fix URL search #1940 #1946 #2003 #2040 #2044
* Fix Autofill popup #2054
* Fix Group notes #2053
* Fix Dialog background #2005 #2004 (Thx @codokie)
* Fix OTP configuration #2042 #2065 (Thx @Dev-ClayP)
* Fix small UI elements #1987 #2007 (Thx @ymcx)
* RTL layout support #2021 (Thx @codokie)
* App Metadata to translation #1823
KeePassDX(4.1.1)
* Fix date parser #1933
* Fix domain search #1820 #1936
KeePassDX(4.1.0)
* Generate keyfile #1290
* Hide template group #1894
* Group count sum recursively #421
* Fix date fields #1695 #1710
* Fix distinct domain names #1105 #1820
* Resets the advanced unlock expiration #1600
* Password entropy #1490 #1355
* Upgrade to API 34 (Android 14) #1730
* Small fixes #1711 #1831 #1780 #1821 #1863 #1889 #1289 #1600 #1467 #1870
KeePassDX(4.0.8)
* Fix graphical bug that prevented databases from being opened on some versions of Android #1848 #1850
KeePassDX(4.0.7)
* Prevent 0 Byte file with cache during a save exception #1620 #1594 #1680
* Fix inline suggestions in keyboard #1840
* Fix broken links by default #1755
* Fix UX by allowing validation in entry edition #1770
* Fix small bugs #1709
KeePassDX(4.0.6)
* Fix form filled recognition #1508 #1735 #1508 #1790 #1783 #1797 #1801 #1802 #1804 #1665
* Fix translations #1707 #1683 #1712
* Update APK verifier #1810
KeePassDX(4.0.5)
* Fix form filled recognition #1572 #1508
* Rollback password color #1686 #1490
KeePassDX(4.0.4)
* Fix form filled recognition #1572 #1677
* Fix device unlock #1682
* Fix password color #1490
KeePassDX(4.0.3)
* Fix "Save as" in Read Only mode #1666
* Fix username autofill #1665 #530 #1572 #1426 #1523 #1556 #1653 #1658 #1508 #1667
* Fix regex OTP recognition #1596
* Change password color dynamically #1490
* Small fixes #1641 #1656 #1649 #1400 #1674
KeePassDX(4.0.2)
* Fix Autofill with API 33
KeePassDX(4.0.1)
* Fix back lock #1635 #1629 #1634
* Fix lock button in settings #1630
* Improve theme translation #1631
KeePassDX(4.0.0)
* New UX/UI with Material 3 #1183 #1529 #1428 #1441 #1607
* Material You theme (follow system colors) #1469
* Refactoring inner code #1371
* Migration to API 33
* Cut, copy and delete from search #891 #1308 #1263
* Fix behaviors #1351 #874 #1327
* Fix bugs #1589 #1584 #1545 #1563 #1371 #1609
KeePassDX(3.5.1)
* Fix action dialog with YubiKey challenge-response #1506
KeePassDX(3.5.0)
* Support YubiKey challenge-response #8 #137
* Better exception management during database save #1346
* Add "Screenshot mode" setting #459 #1377 #1354 (Thx @GianpaMX)
* Hide clipboard sensitive text when copy entry field #1386
* Fix attachment download button #1401
* Add monochrome icon #1403 #1404 (Thx @Sandelinos)
* Fix lock with back button #1412 #1414 (Thx @ryg-git)
* Vanadium compatibility #1447 (Thx @flawedworld)
KeePassDX(3.4.5)
* Fix custom data in group (fix KeeShare) #1335
* Fix device credential unlocking #1344
* New clipboard manager #1343
* Keep screen on by default when viewing an entry
* Change the order of the search filters
* Fix searchable selection
KeePassDX(3.4.4)
* Fix crash in New Android 13 #1321
* Better backstack management for selection mode
* Prevent Tapjacking #1318
* Small changes #1298
KeePassDX(3.4.3)
* Remove "Select share info" setting for Magikeyboard #1304
* Fix quick search and better loadGroup implementation #1302
* Fix small bugs
KeePassDX(3.4.2)
* Fix service parameter and workflow to remove notification when service is killed
* Fix color
KeePassDX(3.4.1)
* Fix search mode with Magikeyboard #1292
* Fix select another entry with Magikeyboard #1293
* Fix unexpected lock with Magikeyboard #1294
* Small UI changes
KeePassDX(3.4.0)
* Passphrase implementation #218
* Show visual password strength indicator with entropy #631 #869 #454 #1270
* Dynamically save password generator configuration #618 #696
* Add advanced password filters #1052 #448 #983 #271 #539
* Better search implementation #175 #1254 #1267
* Manage package name from Magikeyboard #1010 #1261
* Ask confirmation to lock if changes without save #970
* Fix small bugs #1282
KeePassDX(3.3.3)
* Fix shared otpauth link if database not open #1274
* Ellipsize attachment name #1253

10
Gemfile Normal file
View File

@@ -0,0 +1,10 @@
# Autogenerated by fastlane
#
# Ensure this file is checked in to source control!
source "https://rubygems.org"
gem 'fastlane'
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

230
Gemfile.lock Normal file
View File

@@ -0,0 +1,230 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
rexml
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1146.0)
aws-sdk-core (3.229.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.110.0)
aws-sdk-core (~> 3, >= 3.228.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.196.1)
aws-sdk-core (~> 3, >= 3.228.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.3.0)
bigdecimal (3.2.2)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.112.0)
faraday (1.10.4)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.1.1)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
fastlane (2.228.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-plugin-versioning_android (0.1.1)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.5.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.8)
domain_name (~> 0.5)
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.13.2)
jwt (2.10.2)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.17.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.3.0)
nkf (0.2.0)
optparse (0.6.0)
os (1.1.4)
plist (3.7.2)
public_suffix (6.0.2)
rake (13.3.0)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.4.1)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
security (0.1.5)
signet (0.20.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
naturally
sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.27.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.1)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
ruby
DEPENDENCIES
fastlane
fastlane-plugin-versioning_android
BUNDLED WITH
2.6.9

View File

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

View File

@@ -4,16 +4,15 @@ apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 31
buildToolsVersion "31.0.0"
ndkVersion "21.4.7075529"
namespace 'com.kunzisoft.keepass'
compileSdkVersion 36
defaultConfig {
applicationId "com.kunzisoft.keepass"
minSdkVersion 15
targetSdkVersion 31
versionCode = 105
versionName = "3.3.3"
minSdkVersion 19
targetSdkVersion 35
versionCode = 141
versionName = "4.1.8"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
@@ -33,10 +32,16 @@ android {
buildTypes {
release {
minifyEnabled = false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
flavorDimensions "version"
productFlavors {
libre {
@@ -50,7 +55,9 @@ android {
"\"KeepassDXStyle_Reply\"," +
"\"KeepassDXStyle_Reply_Night\"," +
"\"KeepassDXStyle_Purple\"," +
"\"KeepassDXStyle_Purple_Dark\"}"
"\"KeepassDXStyle_Purple_Dark\"," +
"\"KeepassDXStyle_Dynamic_Light\"," +
"\"KeepassDXStyle_Dynamic_Night\"}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
}
free {
@@ -59,16 +66,16 @@ android {
buildConfigField "String", "BUILD_VERSION", "\"free\""
buildConfigField "boolean", "CLOSED_STORE", "true"
buildConfigField "String[]", "STYLES_DISABLED",
"{\"KeepassDXStyle_Simple\"," +
"\"KeepassDXStyle_Simple_Night\"," +
"\"KeepassDXStyle_Blue\"," +
"{\"KeepassDXStyle_Blue\"," +
"\"KeepassDXStyle_Blue_Night\"," +
"\"KeepassDXStyle_Red\"," +
"\"KeepassDXStyle_Red_Night\"," +
"\"KeepassDXStyle_Reply\"," +
"\"KeepassDXStyle_Reply_Night\"," +
"\"KeepassDXStyle_Purple\"," +
"\"KeepassDXStyle_Purple_Dark\"}"
"\"KeepassDXStyle_Purple_Dark\"," +
"\"KeepassDXStyle_Dynamic_Light\"," +
"\"KeepassDXStyle_Dynamic_Night\"}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
}
@@ -84,16 +91,19 @@ android {
}
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "17"
}
buildFeatures {
buildConfig true
}
}
def room_version = "2.4.2"
def room_version = "2.5.1"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
@@ -101,25 +111,26 @@ dependencies {
implementation "androidx.appcompat:appcompat:$android_appcompat_version"
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.1.0'
implementation 'androidx.media:media:1.5.0'
implementation 'androidx.media:media:1.6.0'
// Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:$android_core_version"
implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation "androidx.lifecycle:lifecycle-process:2.6.2"
implementation 'androidx.fragment:fragment-ktx:1.6.0'
implementation "com.google.android.material:material:$android_material_version"
// Token auto complete
// From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed
// implementation "com.splitwise:tokenautocomplete:4.0.0-beta04"
implementation "com.splitwise:tokenautocomplete:4.0.0-beta05"
// Database
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// Autofill
implementation "androidx.autofill:autofill:1.1.0"
// Time
implementation 'joda-time:joda-time:2.10.13'
implementation 'joda-time:joda-time:2.13.0'
// Color
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6'
// Education
@@ -127,13 +138,13 @@ dependencies {
// Apache Commons
implementation 'commons-io:commons-io:2.8.0'
implementation 'commons-codec:commons-codec:1.15'
// Encrypt lib
implementation project(path: ':crypto')
// Icon pack
implementation project(path: ':icon-pack-classic')
implementation project(path: ':icon-pack-material')
// Password generator
implementation 'me.gosimple:nbvcxz:1.5.0'
// Modules import
implementation project(path: ':database')
implementation project(path: ':icon-pack')
// Tests
androidTestImplementation "androidx.test:runner:$android_test_version"
androidTestImplementation "androidx.test:rules:$android_test_version"
}

View File

@@ -0,0 +1,90 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "f8fb4aed546de19ae7ca0797f49b26a4",
"entities": [
{
"tableName": "file_database_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `database_alias` TEXT NOT NULL, `keyfile_uri` TEXT, `hardware_key` TEXT, `updated` INTEGER NOT NULL, PRIMARY KEY(`database_uri`))",
"fields": [
{
"fieldPath": "databaseUri",
"columnName": "database_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "databaseAlias",
"columnName": "database_alias",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "keyFileUri",
"columnName": "keyfile_uri",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "hardwareKey",
"columnName": "hardware_key",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "updated",
"columnName": "updated",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"database_uri"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "cipher_database",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `encrypted_value` TEXT NOT NULL, `specs_parameters` TEXT NOT NULL, PRIMARY KEY(`database_uri`))",
"fields": [
{
"fieldPath": "databaseUri",
"columnName": "database_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "encryptedValue",
"columnName": "encrypted_value",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "specParameters",
"columnName": "specs_parameters",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"database_uri"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f8fb4aed546de19ae7ca0797f49b26a4')"
]
}
}

View File

@@ -0,0 +1,96 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "a20aec7cf09664b1102ec659fa51160a",
"entities": [
{
"tableName": "file_database_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `database_alias` TEXT NOT NULL, `keyfile_uri` TEXT, `hardware_key` TEXT, `read_only` INTEGER, `updated` INTEGER NOT NULL, PRIMARY KEY(`database_uri`))",
"fields": [
{
"fieldPath": "databaseUri",
"columnName": "database_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "databaseAlias",
"columnName": "database_alias",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "keyFileUri",
"columnName": "keyfile_uri",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "hardwareKey",
"columnName": "hardware_key",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "readOnly",
"columnName": "read_only",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "updated",
"columnName": "updated",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"database_uri"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "cipher_database",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `encrypted_value` TEXT NOT NULL, `specs_parameters` TEXT NOT NULL, PRIMARY KEY(`database_uri`))",
"fields": [
{
"fieldPath": "databaseUri",
"columnName": "database_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "encryptedValue",
"columnName": "encrypted_value",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "specParameters",
"columnName": "specs_parameters",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"database_uri"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a20aec7cf09664b1102ec659fa51160a')"
]
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="120"
android:viewportHeight="120">
<group
android:translateX="6"
android:translateY="6">
<path
android:fillColor="#ffffff"
android:strokeWidth="1.99999297"
android:pathData="M63.9961,34.0059 C61.5643,34.096,59.2564,35.102,57.5352,36.8223 C53.7682,40.589,53.7682,46.6982,57.5352,50.4649 C61.3017,54.232,67.4073,54.232,71.1739,50.4649 C74.9409,46.6982,74.9409,40.589,71.1739,36.8223 C69.2766,34.9258,66.6768,33.9054,63.9962,34.0059 Z M68.1992,40.6954 C69.8278,40.6958,71.148,42.016,71.1484,43.6446 C71.148,45.2732,69.8278,46.5934,68.1992,46.5938 C66.5706,46.5934,65.2504,45.2732,65.25,43.6446 C65.2504,42.016,66.5706,40.6958,68.1992,40.6954 Z M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
</group>
</vector>

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="120"
android:viewportHeight="120">
<group
android:translateX="6"
android:translateY="6">
<path
android:fillColor="#ffffff"
android:strokeWidth="1.99999297"
android:pathData="M64.501,35.0576 C63.7095,35.0576,62.918,35.3613,62.3115,35.9678 L55.0127,43.2666 C53.7998,44.4795,53.7998,46.4306,55.0127,47.6436 L62.3115,54.9424 C63.5244,56.1553,65.4775,56.1553,66.6904,54.9424 L73.9873,47.6436 C75.2002,46.4307,75.2002,44.4796,73.9873,43.2666 L66.6904,35.9678 C66.0839,35.3613,65.2924,35.0576,64.5009,35.0576 Z M67.6729,42.6006 C69.3298,42.6006,70.6729,43.9437,70.6729,45.6006 C70.6729,47.2575,69.3298,48.6006,67.6729,48.6006 C66.016,48.6006,64.6729,47.2575,64.6729,45.6006 C64.6729,43.9437,66.016,42.6006,67.6729,42.6006 Z M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
</group>
</vector>

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.kunzisoft.keepass"
android:installLocation="auto">
<supports-screens
android:smallScreens="true"
@@ -10,6 +9,12 @@
android:anyDensity="true" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-permission
android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission
@@ -20,6 +25,12 @@
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<queries>
<intent>
<action android:name="android.intent.action.CREATE_DOCUMENT" />
<data android:mimeType="application/octet-stream" />
</intent>
</queries>
<application
android:label="@string/app_name"
@@ -32,6 +43,7 @@
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
android:largeHeap="true"
android:resizeableActivity="true"
android:supportsRtl="true"
android:theme="@style/KeepassDXStyle.Night"
tools:targetApi="s">
<meta-data
@@ -39,7 +51,6 @@
android:value="${googleAndroidBackupAPIKey}" />
<activity
android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
android:theme="@style/KeepassDXStyle.SplashScreen"
android:launchMode="singleTop"
android:exported="true"
android:configChanges="keyboardHidden"
@@ -112,7 +123,7 @@
android:name="com.kunzisoft.keepass.activities.GroupActivity"
android:exported="false"
android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustPan">
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="android.app.default_searchable"
android:value="com.kunzisoft.keepass.search.SearchResults"
@@ -131,6 +142,9 @@
<activity
android:name="com.kunzisoft.keepass.activities.IconPickerActivity"
android:configChanges="keyboardHidden" />
<activity
android:name="com.kunzisoft.keepass.activities.KeyGeneratorActivity"
android:configChanges="keyboardHidden" />
<activity
android:name="com.kunzisoft.keepass.activities.ImageViewerActivity"
android:configChanges="keyboardHidden" />
@@ -147,14 +161,21 @@
<activity
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden" />
android:configChanges="keyboardHidden"
android:excludeFromRecents="true"/>
<activity
android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
android:name="com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity" />
<activity
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
<activity
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
<activity
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
android:theme="@style/Theme.Transparent" />
<activity
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
android:theme="@style/Theme.Transparent"
android:launchMode="singleInstance"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
@@ -165,13 +186,11 @@
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="otpauth" android:host="totp" />
<data android:scheme="otpauth" android:host="hotp" />
<data android:scheme="otpauth"/>
<data android:host="totp"/>
<data android:host="hotp"/>
</intent-filter>
</activity>
<activity
android:name="com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity"
android:theme="@style/Theme.Transparent" />
<activity
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
android:label="@string/keyboard_setting_label"
@@ -183,18 +202,27 @@
<service
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
android:foregroundServiceType="dataSync"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.services.AttachmentFileNotificationService"
android:foregroundServiceType="dataSync"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.services.ClipboardEntryNotificationService"
android:foregroundServiceType="specialUse"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
android:foregroundServiceType="specialUse"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.services.DeviceUnlockNotificationService"
android:foregroundServiceType="specialUse"
android:enabled="true"
android:exported="false" />
<!-- Receiver for Autofill -->
@@ -221,10 +249,6 @@
<action android:name="android.view.InputMethod" />
</intent-filter>
</service>
<service
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
android:enabled="true"
android:exported="false" />
<receiver
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
android:exported="true">

View File

@@ -38,7 +38,11 @@ import android.graphics.RectF
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.util.TypedValue
import android.view.*
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.DecelerateInterpolator
import android.view.animation.Interpolator
@@ -172,16 +176,16 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
private val onScaleGestureListener: ScaleGestureDetector.OnScaleGestureListener =
object : ScaleGestureDetector.OnScaleGestureListener {
override fun onScale(detector: ScaleGestureDetector?): Boolean {
override fun onScale(detector: ScaleGestureDetector): Boolean {
if (isDragging() || isBitmapTranslateAnimationRunning || isBitmapScaleAnimationRunninng) {
return false
}
val scaleFactor = detector?.scaleFactor ?: 1.0f
val focalX = detector?.focusX ?: bitmapBounds.centerX()
val focalY = detector?.focusY ?: bitmapBounds.centerY()
val scaleFactor = detector.scaleFactor
val focalX = detector.focusX
val focalY = detector.focusY
if (detector?.scaleFactor == 1.0f) {
if (detector.scaleFactor == 1.0f) {
// scale is not changing
return true
}
@@ -191,22 +195,23 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
return true
}
override fun onScaleBegin(p0: ScaleGestureDetector?): Boolean = true
override fun onScaleBegin(p0: ScaleGestureDetector): Boolean = true
override fun onScaleEnd(p0: ScaleGestureDetector) {}
override fun onScaleEnd(p0: ScaleGestureDetector?) {}
}
private val onGestureListener: GestureDetector.OnGestureListener =
object : GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent?): Boolean = true
override fun onDown(e: MotionEvent): Boolean = true
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (e2?.pointerCount != 1) {
if (e2.pointerCount != 1) {
return true
}
@@ -219,13 +224,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
e1 ?: return true
if (scale > minScale) {
processFlingBitmap(velocityX, velocityY)
} else {
@@ -234,9 +237,7 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
return true
}
override fun onDoubleTap(e: MotionEvent?): Boolean {
e ?: return false
override fun onDoubleTap(e: MotionEvent): Boolean {
if (isBitmapScaleAnimationRunninng) {
return true
}
@@ -357,78 +358,41 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
val imageView = imageViewRef.get() ?: return
isViewTranslateAnimationRunning = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
imageView.run {
val translationY = if (velY > 0) {
originalViewBounds.top + height - top
} else {
originalViewBounds.top - height - top
}
animate()
.setDuration(dismissAnimationDuration)
.setInterpolator(dismissAnimationInterpolator)
.translationY(translationY.toFloat())
.setUpdateListener {
val amount = calcTranslationAmount()
changeBackgroundAlpha(amount)
onViewTranslateListener?.onViewTranslate(imageView, amount)
}
.setListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator?) {
}
override fun onAnimationEnd(p0: Animator?) {
isViewTranslateAnimationRunning = false
onViewTranslateListener?.onDismiss(imageView)
cleanup()
}
override fun onAnimationCancel(p0: Animator?) {
isViewTranslateAnimationRunning = false
}
override fun onAnimationRepeat(p0: Animator?) {
// no op
}
})
}
} else {
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, if (velY > 0) {
originalViewBounds.top + imageView.height - imageView.top
imageView.run {
val translationY = if (velY > 0) {
originalViewBounds.top + height - top
} else {
originalViewBounds.top - imageView.height - imageView.top
}.toFloat()).apply {
duration = dismissAnimationDuration
interpolator = dismissAnimationInterpolator
addUpdateListener {
val amount = calcTranslationAmount()
changeBackgroundAlpha(amount)
onViewTranslateListener?.onViewTranslate(imageView, amount)
}
addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator?) {
// no op
}
override fun onAnimationEnd(p0: Animator?) {
isViewTranslateAnimationRunning = false
onViewTranslateListener?.onDismiss(imageView)
cleanup()
}
override fun onAnimationCancel(p0: Animator?) {
isViewTranslateAnimationRunning = false
}
override fun onAnimationRepeat(p0: Animator?) {
// no op
}
})
start()
originalViewBounds.top - height - top
}
animate()
.setDuration(dismissAnimationDuration)
.setInterpolator(dismissAnimationInterpolator)
.translationY(translationY.toFloat())
.setUpdateListener {
val amount = calcTranslationAmount()
changeBackgroundAlpha(amount)
onViewTranslateListener?.onViewTranslate(imageView, amount)
}
.setListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator) {
}
override fun onAnimationEnd(p0: Animator) {
isViewTranslateAnimationRunning = false
onViewTranslateListener?.onDismiss(imageView)
cleanup()
}
override fun onAnimationCancel(p0: Animator) {
isViewTranslateAnimationRunning = false
}
override fun onAnimationRepeat(p0: Animator) {
// no op
}
})
}
}
@@ -480,20 +444,20 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
setTransform()
}
addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator?) {
override fun onAnimationStart(p0: Animator) {
isBitmapTranslateAnimationRunning = true
}
override fun onAnimationEnd(p0: Animator?) {
override fun onAnimationEnd(p0: Animator) {
isBitmapTranslateAnimationRunning = false
constrainBitmapBounds()
}
override fun onAnimationCancel(p0: Animator?) {
override fun onAnimationCancel(p0: Animator) {
isBitmapTranslateAnimationRunning = false
}
override fun onAnimationRepeat(p0: Animator?) {
override fun onAnimationRepeat(p0: Animator) {
// no op
}
})
@@ -531,11 +495,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
setTransform()
}
addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator?) {
override fun onAnimationStart(p0: Animator) {
isBitmapScaleAnimationRunninng = true
}
override fun onAnimationEnd(p0: Animator?) {
override fun onAnimationEnd(p0: Animator) {
isBitmapScaleAnimationRunninng = false
if (endScale == minScale) {
zoomToTargetScale(minScale, focalX, focalY)
@@ -543,11 +507,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
}
}
override fun onAnimationCancel(p0: Animator?) {
override fun onAnimationCancel(p0: Animator) {
isBitmapScaleAnimationRunninng = false
}
override fun onAnimationRepeat(p0: Animator?) {
override fun onAnimationRepeat(p0: Animator) {
// no op
}
})
@@ -585,11 +549,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
setTransform()
}
addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator?) {
override fun onAnimationStart(p0: Animator) {
isBitmapScaleAnimationRunninng = true
}
override fun onAnimationEnd(p0: Animator?) {
override fun onAnimationEnd(p0: Animator) {
isBitmapScaleAnimationRunninng = false
if (endScale == minScale) {
scale = minScale
@@ -599,11 +563,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
}
}
override fun onAnimationCancel(p0: Animator?) {
override fun onAnimationCancel(p0: Animator) {
isBitmapScaleAnimationRunninng = false
}
override fun onAnimationRepeat(p0: Animator?) {
override fun onAnimationRepeat(p0: Animator) {
// no op
}
})
@@ -656,137 +620,76 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
private fun restoreViewTransform() {
val imageView = imageViewRef.get() ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
imageView.run {
animate()
.setDuration(restoreAnimationDuration)
.setInterpolator(restoreAnimationInterpolator)
.translationY((originalViewBounds.top - top).toFloat())
.setUpdateListener {
val amount = calcTranslationAmount()
changeBackgroundAlpha(amount)
onViewTranslateListener?.onViewTranslate(this, amount)
imageView.run {
animate()
.setDuration(restoreAnimationDuration)
.setInterpolator(restoreAnimationInterpolator)
.translationY((originalViewBounds.top - top).toFloat())
.setUpdateListener {
val amount = calcTranslationAmount()
changeBackgroundAlpha(amount)
onViewTranslateListener?.onViewTranslate(this, amount)
}
.setListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator) {
// no op
}
.setListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator?) {
// no op
}
override fun onAnimationEnd(p0: Animator?) {
onViewTranslateListener?.onRestore(imageView)
}
override fun onAnimationEnd(p0: Animator) {
onViewTranslateListener?.onRestore(imageView)
}
override fun onAnimationCancel(p0: Animator?) {
// no op
}
override fun onAnimationCancel(p0: Animator) {
// no op
}
override fun onAnimationRepeat(p0: Animator?) {
// no op
}
})
}
} else {
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, (originalViewBounds.top - imageView.top).toFloat()).apply {
duration = restoreAnimationDuration
interpolator = restoreAnimationInterpolator
addUpdateListener {
val amount = calcTranslationAmount()
changeBackgroundAlpha(amount)
onViewTranslateListener?.onViewTranslate(imageView, amount)
}
addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator?) {
// no op
}
override fun onAnimationEnd(p0: Animator?) {
onViewTranslateListener?.onRestore(imageView)
}
override fun onAnimationCancel(p0: Animator?) {
// no op
}
override fun onAnimationRepeat(p0: Animator?) {
// no op
}
})
start()
}
override fun onAnimationRepeat(p0: Animator) {
// no op
}
})
}
}
private fun startDragToDismissAnimation() {
val imageView = imageViewRef.get() ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
imageView.run {
val translationY = if (y - initialY > 0) {
originalViewBounds.top + height - top
} else {
originalViewBounds.top - height - top
}
animate()
.setDuration(dismissAnimationDuration)
.setInterpolator(AccelerateDecelerateInterpolator())
.translationY(translationY.toFloat())
.setUpdateListener {
val amount = calcTranslationAmount()
changeBackgroundAlpha(amount)
onViewTranslateListener?.onViewTranslate(this, amount)
imageView.run {
val translationY = if (y - initialY > 0) {
originalViewBounds.top + height - top
} else {
originalViewBounds.top - height - top
}
animate()
.setDuration(dismissAnimationDuration)
.setInterpolator(AccelerateDecelerateInterpolator())
.translationY(translationY.toFloat())
.setUpdateListener {
val amount = calcTranslationAmount()
changeBackgroundAlpha(amount)
onViewTranslateListener?.onViewTranslate(this, amount)
}
.setListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator) {
isViewTranslateAnimationRunning = true
}
.setListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator?) {
isViewTranslateAnimationRunning = true
}
override fun onAnimationEnd(p0: Animator?) {
isViewTranslateAnimationRunning = false
onViewTranslateListener?.onDismiss(imageView)
cleanup()
}
override fun onAnimationEnd(p0: Animator) {
isViewTranslateAnimationRunning = false
onViewTranslateListener?.onDismiss(imageView)
cleanup()
}
override fun onAnimationCancel(p0: Animator?) {
isViewTranslateAnimationRunning = false
}
override fun onAnimationCancel(p0: Animator) {
isViewTranslateAnimationRunning = false
}
override fun onAnimationRepeat(p0: Animator?) {
// no op
}
})
}
} else {
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, imageView.translationY.toFloat()).apply {
duration = dismissAnimationDuration
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener {
val amount = calcTranslationAmount()
changeBackgroundAlpha(amount)
onViewTranslateListener?.onViewTranslate(imageView, amount)
}
addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator?) {
isViewTranslateAnimationRunning = true
}
override fun onAnimationEnd(p0: Animator?) {
isViewTranslateAnimationRunning = false
onViewTranslateListener?.onDismiss(imageView)
cleanup()
}
override fun onAnimationCancel(p0: Animator?) {
isViewTranslateAnimationRunning = false
}
override fun onAnimationRepeat(p0: Animator?) {
// no op
}
})
start()
}
override fun onAnimationRepeat(p0: Animator) {
// no op
}
})
}
}
private fun processFlingToDismiss(velocityY: Float) {

View File

@@ -20,16 +20,20 @@
package com.kunzisoft.keepass.activities
import android.content.pm.PackageManager.NameNotFoundException
import android.os.Build
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import androidx.core.text.HtmlCompat
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
import com.kunzisoft.keepass.utils.getPackageInfoCompat
import org.joda.time.DateTime
class AboutActivity : StylishActivity() {
@@ -45,10 +49,16 @@ class AboutActivity : StylishActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
val appName = if (this.isContributingUser())
getString(R.string.app_name) + " " + getString(R.string.app_name_part3)
else
getString(R.string.app_name)
findViewById<TextView>(R.id.activity_about_app_name).text = appName
var version: String
var build: String
try {
version = packageManager.getPackageInfo(packageName, 0).versionName
version = packageManager.getPackageInfoCompat(packageName).versionName ?: ""
build = BuildConfig.BUILD_VERSION
} catch (e: NameNotFoundException) {
Log.w(javaClass.simpleName, "Unable to get the app or the build version", e)
@@ -68,6 +78,14 @@ class AboutActivity : StylishActivity() {
movementMethod = LinkMovementMethod.getInstance()
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
HtmlCompat.FROM_HTML_MODE_LEGACY)
textDirection = View.TEXT_DIRECTION_ANY_RTL
}
findViewById<TextView>(R.id.activity_about_privacy_text).apply {
movementMethod = LinkMovementMethod.getInstance()
text = HtmlCompat.fromHtml(getString(R.string.html_about_privacy),
HtmlCompat.FROM_HTML_MODE_LEGACY)
}
findViewById<TextView>(R.id.activity_about_contribution_text).apply {

View File

@@ -25,6 +25,7 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi
@@ -36,11 +37,13 @@ import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
import com.kunzisoft.keepass.autofill.KeeAutofillService
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.WebDomain
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
@RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : DatabaseModeActivity() {
@@ -58,7 +61,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
// Retrieve selection mode
@@ -69,11 +72,11 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
// To pass extra inline request
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
compatInlineSuggestionsRequest = bundle.getParcelable(KEY_INLINE_SUGGESTION)
compatInlineSuggestionsRequest = bundle.getParcelableCompat(KEY_INLINE_SUGGESTION)
}
// Build search param
bundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
SearchInfo.getConcreteWebDomain(
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
WebDomain.getConcreteWebDomain(
this,
searchInfo.webDomain
) { concreteWebDomain ->
@@ -99,9 +102,9 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
}
SpecialMode.REGISTRATION -> {
// To register info
val registerInfo = intent.getParcelableExtra<RegisterInfo>(KEY_REGISTER_INFO)
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(KEY_REGISTER_INFO)
val searchInfo = SearchInfo(registerInfo?.searchInfo)
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launchRegistration(database, searchInfo, registerInfo)
}
@@ -115,89 +118,91 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
}
}
private fun launchSelection(database: Database?,
private fun launchSelection(database: ContextualDatabase?,
autofillComponent: AutofillComponent?,
searchInfo: SearchInfo) {
if (autofillComponent == null) {
setResult(Activity.RESULT_CANCELED)
finish()
} else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
PreferencesUtil.applicationIdBlocklist(this))
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
PreferencesUtil.webDomainBlocklist(this))) {
} else if (KeeAutofillService.autofillAllowedFor(
applicationId = searchInfo.applicationId,
webDomain = searchInfo.webDomain,
context = this
)) {
// If database is open
SearchHelper.checkAutoSearchInfo(this,
database,
searchInfo,
{ openedDatabase, items ->
// Items found
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
finish()
},
{ openedDatabase ->
// Show the database UI to select the entry
GroupActivity.launchForAutofillResult(this,
openedDatabase,
mAutofillActivityResultLauncher,
autofillComponent,
searchInfo,
false)
},
{
// If database not open
FileDatabaseSelectActivity.launchForAutofillResult(this,
mAutofillActivityResultLauncher,
autofillComponent,
searchInfo)
}
)
} else {
showBlockRestartMessage()
setResult(Activity.RESULT_CANCELED)
finish()
} else {
// If database is open
SearchHelper.checkAutoSearchInfo(this,
database,
searchInfo,
{ openedDatabase, items ->
// Items found
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
finish()
},
{ openedDatabase ->
// Show the database UI to select the entry
GroupActivity.launchForAutofillResult(this,
openedDatabase,
mAutofillActivityResultLauncher,
autofillComponent,
searchInfo,
false)
},
{
// If database not open
FileDatabaseSelectActivity.launchForAutofillResult(this,
mAutofillActivityResultLauncher,
autofillComponent,
searchInfo)
}
)
}
}
private fun launchRegistration(database: Database?,
private fun launchRegistration(database: ContextualDatabase?,
searchInfo: SearchInfo,
registerInfo: RegisterInfo?) {
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
PreferencesUtil.applicationIdBlocklist(this))
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
PreferencesUtil.webDomainBlocklist(this))) {
showBlockRestartMessage()
setResult(Activity.RESULT_CANCELED)
} else {
if (KeeAutofillService.autofillAllowedFor(
applicationId = searchInfo.applicationId,
webDomain = searchInfo.webDomain,
context = this
)) {
val readOnly = database?.isReadOnly != false
SearchHelper.checkAutoSearchInfo(this,
database,
searchInfo,
{ openedDatabase, _ ->
if (!readOnly) {
// Show the database UI to select the entry
GroupActivity.launchForRegistration(this,
openedDatabase,
registerInfo)
} else {
showReadOnlySaveMessage()
}
},
{ openedDatabase ->
if (!readOnly) {
// Show the database UI to select the entry
GroupActivity.launchForRegistration(this,
openedDatabase,
registerInfo)
} else {
showReadOnlySaveMessage()
}
},
{
// If database not open
FileDatabaseSelectActivity.launchForRegistration(this,
registerInfo)
database,
searchInfo,
{ openedDatabase, _ ->
if (!readOnly) {
// Show the database UI to select the entry
GroupActivity.launchForRegistration(this,
openedDatabase,
registerInfo)
} else {
showReadOnlySaveMessage()
}
},
{ openedDatabase ->
if (!readOnly) {
// Show the database UI to select the entry
GroupActivity.launchForRegistration(this,
openedDatabase,
registerInfo)
} else {
showReadOnlySaveMessage()
}
},
{
// If database not open
FileDatabaseSelectActivity.launchForRegistration(this,
registerInfo)
}
)
} else {
showBlockRestartMessage()
setResult(Activity.RESULT_CANCELED)
}
finish()
}
@@ -213,6 +218,8 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
companion object {
private val TAG = AutofillLauncherActivity::class.java.name
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
@@ -221,37 +228,51 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
fun getPendingIntentForSelection(context: Context,
searchInfo: SearchInfo? = null,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent {
return PendingIntent.getActivity(context, 0,
// Doesn't work with direct extra Parcelable (don't know why?)
// Wrap into a bundle to bypass the problem
Intent(context, AutofillLauncherActivity::class.java).apply {
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
putParcelable(KEY_SEARCH_INFO, searchInfo)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
}
})
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
})
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent? {
try {
return PendingIntent.getActivity(
context, 0,
// Doesn't work with direct extra Parcelable (don't know why?)
// Wrap into a bundle to bypass the problem
Intent(context, AutofillLauncherActivity::class.java).apply {
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
putParcelable(KEY_SEARCH_INFO, searchInfo)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
}
})
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
}
)
} catch (e: RuntimeException) {
Log.e(TAG, "Unable to create pending intent for selection", e)
return null
}
}
fun getPendingIntentForRegistration(context: Context,
registerInfo: RegisterInfo): PendingIntent {
return PendingIntent.getActivity(context, 0,
Intent(context, AutofillLauncherActivity::class.java).apply {
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
putExtra(KEY_REGISTER_INFO, registerInfo)
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
})
registerInfo: RegisterInfo): PendingIntent? {
try {
return PendingIntent.getActivity(
context, 0,
Intent(context, AutofillLauncherActivity::class.java).apply {
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
putExtra(KEY_REGISTER_INFO, registerInfo)
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
}
)
} catch (e: RuntimeException) {
Log.e(TAG, "Unable to create pending intent for registration", e)
return null
}
}
fun launchForRegistration(context: Context,

View File

@@ -23,6 +23,7 @@ import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
@@ -30,17 +31,22 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.google.android.material.progressindicator.LinearProgressIndicator
@@ -51,8 +57,8 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TagsAdapter
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryActivityEducation
@@ -67,17 +73,21 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.view.WindowInsetPosition
import com.kunzisoft.keepass.view.applyWindowInsets
import com.kunzisoft.keepass.view.changeControlColor
import com.kunzisoft.keepass.view.changeTitleColor
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.setTransparentNavigationBar
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.EntryViewModel
import java.util.*
import java.util.UUID
class EntryActivity : DatabaseLockActivity() {
private var footer: ViewGroup? = null
private var coordinatorLayout: CoordinatorLayout? = null
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var appBarLayout: AppBarLayout? = null
@@ -98,7 +108,6 @@ class EntryActivity : DatabaseLockActivity() {
private var mMainEntryId: NodeId<UUID>? = null
private var mHistoryPosition: Int = -1
private var mEntryIsHistory: Boolean = false
private var mUrl: String? = null
private var mEntryLoaded = false
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
@@ -111,9 +120,9 @@ class EntryActivity : DatabaseLockActivity() {
}
private var mIcon: IconImage? = null
private var mColorAccent: Int = 0
private var mControlColor: Int = 0
private var mColorPrimary: Int = 0
private var mColorSecondary: Int = 0
private var mColorSurface: Int = 0
private var mColorOnSurface: Int = 0
private var mColorBackground: Int = 0
private var mBackgroundColor: Int? = null
private var mForegroundColor: Int? = null
@@ -129,6 +138,7 @@ class EntryActivity : DatabaseLockActivity() {
supportActionBar?.setDisplayShowHomeEnabled(true)
// Get views
footer = findViewById(R.id.activity_entry_footer)
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
appBarLayout = findViewById(R.id.app_bar)
@@ -140,22 +150,30 @@ class EntryActivity : DatabaseLockActivity() {
lockView = findViewById(R.id.lock_button)
loadingView = findViewById(R.id.loading)
// To apply fit window with transparency
setTransparentNavigationBar {
// To fix margin with API 27
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
}
// Empty title
collapsingToolbarLayout?.title = " "
toolbar?.title = " "
// Retrieve the textColor to tint the toolbar
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
val taControlColor = theme.obtainStyledAttributes(intArrayOf(R.attr.toolbarColorControl))
val taColorPrimary = theme.obtainStyledAttributes(intArrayOf(R.attr.colorPrimary))
val taColorSecondary = theme.obtainStyledAttributes(intArrayOf(R.attr.colorSecondary))
val taColorSurface = theme.obtainStyledAttributes(intArrayOf(R.attr.colorSurface))
val taColorOnSurface = theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnSurface))
val taColorBackground = theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
mColorAccent = taColorAccent.getColor(0, Color.BLACK)
mControlColor = taControlColor.getColor(0, Color.BLACK)
mColorPrimary = taColorPrimary.getColor(0, Color.BLACK)
mColorSecondary = taColorSecondary.getColor(0, Color.BLACK)
mColorSurface = taColorSurface.getColor(0, Color.BLACK)
mColorOnSurface = taColorOnSurface.getColor(0, Color.BLACK)
mColorBackground = taColorBackground.getColor(0, Color.BLACK)
taColorAccent.recycle()
taControlColor.recycle()
taColorPrimary.recycle()
taColorSecondary.recycle()
taColorSurface.recycle()
taColorOnSurface.recycle()
taColorBackground.recycle()
// Init Tags adapter
@@ -180,7 +198,7 @@ class EntryActivity : DatabaseLockActivity() {
// Get Entry from UUID
try {
intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { mainEntryId ->
intent.getParcelableExtraCompat<NodeId<UUID>>(KEY_ENTRY)?.let { mainEntryId ->
intent.removeExtra(KEY_ENTRY)
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
intent.removeExtra(KEY_ENTRY_HISTORY_POSITION)
@@ -224,16 +242,16 @@ class EntryActivity : DatabaseLockActivity() {
this.mEntryIsHistory = entryIsHistory
// Assign history dedicated view
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
// TODO History badge
/*
if (entryIsHistory) {
collapsingToolbarLayout?.contentScrim =
ColorDrawable(mColorAccent)
}
}*/
val entryInfo = entryInfoHistory.entryInfo
// Manage entry copy to start notification if allowed (at the first start)
if (savedInstanceState == null) {
// Manage entry to launch copying notification if allowed
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
ClipboardEntryNotificationService.checkAndLaunchNotification(this, entryInfo)
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
@@ -246,7 +264,6 @@ class EntryActivity : DatabaseLockActivity() {
if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
collapsingToolbarLayout?.title = entryTitle
toolbar?.title = entryTitle
mUrl = entryInfo.url
// Assign tags
val tags = entryInfo.tags
tagsListView?.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
@@ -310,14 +327,14 @@ class EntryActivity : DatabaseLockActivity() {
return coordinatorLayout
}
override fun onDatabaseRetrieved(database: Database?) {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
mEntryViewModel.loadDatabase(database)
}
override fun onDatabaseActionFinished(
database: Database,
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
@@ -365,8 +382,8 @@ class EntryActivity : DatabaseLockActivity() {
}
private fun applyToolbarColors() {
appBarLayout?.setBackgroundColor(mBackgroundColor ?: mColorPrimary)
collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorPrimary)
collapsingToolbarLayout?.setBackgroundColor(mBackgroundColor ?: mColorSurface)
collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorSurface)
val backgroundDarker = if (mBackgroundColor != null) {
ColorUtils.blendARGB(mBackgroundColor!!, Color.WHITE, 0.1f)
} else {
@@ -376,15 +393,15 @@ class EntryActivity : DatabaseLockActivity() {
.createBlendModeColorFilterCompat(backgroundDarker, BlendModeCompat.SRC_IN)
mIcon?.let { icon ->
titleIconView?.let { iconView ->
mIconDrawableFactory?.assignDatabaseIcon(
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(
iconView,
icon,
mForegroundColor ?: mColorAccent
mForegroundColor ?: mColorSecondary
)
}
}
toolbar?.changeControlColor(mForegroundColor ?: mControlColor)
collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mControlColor)
toolbar?.changeControlColor(mForegroundColor ?: mColorOnSurface)
collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mColorOnSurface)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -408,9 +425,6 @@ class EntryActivity : DatabaseLockActivity() {
}
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
if (mUrl?.isEmpty() != false) {
menu?.findItem(R.id.menu_goto_url)?.isVisible = false
}
if (mEntryIsHistory || mDatabaseReadOnly) {
menu?.findItem(R.id.menu_save_database)?.isVisible = false
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
@@ -471,12 +485,6 @@ class EntryActivity : DatabaseLockActivity() {
}
return true
}
R.id.menu_goto_url -> {
mUrl?.let { url ->
UriUtil.gotoUrl(this, url)
}
return true
}
R.id.menu_restore_entry_history -> {
mMainEntryId?.let { mainEntryId ->
restoreEntryHistory(
@@ -526,7 +534,7 @@ class EntryActivity : DatabaseLockActivity() {
* Open standard Entry activity
*/
fun launch(activity: Activity,
database: Database,
database: ContextualDatabase,
entryId: NodeId<UUID>,
activityResultLauncher: ActivityResultLauncher<Intent>) {
if (database.loaded) {
@@ -542,7 +550,7 @@ class EntryActivity : DatabaseLockActivity() {
* Open history Entry activity
*/
fun launch(activity: Activity,
database: Database,
database: ContextualDatabase,
entryId: NodeId<UUID>,
historyPosition: Int,
activityResultLauncher: ActivityResultLauncher<Intent>) {

View File

@@ -19,8 +19,6 @@
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.Context
import android.content.Intent
import android.net.Uri
@@ -32,7 +30,9 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.*
import android.widget.AdapterView
import android.widget.ProgressBar
import android.widget.Spinner
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
@@ -44,10 +44,16 @@ import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.timepicker.MaterialTimePicker
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.dialogs.ColorPickerDialogFragment
import com.kunzisoft.keepass.activities.dialogs.EntryCustomFieldDialogFragment
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
@@ -55,12 +61,21 @@ import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.template.*
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
@@ -72,24 +87,30 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.*
import com.kunzisoft.keepass.utils.TimeUtil.datePickerToDataDate
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.view.ToolbarAction
import com.kunzisoft.keepass.view.WindowInsetPosition
import com.kunzisoft.keepass.view.applyWindowInsets
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.setTransparentNavigationBar
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
import org.joda.time.DateTime
import java.util.*
import kotlin.collections.ArrayList
import java.util.UUID
class EntryEditActivity : DatabaseLockActivity(),
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
GeneratePasswordDialogFragment.GeneratePasswordListener,
SetOTPDialogFragment.CreateOtpListener,
DatePickerDialog.OnDateSetListener,
TimePickerDialog.OnTimeSetListener,
FileTooBigDialogFragment.ActionChooseListener,
ReplaceFileDialogFragment.ActionChooseListener {
// Views
private var footer: View? = null
private var container: View? = null
private var coordinatorLayout: CoordinatorLayout? = null
private var scrollView: NestedScrollView? = null
private var templateSelectorSpinner: Spinner? = null
@@ -119,6 +140,20 @@ class EntryEditActivity : DatabaseLockActivity(),
mEntryEditViewModel.selectIcon(icon)
}
private var mPasswordField: Field? = null
private var mKeyGeneratorResultLauncher = KeyGeneratorActivity.registerForGeneratedKeyResult(this) { keyGenerated ->
keyGenerated?.let {
mPasswordField?.let {
it.protectedValue.stringValue = keyGenerated
mEntryEditViewModel.selectPassword(it)
}
}
mPasswordField = null
Handler(Looper.getMainLooper()).post {
performedNextEducation()
}
}
// To ask data lost only one time
private var backPressedAlreadyApproved = false
@@ -128,10 +163,8 @@ class EntryEditActivity : DatabaseLockActivity(),
// Bottom Bar
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
setSupportActionBar(entryEditAddToolBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.setDisplayShowTitleEnabled(false)
footer = findViewById(R.id.activity_entry_edit_footer)
container = findViewById(R.id.activity_entry_edit_container)
coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout)
scrollView = findViewById(R.id.entry_edit_scroll)
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
@@ -140,19 +173,30 @@ class EntryEditActivity : DatabaseLockActivity(),
validateButton = findViewById(R.id.entry_edit_validate)
loadingView = findViewById(R.id.loading)
setSupportActionBar(entryEditAddToolBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.setDisplayShowTitleEnabled(false)
// To apply fit window with transparency
setTransparentNavigationBar(applyToStatusBar = true) {
container?.applyWindowInsets(WindowInsetPosition.TOP_BOTTOM_IME)
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM_IME)
}
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
// Entry is retrieve, it's an entry to update
var entryId: NodeId<UUID>? = null
intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let { entryToUpdate ->
intent.getParcelableExtraCompat<NodeId<UUID>>(KEY_ENTRY)?.let { entryToUpdate ->
intent.removeExtra(KEY_ENTRY)
entryId = entryToUpdate
}
// Parent is retrieve, it's a new entry to create
var parentId: NodeId<*>? = null
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let { parent ->
intent.getParcelableExtraCompat<NodeId<*>>(KEY_PARENT)?.let { parent ->
intent.removeExtra(KEY_PARENT)
parentId = parent
}
@@ -169,7 +213,7 @@ class EntryEditActivity : DatabaseLockActivity(),
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let { attachmentToUploadUri ->
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
attachmentToUploadUri.getDocumentFile(this)?.also { documentFile ->
documentFile.name?.let { fileName ->
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
@@ -187,7 +231,7 @@ class EntryEditActivity : DatabaseLockActivity(),
// Lock button
lockView?.setOnClickListener { lockAndExit() }
// Save button
validateButton?.setOnClickListener { saveEntry() }
validateButton?.setOnClickListener { validateEntry() }
mEntryEditViewModel.onTemplateChanged.observe(this) { template ->
this.mTemplate = template
@@ -205,7 +249,7 @@ class EntryEditActivity : DatabaseLockActivity(),
this@EntryEditActivity,
templates
).apply {
iconDrawableFactory = mIconDrawableFactory
iconDrawableFactory = mDatabase?.iconDrawableFactory
}
adapter = mTemplatesSelectorAdapter
val selectedTemplate = if (mTemplate != null)
@@ -256,21 +300,26 @@ class EntryEditActivity : DatabaseLockActivity(),
mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
if (dateInstant.type == DateInstant.Type.TIME) {
// Launch the time picker
val dateTime = DateTime(dateInstant.date)
TimePickerFragment.getInstance(dateTime.hourOfDay, dateTime.minuteOfHour)
.show(supportFragmentManager, "TimePickerFragment")
MaterialTimePicker.Builder().build().apply {
addOnPositiveButtonClickListener {
mEntryEditViewModel.selectTime(DataTime(this.hour, this.minute))
}
show(supportFragmentManager, "TimePickerFragment")
}
} else {
// Launch the date picker
val dateTime = DateTime(dateInstant.date)
DatePickerFragment.getInstance(dateTime.year, dateTime.monthOfYear - 1, dateTime.dayOfMonth)
.show(supportFragmentManager, "DatePickerFragment")
MaterialDatePicker.Builder.datePicker().build().apply {
addOnPositiveButtonClickListener {
mEntryEditViewModel.selectDate(datePickerToDataDate(it))
}
show(supportFragmentManager, "DatePickerFragment")
}
}
}
mEntryEditViewModel.requestPasswordSelection.observe(this) { passwordField ->
GeneratePasswordDialogFragment
.getInstance(passwordField)
.show(supportFragmentManager, "PasswordGeneratorFragment")
mPasswordField = passwordField
KeyGeneratorActivity.launch(this, mKeyGeneratorResultLauncher)
}
mEntryEditViewModel.requestCustomFieldEdition.observe(this) { field ->
@@ -353,19 +402,19 @@ class EntryEditActivity : DatabaseLockActivity(),
return true
}
override fun onDatabaseRetrieved(database: Database?) {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
mAllowCustomFields = database?.allowEntryCustomFields() == true
mAllowOTP = database?.allowOTP == true
mEntryEditViewModel.loadDatabase(database)
mTemplatesSelectorAdapter?.apply {
iconDrawableFactory = mIconDrawableFactory
iconDrawableFactory = mDatabase?.iconDrawableFactory
notifyDataSetChanged()
}
}
override fun onDatabaseActionFinished(
database: Database,
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
@@ -418,17 +467,18 @@ class EntryEditActivity : DatabaseLockActivity(),
finishForEntryResult(entry)
}
private fun entryValidatedForKeyboardSelection(database: Database, entry: Entry) {
private fun entryValidatedForKeyboardSelection(database: ContextualDatabase, entry: Entry) {
// Populate Magikeyboard with entry
populateKeyboardAndMoveAppToBackground(this,
entry.getEntryInfo(database),
intent)
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
this,
entry.getEntryInfo(database)
)
onValidateSpecialMode()
// Don't keep activity history for entry edition
finishForEntryResult(entry)
}
private fun entryValidatedForAutofillSelection(database: Database, entry: Entry) {
private fun entryValidatedForAutofillSelection(database: ContextualDatabase, entry: Entry) {
// Build Autofill response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity,
@@ -453,7 +503,7 @@ class EntryEditActivity : DatabaseLockActivity(),
}
// Padding if lock button visible
entryEditAddToolBar?.updateLockPaddingLeft()
entryEditAddToolBar?.updateLockPaddingStart()
mAttachmentFileBinderManager?.apply {
registerProgressTask()
@@ -463,6 +513,11 @@ class EntryEditActivity : DatabaseLockActivity(),
}
}
}
// Keep the screen on
if (PreferencesUtil.isKeepScreenOnEnabled(this)) {
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
override fun onPause() {
@@ -525,9 +580,9 @@ class EntryEditActivity : DatabaseLockActivity(),
}
/**
* Saves the new entry or update an existing entry in the database
* Validate the new entry or update an existing entry in the database
*/
private fun saveEntry() {
private fun validateEntry() {
mAttachmentFileBinderManager?.stopUploadAllAttachments()
mEntryEditViewModel.requestEntryInfoUpdate(mDatabase)
}
@@ -549,16 +604,12 @@ class EntryEditActivity : DatabaseLockActivity(),
isVisible = isEnabled
}
menu?.findItem(R.id.menu_add_attachment)?.apply {
// Attachment not compatible below KitKat
isEnabled = !mIsTemplate
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
isVisible = isEnabled
}
menu?.findItem(R.id.menu_add_otp)?.apply {
// OTP not compatible below KitKat
isEnabled = mAllowOTP
&& !mIsTemplate
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
isVisible = isEnabled
}
return super.onPrepareOptionsMenu(menu)
@@ -609,14 +660,29 @@ class EntryEditActivity : DatabaseLockActivity(),
)
if (!addAttachmentEducationPerformed) {
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
setupOtpView != null
val validateEntryEducationPerformed = setupOtpView != null
&& setupOtpView.isVisible
&& mEntryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
setupOtpView,
{
setupOtp()
},
{
performedNextEducation()
}
)
if (!validateEntryEducationPerformed) {
val entryValidateView = validateButton
mAllowCustomFields
&& entryValidateView != null
&& entryValidateView.isVisible
&& mEntryEditActivityEducation.checkAndPerformedValidateEntryEducation(
entryValidateView,
{
validateEntry()
}
)
}
}
}
}
@@ -637,39 +703,16 @@ class EntryEditActivity : DatabaseLockActivity(),
return true
}
android.R.id.home -> {
onBackPressed()
onDatabaseBackPressed()
}
}
return super.onOptionsItemSelected(item)
}
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
// To fix android 4.4 issue
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
if (datePicker?.isShown == true) {
mEntryEditViewModel.selectDate(year, month, day)
}
}
override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) {
mEntryEditViewModel.selectTime(hours, minutes)
}
override fun acceptPassword(passwordField: Field) {
mEntryEditViewModel.selectPassword(passwordField)
Handler(Looper.getMainLooper()).post {
performedNextEducation()
}
}
override fun cancelPassword(passwordField: Field) {
// Do nothing here
}
override fun onBackPressed() {
override fun onDatabaseBackPressed() {
onApprovedBackPressed {
super@EntryEditActivity.onBackPressed()
super@EntryEditActivity.onDatabaseBackPressed()
}
}
@@ -724,7 +767,7 @@ class EntryEditActivity : DatabaseLockActivity(),
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
entryAddedOrUpdatedListener.invoke(
result.data?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY)
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
)
} else {
entryAddedOrUpdatedListener.invoke(null)
@@ -737,7 +780,7 @@ class EntryEditActivity : DatabaseLockActivity(),
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
entryAddedOrUpdatedListener.invoke(
result.data?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY)
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
)
} else {
entryAddedOrUpdatedListener.invoke(null)
@@ -749,7 +792,7 @@ class EntryEditActivity : DatabaseLockActivity(),
* Launch EntryEditActivity to update an existing entry by his [entryId]
*/
fun launchToUpdate(activity: Activity,
database: Database,
database: ContextualDatabase,
entryId: NodeId<UUID>,
activityResultLauncher: ActivityResultLauncher<Intent>) {
if (database.loaded && !database.isReadOnly) {
@@ -765,7 +808,7 @@ class EntryEditActivity : DatabaseLockActivity(),
* Launch EntryEditActivity to add a new entry in an existent group
*/
fun launchToCreate(activity: Activity,
database: Database,
database: ContextualDatabase,
groupId: NodeId<*>,
activityResultLauncher: ActivityResultLauncher<Intent>) {
if (database.loaded && !database.isReadOnly) {
@@ -778,7 +821,7 @@ class EntryEditActivity : DatabaseLockActivity(),
}
fun launchToUpdateForSave(context: Context,
database: Database,
database: ContextualDatabase,
entryId: NodeId<UUID>,
searchInfo: SearchInfo) {
if (database.loaded && !database.isReadOnly) {
@@ -795,7 +838,7 @@ class EntryEditActivity : DatabaseLockActivity(),
}
fun launchToCreateForSave(context: Context,
database: Database,
database: ContextualDatabase,
groupId: NodeId<*>,
searchInfo: SearchInfo) {
if (database.loaded && !database.isReadOnly) {
@@ -815,7 +858,7 @@ class EntryEditActivity : DatabaseLockActivity(),
* Launch EntryEditActivity to add a new entry in keyboard selection
*/
fun launchForKeyboardSelectionResult(context: Context,
database: Database,
database: ContextualDatabase,
groupId: NodeId<*>,
searchInfo: SearchInfo? = null) {
if (database.loaded && !database.isReadOnly) {
@@ -836,7 +879,7 @@ class EntryEditActivity : DatabaseLockActivity(),
*/
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: AppCompatActivity,
database: Database,
database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
groupId: NodeId<*>,
@@ -860,7 +903,7 @@ class EntryEditActivity : DatabaseLockActivity(),
* Launch EntryEditActivity to register an updated entry (from autofill)
*/
fun launchToUpdateForRegistration(context: Context,
database: Database,
database: ContextualDatabase,
entryId: NodeId<UUID>,
registerInfo: RegisterInfo? = null) {
if (database.loaded && !database.isReadOnly) {
@@ -880,7 +923,7 @@ class EntryEditActivity : DatabaseLockActivity(),
* Launch EntryEditActivity to register a new entry (from autofill)
*/
fun launchToCreateForRegistration(context: Context,
database: Database,
database: ContextualDatabase,
groupId: NodeId<*>,
registerInfo: RegisterInfo? = null) {
if (database.loaded && !database.isReadOnly) {

View File

@@ -19,20 +19,21 @@
*/
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.WebDomain
/**
* Activity to search or select entry in database,
@@ -45,149 +46,181 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
}
override fun finishActivityIfReloadRequested(): Boolean {
return true
return false
}
override fun onDatabaseRetrieved(database: Database?) {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
var sharedWebDomain: String? = null
var otpString: String? = null
when (intent?.action) {
Intent.ACTION_SEND -> {
if ("text/plain" == intent.type) {
// Retrieve web domain or OTP
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
if (keySelectionBundle != null) {
// To manage package name
var searchInfo = SearchInfo()
keySelectionBundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { mSearchInfo ->
searchInfo = mSearchInfo
}
launch(database, searchInfo)
} else {
// To manage share
var sharedWebDomain: String? = null
var otpString: String? = null
when (intent?.action) {
Intent.ACTION_SEND -> {
if ("text/plain" == intent.type) {
// Retrieve web domain or OTP
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
else
sharedWebDomain = Uri.parse(extra).host
}
}
launchSelection(database, sharedWebDomain, otpString)
}
Intent.ACTION_VIEW -> {
// Retrieve OTP
intent.dataString?.let { extra ->
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
else
sharedWebDomain = Uri.parse(extra).host
}
launchSelection(database, sharedWebDomain, otpString)
}
else -> {
if (database != null) {
GroupActivity.launch(this, database)
} else {
FileDatabaseSelectActivity.launch(this)
}
}
}
Intent.ACTION_VIEW -> {
// Retrieve OTP
intent.dataString?.let { extra ->
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
}
}
else -> {}
}
finish()
}
private fun launchSelection(database: ContextualDatabase?,
sharedWebDomain: String?,
otpString: String?) {
// Build domain search param
val searchInfo = SearchInfo().apply {
this.webDomain = sharedWebDomain
this.otpString = otpString
}
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launch(database, searchInfo)
}
}
private fun launch(database: Database?,
private fun launch(database: ContextualDatabase?,
searchInfo: SearchInfo) {
if (!searchInfo.containsOnlyNullValues()) {
// Setting to integrate Magikeyboard
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
// Setting to integrate Magikeyboard
val searchShareForMagikeyboard = isKeyboardActivatedInSettings()
// If database is open
val readOnly = database?.isReadOnly != false
SearchHelper.checkAutoSearchInfo(this,
database,
searchInfo,
{ openedDatabase, items ->
// Items found
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(
this,
openedDatabase,
searchInfo,
false)
} else {
Toast.makeText(applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
}
} else if (searchShareForMagikeyboard) {
if (items.size == 1) {
// Automatically populate keyboard
val entryPopulate = items[0]
populateKeyboardAndMoveAppToBackground(
this,
entryPopulate,
intent)
} else {
// Select the one we want
GroupActivity.launchForKeyboardSelectionResult(this,
openedDatabase,
searchInfo,
true)
}
} else {
GroupActivity.launchForSearchResult(this,
openedDatabase,
searchInfo,
true)
}
},
{ openedDatabase ->
// Show the database UI to select the entry
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(this,
openedDatabase,
searchInfo,
false)
} else {
Toast.makeText(applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
}
} else if (readOnly || searchShareForMagikeyboard) {
GroupActivity.launchForKeyboardSelectionResult(this,
// If database is open
val readOnly = database?.isReadOnly != false
SearchHelper.checkAutoSearchInfo(this,
database,
searchInfo,
{ openedDatabase, items ->
// Items found
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(
this,
openedDatabase,
searchInfo,
false)
} else {
Toast.makeText(applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
}
} else if (searchShareForMagikeyboard) {
MagikeyboardService.performSelection(
items,
{ entryInfo ->
// Automatically populate keyboard
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
this,
entryInfo
)
},
{ autoSearch ->
GroupActivity.launchForKeyboardSelectionResult(this,
openedDatabase,
searchInfo,
autoSearch)
}
)
} else {
GroupActivity.launchForSearchResult(this,
openedDatabase,
searchInfo,
true)
}
},
{ openedDatabase ->
// Show the database UI to select the entry
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(this,
openedDatabase,
searchInfo,
false)
}
},
{
// If database not open
if (searchInfo.otpString != null) {
FileDatabaseSelectActivity.launchForSaveResult(this,
searchInfo)
} else if (searchShareForMagikeyboard) {
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
searchInfo)
} else {
FileDatabaseSelectActivity.launchForSearchResult(this,
searchInfo)
Toast.makeText(applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
}
} else if (searchShareForMagikeyboard) {
GroupActivity.launchForKeyboardSelectionResult(this,
openedDatabase,
searchInfo,
false)
} else {
GroupActivity.launchForSearchResult(this,
openedDatabase,
searchInfo,
false)
}
)
},
{
// If database not open
if (searchInfo.otpString != null) {
FileDatabaseSelectActivity.launchForSaveResult(this,
searchInfo)
} else if (searchShareForMagikeyboard) {
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
searchInfo)
} else {
FileDatabaseSelectActivity.launchForSearchResult(this,
searchInfo)
}
}
)
}
companion object {
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
fun launch(context: Context,
searchInfo: SearchInfo? = null) {
val intent = Intent(context, EntrySelectionLauncherActivity::class.java).apply {
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
putParcelable(KEY_SEARCH_INFO, searchInfo)
})
}
// New task needed because don't launch from an Activity context
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
finish()
}
}
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
entry: EntryInfo,
intent: Intent,
toast: Boolean = true) {
// Populate Magikeyboard with entry
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
// Consume the selection mode
EntrySelectionHelper.removeModesFromIntent(intent)
activity.moveTaskToBack(true)
}

View File

@@ -31,7 +31,6 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
@@ -54,9 +53,10 @@ import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
@@ -65,8 +65,14 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.utils.DexUtil
import com.kunzisoft.keepass.utils.MagikeyboardUtil
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
import com.kunzisoft.keepass.utils.UriUtil.openUrl
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
import java.io.FileNotFoundException
@@ -156,8 +162,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
launchPasswordActivity(
databaseFileUri,
fileDatabaseHistoryEntityToOpen.keyFileUri
databaseFileUri,
fileDatabaseHistoryEntityToOpen.keyFileUri,
fileDatabaseHistoryEntityToOpen.hardwareKey
)
}
}
@@ -171,23 +178,15 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
// Load default database if not an orientation change
if (!(savedInstanceState != null
&& savedInstanceState.containsKey(EXTRA_STAY)
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
val databasePath = PreferencesUtil.getDefaultDatabasePath(this)
UriUtil.parse(databasePath)?.let { databaseFileUri ->
launchPasswordActivityWithPath(databaseFileUri)
} ?: run {
Log.i(TAG, "No default database to prepare")
}
// Load default database the first time
databaseFilesViewModel.doForDefaultDatabase { databaseFileUri ->
launchPasswordActivityWithPath(databaseFileUri)
}
// Retrieve the database URI provided by file manager after an orientation change
if (savedInstanceState != null
&& savedInstanceState.containsKey(EXTRA_DATABASE_URI)) {
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
mDatabaseFileUri = savedInstanceState.getParcelableCompat(EXTRA_DATABASE_URI)
}
// Observe list of databases
@@ -226,7 +225,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
}
override fun onDatabaseRetrieved(database: Database?) {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
if (database != null) {
launchGroupActivityIfLoaded(database)
@@ -234,7 +233,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
override fun onDatabaseActionFinished(
database: Database,
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
@@ -245,13 +244,14 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
when (actionTask) {
ACTION_DATABASE_CREATE_TASK,
ACTION_DATABASE_LOAD_TASK -> {
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
result.data?.getParcelableCompat<Uri>(DATABASE_URI_KEY)?.let { databaseUri ->
val mainCredential =
result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY)
result.data?.getParcelableCompat(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY)
?: MainCredential()
databaseFilesViewModel.addDatabaseFile(
databaseUri,
mainCredential.keyFileUri
mainCredential.keyFileUri,
mainCredential.hardwareKey
)
}
}
@@ -262,25 +262,15 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
GroupActivity.launch(
this@FileDatabaseSelectActivity,
database,
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity)
false
)
}
ACTION_DATABASE_LOAD_TASK -> {
launchGroupActivityIfLoaded(database)
}
}
} else {
var resultError = ""
val resultMessage = result.message
// Show error message
if (resultMessage != null && resultMessage.isNotEmpty()) {
resultError = "$resultError $resultMessage"
}
Log.e(TAG, resultError)
Snackbar.make(coordinatorLayout,
resultError,
Snackbar.LENGTH_LONG).asError().show()
}
coordinatorLayout.showActionErrorIfNeeded(result)
}
/**
@@ -298,10 +288,11 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
}
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
MainCredentialActivity.launch(this,
databaseUri,
keyFile,
hardwareKey,
{ exception ->
fileNoFoundAction(exception)
},
@@ -310,7 +301,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
mAutofillActivityResultLauncher)
}
private fun launchGroupActivityIfLoaded(database: Database) {
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
if (database.loaded) {
GroupActivity.launch(this,
database,
@@ -321,19 +312,10 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
}
override fun onValidateSpecialMode() {
super.onValidateSpecialMode()
finish()
}
override fun onCancelSpecialMode() {
super.onCancelSpecialMode()
finish()
}
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
launchPasswordActivity(databaseUri, null)
launchPasswordActivity(databaseUri, null, null)
// Delete flickering for kitkat <=
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
overridePendingTransition(0, 0)
}
@@ -342,18 +324,12 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
super.onResume()
// Define special title
specialTitle?.isVisible = UriUtil.contributingUser(this)
specialTitle?.isVisible = this.isContributingUser()
// Show open and create button or special mode
when (mSpecialMode) {
SpecialMode.DEFAULT -> {
if (ExternalFileHelper.allowCreateDocumentByStorageAccessFramework(packageManager)) {
// There is an activity which can handle this intent.
createDatabaseButtonView?.visibility = View.VISIBLE
} else{
// No Activity found that can handle this intent.
createDatabaseButtonView?.visibility = View.GONE
}
createDatabaseButtonView?.visibility = View.VISIBLE
}
else -> {
// Disable create button if in selection mode or request for autofill
@@ -375,8 +351,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// only to keep the current activity
outState.putBoolean(EXTRA_STAY, true)
// to retrieve the URI of a created database after an orientation change
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
}
@@ -442,7 +416,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
android.R.id.home -> this.openUrl(R.string.file_manager_explanation_url)
}
MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
return super.onOptionsItemSelected(item)
@@ -451,7 +425,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
companion object {
private const val TAG = "FileDbSelectActivity"
private const val EXTRA_STAY = "EXTRA_STAY"
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
/*

View File

@@ -41,16 +41,24 @@ import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.UriUtil.openUrl
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.updateLockPaddingLeft
import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class IconPickerActivity : DatabaseLockActivity() {
@@ -96,7 +104,7 @@ class IconPickerActivity : DatabaseLockActivity() {
lockAndExit()
}
intent?.getParcelableExtra<IconImage>(EXTRA_ICON)?.let {
intent?.getParcelableExtraCompat<IconImage>(EXTRA_ICON)?.let {
mIconImage = it
}
@@ -112,7 +120,7 @@ class IconPickerActivity : DatabaseLockActivity() {
), ICON_PICKER_FRAGMENT_TAG)
}
} else {
mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage
mIconImage = savedInstanceState.getParcelableCompat(EXTRA_ICON) ?: mIconImage
}
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
@@ -166,7 +174,7 @@ class IconPickerActivity : DatabaseLockActivity() {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
if (database?.allowCustomIcons == true) {
@@ -204,7 +212,7 @@ class IconPickerActivity : DatabaseLockActivity() {
}
// Padding if lock button visible
toolbar.updateLockPaddingLeft()
toolbar.updateLockPaddingStart()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
@@ -231,7 +239,7 @@ class IconPickerActivity : DatabaseLockActivity() {
if (mCustomIconsSelectionMode) {
iconPickerViewModel.deselectAllCustomIcons()
} else {
onBackPressed()
onDatabaseBackPressed()
}
}
R.id.menu_edit -> {
@@ -243,7 +251,7 @@ class IconPickerActivity : DatabaseLockActivity() {
}
}
R.id.menu_external_icon -> {
UriUtil.gotoUrl(this, R.string.external_icon_url)
this.openUrl(R.string.external_icon_url)
}
}
@@ -257,7 +265,7 @@ class IconPickerActivity : DatabaseLockActivity() {
// on Progress with thread
val asyncResult: Deferred<IconPickerViewModel.IconCustomState?> = async {
val iconCustomState = IconPickerViewModel.IconCustomState(null, true, R.string.error_upload_file)
UriUtil.getFileData(this@IconPickerActivity, iconToUploadUri)?.also { documentFile ->
iconToUploadUri?.getDocumentFile(this@IconPickerActivity)?.also { documentFile ->
if (documentFile.length() > MAX_ICON_SIZE) {
iconCustomState.errorStringId = R.string.error_file_to_big
} else {
@@ -321,9 +329,9 @@ class IconPickerActivity : DatabaseLockActivity() {
})
}
override fun onBackPressed() {
override fun onDatabaseBackPressed() {
setResult()
super.onBackPressed()
super.onDatabaseBackPressed()
}
companion object {
@@ -336,7 +344,7 @@ class IconPickerActivity : DatabaseLockActivity() {
listener: (icon: IconImage) -> Unit): ActivityResultLauncher<Intent> {
return context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
listener.invoke(result.data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
listener.invoke(result.data?.getParcelableExtraCompat(EXTRA_ICON) ?: IconImage())
}
}
}

View File

@@ -33,9 +33,10 @@ import androidx.appcompat.widget.Toolbar
import com.igreenwood.loupe.Loupe
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import kotlin.math.max
class ImageViewerActivity : DatabaseLockActivity() {
@@ -100,12 +101,12 @@ class ImageViewerActivity : DatabaseLockActivity() {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
try {
progressView.visibility = View.VISIBLE
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
intent.getParcelableExtraCompat<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
supportActionBar?.title = attachment.name

View File

@@ -0,0 +1,139 @@
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.commit
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.fragments.KeyGeneratorFragment
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
class KeyGeneratorActivity : DatabaseLockActivity() {
private lateinit var toolbar: Toolbar
private lateinit var coordinatorLayout: CoordinatorLayout
private lateinit var validationButton: View
private var lockView: View? = null
private val keyGeneratorViewModel: KeyGeneratorViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_key_generator)
toolbar = findViewById(R.id.toolbar)
toolbar.title = " "
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
coordinatorLayout = findViewById(R.id.key_generator_coordinator)
lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener {
lockAndExit()
}
validationButton = findViewById(R.id.key_generator_validation)
validationButton.setOnClickListener {
keyGeneratorViewModel.validateKeyGenerated()
}
supportFragmentManager.commit {
replace(R.id.key_generator_fragment, KeyGeneratorFragment.getInstance(
// Default selection tab
KeyGeneratorFragment.KeyGeneratorTab.PASSWORD
), KEY_GENERATED_FRAGMENT_TAG
)
}
keyGeneratorViewModel.keyGenerated.observe(this) { keyGenerated ->
setResult(Activity.RESULT_OK, Intent().apply {
putExtra(KEY_GENERATED, keyGenerated)
})
finish()
}
}
override fun viewToInvalidateTimeout(): View? {
return findViewById<ViewGroup>(R.id.key_generator_container)
}
override fun onResume() {
super.onResume()
// Show the lock button
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
View.VISIBLE
} else {
View.GONE
}
// Padding if lock button visible
toolbar.updateLockPaddingStart()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.key_generator, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onDatabaseBackPressed()
}
R.id.menu_generate -> {
keyGeneratorViewModel.requireKeyGeneration()
}
}
return super.onOptionsItemSelected(item)
}
override fun onDatabaseBackPressed() {
setResult(Activity.RESULT_CANCELED, Intent())
super.onDatabaseBackPressed()
}
companion object {
private const val KEY_GENERATED = "KEY_GENERATED"
private const val KEY_GENERATED_FRAGMENT_TAG = "KEY_GENERATED_FRAGMENT_TAG"
fun registerForGeneratedKeyResult(activity: FragmentActivity,
keyGeneratedListener: (String?) -> Unit): ActivityResultLauncher<Intent> {
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
keyGeneratedListener.invoke(
result.data?.getStringExtra(KEY_GENERATED)
)
} else {
keyGeneratedListener.invoke(null)
}
}
}
fun launch(context: FragmentActivity,
resultLauncher: ActivityResultLauncher<Intent>) {
// Create an instance to return the picker icon
resultLauncher.launch(
Intent(context, KeyGeneratorActivity::class.java)
)
}
}
}

View File

@@ -1,59 +0,0 @@
/*
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper
/**
* Activity to select entry in database and populate it in Magikeyboard
*/
class MagikeyboardLauncherActivity : DatabaseModeActivity() {
override fun applyCustomStyle(): Boolean {
return false
}
override fun finishActivityIfReloadRequested(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
SearchHelper.checkAutoSearchInfo(this,
database,
null,
{ _, _ ->
// Not called
// if items found directly returns before calling this activity
},
{ openedDatabase ->
// Select if not found
GroupActivity.launchForKeyboardSelectionResult(this, openedDatabase)
},
{
// Pass extra to get entry
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
}
)
finish()
}
}

View File

@@ -1,6 +1,6 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
@@ -32,7 +32,6 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.CompoundButton
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
@@ -43,55 +42,77 @@ import androidx.appcompat.widget.Toolbar
import androidx.biometric.BiometricManager
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.commit
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
import com.kunzisoft.keepass.biometric.deviceUnlockError
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.CipherDecryptDatabase
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.CredentialStorage
import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
import com.kunzisoft.keepass.settings.AppearanceSettingsActivity
import com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.UriUtil.getUri
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.view.MainCredentialView
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel
import kotlinx.coroutines.launch
import java.io.FileNotFoundException
class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
class MainCredentialActivity : DatabaseModeActivity() {
// Views
private var toolbar: Toolbar? = null
private var filenameView: TextView? = null
private var advancedUnlockButton: View? = null
private var logotypeButton: View? = null
private var deviceUnlockButton: View? = null
private var mainCredentialView: MainCredentialView? = null
private var confirmButtonView: Button? = null
private var infoContainerView: ViewGroup? = null
private lateinit var coordinatorLayout: CoordinatorLayout
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
private var deviceUnlockFragment: DeviceUnlockFragment? = null
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels()
private val mDeviceUnlockViewModel: DeviceUnlockViewModel? by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ViewModelProvider(this)[DeviceUnlockViewModel::class.java]
} else null
}
private val mPasswordActivityEducation = PasswordActivityEducation(this)
@@ -101,6 +122,8 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
private var mRememberKeyFile: Boolean = false
private var mExternalFileHelper: ExternalFileHelper? = null
private var mRememberHardwareKey: Boolean = false
private var mReadOnly: Boolean = false
private var mForceReadOnly: Boolean = false
@@ -121,7 +144,8 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
supportActionBar?.setDisplayShowHomeEnabled(true)
filenameView = findViewById(R.id.filename)
advancedUnlockButton = findViewById(R.id.activity_password_advanced_unlock_button)
logotypeButton = findViewById(R.id.activity_password_logotype)
deviceUnlockButton = findViewById(R.id.fragment_device_unlock_container_view)
mainCredentialView = findViewById(R.id.activity_password_credentials)
confirmButtonView = findViewById(R.id.activity_password_open_button)
infoContainerView = findViewById(R.id.activity_password_info_container)
@@ -130,14 +154,16 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
savedInstanceState.getBoolean(KEY_READ_ONLY)
} else {
PreferencesUtil.enableReadOnlyDatabase(this)
false
}
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this)
mExternalFileHelper = ExternalFileHelper(this@MainCredentialActivity)
// Build elements to manage keyfile selection
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) {
mainCredentialView?.populateKeyFileTextView(uri)
mainCredentialView?.populateKeyFileView(uri)
}
}
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
@@ -148,29 +174,21 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
// If is a view intent
getUriFromIntent(intent)
// Init Biometric elements
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
advancedUnlockButton?.setOnClickListener {
startActivity(Intent(this, SettingsAdvancedUnlockActivity::class.java))
}
}
advancedUnlockFragment = supportFragmentManager
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
if (advancedUnlockFragment == null) {
advancedUnlockFragment = AdvancedUnlockFragment()
supportFragmentManager.commit {
replace(R.id.fragment_advanced_unlock_container_view,
advancedUnlockFragment!!,
UNLOCK_FRAGMENT_TAG)
}
// Show appearance
logotypeButton?.setOnClickListener {
startActivity(Intent(this, AppearanceSettingsActivity::class.java))
}
// Listen password checkbox to init advanced unlock and confirmation button
mainCredentialView?.onPasswordChecked =
CompoundButton.OnCheckedChangeListener { _, _ ->
mAdvancedUnlockViewModel.checkUnlockAvailability()
enableConfirmationButton()
mainCredentialView?.onConditionToStoreCredentialChanged = { _, verified ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mDeviceUnlockViewModel?.checkConditionToStoreCredential(
condition = verified
)
}
// TODO Async by ViewModel
enableConfirmationButton()
}
// Observe if default database
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
@@ -192,6 +210,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
}
mForceReadOnly = databaseFileNotExists
// Restore read-only state from database file if not forced
if (!mForceReadOnly) {
databaseFile?.readOnly?.let { savedReadOnlyState ->
mReadOnly = savedReadOnlyState
}
}
invalidateOptionsMenu()
// Post init uri with KeyFile only if needed
@@ -204,28 +229,85 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
databaseKeyFileUri
}
val databaseHardwareKey = mainCredentialView?.getMainCredential()?.hardwareKey
val hardwareKey =
if (mRememberHardwareKey
&& databaseHardwareKey == null) {
databaseFile?.hardwareKey
} else {
databaseHardwareKey
}
// Define title
filenameView?.text = databaseFile?.databaseAlias ?: ""
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri, hardwareKey)
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mDeviceUnlockViewModel?.let { deviceUnlockViewModel ->
deviceUnlockViewModel.uiState.collect { uiState ->
// New value received
uiState.credentialRequiredCipher?.let { cipher ->
deviceUnlockViewModel.encryptCredential(
credential = getCredentialForEncryption(),
cipher = cipher
)
}
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
onCredentialEncrypted(cipherEncryptDatabase)
deviceUnlockViewModel.consumeCredentialEncrypted()
}
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
onCredentialDecrypted(cipherDecryptDatabase)
deviceUnlockViewModel.consumeCredentialDecrypted()
}
uiState.exception?.let { error ->
Snackbar.make(
coordinatorLayout,
deviceUnlockError(error, this@MainCredentialActivity),
Snackbar.LENGTH_LONG
).asError().show()
deviceUnlockViewModel.exceptionShown()
}
}
}
}
}
}
}
override fun onResume() {
super.onResume()
// Init Biometric elements only if allowed
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& PreferencesUtil.isDeviceUnlockEnable(this)) {
deviceUnlockFragment = supportFragmentManager
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? DeviceUnlockFragment?
if (deviceUnlockFragment == null) {
deviceUnlockFragment = DeviceUnlockFragment().also {
supportFragmentManager.commit {
replace(
R.id.fragment_device_unlock_container_view,
it,
UNLOCK_FRAGMENT_TAG
)
}
}
}
}
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this@MainCredentialActivity)
// Back to previous keyboard is setting activated
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) {
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
}
// Don't allow auto open prompt if lock become when UI visible
if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) {
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
}
mDatabaseFileUri?.let { databaseFileUri ->
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
}
@@ -235,7 +317,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
}
}
override fun onDatabaseRetrieved(database: Database?) {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
if (database != null) {
// Trying to load another database
@@ -252,113 +334,104 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
}
override fun onDatabaseActionFinished(
database: Database,
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> {
// Recheck advanced unlock if error
mAdvancedUnlockViewModel.initAdvancedUnlockMode()
if (result.isSuccess) {
launchGroupActivityIfLoaded(database)
} else {
mainCredentialView?.requestPasswordFocus()
// Manage special exceptions
when (result.exception) {
is DuplicateUuidDatabaseException -> {
// Relaunch loading if we need to fix UUID
showLoadDatabaseDuplicateUuidMessage {
var resultError = ""
val resultException = result.exception
val resultMessage = result.message
var databaseUri: Uri? = null
var mainCredential = MainCredential()
var readOnly = true
var cipherEncryptDatabase: CipherEncryptDatabase? = null
if (resultException != null) {
resultError = resultException.getLocalizedMessage(resources)
when (resultException) {
is DuplicateUuidDatabaseException -> {
// Relaunch loading if we need to fix UUID
showLoadDatabaseDuplicateUuidMessage {
var databaseUri: Uri? = null
var mainCredential = MainCredential()
var readOnly = true
var cipherEncryptDatabase: CipherEncryptDatabase? = null
result.data?.let { resultData ->
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
mainCredential =
resultData.getParcelable(MAIN_CREDENTIAL_KEY)
?: mainCredential
readOnly = resultData.getBoolean(READ_ONLY_KEY)
cipherEncryptDatabase =
resultData.getParcelable(CIPHER_DATABASE_KEY)
}
databaseUri?.let { databaseFileUri ->
showProgressDialogAndLoadDatabase(
databaseFileUri,
mainCredential,
readOnly,
cipherEncryptDatabase,
true
)
}
result.data?.let { resultData ->
databaseUri = resultData.getParcelableCompat(DATABASE_URI_KEY)
mainCredential =
resultData.getParcelableCompat(MAIN_CREDENTIAL_KEY)
?: mainCredential
readOnly = resultData.getBoolean(READ_ONLY_KEY)
cipherEncryptDatabase =
resultData.getParcelableCompat(CIPHER_DATABASE_KEY)
}
}
is FileNotFoundDatabaseException -> {
// Remove this default database inaccessible
if (mDefaultDatabase) {
mDatabaseFileViewModel.removeDefaultDatabase()
databaseUri?.let { databaseFileUri ->
showProgressDialogAndLoadDatabase(
databaseFileUri,
mainCredential,
readOnly,
cipherEncryptDatabase,
true
)
}
}
}
is FileNotFoundDatabaseException -> {
// Remove this default database inaccessible
if (mDefaultDatabase) {
mDatabaseFileViewModel.removeDefaultDatabase()
}
}
}
// Show error message
if (resultMessage != null && resultMessage.isNotEmpty()) {
resultError = "$resultError $resultMessage"
}
Log.e(TAG, resultError)
Snackbar.make(
coordinatorLayout,
resultError,
Snackbar.LENGTH_LONG
).asError().show()
}
}
}
coordinatorLayout.showActionErrorIfNeeded(result)
}
private fun getUriFromIntent(intent: Intent?) {
// If is a view intent
val action = intent?.action
if (action != null
&& action == VIEW_INTENT) {
mDatabaseFileUri = intent.data
mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE))
if (action == VIEW_INTENT) {
fillCredentials(
intent.data,
intent.getUri(KEY_KEYFILE),
HardwareKey.getHardwareKeyFromString(intent.getStringExtra(KEY_HARDWARE_KEY))
)
} else {
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
intent?.getParcelableExtra<Uri?>(KEY_KEYFILE)?.let {
mainCredentialView?.populateKeyFileTextView(it)
}
fillCredentials(
intent?.getParcelableExtraCompat(KEY_FILENAME),
intent?.getParcelableExtraCompat(KEY_KEYFILE),
HardwareKey.getHardwareKeyFromString(intent?.getStringExtra(KEY_HARDWARE_KEY))
)
}
try {
intent?.removeExtra(KEY_KEYFILE)
} catch (e: Exception) {}
intent?.removeExtra(KEY_HARDWARE_KEY)
} catch (_: Exception) {}
mDatabaseFileUri?.let {
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
}
}
private fun fillCredentials(databaseUri: Uri?,
keyFileUri: Uri?,
hardwareKey: HardwareKey?) {
mDatabaseFileUri = databaseUri
mainCredentialView?.populateKeyFileView(keyFileUri)
mainCredentialView?.populateHardwareKeyView(hardwareKey)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
getUriFromIntent(intent)
}
private fun launchGroupActivityIfLoaded(database: Database) {
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
// Check if database really loaded
if (database.loaded) {
clearCredentialsViews(true)
clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true)
GroupActivity.launch(this,
database,
{ onValidateSpecialMode() },
@@ -369,33 +442,6 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
}
}
override fun onValidateSpecialMode() {
super.onValidateSpecialMode()
finish()
}
override fun onCancelSpecialMode() {
super.onCancelSpecialMode()
finish()
}
override fun retrieveCredentialForEncryption(): ByteArray {
return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
?: byteArrayOf()
}
override fun conditionToStoreCredential(): Boolean {
return mainCredentialView?.conditionToStoreCredential() == true
}
override fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
// Load the database if password is registered with biometric
loadDatabase(mDatabaseFileUri,
mainCredentialView?.getMainCredential(),
cipherEncryptDatabase
)
}
private val credentialStorageListener = object: MainCredentialView.CredentialStorageListener {
override fun passwordToStore(password: String?): ByteArray? {
return password?.toByteArray()
@@ -412,13 +458,26 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
}
}
override fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
private fun getCredentialForEncryption(): ByteArray {
return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
?: byteArrayOf()
}
private fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
// Load the database if password is registered with biometric
loadDatabase(mDatabaseFileUri,
mainCredentialView?.getMainCredential(),
cipherEncryptDatabase
)
}
private fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
// Load the database if password is retrieve from biometric
// Retrieve from biometric
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
when (cipherDecryptDatabase.credentialStorage) {
CredentialStorage.PASSWORD -> {
mainCredential.masterPassword = String(cipherDecryptDatabase.decryptedValue)
mainCredential.password = String(cipherDecryptDatabase.decryptedValue)
}
CredentialStorage.KEY_FILE -> {
// TODO advanced unlock key file
@@ -433,14 +492,23 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
)
}
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
private fun onDatabaseFileLoaded(databaseFileUri: Uri?,
keyFileUri: Uri?,
hardwareKey: HardwareKey?) {
// Define Key File text
if (mRememberKeyFile) {
mainCredentialView?.populateKeyFileTextView(keyFileUri)
mainCredentialView?.populateKeyFileView(keyFileUri)
}
// Define hardware key
if (mRememberHardwareKey) {
mainCredentialView?.populateHardwareKeyView(hardwareKey)
}
// Define listener for validate button
confirmButtonView?.setOnClickListener { loadDatabase() }
confirmButtonView?.setOnClickListener {
mainCredentialView?.validateCredential()
}
// If Activity is launch with a password and want to open directly
val intent = intent
@@ -455,7 +523,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
loadDatabase()
} else {
// Init Biometric elements
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mDeviceUnlockViewModel?.connect(databaseFileUri)
}
}
enableConfirmationButton()
@@ -472,18 +542,15 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
}
}
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile,
clearHardwareKey: Boolean = !mRememberHardwareKey) {
mainCredentialView?.populatePasswordTextView(null)
if (clearKeyFile) {
mainCredentialView?.populateKeyFileTextView(null)
mainCredentialView?.populateKeyFileView(null)
}
if (clearHardwareKey) {
mainCredentialView?.populateHardwareKeyView(null)
}
}
override fun onPause() {
// Reinit locking activity UI variable
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
super.onPause()
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -601,7 +668,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
try {
menu.findItem(R.id.menu_open_file_read_mode_key)
} catch (e: Exception) {
Log.e(TAG, "Unable to find read mode menu")
Log.e(TAG, "Unable to find read mode menu", e)
}
performedNextEducation(menu)
},
@@ -611,21 +678,26 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& !readOnlyEducationPerformed) {
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(this)
val biometricPerformed =
(biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
val biometricCanAuthenticate = DeviceUnlockManager.canAuthenticate(this)
if ((biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
&& advancedUnlockButton != null
&& mPasswordActivityEducation.checkAndPerformedBiometricEducation(
advancedUnlockButton!!,
{
startActivity(Intent(this, SettingsAdvancedUnlockActivity::class.java))
},
{
&& deviceUnlockButton != null) {
mPasswordActivityEducation.checkAndPerformedBiometricEducation(
deviceUnlockButton!!,
{
startActivity(
Intent(
this,
DeviceUnlockSettingsActivity::class.java
)
)
},
{
})
})
}
}
} catch (ignored: Exception) {}
} catch (_: Exception) {}
}
}
@@ -646,6 +718,12 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
R.id.menu_open_file_read_mode_key -> {
mReadOnly = !mReadOnly
changeOpenFileReadIcon(item)
// Save the read-only state to database
mDatabaseFileUri?.let { databaseUri ->
FileDatabaseHistoryAction.getInstance(applicationContext).addOrUpdateDatabaseFile(
DatabaseFile(databaseUri = databaseUri, readOnly = mReadOnly)
)
}
}
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
}
@@ -653,6 +731,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
return super.onOptionsItemSelected(item)
}
override fun onDestroy() {
super.onDestroy()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mDeviceUnlockViewModel?.disconnect()
}
}
companion object {
private val TAG = MainCredentialActivity::class.java.name
@@ -661,18 +746,24 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
private const val KEY_FILENAME = "fileName"
private const val KEY_KEYFILE = "keyFile"
private const val KEY_HARDWARE_KEY = "hardwareKey"
private const val VIEW_INTENT = "android.intent.action.VIEW"
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
private fun buildAndLaunchIntent(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
intentBuildLauncher: (Intent) -> Unit) {
val intent = Intent(activity, MainCredentialActivity::class.java)
intent.putExtra(KEY_FILENAME, databaseFile)
if (keyFile != null)
intent.putExtra(KEY_KEYFILE, keyFile)
if (hardwareKey != null)
intent.putExtra(KEY_HARDWARE_KEY, hardwareKey.toString())
intentBuildLauncher.invoke(intent)
}
@@ -685,8 +776,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
@Throws(FileNotFoundException::class)
fun launch(activity: Activity,
databaseFile: Uri,
keyFile: Uri?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
keyFile: Uri?,
hardwareKey: HardwareKey?) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
activity.startActivity(intent)
}
}
@@ -701,8 +793,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForSearchResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSearchModeResult(
activity,
intent,
@@ -720,8 +813,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForSaveResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSaveModeResult(
activity,
intent,
@@ -739,8 +833,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForKeyboardResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
activity,
intent,
@@ -759,10 +854,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForAutofillResult(activity: AppCompatActivity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
AutofillHelper.startActivityForAutofillResult(
activity,
intent,
@@ -780,8 +876,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForRegistration(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
registerInfo: RegisterInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForRegistrationModeResult(
activity,
intent,
@@ -797,6 +894,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launch(activity: AppCompatActivity,
databaseUri: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit,
@@ -805,43 +903,67 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
try {
EntrySelectionHelper.doSpecialAction(activity.intent,
{
MainCredentialActivity.launch(activity,
databaseUri, keyFile)
launch(
activity,
databaseUri,
keyFile,
hardwareKey
)
},
{ searchInfo -> // Search Action
MainCredentialActivity.launchForSearchResult(activity,
databaseUri, keyFile,
searchInfo)
launchForSearchResult(
activity,
databaseUri,
keyFile,
hardwareKey,
searchInfo
)
onLaunchActivitySpecialMode()
},
{ searchInfo -> // Save Action
MainCredentialActivity.launchForSaveResult(activity,
databaseUri, keyFile,
searchInfo)
launchForSaveResult(
activity,
databaseUri,
keyFile,
hardwareKey,
searchInfo
)
onLaunchActivitySpecialMode()
},
{ searchInfo -> // Keyboard Selection Action
MainCredentialActivity.launchForKeyboardResult(activity,
databaseUri, keyFile,
searchInfo)
launchForKeyboardResult(
activity,
databaseUri,
keyFile,
hardwareKey,
searchInfo
)
onLaunchActivitySpecialMode()
},
{ searchInfo, autofillComponent -> // Autofill Selection Action
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
MainCredentialActivity.launchForAutofillResult(activity,
databaseUri, keyFile,
autofillActivityResultLauncher,
autofillComponent,
searchInfo)
launchForAutofillResult(
activity,
databaseUri,
keyFile,
hardwareKey,
autofillActivityResultLauncher,
autofillComponent,
searchInfo
)
onLaunchActivitySpecialMode()
} else {
onCancelSpecialMode()
}
},
{ registerInfo -> // Registration Action
MainCredentialActivity.launchForRegistration(activity,
databaseUri, keyFile,
registerInfo)
launchForRegistration(
activity,
databaseUri,
keyFile,
hardwareKey,
registerInfo
)
onLaunchActivitySpecialMode()
}
)

View File

@@ -25,6 +25,7 @@ import android.text.SpannableStringBuilder
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.utils.getParcelableCompat
class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
@@ -40,8 +41,9 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelable(OLD_FILE_DATABASE_INFO)
val newSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelable(NEW_FILE_DATABASE_INFO)
val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(OLD_FILE_DATABASE_INFO)
val newSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(NEW_FILE_DATABASE_INFO)
val readOnlyDatabase: Boolean = arguments?.getBoolean(READ_ONLY_DATABASE) ?: true
if (oldSnapFileDatabaseInfo != null && newSnapFileDatabaseInfo != null) {
// Use the Builder class for convenient dialog construction
@@ -53,7 +55,13 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
stringBuilder.append("\n\n" +oldSnapFileDatabaseInfo.toString(activity)
+ "\n\n" +
newSnapFileDatabaseInfo.toString(activity) + "\n\n")
stringBuilder.append(getString(R.string.warning_database_info_changed_options))
stringBuilder.append(getString(
if (readOnlyDatabase) {
R.string.warning_database_info_changed_options_read_only
} else {
R.string.warning_database_info_changed_options
}
))
} else {
stringBuilder.append(getString(R.string.warning_database_revoked))
}
@@ -76,14 +84,18 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
const val DATABASE_CHANGED_DIALOG_TAG = "databaseChangedDialogFragment"
private const val OLD_FILE_DATABASE_INFO = "OLD_FILE_DATABASE_INFO"
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE"
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
newSnapFileDatabaseInfo: SnapFileDatabaseInfo)
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
readOnly: Boolean
)
: DatabaseChangedDialogFragment {
val fragment = DatabaseChangedDialogFragment()
fragment.arguments = Bundle().apply {
putParcelable(OLD_FILE_DATABASE_INFO, oldSnapFileDatabaseInfo)
putParcelable(NEW_FILE_DATABASE_INFO, newSnapFileDatabaseInfo)
putBoolean(READ_ONLY_DATABASE, readOnly)
}
return fragment
}

View File

@@ -1,11 +1,14 @@
package com.kunzisoft.keepass.activities.dialogs
import android.os.Bundle
import android.view.View
import android.view.WindowManager.LayoutParams.FLAG_SECURE
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
@@ -13,7 +16,7 @@ import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private var mDatabase: Database? = null
private var mDatabase: ContextualDatabase? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -29,6 +32,18 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Screenshot mode or hide views
context?.let {
if (PreferencesUtil.isScreenshotModeEnabled(it)) {
dialog?.window?.clearFlags(FLAG_SECURE)
} else {
dialog?.window?.setFlags(FLAG_SECURE, FLAG_SECURE)
}
}
}
@Suppress("DEPRECATION")
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
@@ -36,12 +51,12 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
resetAppTimeoutOnTouchOrFocus()
}
override fun onDatabaseRetrieved(database: Database?) {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
// Can be overridden by a subclass
}
override fun onDatabaseActionFinished(
database: Database,
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {

View File

@@ -1,67 +0,0 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.DatePickerDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.DialogFragment
// Not as DatabaseDialogFragment because crash on KitKat
class DatePickerFragment : DialogFragment() {
private var mDefaultYear: Int = 2000
private var mDefaultMonth: Int = 1
private var mDefaultDay: Int = 1
private var mListener: DatePickerDialog.OnDateSetListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as DatePickerDialog.OnDateSetListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// Create a new instance of DatePickerDialog and return it
return context?.let {
arguments?.apply {
if (containsKey(DEFAULT_YEAR_BUNDLE_KEY))
mDefaultYear = getInt(DEFAULT_YEAR_BUNDLE_KEY)
if (containsKey(DEFAULT_MONTH_BUNDLE_KEY))
mDefaultMonth = getInt(DEFAULT_MONTH_BUNDLE_KEY)
if (containsKey(DEFAULT_DAY_BUNDLE_KEY))
mDefaultDay = getInt(DEFAULT_DAY_BUNDLE_KEY)
}
DatePickerDialog(it, mListener, mDefaultYear, mDefaultMonth, mDefaultDay)
} ?: super.onCreateDialog(savedInstanceState)
}
companion object {
private const val DEFAULT_YEAR_BUNDLE_KEY = "DEFAULT_YEAR_BUNDLE_KEY"
private const val DEFAULT_MONTH_BUNDLE_KEY = "DEFAULT_MONTH_BUNDLE_KEY"
private const val DEFAULT_DAY_BUNDLE_KEY = "DEFAULT_DAY_BUNDLE_KEY"
fun getInstance(defaultYear: Int,
defaultMonth: Int,
defaultDay: Int): DatePickerFragment {
return DatePickerFragment().apply {
arguments = Bundle().apply {
putInt(DEFAULT_YEAR_BUNDLE_KEY, defaultYear)
putInt(DEFAULT_MONTH_BUNDLE_KEY, defaultMonth)
putInt(DEFAULT_DAY_BUNDLE_KEY, defaultDay)
}
}
}
}
}

View File

@@ -35,6 +35,7 @@ import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.utils.getParcelableCompat
class EntryCustomFieldDialogFragment: DatabaseDialogFragment() {
@@ -72,7 +73,7 @@ class EntryCustomFieldDialogFragment: DatabaseDialogFragment() {
customFieldDeleteButton = root?.findViewById(R.id.entry_custom_field_delete)
customFieldProtectionButton = root?.findViewById(R.id.entry_custom_field_protection)
oldField = arguments?.getParcelable(KEY_FIELD)
oldField = arguments?.getParcelableCompat(KEY_FIELD)
oldField?.let { oldCustomField ->
customFieldLabel?.text = oldCustomField.name
customFieldProtectionButton?.isChecked = oldCustomField.protectedValue.isProtected

View File

@@ -21,12 +21,12 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.UriUtil.openUrl
class FileManagerDialogFragment : DialogFragment() {
@@ -42,7 +42,7 @@ class FileManagerDialogFragment : DialogFragment() {
textDescription.text = getString(R.string.file_manager_install_description)
root.findViewById<Button>(R.id.file_manager_button).setOnClickListener {
UriUtil.gotoUrl(requireContext(), R.string.file_manager_explanation_url)
context?.openUrl(R.string.file_manager_explanation_url)
dismiss()
}

View File

@@ -27,6 +27,7 @@ import android.text.SpannableStringBuilder
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.getParcelableCompat
/**
* Custom Dialog to confirm big file to upload
@@ -62,7 +63,7 @@ class FileTooBigDialogFragment : DialogFragment() {
})
builder.setPositiveButton(android.R.string.ok) { _, _ ->
mActionChooseListener?.onValidateUploadFileTooBig(
arguments?.getParcelable(KEY_FILE_URI),
arguments?.getParcelableCompat(KEY_FILE_URI),
arguments?.getString(KEY_FILE_NAME))
}
builder.setNegativeButton(android.R.string.cancel) { _, _ ->

View File

@@ -1,224 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.view.View
import android.widget.*
import androidx.appcompat.app.AlertDialog
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.applyFontVisibility
class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
private var mListener: GeneratePasswordListener? = null
private var root: View? = null
private var lengthTextView: EditText? = null
private var passwordInputLayoutView: TextInputLayout? = null
private var passwordView: EditText? = null
private var mPasswordField: Field? = null
private var uppercaseBox: CompoundButton? = null
private var lowercaseBox: CompoundButton? = null
private var digitsBox: CompoundButton? = null
private var minusBox: CompoundButton? = null
private var underlineBox: CompoundButton? = null
private var spaceBox: CompoundButton? = null
private var specialsBox: CompoundButton? = null
private var bracketsBox: CompoundButton? = null
private var extendedBox: CompoundButton? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as GeneratePasswordListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + GeneratePasswordListener::class.java.name)
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater
root = inflater.inflate(R.layout.fragment_generate_password, null)
passwordInputLayoutView = root?.findViewById(R.id.password_input_layout)
passwordView = root?.findViewById(R.id.password)
passwordView?.applyFontVisibility()
val passwordCopyView: ImageView? = root?.findViewById(R.id.password_copy_button)
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(activity))
View.VISIBLE else View.GONE
val clipboardHelper = ClipboardHelper(activity)
passwordCopyView?.setOnClickListener {
clipboardHelper.timeoutCopyToClipboard(passwordView!!.text.toString(),
getString(R.string.copy_field,
getString(R.string.entry_password)))
}
lengthTextView = root?.findViewById(R.id.length)
uppercaseBox = root?.findViewById(R.id.cb_uppercase)
lowercaseBox = root?.findViewById(R.id.cb_lowercase)
digitsBox = root?.findViewById(R.id.cb_digits)
minusBox = root?.findViewById(R.id.cb_minus)
underlineBox = root?.findViewById(R.id.cb_underline)
spaceBox = root?.findViewById(R.id.cb_space)
specialsBox = root?.findViewById(R.id.cb_specials)
bracketsBox = root?.findViewById(R.id.cb_brackets)
extendedBox = root?.findViewById(R.id.cb_extended)
mPasswordField = arguments?.getParcelable(KEY_PASSWORD_FIELD)
assignDefaultCharacters()
val seekBar = root?.findViewById<SeekBar>(R.id.seekbar_length)
seekBar?.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
lengthTextView?.setText(progress.toString())
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
context?.let { context ->
seekBar?.progress = PreferencesUtil.getDefaultPasswordLength(context)
}
root?.findViewById<Button>(R.id.generate_password_button)
?.setOnClickListener { fillPassword() }
builder.setView(root)
.setPositiveButton(R.string.accept) { _, _ ->
mPasswordField?.let { passwordField ->
passwordView?.text?.toString()?.let { passwordValue ->
passwordField.protectedValue.stringValue = passwordValue
}
mListener?.acceptPassword(passwordField)
}
dismiss()
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
mPasswordField?.let { passwordField ->
mListener?.cancelPassword(passwordField)
}
dismiss()
}
// Pre-populate a password to possibly save the user a few clicks
fillPassword()
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun assignDefaultCharacters() {
uppercaseBox?.isChecked = false
lowercaseBox?.isChecked = false
digitsBox?.isChecked = false
minusBox?.isChecked = false
underlineBox?.isChecked = false
spaceBox?.isChecked = false
specialsBox?.isChecked = false
bracketsBox?.isChecked = false
extendedBox?.isChecked = false
context?.let { context ->
PreferencesUtil.getDefaultPasswordCharacters(context)?.let { charSet ->
for (passwordChar in charSet) {
when (passwordChar) {
getString(R.string.value_password_uppercase) -> uppercaseBox?.isChecked = true
getString(R.string.value_password_lowercase) -> lowercaseBox?.isChecked = true
getString(R.string.value_password_digits) -> digitsBox?.isChecked = true
getString(R.string.value_password_minus) -> minusBox?.isChecked = true
getString(R.string.value_password_underline) -> underlineBox?.isChecked = true
getString(R.string.value_password_space) -> spaceBox?.isChecked = true
getString(R.string.value_password_special) -> specialsBox?.isChecked = true
getString(R.string.value_password_brackets) -> bracketsBox?.isChecked = true
getString(R.string.value_password_extended) -> extendedBox?.isChecked = true
}
}
}
}
}
private fun fillPassword() {
root?.findViewById<EditText>(R.id.password)?.setText(generatePassword())
}
fun generatePassword(): String {
var password = ""
try {
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
password = PasswordGenerator(resources).generatePassword(length,
uppercaseBox?.isChecked == true,
lowercaseBox?.isChecked == true,
digitsBox?.isChecked == true,
minusBox?.isChecked == true,
underlineBox?.isChecked == true,
spaceBox?.isChecked == true,
specialsBox?.isChecked == true,
bracketsBox?.isChecked == true,
extendedBox?.isChecked == true)
passwordInputLayoutView?.error = null
} catch (e: NumberFormatException) {
passwordInputLayoutView?.error = getString(R.string.error_wrong_length)
} catch (e: IllegalArgumentException) {
passwordInputLayoutView?.error = e.message
}
return password
}
interface GeneratePasswordListener {
fun acceptPassword(passwordField: Field)
fun cancelPassword(passwordField: Field)
}
companion object {
private const val KEY_PASSWORD_FIELD = "KEY_PASSWORD_FIELD"
fun getInstance(field: Field): GeneratePasswordDialogFragment {
return GeneratePasswordDialogFragment().apply {
arguments = Bundle().apply {
putParcelable(KEY_PASSWORD_FIELD, field)
}
}
}
}
}

View File

@@ -31,11 +31,13 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.TagsAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.DateTimeFieldView
class GroupDialogFragment : DatabaseDialogFragment() {
@@ -60,7 +62,7 @@ class GroupDialogFragment : DatabaseDialogFragment() {
private lateinit var uuidContainerView: ViewGroup
private lateinit var uuidReferenceView: TextView
override fun onDatabaseRetrieved(database: Database?) {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
@@ -106,17 +108,17 @@ class GroupDialogFragment : DatabaseDialogFragment() {
uuidReferenceView = root.findViewById(R.id.group_UUID_reference)
// Retrieve the textColor to tint the icon
val ta = activity.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
val ta = activity.theme.obtainStyledAttributes(intArrayOf(R.attr.colorSecondary))
mIconColor = ta.getColor(0, Color.WHITE)
ta.recycle()
if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
mGroupInfo = savedInstanceState.getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
} else {
arguments?.apply {
if (containsKey(KEY_GROUP_INFO)) {
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
mGroupInfo = getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
}
}
}

View File

@@ -24,23 +24,27 @@ import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.*
import android.widget.Button
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.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.*
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.NONE
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
import com.kunzisoft.keepass.adapters.TagsProposalAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.DateTimeEditFieldView
import com.kunzisoft.keepass.view.InheritedCompletionView
import com.kunzisoft.keepass.view.TagsCompletionView
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
import com.tokenautocomplete.FilteredArrayAdapter
import org.joda.time.DateTime
class GroupEditDialogFragment : DatabaseDialogFragment() {
@@ -85,29 +89,21 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
}
mGroupEditViewModel.onDateSelected.observe(this) { viewModelDate ->
mGroupEditViewModel.onDateSelected.observe(this) { date ->
// Save the date
mGroupInfo.expiryTime = DateInstant(
DateTime(mGroupInfo.expiryTime.date)
.withYear(viewModelDate.year)
.withMonthOfYear(viewModelDate.month + 1)
.withDayOfMonth(viewModelDate.day)
.toDate())
mGroupInfo.expiryTime.setDate(date.year, date.month, date.day)
expirationView.dateTime = mGroupInfo.expiryTime
if (expirationView.dateTime.type == DateInstant.Type.DATE_TIME) {
val instantTime = DateInstant(mGroupInfo.expiryTime.date, DateInstant.Type.TIME)
// Trick to recall selection with time
mGroupEditViewModel.requestDateTimeSelection(instantTime)
mGroupEditViewModel.requestDateTimeSelection(
DateInstant(mGroupInfo.expiryTime.instant, DateInstant.Type.TIME)
)
}
}
mGroupEditViewModel.onTimeSelected.observe(this) { viewModelTime ->
// Save the time
mGroupInfo.expiryTime = DateInstant(
DateTime(mGroupInfo.expiryTime.date)
.withHourOfDay(viewModelTime.hours)
.withMinuteOfHour(viewModelTime.minutes)
.toDate(), mGroupInfo.expiryTime.type)
mGroupInfo.expiryTime.setTime(viewModelTime.hour, viewModelTime.minute)
expirationView.dateTime = mGroupInfo.expiryTime
}
@@ -116,7 +112,7 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
}
}
override fun onDatabaseRetrieved(database: Database?) {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
@@ -170,13 +166,13 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
&& savedInstanceState.containsKey(KEY_ACTION_ID)
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
mGroupInfo = savedInstanceState.getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
} else {
arguments?.apply {
if (containsKey(KEY_ACTION_ID))
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
if (containsKey(KEY_GROUP_INFO)) {
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
mGroupInfo = getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
}
}
}
@@ -252,11 +248,7 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
private fun retrieveGroupInfoFromViews() {
mGroupInfo.title = nameTextView.text.toString()
// Only if there
val newNotes = notesTextView.text.toString()
if (newNotes.isNotEmpty()) {
mGroupInfo.notes = newNotes
}
mGroupInfo.notes = notesTextView.text?.toString()
mGroupInfo.expires = expirationView.activation
mGroupInfo.expiryTime = expirationView.dateTime
mGroupInfo.searchable = searchableView.getValue()

View File

@@ -27,10 +27,11 @@ 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.ContextualDatabase
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.utils.getParcelableCompat
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
class IconEditDialogFragment : DatabaseDialogFragment() {
@@ -44,7 +45,7 @@ class IconEditDialogFragment : DatabaseDialogFragment() {
private var mCustomIcon: IconImageCustom? = null
override fun onDatabaseRetrieved(database: Database?) {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
@@ -63,11 +64,11 @@ class IconEditDialogFragment : DatabaseDialogFragment() {
if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_CUSTOM_ICON_ID)) {
mCustomIcon = savedInstanceState.getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
mCustomIcon = savedInstanceState.getParcelableCompat(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
} else {
arguments?.apply {
if (containsKey(KEY_CUSTOM_ICON_ID)) {
mCustomIcon = getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
mCustomIcon = getParcelableCompat(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
}
}
}

View File

@@ -27,8 +27,9 @@ import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.MainCredentialView
class MainCredentialDialogFragment : DatabaseDialogFragment() {
@@ -65,7 +66,7 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
var databaseUri: Uri? = null
arguments?.apply {
if (containsKey(KEY_ASK_CREDENTIAL_URI))
databaseUri = getParcelable(KEY_ASK_CREDENTIAL_URI)
databaseUri = getParcelableCompat(KEY_ASK_CREDENTIAL_URI)
}
val builder = AlertDialog.Builder(activity)
@@ -74,7 +75,7 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
mainCredentialView = root.findViewById(R.id.main_credential_view)
databaseUri?.let {
root.findViewById<TextView>(R.id.title_database)?.text =
UriUtil.getFileData(requireContext(), it)?.name
it.getDocumentFile(requireContext())?.name
}
builder.setView(root)
// Add action buttons
@@ -95,7 +96,7 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) {
mainCredentialView?.populateKeyFileTextView(uri)
mainCredentialView?.populateKeyFileView(uri)
}
}
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)

View File

@@ -26,7 +26,8 @@ import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.utils.getParcelableCompat
class PasswordEncodingDialogFragment : DialogFragment() {
@@ -49,8 +50,8 @@ class PasswordEncodingDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
val mainCredential: MainCredential = savedInstanceState?.getParcelable(MAIN_CREDENTIAL) ?: MainCredential()
val databaseUri: Uri? = savedInstanceState?.getParcelableCompat(DATABASE_URI_KEY)
val mainCredential: MainCredential = savedInstanceState?.getParcelableCompat(MAIN_CREDENTIAL) ?: MainCredential()
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
@@ -78,8 +79,10 @@ class PasswordEncodingDialogFragment : DialogFragment() {
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
private const val MAIN_CREDENTIAL = "MAIN_CREDENTIAL"
fun getInstance(databaseUri: Uri,
mainCredential: MainCredential): SortDialogFragment {
fun getInstance(
databaseUri: Uri,
mainCredential: MainCredential
): SortDialogFragment {
val fragment = SortDialogFragment()
fragment.arguments = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri)

View File

@@ -28,7 +28,7 @@ import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.UriUtil.openUrl
/**
* Custom Dialog that asks the user to download the pro version or make a donation.
@@ -45,13 +45,16 @@ class ProFeatureDialogFragment : DialogFragment() {
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.download) { _, _ ->
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
activity.openUrl(
activity.getString(R.string.play_store_url,
activity.getString(R.string.keepro_app_id))
)
}
} else {
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n")
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.contribute) { _, _ ->
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
activity.openUrl(R.string.contribution_url)
}
}
builder.setMessage(stringBuilder)

View File

@@ -27,6 +27,7 @@ import android.text.SpannableStringBuilder
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.utils.getParcelableCompat
/**
* Custom Dialog to confirm big file to upload
@@ -62,8 +63,8 @@ class ReplaceFileDialogFragment : DatabaseDialogFragment() {
})
builder.setPositiveButton(android.R.string.ok) { _, _ ->
mActionChooseListener?.onValidateReplaceFile(
arguments?.getParcelable(KEY_FILE_URI),
arguments?.getParcelable(KEY_ENTRY_ATTACHMENT))
arguments?.getParcelableCompat(KEY_FILE_URI),
arguments?.getParcelableCompat(KEY_ENTRY_ATTACHMENT))
}
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
dismiss()

View File

@@ -35,42 +35,60 @@ import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.UriUtil.openUrl
import com.kunzisoft.keepass.view.HardwareKeySelectionView
import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.PasswordEditView
import com.kunzisoft.keepass.view.applyFontVisibility
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.security.SecureRandom
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private var mMasterPassword: String? = null
private var mKeyFile: Uri? = null
private var mKeyFileUri: Uri? = null
private var mHardwareKey: HardwareKey? = null
private var rootView: View? = null
private lateinit var rootView: View
private var passwordCheckBox: CompoundButton? = null
private lateinit var passwordCheckBox: CompoundButton
private lateinit var passwordEditView: PasswordEditView
private lateinit var passwordRepeatTextInputLayout: TextInputLayout
private lateinit var passwordRepeatView: TextView
private var passwordTextInputLayout: TextInputLayout? = null
private var passwordView: TextView? = null
private var passwordRepeatTextInputLayout: TextInputLayout? = null
private var passwordRepeatView: TextView? = null
private lateinit var keyFileCheckBox: CompoundButton
private lateinit var keyFileGenerateButton: View
private lateinit var keyFileSelectionView: KeyFileSelectionView
private var keyFileCheckBox: CompoundButton? = null
private var keyFileSelectionView: KeyFileSelectionView? = null
private lateinit var hardwareKeyCheckBox: CompoundButton
private lateinit var hardwareKeySelectionView: HardwareKeySelectionView
private var mListener: AssignMainCredentialDialogListener? = null
private var mExternalFileHelper: ExternalFileHelper? = null
private var mPasswordEntropyCalculator: PasswordEntropy? = null
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
private var mNoKeyConfirmationDialog: AlertDialog? = null
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
private var mAllowNoMasterKey: Boolean = false
private val passwordTextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
passwordCheckBox?.isChecked = true
passwordCheckBox.isChecked = true
}
}
@@ -100,13 +118,19 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
super.onDetach()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create the password entropy object
mPasswordEntropyCalculator = PasswordEntropy()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
var allowNoMasterKey = false
arguments?.apply {
if (containsKey(ALLOW_NO_MASTER_KEY_ARG))
allowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
mAllowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
}
val builder = AlertDialog.Builder(activity)
@@ -118,63 +142,73 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
rootView?.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
rootView.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
activity.openUrl(R.string.credentials_explanation_url)
}
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
passwordView = rootView?.findViewById(R.id.pass_password)
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
passwordCheckBox = rootView.findViewById(R.id.password_checkbox)
passwordEditView = rootView.findViewById(R.id.password_view)
passwordRepeatTextInputLayout = rootView.findViewById(R.id.password_repeat_input_layout)
passwordRepeatView = rootView.findViewById(R.id.password_confirmation)
passwordRepeatView.applyFontVisibility()
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
keyFileCheckBox = rootView.findViewById(R.id.keyfile_checkbox)
keyFileGenerateButton = rootView.findViewById(R.id.keyfile_generate)
keyFileSelectionView = rootView.findViewById(R.id.keyfile_selection)
hardwareKeyCheckBox = rootView.findViewById(R.id.hardware_key_checkbox)
hardwareKeySelectionView = rootView.findViewById(R.id.hardware_key_selection)
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
createdFileUri?.let { uri ->
createKeyFile(uri)
keyFileSelectionView.error = null
keyFileCheckBox.isChecked = true
keyFileSelectionView.uri = uri
}
}
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let { pathUri ->
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
keyFileSelectionView?.error = null
keyFileCheckBox?.isChecked = true
keyFileSelectionView?.uri = pathUri
if (lengthFile <= 0L) {
showEmptyKeyFileConfirmationDialog()
}
pathUri.getDocumentFile(requireContext())?.length()?.let { lengthFile ->
keyFileSelectionView.error = null
keyFileCheckBox.isChecked = true
keyFileSelectionView.uri = pathUri
showLengthKeyFileConfirmationDialog(lengthFile)
}
}
}
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
keyFileGenerateButton.setOnClickListener {
mExternalFileHelper?.createDocument(DEFAULT_KEYFILE_NAME)
}
keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper)
hardwareKeySelectionView.selectionListener = { hardwareKey ->
hardwareKeyCheckBox.isChecked = true
hardwareKeySelectionView.error =
if (!HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)) {
// show hardware driver dialog if required
getString(R.string.error_driver_required, hardwareKey.toString())
} else {
null
}
}
val dialog = builder.create()
dialog.setOnShowListener { dialog1 ->
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
positiveButton.setOnClickListener {
if (passwordCheckBox != null && keyFileCheckBox!= null) {
dialog.setOnShowListener { dialog1 ->
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
positiveButton.setOnClickListener {
mMasterPassword = ""
mKeyFileUri = null
mHardwareKey = null
mMasterPassword = ""
mKeyFile = null
var error = verifyPassword() || verifyKeyFile()
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
error = true
if (allowNoMasterKey)
showNoKeyConfirmationDialog()
else {
passwordTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
}
}
if (!error) {
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
dismiss()
}
}
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
negativeButton.setOnClickListener {
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
dismiss()
}
approveMainCredential()
}
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
negativeButton.setOnClickListener {
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
dismiss()
}
}
@@ -184,67 +218,123 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
return super.onCreateDialog(savedInstanceState)
}
private fun createKeyFile(uri: Uri) {
CoroutineScope(Dispatchers.IO).launch {
activity?.contentResolver?.openOutputStream(uri)?.use { outputStream ->
val randomBytes = ByteArray(DEFAULT_KEYFILE_SIZE)
SecureRandom().nextBytes(randomBytes)
outputStream.write(randomBytes)
}
}
}
private fun approveMainCredential() {
val errorPassword = verifyPassword()
val errorKeyFile = verifyKeyFile()
val errorHardwareKey = verifyHardwareKey()
// Check all to fill error
var error = errorPassword || errorKeyFile || errorHardwareKey
val hardwareKey = hardwareKeySelectionView.hardwareKey
if (!error
&& (!passwordCheckBox.isChecked)
&& (!keyFileCheckBox.isChecked)
&& (!hardwareKeyCheckBox.isChecked)
) {
error = true
if (mAllowNoMasterKey) {
// show no key dialog if required
showNoKeyConfirmationDialog()
} else {
passwordRepeatTextInputLayout.error =
getString(R.string.error_disallow_no_credentials)
}
} else if (!error
&& mMasterPassword.isNullOrEmpty()
&& !keyFileCheckBox.isChecked
&& !hardwareKeyCheckBox.isChecked
) {
// show empty password dialog if required
error = true
showEmptyPasswordConfirmationDialog()
} else if (!error
&& hardwareKey != null
&& !HardwareKeyActivity.isHardwareKeyAvailable(
requireActivity(), hardwareKey, false)
) {
// show hardware driver dialog if required
error = true
hardwareKeySelectionView.error =
getString(R.string.error_driver_required, hardwareKey.toString())
}
if (!error) {
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
dismiss()
}
}
private fun verifyPassword(): Boolean {
var error = false
passwordRepeatTextInputLayout.error = null
if (passwordCheckBox.isChecked) {
mMasterPassword = passwordEditView.passwordString
val confPassword = passwordRepeatView.text.toString()
// Verify that passwords match
if (mMasterPassword != confPassword) {
error = true
// Passwords do not match
passwordRepeatTextInputLayout.error = getString(R.string.error_pass_match)
}
}
return error
}
private fun verifyKeyFile(): Boolean {
var error = false
keyFileSelectionView.error = null
if (keyFileCheckBox.isChecked) {
keyFileSelectionView.uri?.let { uri ->
mKeyFileUri = uri
} ?: run {
error = true
keyFileSelectionView.error = getString(R.string.error_nokeyfile)
}
}
return error
}
private fun verifyHardwareKey(): Boolean {
var error = false
hardwareKeySelectionView.error = null
if (hardwareKeyCheckBox.isChecked) {
hardwareKeySelectionView.hardwareKey?.let { hardwareKey ->
mHardwareKey = hardwareKey
} ?: run {
error = true
hardwareKeySelectionView.error = getString(R.string.error_no_hardware_key)
}
}
return error
}
private fun retrieveMainCredential(): MainCredential {
val masterPassword = if (passwordCheckBox!!.isChecked) mMasterPassword else null
val keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null
return MainCredential(masterPassword, keyFile)
val masterPassword = if (passwordCheckBox.isChecked) mMasterPassword else null
val keyFileUri = if (keyFileCheckBox.isChecked) mKeyFileUri else null
val hardwareKey = if (hardwareKeyCheckBox.isChecked) mHardwareKey else null
return MainCredential(masterPassword, keyFileUri, hardwareKey)
}
override fun onResume() {
super.onResume()
// To check checkboxes if a text is present
passwordView?.addTextChangedListener(passwordTextWatcher)
passwordEditView.addTextChangedListener(passwordTextWatcher)
}
override fun onPause() {
super.onPause()
passwordView?.removeTextChangedListener(passwordTextWatcher)
}
private fun verifyPassword(): Boolean {
var error = false
if (passwordCheckBox != null
&& passwordCheckBox!!.isChecked
&& passwordView != null
&& passwordRepeatView != null) {
mMasterPassword = passwordView!!.text.toString()
val confPassword = passwordRepeatView!!.text.toString()
// Verify that passwords match
if (mMasterPassword != confPassword) {
error = true
// Passwords do not match
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
}
if ((mMasterPassword == null
|| mMasterPassword!!.isEmpty())
&& (keyFileCheckBox == null
|| !keyFileCheckBox!!.isChecked
|| keyFileSelectionView?.uri == null)) {
error = true
showEmptyPasswordConfirmationDialog()
}
}
return error
}
private fun verifyKeyFile(): Boolean {
var error = false
if (keyFileCheckBox != null
&& keyFileCheckBox!!.isChecked) {
keyFileSelectionView?.uri?.let { uri ->
mKeyFile = uri
} ?: run {
error = true
keyFileSelectionView?.error = getString(R.string.error_nokeyfile)
}
}
return error
passwordEditView.removeTextChangedListener(passwordTextWatcher)
}
private fun showEmptyPasswordConfirmationDialog() {
@@ -252,10 +342,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
val builder = AlertDialog.Builder(it)
builder.setMessage(R.string.warning_empty_password)
.setPositiveButton(android.R.string.ok) { _, _ ->
if (!verifyKeyFile()) {
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
this@SetMainCredentialDialogFragment.dismiss()
}
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
this@SetMainCredentialDialogFragment.dismiss()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
mEmptyPasswordConfirmationDialog = builder.create()
@@ -277,21 +365,31 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
}
}
private fun showEmptyKeyFileConfirmationDialog() {
private fun showLengthKeyFileConfirmationDialog(length: Long) {
activity?.let {
val builder = AlertDialog.Builder(it)
builder.setMessage(SpannableStringBuilder().apply {
append(getString(R.string.warning_empty_keyfile))
append("\n\n")
append(getString(R.string.warning_empty_keyfile_explanation))
append("\n\n")
append(getString(R.string.warning_sure_add_file))
})
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ ->
keyFileCheckBox?.isChecked = false
keyFileSelectionView?.uri = null
}
var warning = false
if (length <= 0L) {
warning = true
append("\n\n")
append(getString(R.string.warning_empty_keyfile))
} else if (length > 10485760L) {
warning = true
append("\n\n")
append(getString(R.string.warning_large_keyfile))
}
if (warning) {
append("\n\n")
append(getString(R.string.warning_sure_add_file))
}
})
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ ->
keyFileCheckBox.isChecked = false
keyFileSelectionView.uri = null
}
mEmptyKeyFileConfirmationDialog = builder.create()
mEmptyKeyFileConfirmationDialog?.show()
}
@@ -300,6 +398,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
companion object {
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
private const val DEFAULT_KEYFILE_NAME = "keyfile.bin"
private const val DEFAULT_KEYFILE_SIZE = 128
fun getInstance(allowNoMasterKey: Boolean): SetMainCredentialDialogFragment {
val fragment = SetMainCredentialDialogFragment()

View File

@@ -32,7 +32,6 @@ import android.view.inputmethod.EditorInfo
import android.widget.*
import androidx.appcompat.app.AlertDialog
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.OtpModel
import com.kunzisoft.keepass.otp.OtpElement
@@ -42,10 +41,13 @@ import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_TOTP_PERIOD
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_HOTP_COUNTER
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_DIGITS
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_SECRET
import com.kunzisoft.keepass.otp.OtpTokenType
import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.otp.TokenCalculator
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
import com.kunzisoft.keepass.utils.UriUtil.openUrl
import com.kunzisoft.keepass.utils.getParcelableCompat
import java.util.*
class SetOTPDialogFragment : DatabaseDialogFragment() {
@@ -126,14 +128,14 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
// Retrieve OTP model from instance state
if (savedInstanceState != null) {
if (savedInstanceState.containsKey(KEY_OTP)) {
savedInstanceState.getParcelable<OtpModel>(KEY_OTP)?.let { otpModel ->
savedInstanceState.getParcelableCompat<OtpModel>(KEY_OTP)?.let { otpModel ->
mOtpElement = OtpElement(otpModel)
}
}
} else {
arguments?.apply {
if (containsKey(KEY_OTP)) {
getParcelable<OtpModel?>(KEY_OTP)?.let { otpModel ->
getParcelableCompat<OtpModel>(KEY_OTP)?.let { otpModel ->
mOtpElement = OtpElement(otpModel)
}
}
@@ -206,7 +208,7 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
}
// Proprietary only on full version
mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
UriUtil.contributingUser(activity)
activity.isContributingUser()
)
totpTokenTypeAdapter = ArrayAdapter(activity,
android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply {
@@ -223,6 +225,9 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
}
otpAlgorithmSpinner?.adapter = otpAlgorithmAdapter
// Ensure that the UX does not prevent user from hiding/unhiding text
otpSecretContainer?.errorIconDrawable = null
// Set the default value of OTP element
upgradeType()
upgradeTokenType()
@@ -242,7 +247,7 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
}
root?.findViewById<View>(R.id.otp_information)?.setOnClickListener {
UriUtil.gotoUrl(activity, R.string.otp_explanation_url)
activity.openUrl(R.string.otp_explanation_url)
}
return builder.create()
@@ -309,11 +314,16 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
otpSecretTextView?.addTextChangedListener(object: TextWatcher {
override fun afterTextChanged(s: Editable?) {
s?.toString()?.let { userString ->
try {
mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
otpSecretContainer?.error = null
} catch (exception: Exception) {
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
if (userString.length >= MIN_OTP_SECRET) {
try {
mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
otpSecretContainer?.error = null
} catch (exception: Exception) {
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
}
} else {
otpSecretContainer?.error = getString(R.string.error_otp_secret_length,
MIN_OTP_SECRET)
}
mSecretWellFormed = otpSecretContainer?.error == null
}
@@ -470,4 +480,4 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
}
}
}
}
}

View File

@@ -176,21 +176,14 @@ class SortDialogFragment : DatabaseDialogFragment() {
return bundle
}
fun getInstance(sortNodeEnum: SortNodeEnum,
ascending: Boolean,
groupsBefore: Boolean): SortDialogFragment {
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
val fragment = SortDialogFragment()
fragment.arguments = bundle
return fragment
}
fun getInstance(sortNodeEnum: SortNodeEnum,
ascending: Boolean,
groupsBefore: Boolean,
recycleBinBottom: Boolean): SortDialogFragment {
recycleBinBottom: Boolean?): SortDialogFragment {
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
recycleBinBottom?.let {
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
}
val fragment = SortDialogFragment()
fragment.arguments = bundle
return fragment

View File

@@ -1,63 +0,0 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.DatePickerDialog
import android.app.Dialog
import android.app.TimePickerDialog
import android.content.Context
import android.os.Bundle
import android.text.format.DateFormat
import androidx.fragment.app.DialogFragment
// Not as DatabaseDialogFragment because crash on KitKat
class TimePickerFragment : DialogFragment() {
private var defaultHour: Int = 0
private var defaultMinute: Int = 0
private var mListener: TimePickerDialog.OnTimeSetListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as TimePickerDialog.OnTimeSetListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// Create a new instance of DatePickerDialog and return it
return context?.let {
arguments?.apply {
if (containsKey(DEFAULT_HOUR_BUNDLE_KEY))
defaultHour = getInt(DEFAULT_HOUR_BUNDLE_KEY)
if (containsKey(DEFAULT_MINUTE_BUNDLE_KEY))
defaultMinute = getInt(DEFAULT_MINUTE_BUNDLE_KEY)
}
TimePickerDialog(it, mListener, defaultHour, defaultMinute, DateFormat.is24HourFormat(activity))
} ?: super.onCreateDialog(savedInstanceState)
}
companion object {
private const val DEFAULT_HOUR_BUNDLE_KEY = "DEFAULT_HOUR_BUNDLE_KEY"
private const val DEFAULT_MINUTE_BUNDLE_KEY = "DEFAULT_MINUTE_BUNDLE_KEY"
fun getInstance(defaultHour: Int,
defaultMinute: Int): TimePickerFragment {
return TimePickerFragment().apply {
arguments = Bundle().apply {
putInt(DEFAULT_HOUR_BUNDLE_KEY, defaultHour)
putInt(DEFAULT_MINUTE_BUNDLE_KEY, defaultMinute)
}
}
}
}
}

View File

@@ -22,12 +22,12 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.os.Build
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.text.HtmlCompat
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
class UnavailableFeatureDialogFragment : DialogFragment() {

View File

@@ -26,7 +26,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.text.HtmlCompat
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.UriUtil.openUrl
/**
* Custom Dialog that asks the user to download the pro version or make a donation.
@@ -39,21 +39,22 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
val builder = AlertDialog.Builder(activity)
val stringBuilder = SpannableStringBuilder()
if (UriUtil.contributingUser(activity)) {
/*
if (activity.isContributingUser()) {
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_work_hard), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
} else {
*/
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.contribute) { _, _ ->
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
context?.openUrl(R.string.contribution_url)
}
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
}
//}
builder.setMessage(stringBuilder)
// Create the AlertDialog object and return it
return builder.create()

View File

@@ -2,19 +2,19 @@ package com.kunzisoft.keepass.activities.fragments
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
abstract class DatabaseFragment : StylishFragment(), DatabaseRetrieval {
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
protected var mDatabase: Database? = null
protected var mDatabase: ContextualDatabase? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -38,7 +38,7 @@ abstract class DatabaseFragment : StylishFragment(), DatabaseRetrieval {
}
override fun onDatabaseActionFinished(
database: Database,
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {

View File

@@ -35,14 +35,20 @@ import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.adapters.TagsProposalAdapter
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.view.*
import com.kunzisoft.keepass.utils.getParcelableList
import com.kunzisoft.keepass.utils.putParcelableList
import com.kunzisoft.keepass.view.TagsCompletionView
import com.kunzisoft.keepass.view.TemplateEditView
import com.kunzisoft.keepass.view.collapse
import com.kunzisoft.keepass.view.expand
import com.kunzisoft.keepass.view.showByFading
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
import com.tokenautocomplete.FilteredArrayAdapter
@@ -71,12 +77,11 @@ class EntryEditFragment: DatabaseFragment() {
super.onCreateView(inflater, container, savedInstanceState)
// Retrieve the textColor to tint the icon
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
val taIconColor = context?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
mIconColor = taIconColor?.getColor(0, Color.BLACK) ?: Color.BLACK
taIconColor?.recycle()
return inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_entry_edit, container, false)
return inflater.inflate(R.layout.fragment_entry_edit, container, false)
}
override fun onViewCreated(view: View,
@@ -124,7 +129,7 @@ class EntryEditFragment: DatabaseFragment() {
if (savedInstanceState != null) {
val attachments: List<Attachment> =
savedInstanceState.getParcelableArrayList(ATTACHMENTS_TAG) ?: listOf()
savedInstanceState.getParcelableList(ATTACHMENTS_TAG) ?: listOf()
setAttachments(attachments)
}
@@ -268,7 +273,7 @@ class EntryEditFragment: DatabaseFragment() {
}
}
override fun onDatabaseRetrieved(database: Database?) {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
templateView.populateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
@@ -379,7 +384,7 @@ class EntryEditFragment: DatabaseFragment() {
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelableArrayList(ATTACHMENTS_TAG, ArrayList(getAttachments()))
outState.putParcelableList(ATTACHMENTS_TAG, getAttachments())
}
/* -------------

View File

@@ -14,20 +14,21 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.view.TemplateView
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.showByFading
import com.kunzisoft.keepass.viewmodels.EntryViewModel
import java.util.*
class EntryFragment: DatabaseFragment() {
@@ -58,8 +59,7 @@ class EntryFragment: DatabaseFragment() {
savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_entry, container, false)
return inflater.inflate(R.layout.fragment_entry, container, false)
}
override fun onViewCreated(view: View,
@@ -133,7 +133,7 @@ class EntryFragment: DatabaseFragment() {
}
}
override fun onDatabaseRetrieved(database: Database?) {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
context?.let { context ->
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
attachmentsAdapter?.database = database
@@ -158,11 +158,9 @@ class EntryFragment: DatabaseFragment() {
setOnCopyActionClickListener { field ->
mClipboardHelper?.timeoutCopyToClipboard(
TemplateField.getLocalizedName(context, field.name),
field.protectedValue.stringValue,
getString(
R.string.copy_field,
TemplateField.getLocalizedName(context, field.name)
)
field.protectedValue.isProtected
)
}
}
@@ -251,8 +249,7 @@ class EntryFragment: DatabaseFragment() {
fun launchEntryCopyEducationAction() {
val appNameString = getString(R.string.app_name)
mClipboardHelper?.timeoutCopyToClipboard(appNameString,
getString(R.string.copy_field, appNameString))
mClipboardHelper?.timeoutCopyToClipboard(appNameString, appNameString)
}
companion object {

View File

@@ -4,16 +4,16 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.viewmodels.EntryViewModel
class EntryHistoryFragment: StylishFragment() {
class EntryHistoryFragment: Fragment() {
private lateinit var historyContainerView: View
private lateinit var historyListView: RecyclerView
@@ -28,8 +28,7 @@ class EntryHistoryFragment: StylishFragment() {
): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_entry_history, container, false)
return inflater.inflate(R.layout.fragment_entry_history, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@@ -22,27 +22,33 @@ package com.kunzisoft.keepass.activities.fragments
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.*
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ActionMode
import androidx.core.view.MenuProvider
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.EntryEditActivity
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.adapters.NodesAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.KeyboardUtil.hideKeyboard
import com.kunzisoft.keepass.viewmodels.GroupViewModel
import java.util.*
import java.util.LinkedList
class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListener {
@@ -70,22 +76,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
private var specialMode: SpecialMode = SpecialMode.DEFAULT
private var mRecycleBinEnable: Boolean = false
private var mRecycleBin: Group? = null
var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) { entryId ->
entryId?.let {
// Simply refresh the list
rebuildList()
// Scroll to the new entry
mDatabase?.getEntryById(it)?.let { entry ->
mAdapter?.indexOf(entry)?.let { position ->
mNodesRecyclerView?.scrollToPosition(position)
}
}
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
}
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
@@ -99,6 +89,33 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
}
}
private val menuProvider: MenuProvider = object: MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.tree, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.menu_sort -> {
context?.let { context ->
val sortDialogFragment: SortDialogFragment =
SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context),
if (mDatabase?.isRecycleBinEnabled == true) {
PreferencesUtil.getRecycleBinBottomSort(context)
} else null
)
sortDialogFragment.show(childFragmentManager, "sortDialog")
}
true
}
else -> false
}
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
@@ -137,23 +154,13 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
super.onDetach()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onDatabaseRetrieved(database: Database?) {
mRecycleBinEnable = database?.isRecycleBinEnabled == true
mRecycleBin = database?.recycleBin
contextThemed?.let { context ->
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
context?.let { context ->
database?.let { database ->
mAdapter = NodesAdapter(context, database).apply {
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
override fun onNodeClick(database: Database, node: Node) {
if (mCurrentGroup?.isVirtual == false
&& nodeActionSelectionMode) {
override fun onNodeClick(database: ContextualDatabase, node: Node) {
if (nodeActionSelectionMode) {
if (listActionNodes.contains(node)) {
// Remove selected item if already selected
listActionNodes.remove(node)
@@ -169,9 +176,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
}
}
override fun onNodeLongClick(database: Database, node: Node): Boolean {
if (mCurrentGroup?.isVirtual == false
&& nodeActionPasteMode == PasteMode.UNDEFINED) {
override fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean {
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
// Select the first item after a long click
if (!listActionNodes.contains(node))
listActionNodes.add(node)
@@ -180,6 +186,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
setActionNodes(listActionNodes)
notifyNodeChanged(node)
activity?.hideKeyboard()
}
return true
}
@@ -191,7 +198,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
}
override fun onDatabaseActionFinished(
database: Database,
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
@@ -207,13 +214,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
// To apply theme
return inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_nodes, container, false)
return inflater.inflate(R.layout.fragment_nodes, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
mNodesRecyclerView = view.findViewById(R.id.nodes_list)
notFoundView = view.findViewById(R.id.not_found_container)
@@ -242,8 +250,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
activity?.intent?.let {
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
}
rebuildList()
}
override fun onPause() {
@@ -293,43 +299,12 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.tree, menu)
super.onCreateOptionsMenu(menu, inflater)
private fun containsRecycleBin(nodes: List<Node>): Boolean {
return mDatabase?.isRecycleBinEnabled == true
&& nodes.any { it == mDatabase?.recycleBin }
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_sort -> {
context?.let { context ->
val sortDialogFragment: SortDialogFragment =
if (mRecycleBinEnable) {
SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context),
PreferencesUtil.getRecycleBinBottomSort(context)
)
} else {
SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context)
)
}
sortDialogFragment.show(childFragmentManager, "sortDialog")
}
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
fun actionNodesCallback(database: Database,
fun actionNodesCallback(database: ContextualDatabase,
nodes: List<Node>,
menuListener: NodesActionMenuListener?,
onDestroyActionMode: (mode: ActionMode?) -> Unit) : ActionMode.Callback {
@@ -353,8 +328,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
// Open and Edit for a single item
if (nodes.size == 1) {
// Edition
if (database.isReadOnly
|| (mRecycleBinEnable && nodes[0] == mRecycleBin)) {
if (database.isReadOnly || containsRecycleBin(nodes)) {
menu?.removeItem(R.id.menu_edit)
}
} else {
@@ -363,21 +337,18 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
}
// Move
if (database.isReadOnly
|| isASearchResult) {
if (database.isReadOnly) {
menu?.removeItem(R.id.menu_move)
}
// Copy (not allowed for group)
if (database.isReadOnly
|| isASearchResult
|| nodes.any { it.type == Type.GROUP }) {
menu?.removeItem(R.id.menu_copy)
}
// Deletion
if (database.isReadOnly
|| (mRecycleBinEnable && nodes.any { it == mRecycleBin })) {
if (database.isReadOnly || containsRecycleBin(nodes)) {
menu?.removeItem(R.id.menu_delete)
}
}
@@ -433,20 +404,20 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
* Callback listener to redefine to do an action when a node is click
*/
interface NodeClickListener {
fun onNodeClick(database: Database, node: Node)
fun onNodeSelected(database: Database, nodes: List<Node>): Boolean
fun onNodeClick(database: ContextualDatabase, node: Node)
fun onNodeSelected(database: ContextualDatabase, nodes: List<Node>): Boolean
}
/**
* Menu listener to redefine to do an action in menu
*/
interface NodesActionMenuListener {
fun onOpenMenuClick(database: Database, node: Node): Boolean
fun onEditMenuClick(database: Database, node: Node): Boolean
fun onCopyMenuClick(database: Database, nodes: List<Node>): Boolean
fun onMoveMenuClick(database: Database, nodes: List<Node>): Boolean
fun onDeleteMenuClick(database: Database, nodes: List<Node>): Boolean
fun onPasteMenuClick(database: Database, pasteMode: PasteMode?, nodes: List<Node>): Boolean
fun onOpenMenuClick(database: ContextualDatabase, node: Node): Boolean
fun onEditMenuClick(database: ContextualDatabase, node: Node): Boolean
fun onCopyMenuClick(database: ContextualDatabase, nodes: List<Node>): Boolean
fun onMoveMenuClick(database: ContextualDatabase, nodes: List<Node>): Boolean
fun onDeleteMenuClick(database: ContextualDatabase, nodes: List<Node>): Boolean
fun onPasteMenuClick(database: ContextualDatabase, pasteMode: PasteMode?, nodes: List<Node>): Boolean
}
enum class PasteMode {

View File

@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.activities.fragments
import android.os.Bundle
import android.view.View
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
@@ -32,7 +32,7 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
return R.layout.fragment_icon_grid
}
override fun defineIconList(database: Database?) {
override fun defineIconList(database: ContextualDatabase?) {
database?.doForEachCustomIcons { customIcon, _ ->
iconPickerAdapter.addIcon(customIcon, false)
}

View File

@@ -28,7 +28,7 @@ import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.IconPickerAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
import kotlinx.coroutines.CoroutineScope
@@ -47,7 +47,7 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
abstract fun retrieveMainLayoutId(): Int
abstract fun defineIconList(database: Database?)
abstract fun defineIconList(database: ContextualDatabase?)
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
@@ -59,7 +59,7 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
super.onViewCreated(view, savedInstanceState)
// Retrieve the textColor to tint the icon
val ta = contextThemed?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
val ta = context?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK
ta?.recycle()
@@ -71,7 +71,7 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
resetAppTimeoutWhenViewFocusedOrChanged(view)
}
override fun onDatabaseRetrieved(database: Database?) {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
CoroutineScope(Dispatchers.IO).launch {

View File

@@ -10,7 +10,7 @@ import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
class IconPickerFragment : DatabaseFragment() {
@@ -26,14 +26,14 @@ class IconPickerFragment : DatabaseFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_icon_picker, container, false)
return inflater.inflate(R.layout.fragment_tabs_pagination, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewPager = view.findViewById(R.id.icon_picker_pager)
tabLayout = view.findViewById(R.id.icon_picker_tabs)
viewPager = view.findViewById(R.id.tabs_view_pager)
tabLayout = view.findViewById(R.id.tabs_layout)
resetAppTimeoutWhenViewFocusedOrChanged(view)
arguments?.apply {
@@ -48,7 +48,7 @@ class IconPickerFragment : DatabaseFragment() {
}
}
override fun onDatabaseRetrieved(database: Database?) {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
if (database?.allowCustomIcons == true) 2 else 1)
viewPager.adapter = iconPickerPagerAdapter

View File

@@ -20,7 +20,7 @@
package com.kunzisoft.keepass.activities.fragments
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
@@ -30,7 +30,7 @@ class IconStandardFragment : IconFragment<IconImageStandard>() {
return R.layout.fragment_icon_grid
}
override fun defineIconList(database: Database?) {
override fun defineIconList(database: ContextualDatabase?) {
database?.doForEachStandardIcons { standardIcon ->
iconPickerAdapter.addIcon(standardIcon, false)
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2022 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.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.fragment.app.activityViewModels
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.KeyGeneratorPagerAdapter
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
class KeyGeneratorFragment : DatabaseFragment() {
private var keyGeneratorPagerAdapter: KeyGeneratorPagerAdapter? = null
private lateinit var viewPager: ViewPager2
private lateinit var tabLayout: TabLayout
private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()
private var mSelectedTab = KeyGeneratorTab.PASSWORD
private var mOnPageChangeCallback: ViewPager2.OnPageChangeCallback = object:
ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
mSelectedTab = KeyGeneratorTab.getKeyGeneratorTabByPosition(position)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_tabs_pagination, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
keyGeneratorPagerAdapter = KeyGeneratorPagerAdapter(this, )
viewPager = view.findViewById(R.id.tabs_view_pager)
tabLayout = view.findViewById(R.id.tabs_layout)
viewPager.adapter = keyGeneratorPagerAdapter
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = getString(KeyGeneratorTab.getKeyGeneratorTabByPosition(position).stringId)
}.attach()
viewPager.registerOnPageChangeCallback(mOnPageChangeCallback)
resetAppTimeoutWhenViewFocusedOrChanged(view)
arguments?.apply {
if (containsKey(PASSWORD_TAB_ARG)) {
viewPager.currentItem = getInt(PASSWORD_TAB_ARG)
}
remove(PASSWORD_TAB_ARG)
}
mKeyGeneratorViewModel.requireKeyGeneration.observe(viewLifecycleOwner) {
when (mSelectedTab) {
KeyGeneratorTab.PASSWORD -> {
mKeyGeneratorViewModel.requirePasswordGeneration()
}
KeyGeneratorTab.PASSPHRASE -> {
mKeyGeneratorViewModel.requirePassphraseGeneration()
}
}
}
mKeyGeneratorViewModel.keyGeneratedValidated.observe(viewLifecycleOwner) {
when (mSelectedTab) {
KeyGeneratorTab.PASSWORD -> {
mKeyGeneratorViewModel.validatePasswordGenerated()
}
KeyGeneratorTab.PASSPHRASE -> {
mKeyGeneratorViewModel.validatePassphraseGenerated()
}
}
}
}
override fun onDestroyView() {
viewPager.unregisterOnPageChangeCallback(mOnPageChangeCallback)
super.onDestroyView()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
// Nothing here
}
enum class KeyGeneratorTab(@StringRes val stringId: Int) {
PASSWORD(R.string.password), PASSPHRASE(R.string.passphrase);
companion object {
fun getKeyGeneratorTabByPosition(position: Int): KeyGeneratorTab {
return when (position) {
0 -> PASSWORD
else -> PASSPHRASE
}
}
}
}
companion object {
private const val PASSWORD_TAB_ARG = "PASSWORD_TAB_ARG"
fun getInstance(keyGeneratorTab: KeyGeneratorTab): KeyGeneratorFragment {
val fragment = KeyGeneratorFragment()
fragment.arguments = Bundle().apply {
putInt(PASSWORD_TAB_ARG, keyGeneratorTab.ordinal)
}
return fragment
}
}
}

View File

@@ -0,0 +1,254 @@
/*
* Copyright 2022 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.fragments
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.activityViewModels
import com.google.android.material.slider.Slider
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.password.PassphraseGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.PasswordEditView
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
class PassphraseGeneratorFragment : DatabaseFragment() {
private lateinit var passwordEditView: PasswordEditView
private lateinit var sliderWordCount: Slider
private lateinit var wordCountText: EditText
private lateinit var charactersCountText: TextView
private lateinit var wordSeparator: EditText
private lateinit var wordCaseSpinner: Spinner
private var minSliderWordCount: Int = 0
private var maxSliderWordCount: Int = 0
private var wordCaseAdapter: ArrayAdapter<String>? = null
private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_generate_passphrase, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
passwordEditView = view.findViewById(R.id.passphrase_view)
val passphraseCopyView: ImageView? = view.findViewById(R.id.passphrase_copy_button)
sliderWordCount = view.findViewById(R.id.slider_word_count)
wordCountText = view.findViewById(R.id.word_count)
charactersCountText = view.findViewById(R.id.character_count)
wordSeparator = view.findViewById(R.id.word_separator)
wordCaseSpinner = view.findViewById(R.id.word_case)
minSliderWordCount = resources.getInteger(R.integer.passphrase_generator_word_count_min)
maxSliderWordCount = resources.getInteger(R.integer.passphrase_generator_word_count_max)
context?.let { context ->
passphraseCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(context))
View.VISIBLE else View.GONE
val clipboardHelper = ClipboardHelper(context)
passphraseCopyView?.setOnClickListener {
clipboardHelper.timeoutCopyToClipboard(
getString(R.string.passphrase),
passwordEditView.passwordString,
true
)
}
wordCaseAdapter = ArrayAdapter(context,
android.R.layout.simple_spinner_item, resources.getStringArray(R.array.word_case_array)).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
wordCaseSpinner.adapter = wordCaseAdapter
}
loadSettings()
var listenSlider = true
var listenEditText = true
sliderWordCount.addOnChangeListener { _, value, _ ->
try {
listenEditText = false
if (listenSlider) {
wordCountText.setText(value.toInt().toString())
}
} catch (e: Exception) {
Log.e(TAG, "Unable to set the word count value", e)
} finally {
listenEditText = true
}
}
sliderWordCount.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
// TODO upgrade material-components lib
// https://stackoverflow.com/questions/70873160/material-slider-onslidertouchlisteners-methods-can-only-be-called-from-within-t
@SuppressLint("RestrictedApi")
override fun onStartTrackingTouch(slider: Slider) {}
@SuppressLint("RestrictedApi")
override fun onStopTrackingTouch(slider: Slider) {
generatePassphrase()
}
})
wordCountText.doOnTextChanged { _, _, _, _ ->
if (listenEditText) {
try {
listenSlider = false
setSliderValue(getWordCount())
} catch (e: Exception) {
Log.e(TAG, "Unable to get the word count value", e)
} finally {
listenSlider = true
generatePassphrase()
}
}
}
wordSeparator.doOnTextChanged { _, _, _, _ ->
generatePassphrase()
}
wordCaseSpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
generatePassphrase()
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
generatePassphrase()
mKeyGeneratorViewModel.passphraseGeneratedValidated.observe(viewLifecycleOwner) {
mKeyGeneratorViewModel.setKeyGenerated(passwordEditView.passwordString)
}
mKeyGeneratorViewModel.requirePassphraseGeneration.observe(viewLifecycleOwner) {
generatePassphrase()
}
resetAppTimeoutWhenViewFocusedOrChanged(view)
}
private fun getWordCount(): Int {
return try {
Integer.valueOf(wordCountText.text.toString())
} catch (numberException: NumberFormatException) {
minSliderWordCount
}
}
private fun setWordCount(wordCount: Int) {
setSliderValue(wordCount)
wordCountText.setText(wordCount.toString())
}
private fun setSliderValue(value: Int) {
when {
value < minSliderWordCount -> {
sliderWordCount.value = minSliderWordCount.toFloat()
}
value > maxSliderWordCount -> {
sliderWordCount.value = maxSliderWordCount.toFloat()
}
else -> {
sliderWordCount.value = value.toFloat()
}
}
}
private fun getWordSeparator(): String {
return wordSeparator.text.toString().ifEmpty { " " }
}
private fun getWordCase(): PassphraseGenerator.WordCase {
var wordCase = PassphraseGenerator.WordCase.LOWER_CASE
try {
wordCase = PassphraseGenerator.WordCase.getByOrdinal(wordCaseSpinner.selectedItemPosition)
} catch (caseException: Exception) {
Log.e(TAG, "Unable to retrieve the word case", caseException)
}
return wordCase
}
private fun setWordCase(wordCase: PassphraseGenerator.WordCase) {
wordCaseSpinner.setSelection(wordCase.ordinal)
}
private fun getSeparator(): String {
return wordSeparator.text?.toString() ?: ""
}
private fun setSeparator(separator: String) {
wordSeparator.setText(separator)
}
private fun generatePassphrase() {
var passphrase = ""
try {
passphrase = PassphraseGenerator().generatePassphrase(
getWordCount(),
getWordSeparator(),
getWordCase())
} catch (e: Exception) {
Log.e(TAG, "Unable to generate a passphrase", e)
}
passwordEditView.passwordString = passphrase
charactersCountText.text = getString(R.string.character_count, passphrase.length)
}
override fun onDestroy() {
saveSettings()
super.onDestroy()
}
private fun saveSettings() {
context?.let { context ->
PreferencesUtil.setDefaultPassphraseWordCount(context, getWordCount())
PreferencesUtil.setDefaultPassphraseWordCase(context, getWordCase())
PreferencesUtil.setDefaultPassphraseSeparator(context, getSeparator())
}
}
private fun loadSettings() {
context?.let { context ->
setWordCount(PreferencesUtil.getDefaultPassphraseWordCount(context))
setWordCase(PreferencesUtil.getDefaultPassphraseWordCase(context))
setSeparator(PreferencesUtil.getDefaultPassphraseSeparator(context))
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
// Nothing here
}
companion object {
private const val TAG = "PassphraseGnrtrFrgmt"
}
}

View File

@@ -0,0 +1,360 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.fragments
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.ImageView
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.activityViewModels
import com.google.android.material.slider.Slider
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.PasswordEditView
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
class PasswordGeneratorFragment : DatabaseFragment() {
private lateinit var passwordEditView: PasswordEditView
private lateinit var sliderLength: Slider
private lateinit var lengthEditView: EditText
private lateinit var uppercaseCompound: CompoundButton
private lateinit var lowercaseCompound: CompoundButton
private lateinit var digitsCompound: CompoundButton
private lateinit var minusCompound: CompoundButton
private lateinit var underlineCompound: CompoundButton
private lateinit var spaceCompound: CompoundButton
private lateinit var specialsCompound: CompoundButton
private lateinit var bracketsCompound: CompoundButton
private lateinit var extendedCompound: CompoundButton
private lateinit var considerCharsEditText: EditText
private lateinit var ignoreCharsEditText: EditText
private lateinit var atLeastOneCompound: CompoundButton
private lateinit var excludeAmbiguousCompound: CompoundButton
private var minLengthSlider: Int = 0
private var maxLengthSlider: Int = 0
private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_generate_password, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
passwordEditView = view.findViewById(R.id.password_view)
val passwordCopyView: ImageView? = view.findViewById(R.id.password_copy_button)
sliderLength = view.findViewById(R.id.slider_length)
lengthEditView = view.findViewById(R.id.length)
uppercaseCompound = view.findViewById(R.id.upperCase_filter)
lowercaseCompound = view.findViewById(R.id.lowerCase_filter)
digitsCompound = view.findViewById(R.id.digits_filter)
minusCompound = view.findViewById(R.id.minus_filter)
underlineCompound = view.findViewById(R.id.underline_filter)
spaceCompound = view.findViewById(R.id.space_filter)
specialsCompound = view.findViewById(R.id.special_filter)
bracketsCompound = view.findViewById(R.id.brackets_filter)
extendedCompound = view.findViewById(R.id.extendedASCII_filter)
considerCharsEditText = view.findViewById(R.id.consider_chars_filter)
ignoreCharsEditText = view.findViewById(R.id.ignore_chars_filter)
atLeastOneCompound = view.findViewById(R.id.atLeastOne_filter)
excludeAmbiguousCompound = view.findViewById(R.id.excludeAmbiguous_filter)
context?.let { context ->
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(context))
View.VISIBLE else View.GONE
val clipboardHelper = ClipboardHelper(context)
passwordCopyView?.setOnClickListener {
clipboardHelper.timeoutCopyToClipboard(
getString(R.string.password),
passwordEditView.passwordString,
true
)
}
}
minLengthSlider = resources.getInteger(R.integer.password_generator_length_min)
maxLengthSlider = resources.getInteger(R.integer.password_generator_length_max)
loadSettings()
uppercaseCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
lowercaseCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
digitsCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
minusCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
underlineCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
spaceCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
specialsCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
bracketsCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
extendedCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
considerCharsEditText.doOnTextChanged { _, _, _, _ ->
generatePassword()
}
ignoreCharsEditText.doOnTextChanged { _, _, _, _ ->
generatePassword()
}
atLeastOneCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
excludeAmbiguousCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
var listenSlider = true
var listenEditText = true
sliderLength.addOnChangeListener { _, value, _ ->
try {
listenEditText = false
if (listenSlider) {
lengthEditView.setText(value.toInt().toString())
}
} catch (e: Exception) {
Log.e(TAG, "Unable to set the length value", e)
} finally {
listenEditText = true
}
}
sliderLength.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
// TODO upgrade material-components lib
// https://stackoverflow.com/questions/70873160/material-slider-onslidertouchlisteners-methods-can-only-be-called-from-within-t
@SuppressLint("RestrictedApi")
override fun onStartTrackingTouch(slider: Slider) {}
@SuppressLint("RestrictedApi")
override fun onStopTrackingTouch(slider: Slider) {
generatePassword()
}
})
lengthEditView.doOnTextChanged { _, _, _, _ ->
if (listenEditText) {
try {
listenSlider = false
setSliderValue(getPasswordLength())
} catch (e: Exception) {
Log.e(TAG, "Unable to get the length value", e)
} finally {
listenSlider = true
generatePassword()
}
}
}
// Pre-populate a password to possibly save the user a few clicks
generatePassword()
mKeyGeneratorViewModel.passwordGeneratedValidated.observe(viewLifecycleOwner) {
mKeyGeneratorViewModel.setKeyGenerated(passwordEditView.passwordString)
}
mKeyGeneratorViewModel.requirePasswordGeneration.observe(viewLifecycleOwner) {
generatePassword()
}
resetAppTimeoutWhenViewFocusedOrChanged(view)
}
private fun getPasswordLength(): Int {
return try {
Integer.valueOf(lengthEditView.text.toString())
} catch (numberException: NumberFormatException) {
minLengthSlider
}
}
private fun setPasswordLength(passwordLength: Int) {
setSliderValue(passwordLength)
lengthEditView.setText(passwordLength.toString())
}
private fun getOptions(): Set<String> {
val optionsSet = mutableSetOf<String>()
if (uppercaseCompound.isChecked)
optionsSet.add(getString(R.string.value_password_uppercase))
if (lowercaseCompound.isChecked)
optionsSet.add(getString(R.string.value_password_lowercase))
if (digitsCompound.isChecked)
optionsSet.add(getString(R.string.value_password_digits))
if (minusCompound.isChecked)
optionsSet.add(getString(R.string.value_password_minus))
if (underlineCompound.isChecked)
optionsSet.add(getString(R.string.value_password_underline))
if (spaceCompound.isChecked)
optionsSet.add(getString(R.string.value_password_space))
if (specialsCompound.isChecked)
optionsSet.add(getString(R.string.value_password_special))
if (bracketsCompound.isChecked)
optionsSet.add(getString(R.string.value_password_brackets))
if (extendedCompound.isChecked)
optionsSet.add(getString(R.string.value_password_extended))
if (atLeastOneCompound.isChecked)
optionsSet.add(getString(R.string.value_password_atLeastOne))
if (excludeAmbiguousCompound.isChecked)
optionsSet.add(getString(R.string.value_password_excludeAmbiguous))
return optionsSet
}
private fun setOptions(options: Set<String>) {
uppercaseCompound.isChecked = false
lowercaseCompound.isChecked = false
digitsCompound.isChecked = false
minusCompound.isChecked = false
underlineCompound.isChecked = false
spaceCompound.isChecked = false
specialsCompound.isChecked = false
bracketsCompound.isChecked = false
extendedCompound.isChecked = false
atLeastOneCompound.isChecked = false
excludeAmbiguousCompound.isChecked = false
for (option in options) {
when (option) {
getString(R.string.value_password_uppercase) -> uppercaseCompound.isChecked = true
getString(R.string.value_password_lowercase) -> lowercaseCompound.isChecked = true
getString(R.string.value_password_digits) -> digitsCompound.isChecked = true
getString(R.string.value_password_minus) -> minusCompound.isChecked = true
getString(R.string.value_password_underline) -> underlineCompound.isChecked = true
getString(R.string.value_password_space) -> spaceCompound.isChecked = true
getString(R.string.value_password_special) -> specialsCompound.isChecked = true
getString(R.string.value_password_brackets) -> bracketsCompound.isChecked = true
getString(R.string.value_password_extended) -> extendedCompound.isChecked = true
getString(R.string.value_password_atLeastOne) -> atLeastOneCompound.isChecked = true
getString(R.string.value_password_excludeAmbiguous) -> excludeAmbiguousCompound.isChecked = true
}
}
}
private fun getConsiderChars(): String {
return considerCharsEditText.text.toString()
}
private fun setConsiderChars(chars: String) {
considerCharsEditText.setText(chars)
}
private fun getIgnoreChars(): String {
return ignoreCharsEditText.text.toString()
}
private fun setIgnoreChars(chars: String) {
ignoreCharsEditText.setText(chars)
}
private fun generatePassword() {
var password = ""
try {
password = PasswordGenerator(resources).generatePassword(getPasswordLength(),
uppercaseCompound.isChecked,
lowercaseCompound.isChecked,
digitsCompound.isChecked,
minusCompound.isChecked,
underlineCompound.isChecked,
spaceCompound.isChecked,
specialsCompound.isChecked,
bracketsCompound.isChecked,
extendedCompound.isChecked,
getConsiderChars(),
getIgnoreChars(),
atLeastOneCompound.isChecked,
excludeAmbiguousCompound.isChecked)
} catch (e: Exception) {
Log.e(TAG, "Unable to generate a password", e)
}
passwordEditView.passwordString = password
}
override fun onDestroy() {
saveSettings()
super.onDestroy()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
// Nothing here
}
private fun saveSettings() {
context?.let { context ->
PreferencesUtil.setDefaultPasswordOptions(context, getOptions())
PreferencesUtil.setDefaultPasswordLength(context, getPasswordLength())
PreferencesUtil.setDefaultPasswordConsiderChars(context, getConsiderChars())
PreferencesUtil.setDefaultPasswordIgnoreChars(context, getIgnoreChars())
}
}
private fun loadSettings() {
context?.let { context ->
setOptions(PreferencesUtil.getDefaultPasswordOptions(context))
setPasswordLength(PreferencesUtil.getDefaultPasswordLength(context))
setConsiderChars(PreferencesUtil.getDefaultPasswordConsiderChars(context))
setIgnoreChars(PreferencesUtil.getDefaultPasswordIgnoreChars(context))
}
}
private fun setSliderValue(value: Int) {
when {
value < minLengthSlider -> {
sliderLength.value = minLengthSlider.toFloat()
}
value > maxLengthSlider -> {
sliderLength.value = maxLengthSlider.toFloat()
}
else -> {
sliderLength.value = value.toFloat()
}
}
}
companion object {
private const val TAG = "PasswordGeneratorFrgmt"
}
}

View File

@@ -26,7 +26,9 @@ import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import java.io.Serializable
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.utils.getEnumExtra
import com.kunzisoft.keepass.utils.putEnumExtra
object EntrySelectionHelper {
@@ -82,17 +84,17 @@ object EntrySelectionHelper {
}
fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? {
return intent.getParcelableExtra(KEY_SEARCH_INFO)
return intent.getParcelableExtraCompat(KEY_SEARCH_INFO)
}
fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
private fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
registerInfo?.let {
intent.putExtra(KEY_REGISTER_INFO, it)
}
}
fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? {
return intent.getParcelableExtra(KEY_REGISTER_INFO)
return intent.getParcelableExtraCompat(KEY_REGISTER_INFO)
}
fun removeInfoFromIntent(intent: Intent) {
@@ -101,7 +103,7 @@ object EntrySelectionHelper {
}
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
intent.putExtra(KEY_SPECIAL_MODE, specialMode as Serializable)
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
}
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
@@ -109,12 +111,11 @@ object EntrySelectionHelper {
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
return SpecialMode.SELECTION
}
return intent.getSerializableExtra(KEY_SPECIAL_MODE) as SpecialMode?
?: SpecialMode.DEFAULT
return intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
}
fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
intent.putExtra(KEY_TYPE_MODE, typeMode as Serializable)
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
}
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
@@ -122,7 +123,7 @@ object EntrySelectionHelper {
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
return TypeMode.AUTOFILL
}
return intent.getSerializableExtra(KEY_TYPE_MODE) as TypeMode? ?: TypeMode.DEFAULT
return intent.getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
}
fun removeModesFromIntent(intent: Intent) {
@@ -175,7 +176,7 @@ object EntrySelectionHelper {
}
}
if (!autofillComponentInit) {
if (intent.getSerializableExtra(KEY_SPECIAL_MODE) != null) {
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
when (retrieveTypeModeFromIntent(intent)) {
TypeMode.DEFAULT -> {
removeModesFromIntent(intent)

View File

@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.activities.helpers
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.util.Log
@@ -33,7 +32,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.UriUtil.takeUriPermission
class ExternalFileHelper {
@@ -56,11 +55,9 @@ class ExternalFileHelper {
fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) {
val resultCallback = ActivityResultCallback<Uri> { result ->
result?.let { uri ->
UriUtil.takeUriPermission(activity?.contentResolver, uri)
onFileSelected?.invoke(uri)
}
val resultCallback = ActivityResultCallback<Uri?> { result ->
activity?.contentResolver?.takeUriPermission(result)
onFileSelected?.invoke(result)
}
getContentResultLauncher = if (fragment != null) {
@@ -91,7 +88,7 @@ class ExternalFileHelper {
fun buildCreateDocument(typeString: String = "application/octet-stream",
onFileCreated: (fileCreated: Uri?)->Unit) {
val resultCallback = ActivityResultCallback<Uri> { result ->
val resultCallback = ActivityResultCallback<Uri?> { result ->
onFileCreated.invoke(result)
}
@@ -150,7 +147,7 @@ class ExternalFileHelper {
class OpenDocument : ActivityResultContracts.OpenDocument() {
@SuppressLint("InlinedApi")
override fun createIntent(context: Context, input: Array<out String>): Intent {
override fun createIntent(context: Context, input: Array<String>): Intent {
return super.createIntent(context, input).apply {
addCategory(Intent.CATEGORY_OPENABLE)
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
@@ -178,34 +175,17 @@ class ExternalFileHelper {
}
}
class CreateDocument(private val typeString: String) : ActivityResultContracts.CreateDocument() {
class CreateDocument(typeString: String) : ActivityResultContracts.CreateDocument(typeString) {
override fun createIntent(context: Context, input: String): Intent {
return super.createIntent(context, input).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
}
}
}
companion object {
private const val TAG = "OpenFileHelper"
@SuppressLint("InlinedApi")
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager,
typeString: String = "application/octet-stream"): Boolean {
return when {
// To check if a custom file manager can manage the ACTION_CREATE_DOCUMENT
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT -> {
packageManager.queryIntentActivities(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
}, PackageManager.MATCH_DEFAULT_ONLY).isNotEmpty()
}
else -> true
}
}
}
}

View File

@@ -4,23 +4,24 @@ import android.net.Uri
import android.os.Bundle
import androidx.activity.viewModels
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.getBinaryDir
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
protected var mDatabase: Database? = null
protected var mDatabase: ContextualDatabase? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDatabaseTaskProvider = DatabaseTaskProvider(this)
mDatabaseTaskProvider = DatabaseTaskProvider(this, showDatabaseDialog())
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
val databaseWasReloaded = database?.wasReloaded == true
@@ -36,14 +37,25 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
}
}
override fun onDatabaseRetrieved(database: Database?) {
protected open fun showDatabaseDialog(): Boolean {
return true
}
override fun onDestroy() {
mDatabaseTaskProvider?.destroy()
mDatabaseTaskProvider = null
mDatabase = null
super.onDestroy()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
mDatabase = database
mDatabaseViewModel.defineDatabase(database)
// optional method implementation
}
override fun onDatabaseActionFinished(
database: Database,
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
@@ -51,21 +63,25 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
// optional method implementation
}
fun createDatabase(databaseUri: Uri,
mainCredential: MainCredential) {
fun createDatabase(
databaseUri: Uri,
mainCredential: MainCredential
) {
mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential)
}
fun loadDatabase(databaseUri: Uri,
mainCredential: MainCredential,
readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean) {
fun loadDatabase(
databaseUri: Uri,
mainCredential: MainCredential,
readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean
) {
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
}
protected fun closeDatabase() {
mDatabase?.clearAndClose(this)
mDatabase?.clearAndClose(this.getBinaryDir())
}
override fun onResume() {

View File

@@ -24,32 +24,37 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.LockReceiver
import com.kunzisoft.keepass.utils.closeDatabase
import com.kunzisoft.keepass.utils.registerLockReceiver
import com.kunzisoft.keepass.utils.unregisterLockReceiver
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.NodesViewModel
import java.util.*
import java.util.UUID
abstract class DatabaseLockActivity : DatabaseModeActivity(),
PasswordEncodingDialogFragment.Listener {
@@ -65,8 +70,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
protected var mMergeDataAllowed: Boolean = false
private var mAutoSaveEnable: Boolean = true
protected var mIconDrawableFactory: IconDrawableFactory? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -88,8 +91,8 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabaseTaskProvider?.startDatabaseSave(save)
}
mDatabaseViewModel.mergeDatabase.observe(this) {
mDatabaseTaskProvider?.startDatabaseMerge()
mDatabaseViewModel.mergeDatabase.observe(this) { save ->
mDatabaseTaskProvider?.startDatabaseMerge(save)
}
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
@@ -165,7 +168,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
return true
}
override fun onDatabaseRetrieved(database: Database?) {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
// End activity if database not loaded
@@ -185,8 +188,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mLockReceiver = LockReceiver {
mDatabase = null
closeDatabase(database)
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
mExitLock = true
closeOptionsMenu()
finish()
@@ -205,16 +206,24 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabaseReadOnly = database.isReadOnly
mMergeDataAllowed = database.isMergeDataAllowed()
mIconDrawableFactory = database.iconDrawableFactory
checkRegister()
}
}
override fun finish() {
// To fix weird crash
try {
super.finish()
} catch (e: Exception) {
Log.e(TAG, "Unable to finish the activity", e)
}
}
abstract fun viewToInvalidateTimeout(): View?
override fun onDatabaseActionFinished(
database: Database,
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
@@ -225,6 +234,9 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
// Reload the current activity
if (result.isSuccess) {
reloadActivity()
if (actionTask == DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK) {
Toast.makeText(this, R.string.merge_success, Toast.LENGTH_LONG).show()
}
} else {
this.showActionErrorIfNeeded(result)
finish()
@@ -233,15 +245,19 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
}
override fun onPasswordEncodingValidateListener(databaseUri: Uri?,
mainCredential: MainCredential) {
override fun onPasswordEncodingValidateListener(
databaseUri: Uri?,
mainCredential: MainCredential
) {
assignDatabasePassword(databaseUri, mainCredential)
}
private fun assignDatabasePassword(databaseUri: Uri?,
mainCredential: MainCredential) {
private fun assignDatabasePassword(
databaseUri: Uri?,
mainCredential: MainCredential
) {
if (databaseUri != null) {
mDatabaseTaskProvider?.startDatabaseAssignPassword(databaseUri, mainCredential)
mDatabaseTaskProvider?.startDatabaseAssignCredential(databaseUri, mainCredential)
}
}
@@ -249,7 +265,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabase?.let { database ->
database.fileUri?.let { databaseUri ->
// Show the progress dialog now or after dialog confirmation
if (database.validatePasswordEncoding(mainCredential)) {
if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) {
assignDatabasePassword(databaseUri, mainCredential)
} else {
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
@@ -268,11 +284,11 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
fun mergeDatabase() {
mDatabaseTaskProvider?.startDatabaseMerge()
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable)
}
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
mDatabaseTaskProvider?.startDatabaseMerge(uri, mainCredential)
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable, uri, mainCredential)
}
fun reloadDatabase() {
@@ -301,7 +317,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable)
}
private fun eachNodeRecyclable(database: Database, nodes: List<Node>): Boolean {
private fun eachNodeRecyclable(database: ContextualDatabase, nodes: List<Node>): Boolean {
return nodes.find { node ->
var cannotRecycle = true
if (node is Entry) {
@@ -317,7 +333,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabase?.let { database ->
// If recycle bin enabled, ensure it exists
if (database.isRecycleBinEnabled) {
database.ensureRecycleBinExists(resources)
database.ensureRecycleBinExists(resources.getString(R.string.recycle_bin))
}
// If recycle bin enabled and not in recycle bin, move in recycle bin
@@ -399,8 +415,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
invalidateOptionsMenu()
LOCKING_ACTIVITY_UI_VISIBLE = true
}
protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) {
@@ -415,8 +429,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
override fun onPause() {
LOCKING_ACTIVITY_UI_VISIBLE = false
super.onPause()
if (mTimeoutEnable) {
@@ -431,7 +443,17 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
protected fun lockAndExit() {
sendBroadcast(Intent(LOCK_ACTION))
// Ask confirmation if modification not saved
if (mDatabase?.dataModifiedSinceLastLoading == true) {
AlertDialog.Builder(this)
.setMessage(R.string.discard_changes)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.lock) { _, _ ->
sendBroadcast(Intent(LOCK_ACTION))
}.create().show()
} else {
sendBroadcast(Intent(LOCK_ACTION))
}
}
fun resetAppTimeout() {
@@ -439,14 +461,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabase?.loaded ?: false)
}
override fun onBackPressed() {
override fun onDatabaseBackPressed() {
if (mTimeoutEnable) {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
mDatabase?.loaded == true) {
super.onBackPressed()
super.onDatabaseBackPressed()
}
} else {
super.onBackPressed()
super.onDatabaseBackPressed()
}
}
@@ -456,9 +478,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
private var LOCKING_ACTIVITY_UI_VISIBLE = false
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
}
}
@@ -467,25 +486,33 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
*/
@SuppressLint("ClickableViewAccessibility")
fun View.resetAppTimeoutWhenViewTouchedOrFocused(context: Context, databaseLoaded: Boolean?) {
// Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
databaseLoaded ?: false)
try {
// Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(
context,
databaseLoaded ?: false
)
}
}
false
}
setOnFocusChangeListener { _, _ ->
// Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(
context,
databaseLoaded ?: false
)
}
if (this is ViewGroup) {
for (i in 0..childCount) {
getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded)
}
}
false
}
setOnFocusChangeListener { _, _ ->
// Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
databaseLoaded ?: false)
}
if (this is ViewGroup) {
for (i in 0..childCount) {
getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded)
}
} catch (e: Exception) {
Log.e("AppTimeout", "Unable to reset app timeout", e)
}
}

View File

@@ -1,17 +1,16 @@
package com.kunzisoft.keepass.activities.legacy
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.helpers.TypeMode
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.SpecialModeView
import com.kunzisoft.keepass.view.ToolbarSpecial
/**
@@ -22,20 +21,22 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
private var mTypeMode: TypeMode = TypeMode.DEFAULT
private var mSpecialModeView: SpecialModeView? = null
private var mToolbarSpecial: ToolbarSpecial? = null
override fun onBackPressed() {
open fun onDatabaseBackPressed() {
if (mSpecialMode != SpecialMode.DEFAULT)
onCancelSpecialMode()
else
super.onBackPressed()
onRegularBackPressed()
}
/**
* To call the regular onBackPressed() method in special mode
*/
protected fun onRegularBackPressed() {
super.onBackPressed()
// Do not call onBackPressedDispatcher.onBackPressed() to avoid loop
// Calling onBackPressed() is now deprecated, directly finish the activity
finish()
}
/**
@@ -74,7 +75,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
open fun onCancelSpecialMode() {
if (isIntentSender()) {
// To get the app caller, only for IntentSender
super.onBackPressed()
onRegularBackPressed()
} else {
EntrySelectionHelper.removeModesFromIntent(intent)
EntrySelectionHelper.removeInfoFromIntent(intent)
@@ -87,7 +88,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
protected fun backToTheAppCaller() {
if (isIntentSender()) {
// To get the app caller, only for IntentSender
super.onBackPressed()
onRegularBackPressed()
} else {
backToTheMainAppAndFinish()
}
@@ -96,15 +97,18 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
private fun backToTheMainAppAndFinish() {
// To move the app in background and return to the main app
moveTaskToBack(true)
// To remove this instance in the OS app selector
Handler(Looper.getMainLooper()).postDelayed({
finish()
}, 500)
// Not using FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS or finish() because kills the service
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
onDatabaseBackPressed()
}
})
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
}
@@ -118,8 +122,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
// To show the selection mode
mSpecialModeView = findViewById(R.id.special_mode_view)
mSpecialModeView?.apply {
mToolbarSpecial = findViewById(R.id.special_mode_view)
mToolbarSpecial?.apply {
// Populate title
val selectionModeStringId = when (mSpecialMode) {
SpecialMode.DEFAULT, // Not important because hidden

View File

@@ -1,11 +1,11 @@
package com.kunzisoft.keepass.activities.legacy
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.tasks.ActionRunnable
interface DatabaseRetrieval {
fun onDatabaseRetrieved(database: Database?)
fun onDatabaseActionFinished(database: Database,
fun onDatabaseRetrieved(database: ContextualDatabase?)
fun onDatabaseActionFinished(database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result)
}

View File

@@ -23,6 +23,7 @@ import android.content.Context
import android.content.res.Configuration
import android.util.Log
import androidx.annotation.StyleRes
import com.google.android.material.color.DynamicColors
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.PreferencesUtil
@@ -38,7 +39,7 @@ object Stylish {
* @param context Context to retrieve the theme preference
*/
fun load(context: Context) {
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
Log.d(Stylish::class.java.name, "Attaching to " + context.packageName)
try {
themeString = PreferencesUtil.getStyle(context)
} catch (e: Exception) {
@@ -46,7 +47,7 @@ object Stylish {
}
}
fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
fun retrieveEquivalentSystemStyle(context: Context, styleString: String?): String {
val systemNightMode = when (PreferencesUtil.getStyleBrightness(context)) {
context.getString(R.string.list_style_brightness_light) -> false
context.getString(R.string.list_style_brightness_night) -> true
@@ -58,14 +59,21 @@ object Stylish {
}
}
return if (systemNightMode) {
retrieveEquivalentNightStyle(context, styleString)
retrieveEquivalentNightStyle(
context,
styleString ?: context.getString(R.string.list_style_name_night)
)
} else {
retrieveEquivalentLightStyle(context, styleString)
retrieveEquivalentLightStyle(
context,
styleString ?: context.getString(R.string.list_style_name_light)
)
}
}
fun retrieveEquivalentLightStyle(context: Context, styleString: String): String {
return when (styleString) {
context.getString(R.string.list_style_name_dynamic_night) -> context.getString(R.string.list_style_name_dynamic_light)
context.getString(R.string.list_style_name_night) -> context.getString(R.string.list_style_name_light)
context.getString(R.string.list_style_name_black) -> context.getString(R.string.list_style_name_white)
context.getString(R.string.list_style_name_dark) -> context.getString(R.string.list_style_name_clear)
@@ -80,6 +88,7 @@ object Stylish {
private fun retrieveEquivalentNightStyle(context: Context, styleString: String): String {
return when (styleString) {
context.getString(R.string.list_style_name_dynamic_light) -> context.getString(R.string.list_style_name_dynamic_night)
context.getString(R.string.list_style_name_light) -> context.getString(R.string.list_style_name_night)
context.getString(R.string.list_style_name_white) -> context.getString(R.string.list_style_name_black)
context.getString(R.string.list_style_name_clear) -> context.getString(R.string.list_style_name_dark)
@@ -104,6 +113,13 @@ object Stylish {
PreferencesUtil.setStyle(context, styleString)
}
fun isDynamic(context: Context): Boolean {
return DynamicColors.isDynamicColorAvailable() && (
themeString == context.getString(R.string.list_style_name_dynamic_night)
|| themeString == context.getString(R.string.list_style_name_dynamic_light)
)
}
/**
* Function that returns the current id of the style selected in the preference
* @param context Context to retrieve the id
@@ -111,7 +127,7 @@ object Stylish {
*/
@StyleRes
fun getThemeId(context: Context): Int {
return when (retrieveEquivalentSystemStyle(context, themeString ?: context.getString(R.string.list_style_name_light))) {
return when (retrieveEquivalentSystemStyle(context, themeString)) {
context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night
context.getString(R.string.list_style_name_white) -> R.style.KeepassDXStyle_White
context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black
@@ -127,6 +143,8 @@ object Stylish {
context.getString(R.string.list_style_name_reply_night) -> R.style.KeepassDXStyle_Reply_Night
context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple
context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark
context.getString(R.string.list_style_name_dynamic_light) -> R.style.KeepassDXStyle_Light_Dynamic
context.getString(R.string.list_style_name_dynamic_night) -> R.style.KeepassDXStyle_Night_Dynamic
else -> R.style.KeepassDXStyle_Light
}
}

View File

@@ -21,14 +21,23 @@ package com.kunzisoft.keepass.activities.stylish
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.WindowManager
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.WindowManager.LayoutParams.FLAG_SECURE
import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
import com.google.android.material.color.DynamicColors
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_PREFERENCE_CHANGED
import com.kunzisoft.keepass.settings.PreferencesUtil
/**
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
@@ -69,7 +78,18 @@ abstract class StylishActivity : AppCompatActivity() {
startActivity(intent)
}
finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
overrideActivityTransition(
OVERRIDE_TRANSITION_OPEN,
android.R.anim.fade_in,
android.R.anim.fade_out
)
else
overridePendingTransition(
android.R.anim.fade_in,
android.R.anim.fade_out
)
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -77,12 +97,33 @@ abstract class StylishActivity : AppCompatActivity() {
customStyle = applyCustomStyle()
if (customStyle) {
// Preconfigured themes
this.themeId = Stylish.getThemeId(this)
setTheme(themeId)
if (Stylish.isDynamic(this)) {
// Material You theme
DynamicColors.applyToActivityIfAvailable(this)
}
}
PreferenceManager.getDefaultSharedPreferences(this)
.registerOnSharedPreferenceChangeListener(onScreenshotModePrefListener)
}
private val onScreenshotModePrefListener = OnSharedPreferenceChangeListener { _, key ->
if (key != getString(R.string.enable_screenshot_mode_key)) return@OnSharedPreferenceChangeListener
setScreenshotMode(PreferencesUtil.isScreenshotModeEnabled(this))
}
private fun setScreenshotMode(isEnabled: Boolean) {
findViewById<View>(R.id.screenshot_mode_banner)?.visibility = if (isEnabled) VISIBLE else GONE
// Several gingerbread devices have problems with FLAG_SECURE
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
if (isEnabled) {
window.clearFlags(FLAG_SECURE)
} else {
window.setFlags(FLAG_SECURE, FLAG_SECURE)
}
}
override fun onResume() {
@@ -94,6 +135,7 @@ abstract class StylishActivity : AppCompatActivity() {
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
recreateActivity()
}
setScreenshotMode(PreferencesUtil.isScreenshotModeEnabled(this))
}
private fun recreateActivity() {

View File

@@ -1,99 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.stylish
import android.content.Context
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StyleRes
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment
abstract class StylishFragment : Fragment() {
@StyleRes
protected var themeId: Int = 0
protected var contextThemed: Context? = null
override fun onAttach(context: Context) {
super.onAttach(context)
this.themeId = Stylish.getThemeId(context)
contextThemed = ContextThemeWrapper(context, themeId)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// To fix status bar color
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val window = requireActivity().window
val defaultColor = Color.BLACK
val windowInset = WindowInsetsControllerCompat(window, window.decorView)
try {
val taStatusBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor))
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
taStatusBarColor?.recycle()
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve theme : status bar color", e)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
windowInset.isAppearanceLightStatusBars = taWindowStatusLight
?.getBoolean(0, false) == true
taWindowStatusLight?.recycle()
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve theme : window light status bar", e)
}
}
try {
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
taNavigationBarColor?.recycle()
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve theme : navigation bar color", e)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
try {
val taWindowLightNavigationBar = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightNavigationBar))
windowInset.isAppearanceLightNavigationBars = taWindowLightNavigationBar
?.getBoolean(0, false) == true
taWindowLightNavigationBar?.recycle()
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve theme : navigation light navigation bar", e)
}
}
}
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onDetach() {
contextThemed = null
super.onDetach()
}
companion object {
private val TAG = StylishFragment::class.java.simpleName
}
}

View File

@@ -10,6 +10,7 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type
@@ -17,7 +18,7 @@ import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.strikeOut
class BreadcrumbAdapter(val context: Context)
class BreadcrumbAdapter(val context: Context, val database: Database?)
: RecyclerView.Adapter<BreadcrumbAdapter.BreadcrumbGroupViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
@@ -31,6 +32,8 @@ class BreadcrumbAdapter(val context: Context)
var onItemClickListener: ((item: Node, position: Int)->Unit)? = null
var onLongItemClickListener: ((item: Node, position: Int)->Unit)? = null
private var mNodeFilter: NodeFilter = NodeFilter(context, database)
private var mShowNumberEntries = false
private var mShowUUID = false
private var mIconColor: Int = 0
@@ -39,10 +42,10 @@ class BreadcrumbAdapter(val context: Context)
mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
mShowUUID = PreferencesUtil.showUUID(context)
// Retrieve the textColor to tint the icon
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
mIconColor = taTextColor.getColor(0, Color.WHITE)
taTextColor.recycle()
// Retrieve the color to tint the icon
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnSurface))
mIconColor = taIconColor.getColor(0, Color.WHITE)
taIconColor.recycle()
}
@SuppressLint("NotifyDataSetChanged")
@@ -71,7 +74,7 @@ class BreadcrumbAdapter(val context: Context)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BreadcrumbGroupViewHolder {
return BreadcrumbGroupViewHolder(inflater.inflate(
when (viewType) {
0 -> R.layout.item_group
0 -> R.layout.item_breadcrumb_important
else -> R.layout.item_breadcrumb
}, parent, false)
)
@@ -112,8 +115,10 @@ class BreadcrumbAdapter(val context: Context)
holder.groupNumbersView?.apply {
if (mShowNumberEntries) {
group.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context))
text = group.numberOfChildEntries.toString()
text = group.getNumberOfChildEntries(
mNodeFilter.recursiveNumberOfEntries,
mNodeFilter.filter
).toString()
visibility = View.VISIBLE
} else {
visibility = View.GONE

View File

@@ -27,16 +27,18 @@ import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.progressindicator.CircularProgressIndicator
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.ImageViewerActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.FILE_PROGRESSION_MAX
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import com.kunzisoft.keepass.view.expand
import kotlin.math.max
@@ -45,7 +47,7 @@ import kotlin.math.max
class EntryAttachmentsItemsAdapter(context: Context)
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
var database: Database? = null
var database: ContextualDatabase? = null
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
var onBinaryPreviewLoaded: ((item: EntryAttachmentState) -> Unit)? = null
@@ -130,13 +132,14 @@ class EntryAttachmentsItemsAdapter(context: Context)
holder.binaryFileSize.text = Formatter.formatFileSize(context, size)
holder.binaryFileCompression.apply {
if (entryAttachmentState.attachment.binaryData.isCompressed) {
text = CompressionAlgorithm.GZip.getName(context.resources)
text = CompressionAlgorithm.GZIP.getLocalizedName(context.resources)
visibility = View.VISIBLE
} else {
text = ""
visibility = View.GONE
}
}
holder.binaryFileProgress.max = FILE_PROGRESSION_MAX
when (entryAttachmentState.streamDirection) {
StreamDirection.UPLOAD -> {
holder.binaryFileProgressIcon.isActivated = true
@@ -181,7 +184,7 @@ class EntryAttachmentsItemsAdapter(context: Context)
AttachmentState.START,
AttachmentState.IN_PROGRESS -> View.VISIBLE
}
progress = entryAttachmentState.downloadProgression
setProgressCompat(entryAttachmentState.downloadProgression, true)
}
holder.binaryFileInfo.setOnClickListener {
onItemClickListener?.invoke(entryAttachmentState)
@@ -200,7 +203,7 @@ class EntryAttachmentsItemsAdapter(context: Context)
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
var binaryFileProgressContainer: View = itemView.findViewById(R.id.item_attachment_progress_container)
var binaryFileProgressIcon: ImageView = itemView.findViewById(R.id.item_attachment_icon)
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
var binaryFileProgress: CircularProgressIndicator = itemView.findViewById(R.id.item_attachment_progress)
var binaryFileDeleteButton: View = itemView.findViewById(R.id.item_attachment_delete_button)
}
}
}

View File

@@ -27,6 +27,7 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() {

View File

@@ -23,12 +23,14 @@ import android.content.Context
import android.graphics.Color
import android.graphics.PorterDuff
import android.net.Uri
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.annotation.ColorInt
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import android.widget.ViewSwitcher
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback
@@ -84,20 +86,6 @@ class FileDatabaseHistoryAdapter(context: Context)
}
)
@ColorInt
private val defaultColor: Int
@ColorInt
private val warningColor: Int
init {
val typedValue = TypedValue()
val theme = context.theme
theme.resolveAttribute(R.attr.colorAccent, typedValue, true)
warningColor = typedValue.data
theme.resolveAttribute(android.R.attr.textColorHintInverse, typedValue, true)
defaultColor = typedValue.data
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileDatabaseHistoryViewHolder {
val view = inflater.inflate(R.layout.item_file_info, parent, false)
return FileDatabaseHistoryViewHolder(view)

View File

@@ -0,0 +1,25 @@
package com.kunzisoft.keepass.adapters
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.kunzisoft.keepass.activities.fragments.PassphraseGeneratorFragment
import com.kunzisoft.keepass.activities.fragments.PasswordGeneratorFragment
import com.kunzisoft.keepass.activities.fragments.KeyGeneratorFragment
class KeyGeneratorPagerAdapter(fragment: Fragment)
: FragmentStateAdapter(fragment) {
private val passwordGeneratorFragment = PasswordGeneratorFragment()
private val passphraseGeneratorFragment = PassphraseGeneratorFragment()
override fun getItemCount(): Int {
return KeyGeneratorFragment.KeyGeneratorTab.values().size
}
override fun createFragment(position: Int): Fragment {
return when (KeyGeneratorFragment.KeyGeneratorTab.getKeyGeneratorTabByPosition(position)) {
KeyGeneratorFragment.KeyGeneratorTab.PASSWORD -> passwordGeneratorFragment
KeyGeneratorFragment.KeyGeneratorTab.PASSPHRASE -> passphraseGeneratorFragment
}
}
}

View File

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

View File

@@ -20,21 +20,24 @@
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Build
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.ColorInt
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback
import com.google.android.material.progressindicator.CircularProgressIndicator
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum
@@ -42,26 +45,29 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.setTextSize
import com.kunzisoft.keepass.view.strikeOut
import java.util.*
import java.util.LinkedList
/**
* Create node list adapter with contextMenu or not
* @param context Context to use
*/
class NodesAdapter (private val context: Context,
private val database: Database)
: RecyclerView.Adapter<NodesAdapter.NodeViewHolder>() {
class NodesAdapter (
private val context: Context,
private val database: ContextualDatabase
) : RecyclerView.Adapter<NodesAdapter.NodeViewHolder>() {
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
private val mNodeSortedListCallback: NodeSortedListCallback
private val mNodeSortedList: SortedList<Node>
private val mInflater: LayoutInflater = LayoutInflater.from(context)
private val mNodeFilter: NodeFilter = NodeFilter(context, database)
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
@@ -78,7 +84,7 @@ class NodesAdapter (private val context: Context,
private var mShowNumberEntries: Boolean = true
private var mShowOTP: Boolean = false
private var mShowUUID: Boolean = false
private var mEntryFilters = arrayOf<Group.ChildFilter>()
private var mNodeFilters: NodeFilter? = null
private var mOldVirtualGroup = false
private var mVirtualGroup = false
@@ -86,6 +92,8 @@ class NodesAdapter (private val context: Context,
private var mNodeClickCallback: NodeClickCallback? = null
private var mClipboardHelper = ClipboardHelper(context)
@ColorInt
private val mColorSurfaceContainer: Int
@ColorInt
private val mTextColorPrimary: Int
@ColorInt
@@ -93,9 +101,9 @@ class NodesAdapter (private val context: Context,
@ColorInt
private val mTextColorSecondary: Int
@ColorInt
private val mColorAccentLight: Int
private val mColorSecondary: Int
@ColorInt
private val mColorOnAccentColor: Int
private val mColorOnSecondary: Int
/**
* Determine if the adapter contains or not any element
@@ -112,26 +120,29 @@ class NodesAdapter (private val context: Context,
this.mNodeSortedListCallback = NodeSortedListCallback()
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
val taColorSurfaceContainer = context.obtainStyledAttributes(intArrayOf(R.attr.colorSurfaceContainer))
this.mColorSurfaceContainer = taColorSurfaceContainer.getColor(0, Color.BLACK)
taColorSurfaceContainer.recycle()
// Retrieve the color to tint the icon
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
val taTextColorPrimary = context.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
this.mTextColorPrimary = taTextColorPrimary.getColor(0, Color.BLACK)
taTextColorPrimary.recycle()
// To get text color
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
val taTextColor = context.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
this.mTextColor = taTextColor.getColor(0, Color.BLACK)
taTextColor.recycle()
// To get text color secondary
val taTextColorSecondary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorSecondary))
val taTextColorSecondary = context.obtainStyledAttributes(intArrayOf(android.R.attr.textColorSecondary))
this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK)
taTextColorSecondary.recycle()
// To get background color for selection
val taColorAccentLight = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccentLight))
this.mColorAccentLight = taColorAccentLight.getColor(0, Color.GRAY)
taColorAccentLight.recycle()
val taColorSecondary = context.obtainStyledAttributes(intArrayOf(R.attr.colorSecondary))
this.mColorSecondary = taColorSecondary.getColor(0, Color.GRAY)
taColorSecondary.recycle()
// To get text color for selection
val taColorOnAccentColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnAccentColor))
this.mColorOnAccentColor = taColorOnAccentColor.getColor(0, Color.WHITE)
taColorOnAccentColor.recycle()
val taColorOnSecondary = context.obtainStyledAttributes(intArrayOf(R.attr.colorOnSecondary))
this.mColorOnSecondary = taColorOnSecondary.getColor(0, Color.WHITE)
taColorOnSecondary.recycle()
}
private fun assignPreferences() {
@@ -152,7 +163,7 @@ class NodesAdapter (private val context: Context,
this.mShowOTP = PreferencesUtil.showOTPToken(context)
this.mShowUUID = PreferencesUtil.showUUID(context)
this.mEntryFilters = Group.ChildFilter.getDefaults(context)
this.mNodeFilters = NodeFilter(context, database)
// Reinit textSize for all view type
mCalculateViewTypeTextSize.forEachIndexed { index, _ -> mCalculateViewTypeTextSize[index] = true }
@@ -165,7 +176,7 @@ class NodesAdapter (private val context: Context,
mOldVirtualGroup = mVirtualGroup
mVirtualGroup = group.isVirtual
assignPreferences()
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
mNodeSortedList.replaceAll(group.getChildren(mNodeFilter.filter))
}
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
@@ -186,6 +197,7 @@ class NodesAdapter (private val context: Context,
&& oldItem.containsAttachment() == newItem.containsAttachment()
} else if (oldItem is Group && newItem is Group) {
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
&& oldItem.recursiveNumberOfChildEntries == newItem.recursiveNumberOfChildEntries
&& oldItem.notes == newItem.notes
}
return typeContentTheSame
@@ -193,6 +205,11 @@ class NodesAdapter (private val context: Context,
&& oldItem.type == newItem.type
&& oldItem.title == newItem.title
&& oldItem.icon == newItem.icon
&& oldItem.creationTime == newItem.creationTime
&& oldItem.lastModificationTime == newItem.lastModificationTime
&& oldItem.lastAccessTime == newItem.lastAccessTime
&& oldItem.expiryTime == newItem.expiryTime
&& oldItem.expires == newItem.expires
&& oldItem.isCurrentlyExpires == newItem.isCurrentlyExpires
}
@@ -376,10 +393,10 @@ class NodesAdapter (private val context: Context,
// Assign icon colors
var iconColor = if (holder.container.isSelected)
mColorOnAccentColor
mColorOnSecondary
else when (subNode.type) {
Type.GROUP -> mTextColorPrimary
Type.ENTRY -> mTextColor
Type.GROUP -> mTextColor
Type.ENTRY -> mColorSecondary
}
// Specific elements for entry
@@ -424,16 +441,8 @@ class NodesAdapter (private val context: Context,
if (entry.containsAttachment()) View.VISIBLE else View.GONE
// Assign colors
val backgroundColor = if (mShowEntryColors) entry.backgroundColor else null
if (!holder.container.isSelected) {
if (backgroundColor != null) {
holder.container.setBackgroundColor(backgroundColor)
} else {
holder.container.setBackgroundColor(Color.TRANSPARENT)
}
} else {
holder.container.setBackgroundColor(mColorAccentLight)
}
assignBackgroundColor(holder.container, entry)
assignBackgroundColor(holder.otpContainer, entry)
val foregroundColor = if (mShowEntryColors) entry.foregroundColor else null
if (!holder.container.isSelected) {
if (foregroundColor != null) {
@@ -453,12 +462,12 @@ class NodesAdapter (private val context: Context,
holder.meta.setTextColor(mTextColor)
}
} else {
holder.text.setTextColor(mColorOnAccentColor)
holder.subText?.setTextColor(mColorOnAccentColor)
holder.otpToken?.setTextColor(mColorOnAccentColor)
holder.otpProgress?.setIndicatorColor(mColorOnAccentColor)
holder.attachmentIcon?.setColorFilter(mColorOnAccentColor)
holder.meta.setTextColor(mColorOnAccentColor)
holder.text.setTextColor(mColorOnSecondary)
holder.subText?.setTextColor(mColorOnSecondary)
holder.otpToken?.setTextColor(mColorOnSecondary)
holder.otpProgress?.setIndicatorColor(mColorOnSecondary)
holder.attachmentIcon?.setColorFilter(mColorOnSecondary)
holder.meta.setTextColor(mColorOnSecondary)
}
database.stopManageEntry(entry)
@@ -469,7 +478,10 @@ class NodesAdapter (private val context: Context,
if (mShowNumberEntries) {
holder.numberChildren?.apply {
text = (subNode as Group)
.numberOfChildEntries
.getNumberOfChildEntries(
mNodeFilter.recursiveNumberOfEntries,
mNodeFilter.filter
)
.toString()
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
visibility = View.VISIBLE
@@ -516,22 +528,42 @@ class NodesAdapter (private val context: Context,
null -> {}
}
holder?.otpToken?.apply {
text = otpElement?.token
text = otpElement?.tokenString
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
textDirection = View.TEXT_DIRECTION_LTR
}
holder?.otpContainer?.setOnClickListener {
otpElement?.token?.let { token ->
Toast.makeText(
context,
context.getString(R.string.copy_field,
TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN)),
Toast.LENGTH_LONG
).show()
mClipboardHelper.copyToClipboard(token)
try {
mClipboardHelper.copyToClipboard(
TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN),
token,
true
)
} catch (e: Exception) {
Log.e(TAG, "Unable to copy the OTP token", e)
}
}
}
}
private fun assignBackgroundColor(view: View?, entry: Entry) {
view?.let {
ViewCompat.setBackgroundTintList(
view,
ColorStateList.valueOf(
if (!view.isSelected) {
(if (mShowEntryColors) entry.backgroundColor else null)
?: mColorSurfaceContainer
} else {
mColorSecondary
}
)
)
}
}
class OtpRunnable(val view: View?): Runnable {
var action: (() -> Unit)? = null
@@ -561,8 +593,8 @@ class NodesAdapter (private val context: Context,
* Callback listener to redefine to do an action when a node is click
*/
interface NodeClickCallback {
fun onNodeClick(database: Database, node: Node)
fun onNodeLongClick(database: Database, node: Node): Boolean
fun onNodeClick(database: ContextualDatabase, node: Node)
fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean
}
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

View File

@@ -11,11 +11,12 @@ import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.icons.IconDrawableFactory
class TemplatesSelectorAdapter(
context: Context,
private val context: Context,
private var templates: List<Template>): BaseAdapter() {
var iconDrawableFactory: IconDrawableFactory? = null
@@ -35,7 +36,9 @@ class TemplatesSelectorAdapter(
var templateView = convertView
if (templateView == null) {
holder = TemplateSelectorViewHolder()
templateView = inflater.inflate(R.layout.item_template, parent, false)
templateView = inflater
.cloneInContext(context)
.inflate(R.layout.item_template, parent, false)
holder.background = templateView?.findViewById(R.id.template_background)
holder.icon = templateView?.findViewById(R.id.template_image)
holder.name = templateView?.findViewById(R.id.template_name)
@@ -74,4 +77,4 @@ class TemplatesSelectorAdapter(
var icon: ImageView? = null
var name: TextView? = null
}
}
}

View File

@@ -19,15 +19,53 @@
*/
package com.kunzisoft.keepass.app
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication
import com.kunzisoft.keepass.activities.stylish.Stylish
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
class App : MultiDexApplication() {
override fun onCreate() {
super.onCreate()
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver)
Stylish.load(this)
PRNGFixes.apply()
}
}
object AppLifecycleObserver : DefaultLifecycleObserver {
var isAppInForeground: Boolean = false
private set
var lockBackgroundEvent = false
private val _appJustLaunched = MutableSharedFlow<Unit>(replay = 0)
val appJustLaunched = _appJustLaunched.asSharedFlow()
@OptIn(DelicateCoroutinesApi::class)
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
val wasPreviouslyInBackground = !isAppInForeground
isAppInForeground = true
if (!lockBackgroundEvent && wasPreviouslyInBackground) {
GlobalScope.launch {
_appJustLaunched.emit(Unit)
}
}
}
override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
isAppInForeground = false
}
}

View File

@@ -1,399 +0,0 @@
package com.kunzisoft.keepass.app;
/*
* This software is provided 'as-is', without any express or implied
* warranty. In no event will Google be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, as long as the origin is not misrepresented.
*/
import android.os.Build;
import android.os.Process;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.SecureRandomSpi;
import java.security.Security;
import java.util.Locale;
/**
* Fixes for the output of the default PRNG having low entropy.
*
* The fixes need to be applied via {@link #apply()} before any use of Java
* Cryptography Architecture primitives. A good place to invoke them is in the
* application's {@code onCreate}.
*/
public final class PRNGFixes {
private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
getBuildFingerprintAndDeviceSerial();
/** Hidden constructor to prevent instantiation. */
private PRNGFixes() {}
/**
* Applies all fixes.
*
* @throws SecurityException if a fix is needed but could not be applied.
*/
public static void apply() {
try {
if (supportedOnThisDevice()) {
applyOpenSSLFix();
installLinuxPRNGSecureRandom();
}
} catch (Exception e) {
// Do nothing, do the best we can to implement the workaround
}
}
private static boolean supportedOnThisDevice() {
// Blacklist on samsung devices
if (Build.MANUFACTURER.toLowerCase(Locale.ENGLISH).contains("samsung")) {
return false;
}
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
return false;
}
if (onSELinuxEnforce()) {
return false;
}
File urandom = new File("/dev/urandom");
// Test permissions
if ( !(urandom.canRead() && urandom.canWrite()) ) {
return false;
}
// Test actually writing to urandom
try {
FileOutputStream fos = new FileOutputStream(urandom);
fos.write(0);
} catch (Exception e) {
return false;
}
return true;
}
private static boolean onSELinuxEnforce() {
try {
ProcessBuilder builder = new ProcessBuilder("getenforce");
builder.redirectErrorStream(true);
java.lang.Process process = builder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
process.waitFor();
String output = reader.readLine();
if (output == null) {
return false;
}
return output.toLowerCase(Locale.US).startsWith("enforcing");
} catch (Exception e) {
return false;
}
}
/**
* Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
* fix is not needed.
*
* @throws SecurityException if the fix is needed but could not be applied.
*/
private static void applyOpenSSLFix() throws SecurityException {
if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
|| (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2)) {
// No need to apply the fix
return;
}
try {
// Mix in the device- and invocation-specific seed.
Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
.getMethod("RAND_seed", byte[].class)
.invoke(null, generateSeed());
// Mix output of Linux PRNG into OpenSSL's PRNG
int bytesRead = (Integer) Class.forName(
"org.apache.harmony.xnet.provider.jsse.NativeCrypto")
.getMethod("RAND_load_file", String.class, long.class)
.invoke(null, "/dev/urandom", 1024);
if (bytesRead != 1024) {
throw new IOException(
"Unexpected number of bytes read from Linux PRNG: "
+ bytesRead);
}
} catch (Exception e) {
throw new SecurityException("Failed to seed OpenSSL PRNG", e);
}
}
/**
* Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
* default. Does nothing if the implementation is already the default or if
* there is not need to install the implementation.
*
* @throws SecurityException if the fix is needed but could not be applied.
*/
private static void installLinuxPRNGSecureRandom()
throws SecurityException {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
// No need to apply the fix
return;
}
// Install a Linux PRNG-based SecureRandom implementation as the
// default, if not yet installed.
Provider[] secureRandomProviders =
Security.getProviders("SecureRandom.SHA1PRNG");
if ((secureRandomProviders == null)
|| (secureRandomProviders.length < 1)
|| (!LinuxPRNGSecureRandomProvider.class.equals(
secureRandomProviders[0].getClass()))) {
Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
}
// Assert that new SecureRandom() and
// SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
// by the Linux PRNG-based SecureRandom implementation.
SecureRandom rng1 = new SecureRandom();
if (!LinuxPRNGSecureRandomProvider.class.equals(
rng1.getProvider().getClass())) {
throw new SecurityException(
"new SecureRandom() backed by wrong Provider: "
+ rng1.getProvider().getClass());
}
SecureRandom rng2;
try {
rng2 = SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
throw new SecurityException("SHA1PRNG not available", e);
}
if (!LinuxPRNGSecureRandomProvider.class.equals(
rng2.getProvider().getClass())) {
throw new SecurityException(
"SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
+ " Provider: " + rng2.getProvider().getClass());
}
}
/**
* {@code Provider} of {@code SecureRandom} engines which pass through
* all requests to the Linux PRNG.
*/
private static class LinuxPRNGSecureRandomProvider extends Provider {
public LinuxPRNGSecureRandomProvider() {
super("LinuxPRNG",
1.0,
"A Linux-specific random number provider that uses"
+ " /dev/urandom");
// Although /dev/urandom is not a SHA-1 PRNG, some apps
// explicitly request a SHA1PRNG SecureRandom and we thus need to
// prevent them from getting the default implementation whose output
// may have low entropy.
put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
}
}
/**
* {@link SecureRandomSpi} which passes all requests to the Linux PRNG
* ({@code /dev/urandom}).
*/
public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
/*
* IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
* are passed through to the Linux PRNG (/dev/urandom). Instances of
* this class seed themselves by mixing in the current time, PID, UID,
* build fingerprint, and hardware serial number (where available) into
* Linux PRNG.
*
* Concurrency: Read requests to the underlying Linux PRNG are
* serialized (on sLock) to ensure that multiple threads do not get
* duplicated PRNG output.
*/
private static final File URANDOM_FILE = new File("/dev/urandom");
private static final Object sLock = new Object();
/**
* Input stream for reading from Linux PRNG or {@code null} if not yet
* opened.
*
* @GuardedBy("sLock")
*/
private static DataInputStream sUrandomIn;
/**
* Output stream for writing to Linux PRNG or {@code null} if not yet
* opened.
*
* @GuardedBy("sLock")
*/
private static OutputStream sUrandomOut;
/**
* Whether this engine instance has been seeded. This is needed because
* each instance needs to seed itself if the client does not explicitly
* seed it.
*/
private boolean mSeeded;
@Override
protected void engineSetSeed(byte[] bytes) {
try {
OutputStream out;
synchronized (sLock) {
out = getUrandomOutputStream();
}
out.write(bytes);
out.flush();
mSeeded = true;
} catch (IOException e) {
throw new SecurityException(
"Failed to mix seed into " + URANDOM_FILE, e);
}
}
@Override
protected void engineNextBytes(byte[] bytes) {
if (!mSeeded) {
// Mix in the device- and invocation-specific seed.
engineSetSeed(generateSeed());
}
try {
DataInputStream in;
synchronized (sLock) {
in = getUrandomInputStream();
}
synchronized (in) {
in.readFully(bytes);
}
} catch (IOException e) {
throw new SecurityException(
"Failed to read from " + URANDOM_FILE, e);
}
}
@Override
protected byte[] engineGenerateSeed(int size) {
byte[] seed = new byte[size];
engineNextBytes(seed);
return seed;
}
private DataInputStream getUrandomInputStream() {
synchronized (sLock) {
if (sUrandomIn == null) {
// NOTE: Consider inserting a BufferedInputStream between
// DataInputStream and FileInputStream if you need higher
// PRNG output performance and can live with future PRNG
// output being pulled into this process prematurely.
try {
sUrandomIn = new DataInputStream(
new FileInputStream(URANDOM_FILE));
} catch (IOException e) {
throw new SecurityException("Failed to open "
+ URANDOM_FILE + " for reading", e);
}
}
return sUrandomIn;
}
}
private OutputStream getUrandomOutputStream() {
synchronized (sLock) {
if (sUrandomOut == null) {
try {
sUrandomOut = new FileOutputStream(URANDOM_FILE);
} catch (IOException e) {
throw new SecurityException("Failed to open "
+ URANDOM_FILE + " for writing", e);
}
}
return sUrandomOut;
}
}
}
/**
* Generates a device- and invocation-specific seed to be mixed into the
* Linux PRNG.
*/
private static byte[] generateSeed() {
try {
ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
DataOutputStream seedBufferOut =
new DataOutputStream(seedBuffer);
seedBufferOut.writeLong(System.currentTimeMillis());
seedBufferOut.writeLong(System.nanoTime());
seedBufferOut.writeInt(Process.myPid());
seedBufferOut.writeInt(Process.myUid());
seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
seedBufferOut.close();
return seedBuffer.toByteArray();
} catch (IOException e) {
throw new SecurityException("Failed to generate seed", e);
}
}
/**
* Gets the hardware serial number of this device.
*
* @return serial number or {@code null} if not available.
*/
private static String getDeviceSerialNumber() {
// We're using the Reflection API because Build.SERIAL is only available
// since API Level 9 (Gingerbread, Android 2.3).
try {
return (String) Build.class.getField("SERIAL").get(null);
} catch (Exception ignored) {
return null;
}
}
private static byte[] getBuildFingerprintAndDeviceSerial() {
StringBuilder result = new StringBuilder();
String fingerprint = Build.FINGERPRINT;
if (fingerprint != null) {
result.append(fingerprint);
}
String serial = getDeviceSerialNumber();
if (serial != null) {
result.append(serial);
}
try {
return result.toString().getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-8 encoding not supported");
}
}
}

View File

@@ -23,8 +23,16 @@ import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import android.content.Context
import androidx.room.AutoMigration
@Database(version = 1, entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class])
@Database(
version = 3,
entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class],
autoMigrations = [
AutoMigration (from = 1, to = 2),
AutoMigration (from = 2, to = 3)
]
)
abstract class AppDatabase : RoomDatabase() {
abstract fun fileDatabaseHistoryDao(): FileDatabaseHistoryDao

View File

@@ -19,39 +19,44 @@
*/
package com.kunzisoft.keepass.app.database
import android.content.*
import android.content.ComponentName
import android.content.Context
import android.content.IntentFilter
import android.content.ServiceConnection
import android.net.Uri
import android.os.IBinder
import android.util.Base64
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.kunzisoft.keepass.database.element.binary.BinaryData.Companion.BASE64_FLAG
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.services.DeviceUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.IOActionTask
import com.kunzisoft.keepass.utils.SingletonHolderParameter
import java.util.*
import java.util.LinkedList
class CipherDatabaseAction(context: Context) {
private val applicationContext = context.applicationContext
private val cipherDatabaseDao =
AppDatabase
.getDatabase(applicationContext)
.cipherDatabaseDao()
AppDatabase.getDatabase(applicationContext).cipherDatabaseDao()
// Temp DAO to easily remove content if object no longer in memory
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
private var useTempDao = PreferencesUtil.isTempDeviceUnlockEnable(applicationContext)
private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null
private var mBinder: DeviceUnlockNotificationService.DeviceUnlockBinder? = null
private var mServiceConnection: ServiceConnection? = null
private var mDatabaseListeners = LinkedList<CipherDatabaseListener>()
private var mAdvancedUnlockBroadcastReceiver = AdvancedUnlockNotificationService.AdvancedUnlockReceiver {
private var mDeviceUnlockBroadcastReceiver = DeviceUnlockNotificationService.DeviceUnlockReceiver {
deleteAll()
removeAllDataAndDetach()
}
fun reloadPreferences() {
useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
private fun reloadPreferences() {
useTempDao = PreferencesUtil.isTempDeviceUnlockEnable(applicationContext)
}
@Synchronized
@@ -66,13 +71,15 @@ class CipherDatabaseAction(context: Context) {
@Synchronized
private fun attachService(performedAction: () -> Unit) {
applicationContext.registerReceiver(mAdvancedUnlockBroadcastReceiver, IntentFilter().apply {
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION)
})
ContextCompat.registerReceiver(applicationContext, mDeviceUnlockBroadcastReceiver,
IntentFilter().apply {
addAction(DeviceUnlockNotificationService.REMOVE_DEVICE_UNLOCK_KEY_ACTION)
}, ContextCompat.RECEIVER_EXPORTED
)
mServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as AdvancedUnlockNotificationService.AdvancedUnlockBinder)
mBinder = (serviceBinder as DeviceUnlockNotificationService.DeviceUnlockBinder)
performedAction.invoke()
}
@@ -81,9 +88,9 @@ class CipherDatabaseAction(context: Context) {
}
}
try {
AdvancedUnlockNotificationService.bindService(applicationContext,
DeviceUnlockNotificationService.bindService(applicationContext,
mServiceConnection!!,
Context.BIND_AUTO_CREATE)
Context.BIND_AUTO_CREATE)
} catch (e: Exception) {
Log.e(TAG, "Unable to start cipher action", e)
performedAction.invoke()
@@ -93,11 +100,11 @@ class CipherDatabaseAction(context: Context) {
@Synchronized
private fun detachService() {
try {
applicationContext.unregisterReceiver(mAdvancedUnlockBroadcastReceiver)
} catch (e: Exception) {}
applicationContext.unregisterReceiver(mDeviceUnlockBroadcastReceiver)
} catch (_: Exception) {}
mServiceConnection?.let {
AdvancedUnlockNotificationService.unbindService(applicationContext, it)
DeviceUnlockNotificationService.unbindService(applicationContext, it)
}
}
@@ -117,41 +124,50 @@ class CipherDatabaseAction(context: Context) {
private fun onClear() {
mBinder = null
mServiceConnection = null
mDatabaseListeners.forEach {
it.onCipherDatabaseCleared()
mDatabaseListeners.forEach { listener ->
listener.onCipherDatabaseCleared()
}
}
interface CipherDatabaseListener {
fun onCipherDatabaseRetrieved(databaseUri: Uri, cipherDatabase: CipherEncryptDatabase?)
fun onCipherDatabaseAddedOrUpdated(cipherDatabase: CipherEncryptDatabase)
fun onCipherDatabaseDeleted(databaseUri: Uri)
fun onAllCipherDatabasesDeleted()
fun onCipherDatabaseCleared()
}
fun getCipherDatabase(databaseUri: Uri,
cipherDatabaseResultListener: (CipherEncryptDatabase?) -> Unit) {
cipherDatabaseResultListener: ((CipherEncryptDatabase?) -> Unit)? = null) {
if (useTempDao) {
serviceActionTask {
var cipherDatabase: CipherEncryptDatabase? = null
mBinder?.getCipherDatabase(databaseUri)?.let { cipherDatabaseEntity ->
cipherDatabase = CipherEncryptDatabase().apply {
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
this.databaseUri = cipherDatabaseEntity.databaseUri.toUri()
this.encryptedValue = Base64.decode(
cipherDatabaseEntity.encryptedValue,
Base64.NO_WRAP
BASE64_FLAG
)
this.specParameters = Base64.decode(
cipherDatabaseEntity.specParameters,
Base64.NO_WRAP
BASE64_FLAG
)
}
}
cipherDatabaseResultListener.invoke(cipherDatabase)
cipherDatabaseResultListener?.invoke(cipherDatabase) ?: run {
mDatabaseListeners.forEach { listener ->
listener.onCipherDatabaseRetrieved(databaseUri, cipherDatabase)
}
}
}
} else {
IOActionTask(
{
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())?.let { cipherDatabaseEntity ->
{
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
?.let { cipherDatabaseEntity ->
CipherEncryptDatabase().apply {
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
this.databaseUri = cipherDatabaseEntity.databaseUri.toUri()
this.encryptedValue = Base64.decode(
cipherDatabaseEntity.encryptedValue,
Base64.NO_WRAP
@@ -162,18 +178,34 @@ class CipherDatabaseAction(context: Context) {
)
}
}
},
{
cipherDatabaseResultListener.invoke(it)
},
{ cipherDatabase ->
cipherDatabaseResultListener?.invoke(cipherDatabase) ?: run {
mDatabaseListeners.forEach { listener ->
listener.onCipherDatabaseRetrieved(databaseUri, cipherDatabase)
}
}
}
).execute()
}
}
fun containsCipherDatabase(databaseUri: Uri,
private fun containsCipherDatabase(databaseUri: Uri?,
contains: (Boolean) -> Unit) {
getCipherDatabase(databaseUri) {
contains.invoke(it != null)
if (databaseUri == null) {
contains.invoke(false)
} else {
getCipherDatabase(databaseUri) {
contains.invoke(it != null)
}
}
}
fun resetCipherParameters(databaseUri: Uri?) {
containsCipherDatabase(databaseUri) { contains ->
if (contains) {
mBinder?.resetTimer()
}
}
}
@@ -183,15 +215,19 @@ class CipherDatabaseAction(context: Context) {
val cipherDatabaseEntity = CipherDatabaseEntity(
databaseUri.toString(),
Base64.encodeToString(cipherEncryptDatabase.encryptedValue, Base64.NO_WRAP),
Base64.encodeToString(cipherEncryptDatabase.specParameters, Base64.NO_WRAP),
Base64.encodeToString(cipherEncryptDatabase.encryptedValue, BASE64_FLAG),
Base64.encodeToString(cipherEncryptDatabase.specParameters, BASE64_FLAG),
)
if (useTempDao) {
// The only case to create service (not needed to get an info)
serviceActionTask(true) {
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
cipherDatabaseResultListener?.invoke()
cipherDatabaseResultListener?.invoke() ?: run {
mDatabaseListeners.forEach { listener ->
listener.onCipherDatabaseAddedOrUpdated(cipherEncryptDatabase)
}
}
}
} else {
IOActionTask(
@@ -206,7 +242,11 @@ class CipherDatabaseAction(context: Context) {
}
},
{
cipherDatabaseResultListener?.invoke()
cipherDatabaseResultListener?.invoke() ?: run {
mDatabaseListeners.forEach { listener ->
listener.onCipherDatabaseAddedOrUpdated(cipherEncryptDatabase)
}
}
}
).execute()
}
@@ -218,18 +258,27 @@ class CipherDatabaseAction(context: Context) {
if (useTempDao) {
serviceActionTask {
mBinder?.deleteByDatabaseUri(databaseUri)
cipherDatabaseResultListener?.invoke()
cipherDatabaseResultListener?.invoke() ?: run {
mDatabaseListeners.forEach { listener ->
listener.onCipherDatabaseDeleted(databaseUri)
}
}
}
} else {
IOActionTask(
{
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
},
{
cipherDatabaseResultListener?.invoke()
{
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
},
{
cipherDatabaseResultListener?.invoke() ?: run {
mDatabaseListeners.forEach { listener ->
listener.onCipherDatabaseDeleted(databaseUri)
}
}
}
).execute()
}
reloadPreferences()
}
fun deleteAll() {
@@ -240,15 +289,19 @@ class CipherDatabaseAction(context: Context) {
}
// To erase the residues
IOActionTask(
{
cipherDatabaseDao.deleteAll()
}
{
cipherDatabaseDao.deleteAll()
}
).execute()
mDatabaseListeners.forEach { listener ->
listener.onAllCipherDatabasesDeleted()
}
// Unbind
removeAllDataAndDetach()
reloadPreferences()
}
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {
private val TAG = CipherDatabaseAction::class.java.name
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
@@ -22,204 +22,231 @@ package com.kunzisoft.keepass.app.database
import android.content.Context
import android.net.Uri
import android.util.Log
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.IOActionTask
import com.kunzisoft.keepass.utils.SingletonHolderParameter
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.decodeUri
import com.kunzisoft.keepass.utils.parseUri
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
class FileDatabaseHistoryAction(private val applicationContext: Context) {
private val databaseFileHistoryDao =
AppDatabase
.getDatabase(applicationContext)
.fileDatabaseHistoryDao()
AppDatabase.getDatabase(applicationContext).fileDatabaseHistoryDao()
fun getDatabaseFile(databaseUri: Uri,
databaseFileResult: (DatabaseFile?) -> Unit) {
IOActionTask(
{
val fileDatabaseHistoryEntity = databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
val fileDatabaseInfo = FileDatabaseInfo(applicationContext, databaseUri)
DatabaseFile(
databaseUri,
UriUtil.parse(fileDatabaseHistoryEntity?.keyFileUri),
UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""),
fileDatabaseInfo.exists,
fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString()
)
},
{
databaseFileResult.invoke(it)
}
{
val fileDatabaseHistoryEntity =
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
val fileDatabaseInfo = FileDatabaseInfo(
applicationContext,
databaseUri)
DatabaseFile(
databaseUri,
fileDatabaseHistoryEntity?.keyFileUri?.parseUri(),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey),
fileDatabaseHistoryEntity?.readOnly,
fileDatabaseHistoryEntity?.databaseUri?.decodeUri(),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias
?: ""),
fileDatabaseInfo.exists,
fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString()
)
},
{
databaseFileResult.invoke(it)
}
).execute()
}
fun getKeyFileUriByDatabaseUri(databaseUri: Uri,
keyFileUriResultListener: (Uri?) -> Unit) {
IOActionTask(
{
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
},
{
it?.let { fileHistoryEntity ->
fileHistoryEntity.keyFileUri?.let { keyFileUri ->
keyFileUriResultListener.invoke(UriUtil.parse(keyFileUri))
}
} ?: keyFileUriResultListener.invoke(null)
}
{
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
},
{
it?.let { fileHistoryEntity ->
fileHistoryEntity.keyFileUri?.let { keyFileUri ->
keyFileUriResultListener.invoke(keyFileUri.parseUri())
}
} ?: keyFileUriResultListener.invoke(null)
}
).execute()
}
fun getDatabaseFileList(databaseFileListResult: (List<DatabaseFile>) -> Unit) {
IOActionTask(
{
val hideBrokenLocations = PreferencesUtil.hideBrokenLocations(applicationContext)
// Show only uri accessible
val databaseFileListLoaded = ArrayList<DatabaseFile>()
databaseFileHistoryDao.getAll().forEach { fileDatabaseHistoryEntity ->
val fileDatabaseInfo = FileDatabaseInfo(applicationContext, fileDatabaseHistoryEntity.databaseUri)
if (hideBrokenLocations && fileDatabaseInfo.exists
|| !hideBrokenLocations) {
databaseFileListLoaded.add(
DatabaseFile(
UriUtil.parse(fileDatabaseHistoryEntity.databaseUri),
UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri),
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
fileDatabaseInfo.exists,
fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString()
)
{
val hideBrokenLocations =
PreferencesUtil.hideBrokenLocations(
applicationContext)
// Show only uri accessible
val databaseFileListLoaded = ArrayList<DatabaseFile>()
databaseFileHistoryDao.getAll().forEach { fileDatabaseHistoryEntity ->
val fileDatabaseInfo = FileDatabaseInfo(
applicationContext,
fileDatabaseHistoryEntity.databaseUri)
if (hideBrokenLocations && fileDatabaseInfo.exists
|| !hideBrokenLocations
) {
databaseFileListLoaded.add(
DatabaseFile(
fileDatabaseHistoryEntity.databaseUri.parseUri(),
fileDatabaseHistoryEntity.keyFileUri?.parseUri(),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey),
fileDatabaseHistoryEntity.readOnly,
fileDatabaseHistoryEntity.databaseUri.decodeUri(),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
fileDatabaseInfo.exists,
fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString()
)
}
}
databaseFileListLoaded
},
{
databaseFileList ->
databaseFileList?.let {
databaseFileListResult.invoke(it)
)
}
}
databaseFileListLoaded
},
{ databaseFileList ->
databaseFileList?.let {
databaseFileListResult.invoke(it)
}
}
).execute()
}
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null,
fun addOrUpdateDatabaseUri(databaseUri: Uri,
keyFileUri: Uri? = null,
hardwareKey: HardwareKey? = null,
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
addOrUpdateDatabaseFile(DatabaseFile(
databaseUri,
keyFileUri
databaseUri,
keyFileUri,
hardwareKey
), databaseFileAddedOrUpdatedResult)
}
fun addOrUpdateDatabaseFile(databaseFileToAddOrUpdate: DatabaseFile,
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
IOActionTask(
{
databaseFileToAddOrUpdate.databaseUri?.let { databaseUri ->
// Try to get info in database first
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
{
databaseFileToAddOrUpdate.databaseUri?.let { databaseUri ->
// Try to get info in database first
val fileDatabaseHistoryRetrieve =
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
// Complete alias if not exists
val fileDatabaseHistory = FileDatabaseHistoryEntity(
databaseUri.toString(),
databaseFileToAddOrUpdate.databaseAlias
?: fileDatabaseHistoryRetrieve?.databaseAlias
?: "",
databaseFileToAddOrUpdate.keyFileUri?.toString(),
System.currentTimeMillis()
// Complete alias if not exists
val fileDatabaseHistory =
FileDatabaseHistoryEntity(
databaseUri.toString(),
databaseFileToAddOrUpdate.databaseAlias
?: fileDatabaseHistoryRetrieve?.databaseAlias
?: "",
databaseFileToAddOrUpdate.keyFileUri?.toString(),
databaseFileToAddOrUpdate.hardwareKey?.value,
databaseFileToAddOrUpdate.readOnly
?: fileDatabaseHistoryRetrieve?.readOnly,
System.currentTimeMillis()
)
// Update values if history element not yet in the database
try {
if (fileDatabaseHistoryRetrieve == null) {
databaseFileHistoryDao.add(fileDatabaseHistory)
} else {
databaseFileHistoryDao.update(fileDatabaseHistory)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to add or update database history", e)
// Update values if history element not yet in the database
try {
if (fileDatabaseHistoryRetrieve == null) {
databaseFileHistoryDao.add(fileDatabaseHistory)
} else {
databaseFileHistoryDao.update(fileDatabaseHistory)
}
val fileDatabaseInfo = FileDatabaseInfo(applicationContext,
fileDatabaseHistory.databaseUri)
DatabaseFile(
UriUtil.parse(fileDatabaseHistory.databaseUri),
UriUtil.parse(fileDatabaseHistory.keyFileUri),
UriUtil.decode(fileDatabaseHistory.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
fileDatabaseInfo.exists,
fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString()
)
} catch (e: Exception) {
Log.e(TAG, "Unable to add or update database history", e)
}
},
{
databaseFileAddedOrUpdatedResult?.invoke(it)
val fileDatabaseInfo =
FileDatabaseInfo(applicationContext,
fileDatabaseHistory.databaseUri)
DatabaseFile(
fileDatabaseHistory.databaseUri.parseUri(),
fileDatabaseHistory.keyFileUri?.parseUri(),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
fileDatabaseHistory.readOnly,
fileDatabaseHistory.databaseUri.decodeUri(),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
fileDatabaseInfo.exists,
fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString()
)
}
},
{
databaseFileAddedOrUpdatedResult?.invoke(it)
}
).execute()
}
fun deleteDatabaseFile(databaseFileToDelete: DatabaseFile,
databaseFileDeletedResult: (DatabaseFile?) -> Unit) {
IOActionTask(
{
databaseFileToDelete.databaseUri?.let { databaseUri ->
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())?.let { fileDatabaseHistory ->
{
databaseFileToDelete.databaseUri?.let { databaseUri ->
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
?.let { fileDatabaseHistory ->
val returnValue = databaseFileHistoryDao.delete(fileDatabaseHistory)
if (returnValue > 0) {
DatabaseFile(
UriUtil.parse(fileDatabaseHistory.databaseUri),
UriUtil.parse(fileDatabaseHistory.keyFileUri),
UriUtil.decode(fileDatabaseHistory.databaseUri),
databaseFileToDelete.databaseAlias
fileDatabaseHistory.databaseUri.parseUri(),
fileDatabaseHistory.keyFileUri?.parseUri(),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
fileDatabaseHistory.readOnly,
fileDatabaseHistory.databaseUri.decodeUri(),
databaseFileToDelete.databaseAlias
)
} else {
null
}
}
}
},
{
databaseFileDeletedResult.invoke(it)
}
},
{
databaseFileDeletedResult.invoke(it)
}
).execute()
}
fun deleteKeyFileByDatabaseUri(databaseUri: Uri,
result: (() ->Unit)? = null) {
IOActionTask(
{
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
},
{
result?.invoke()
}
{
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
},
{
result?.invoke()
}
).execute()
}
fun deleteAllKeyFiles(result: (() ->Unit)? = null) {
IOActionTask(
{
databaseFileHistoryDao.deleteAllKeyFiles()
},
{
result?.invoke()
}
{
databaseFileHistoryDao.deleteAllKeyFiles()
},
{
result?.invoke()
}
).execute()
}
fun deleteAll(result: (() ->Unit)? = null) {
IOActionTask(
{
databaseFileHistoryDao.deleteAll()
},
{
result?.invoke()
}
{
databaseFileHistoryDao.deleteAll()
},
{
result?.invoke()
}
).execute()
}

View File

@@ -35,6 +35,12 @@ data class FileDatabaseHistoryEntity(
@ColumnInfo(name = "keyfile_uri")
var keyFileUri: String?,
@ColumnInfo(name = "hardware_key")
var hardwareKey: String?,
@ColumnInfo(name = "read_only")
var readOnly: Boolean?,
@ColumnInfo(name = "updated")
val updated: Long
) {

View File

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

View File

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

View File

@@ -26,6 +26,7 @@ import android.os.Parcelable
import android.service.autofill.FillRequest
import android.view.inputmethod.InlineSuggestionsRequest
import androidx.annotation.RequiresApi
import com.kunzisoft.keepass.utils.readParcelableCompat
/**
* Utility class only to prevent java.lang.NoClassDefFoundError for old Android version and new lib compilation
@@ -52,8 +53,7 @@ class CompatInlineSuggestionsRequest : Parcelable {
constructor(parcel: Parcel) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
this.inlineSuggestionsRequest =
parcel.readParcelable(FillRequest::class.java.classLoader)
this.inlineSuggestionsRequest = parcel.readParcelableCompat()
}
else {
this.inlineSuggestionsRequest = null

View File

@@ -21,43 +21,51 @@ package com.kunzisoft.keepass.autofill
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.BlendMode
import android.graphics.drawable.Icon
import android.os.Build
import android.os.CancellationSignal
import android.service.autofill.*
import android.service.autofill.AutofillService
import android.service.autofill.FillCallback
import android.service.autofill.FillRequest
import android.service.autofill.FillResponse
import android.service.autofill.InlinePresentation
import android.service.autofill.Presentations
import android.service.autofill.SaveCallback
import android.service.autofill.SaveInfo
import android.service.autofill.SaveRequest
import android.util.Log
import android.view.autofill.AutofillId
import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.CreditCard
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.WebDomain
import org.joda.time.DateTime
import java.util.concurrent.atomic.AtomicBoolean
@RequiresApi(api = Build.VERSION_CODES.O)
class KeeAutofillService : AutofillService() {
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
private var mDatabase: Database? = null
private var mDatabase: ContextualDatabase? = null
private var applicationIdBlocklist: Set<String>? = null
private var webDomainBlocklist: Set<String>? = null
private var askToSaveData: Boolean = false
private var autofillInlineSuggestionsEnabled: Boolean = false
private var mLock = AtomicBoolean()
override fun onCreate() {
super.onCreate()
@@ -90,41 +98,47 @@ class KeeAutofillService : AutofillService() {
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
// Lock
if (!mLock.get()) {
mLock.set(true)
// Check user's settings for authenticating Responses and Datasets.
val latestStructure = request.fillContexts.last().structure
StructureParser(latestStructure).parse()?.let { parseResult ->
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
Log.d(TAG, "Autofill requested in compatibility mode")
} else {
Log.d(TAG, "Autofill requested in native mode")
}
// Build search info only if applicationId or webDomain are not blocked
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
}
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
searchInfo.webDomain = webDomainWithoutSubDomain
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
CompatInlineSuggestionsRequest(request)
} else {
null
}
launchSelection(mDatabase,
searchInfo,
parseResult,
inlineSuggestionsRequest,
callback)
// Check user's settings for authenticating Responses and Datasets.
val latestStructure = request.fillContexts.last().structure
StructureParser(latestStructure).parse()?.let { parseResult ->
// Build search info only if applicationId or webDomain are not blocked
if (autofillAllowedFor(
applicationId = parseResult.applicationId,
applicationIdBlocklist = applicationIdBlocklist,
webDomain = parseResult.webDomain,
webDomainBlocklist = webDomainBlocklist)
) {
val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
}
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
searchInfo.webDomain = webDomainWithoutSubDomain
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
CompatInlineSuggestionsRequest(request)
} else {
null
}
launchSelection(mDatabase,
searchInfo,
parseResult,
inlineSuggestionsRequest,
callback)
}
}
}
}
private fun launchSelection(database: Database?,
private fun launchSelection(database: ContextualDatabase?,
searchInfo: SearchInfo,
parseResult: StructureParser.Result,
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
@@ -153,149 +167,207 @@ class KeeAutofillService : AutofillService() {
@SuppressLint("RestrictedApi")
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
database: Database?,
database: ContextualDatabase?,
searchInfo: SearchInfo,
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
callback: FillCallback) {
var success = false
parseResult.allAutofillIds().let { autofillIds ->
if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response.
val intentSender = AutofillLauncherActivity.getPendingIntentForSelection(this,
searchInfo, inlineSuggestionsRequest).intentSender
val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (database == null) {
if (!parseResult.webDomain.isNullOrEmpty()) {
RemoteViews(
packageName,
R.layout.item_autofill_unlock_web_domain
).apply {
setTextViewText(
R.id.autofill_web_domain_text,
parseResult.webDomain
)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
setTextViewText(
R.id.autofill_app_id_text,
parseResult.applicationId
)
AutofillLauncherActivity.getPendingIntentForSelection(this,
searchInfo, inlineSuggestionsRequest)?.intentSender?.let { intentSender ->
val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (database == null) {
if (!parseResult.webDomain.isNullOrEmpty()) {
RemoteViews(
packageName,
R.layout.item_autofill_unlock_web_domain
).apply {
setTextViewText(
R.id.autofill_web_domain_text,
parseResult.webDomain
)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
setTextViewText(
R.id.autofill_app_id_text,
parseResult.applicationId
)
}
} else {
RemoteViews(packageName, R.layout.item_autofill_unlock)
}
} else {
RemoteViews(packageName, R.layout.item_autofill_unlock)
}
} else {
if (!parseResult.webDomain.isNullOrEmpty()) {
RemoteViews(
packageName,
R.layout.item_autofill_select_entry_web_domain
).apply {
setTextViewText(
R.id.autofill_web_domain_text,
parseResult.webDomain
)
if (!parseResult.webDomain.isNullOrEmpty()) {
RemoteViews(
packageName,
R.layout.item_autofill_select_entry_web_domain
).apply {
setTextViewText(
R.id.autofill_web_domain_text,
parseResult.webDomain
)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(
packageName,
R.layout.item_autofill_select_entry_app_id
).apply {
setTextViewText(
R.id.autofill_app_id_text,
parseResult.applicationId
)
}
} else {
RemoteViews(packageName, R.layout.item_autofill_select_entry)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_select_entry_app_id).apply {
setTextViewText(
R.id.autofill_app_id_text,
parseResult.applicationId
)
}
} else {
RemoteViews(packageName, R.layout.item_autofill_select_entry)
}
}
// Tell the autofill framework the interest to save credentials
if (askToSaveData) {
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
val requiredIds = ArrayList<AutofillId>()
val optionalIds = ArrayList<AutofillId>()
// Tell the autofill framework the interest to save credentials
if (askToSaveData) {
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
val requiredIds = ArrayList<AutofillId>()
val optionalIds = ArrayList<AutofillId>()
// Only if at least a password
parseResult.passwordId?.let { passwordInfo ->
parseResult.usernameId?.let { usernameInfo ->
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
requiredIds.add(usernameInfo)
// Only if at least a password
parseResult.passwordId?.let { passwordInfo ->
parseResult.usernameId?.let { usernameInfo ->
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
requiredIds.add(usernameInfo)
}
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
requiredIds.add(passwordInfo)
}
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
requiredIds.add(passwordInfo)
}
// or a credit card form
if (requiredIds.isEmpty()) {
parseResult.creditCardNumberId?.let { numberId ->
types = types or SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD
requiredIds.add(numberId)
Log.d(TAG, "Asking to save credit card number")
// or a credit card form
if (requiredIds.isEmpty()) {
parseResult.creditCardNumberId?.let { numberId ->
types = types or SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD
requiredIds.add(numberId)
Log.d(TAG, "Asking to save credit card number")
}
parseResult.creditCardExpirationDateId?.let { id -> optionalIds.add(id) }
parseResult.creditCardExpirationYearId?.let { id -> optionalIds.add(id) }
parseResult.creditCardExpirationMonthId?.let { id -> optionalIds.add(id) }
parseResult.creditCardHolderId?.let { id -> optionalIds.add(id) }
parseResult.cardVerificationValueId?.let { id -> optionalIds.add(id) }
}
parseResult.creditCardExpirationDateId?.let { id -> optionalIds.add(id) }
parseResult.creditCardExpirationYearId?.let { id -> optionalIds.add(id) }
parseResult.creditCardExpirationMonthId?.let { id -> optionalIds.add(id) }
parseResult.creditCardHolderId?.let { id -> optionalIds.add(id) }
parseResult.cardVerificationValueId?.let { id -> optionalIds.add(id) }
}
if (requiredIds.isNotEmpty()) {
val builder = SaveInfo.Builder(types, requiredIds.toTypedArray())
if (optionalIds.isNotEmpty()) {
builder.setOptionalIds(optionalIds.toTypedArray())
if (requiredIds.isNotEmpty()) {
val builder = SaveInfo.Builder(types, requiredIds.toTypedArray())
if (optionalIds.isNotEmpty()) {
builder.setOptionalIds(optionalIds.toTypedArray())
}
responseBuilder.setSaveInfo(builder.build())
}
responseBuilder.setSaveInfo(builder.build())
}
}
// Build inline presentation
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
var inlinePresentation: InlinePresentation? = null
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
if (inlineSuggestionsRequest.maxSuggestionCount > 0
&& inlinePresentationSpecs.size > 0) {
val inlinePresentationSpec = inlinePresentationSpecs[0]
// Build inline presentation
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled
) {
var inlinePresentation: InlinePresentation? = null
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs =
inlineSuggestionsRequest.inlinePresentationSpecs
if (inlineSuggestionsRequest.maxSuggestionCount > 0
&& inlinePresentationSpecs.isNotEmpty()
) {
val inlinePresentationSpec = inlinePresentationSpecs[0]
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1)) {
// Build the content for IME UI
inlinePresentation = InlinePresentation(
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (UiVersions.getVersions(imeStyle)
.contains(UiVersions.INLINE_UI_VERSION_1)
) {
// Build the content for IME UI
inlinePresentation = InlinePresentation(
InlineSuggestionUi.newContentBuilder(
PendingIntent.getActivity(this,
0,
Intent(this, AutofillSettingsActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
})
PendingIntent.getActivity(
this,
0,
Intent(this, AutofillSettingsActivity::class.java),
PendingIntent.FLAG_IMMUTABLE
)
).apply {
setContentDescription(getString(R.string.autofill_sign_in_prompt))
setTitle(getString(R.string.autofill_sign_in_prompt))
setStartIcon(Icon.createWithResource(this@KeeAutofillService, R.mipmap.ic_launcher_round).apply {
setTintBlendMode(BlendMode.DST)
})
}.build().slice, inlinePresentationSpec, false)
setStartIcon(
Icon.createWithResource(
this@KeeAutofillService,
R.mipmap.ic_launcher_round
).apply {
setTintBlendMode(BlendMode.DST)
})
}.build().slice, inlinePresentationSpec, false
)
}
}
}
// Build response
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
try {
// Buggy method on some API 33 devices
responseBuilder.setAuthentication(
autofillIds,
intentSender,
Presentations.Builder().apply {
inlinePresentation?.let {
setInlinePresentation(it)
}
setDialogPresentation(remoteViewsUnlock)
}.build()
)
} catch (e: Exception) {
Log.e(TAG, "Unable to use the new setAuthentication method.", e)
@Suppress("DEPRECATION")
responseBuilder.setAuthentication(
autofillIds,
intentSender,
remoteViewsUnlock,
inlinePresentation
)
}
} else {
@Suppress("DEPRECATION")
responseBuilder.setAuthentication(
autofillIds,
intentSender,
remoteViewsUnlock,
inlinePresentation
)
}
} else {
@Suppress("DEPRECATION")
responseBuilder.setAuthentication(
autofillIds,
intentSender,
remoteViewsUnlock
)
}
// Build response
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
} else {
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
success = true
callback.onSuccess(responseBuilder.build())
}
callback.onSuccess(responseBuilder.build())
}
}
if (!success)
callback.onFailure("Unable to get Autofill ids for UI selection")
}
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
var success = false
if (askToSaveData) {
val latestStructure = request.fillContexts.last().structure
StructureParser(latestStructure).parse(true)?.let { parseResult ->
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
if (autofillAllowedFor(
applicationId = parseResult.applicationId,
applicationIdBlocklist = applicationIdBlocklist,
webDomain = parseResult.webDomain,
webDomainBlocklist = webDomainBlocklist)
) {
Log.d(TAG, "autofill onSaveRequest password")
// Build expiration from date or from year and month
@@ -332,14 +404,16 @@ class KeeAutofillService : AutofillService() {
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
// registerInfo))
//} else {
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
callback.onSuccess()
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
success = true
callback.onSuccess()
//}
return
}
}
}
callback.onFailure("Saving form values is not allowed")
if (!success) {
callback.onFailure("Saving form values is not allowed")
}
}
override fun onConnected() {
@@ -348,13 +422,34 @@ class KeeAutofillService : AutofillService() {
}
override fun onDisconnected() {
mLock.set(false)
Log.d(TAG, "onDisconnected")
}
companion object {
private val TAG = KeeAutofillService::class.java.name
fun autofillAllowedFor(applicationId: String?,
webDomain: String?,
context: Context
): Boolean {
return autofillAllowedFor(
applicationId = applicationId,
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(context),
webDomain = webDomain,
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(context))
}
fun autofillAllowedFor(applicationId: String?,
applicationIdBlocklist: Set<String>?,
webDomain: String?,
webDomainBlocklist: Set<String>?
): Boolean {
return autofillAllowedFor(applicationId, applicationIdBlocklist)
// To prevent unrecognized autofill popup id
&& applicationId?.contains(APPLICATION_ID_POPUP_WINDOW) != true
&& autofillAllowedFor(webDomain, webDomainBlocklist)
}
fun autofillAllowedFor(element: String?, blockList: Set<String>?): Boolean {
element?.let { elementNotNull ->
if (blockList?.any { appIdBlocked ->

View File

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

View File

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

View File

@@ -1,671 +0,0 @@
/*
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.biometric
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.view.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import com.getkeepsafe.taptargetview.TapTargetView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.database.exception.IODatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.model.CipherDecryptDatabase
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.CredentialStorage
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.showByFading
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedUnlockCallback {
private var mBuilderListener: BuilderListener? = null
private var mAdvancedUnlockEnabled = false
private var mAutoOpenPromptEnabled = false
private var advancedUnlockManager: AdvancedUnlockManager? = null
private var biometricMode: Mode = Mode.BIOMETRIC_UNAVAILABLE
private var mAdvancedUnlockInfoView: AdvancedUnlockInfoView? = null
var databaseFileUri: Uri? = null
private set
// TODO Retrieve credential storage from app database
var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT
/**
* Manage setting to auto open biometric prompt
*/
private var mAutoOpenPrompt: Boolean
get() {
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)
// checkBiometricAvailability() allows open biometric prompt and onDestroy() removes the authorization
private var allowOpenBiometricPrompt = false
private lateinit var cipherDatabaseAction : CipherDatabaseAction
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by activityViewModels()
// Only to fix multiple fingerprint menu #332
private var mAllowAdvancedUnlockMenu = false
private var mAddBiometricMenuInProgress = false
// Only keep connection when we request a device credential activity
private var keepConnection = false
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) {
super.onAttach(context)
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(context)
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(context)
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mBuilderListener = context as BuilderListener
}
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + BuilderListener::class.java.name)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
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? {
super.onCreateView(inflater, container, savedInstanceState)
val rootView = inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_advanced_unlock, container, false)
mAdvancedUnlockInfoView = rootView.findViewById(R.id.advanced_unlock_view)
return rootView
}
override fun onResume() {
super.onResume()
context?.let {
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(it)
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(it)
}
keepConnection = false
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// biometric menu
if (mAllowAdvancedUnlockMenu)
inflater.inflate(R.menu.advanced_unlock, menu)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
deleteEncryptedDatabaseKey()
}
}
return super.onOptionsItemSelected(item)
}
private fun onDatabaseLoaded(databaseUri: Uri?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// To get device credential unlock result, only if same database uri
if (databaseUri != null
&& mAdvancedUnlockEnabled) {
val deviceCredentialAuthSucceeded = mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded
deviceCredentialAuthSucceeded?.let {
if (databaseUri == databaseFileUri) {
if (deviceCredentialAuthSucceeded == true) {
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationSucceeded()
} else {
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationFailed()
}
} else {
disconnect()
}
} ?: run {
if (databaseUri != databaseFileUri) {
connect(databaseUri)
}
}
} else {
disconnect()
}
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = null
}
}
/**
* Check unlock availability and change the current mode depending of device's state
*/
private fun checkUnlockAvailability() {
context?.let { context ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
allowOpenBiometricPrompt = true
if (PreferencesUtil.isBiometricUnlockEnable(context)) {
mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
// biometric not supported (by API level or hardware) so keep option hidden
// or manually disable
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(context)
if (!PreferencesUtil.isAdvancedUnlockEnable(context)
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
} else {
// biometric is available but not configured, show icon but in disabled state with some information
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
} else {
selectMode()
}
}
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
mAdvancedUnlockInfoView?.setIconResource(R.drawable.bolt)
if (AdvancedUnlockManager.isDeviceSecure(context)) {
selectMode()
} else {
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
}
}
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun selectMode() {
// Check if fingerprint well init (be called the first time the fingerprint is configured
// and the activity still active)
if (advancedUnlockManager?.isKeyManagerInitialized != true) {
advancedUnlockManager = AdvancedUnlockManager { requireActivity() }
// callback for fingerprint findings
advancedUnlockManager?.advancedUnlockCallback = this
}
// Recheck to change the mode
if (advancedUnlockManager?.isKeyManagerInitialized != true) {
toggleMode(Mode.KEY_MANAGER_UNAVAILABLE)
} else {
if (mBuilderListener?.conditionToStoreCredential() == true) {
// listen for encryption
toggleMode(Mode.STORE_CREDENTIAL)
} else {
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
// biometric available but no stored password found yet for this DB so show info don't listen
toggleMode(if (containsCipher) {
// listen for decryption
Mode.EXTRACT_CREDENTIAL
} else {
// wait for typing
Mode.WAIT_CREDENTIAL
})
}
}
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun toggleMode(newBiometricMode: Mode) {
if (newBiometricMode != biometricMode) {
biometricMode = newBiometricMode
initAdvancedUnlockMode()
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initNotAvailable() {
showViews(false)
mAdvancedUnlockInfoView?.setIconViewClickListener(false, null)
}
@RequiresApi(Build.VERSION_CODES.M)
private fun openBiometricSetting() {
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
context?.startActivity(Intent(Settings.ACTION_SETTINGS))
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initSecurityUpdateRequired() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
openBiometricSetting()
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initNotConfigured() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.configure_biometric)
setAdvancedUnlockedMessageView("")
openBiometricSetting()
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initKeyManagerNotAvailable() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
openBiometricSetting()
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initWaitData() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
setAdvancedUnlockedMessageView("")
context?.let { context ->
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
context.getString(R.string.credential_before_click_advanced_unlock_button))
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
lifecycleScope.launch(Dispatchers.Main) {
if (allowOpenBiometricPrompt) {
if (cryptoPrompt.isDeviceCredentialOperation)
keepConnection = true
try {
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt,
mDeviceCredentialResultLauncher)
} catch (e: Exception) {
Log.e(TAG, "Unable to open advanced unlock prompt", e)
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
}
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initEncryptData() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_store_credential)
setAdvancedUnlockedMessageView("")
advancedUnlockManager?.initEncryptData { cryptoPrompt ->
// Set listener to open the biometric dialog and save credential
mAdvancedUnlockInfoView?.setIconViewClickListener { _ ->
openAdvancedUnlockPrompt(cryptoPrompt)
}
} ?: throw Exception("AdvancedUnlockManager not initialized")
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initDecryptData() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_unlock_database)
setAdvancedUnlockedMessageView("")
advancedUnlockManager?.let { unlockHelper ->
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
cipherDatabase?.let {
unlockHelper.initDecryptData(it.specParameters) { cryptoPrompt ->
// Set listener to open the biometric dialog and check credential
mAdvancedUnlockInfoView?.setIconViewClickListener { _ ->
openAdvancedUnlockPrompt(cryptoPrompt)
}
// Auto open the biometric prompt
if (mAutoOpenPrompt) {
mAutoOpenPrompt = false
openAdvancedUnlockPrompt(cryptoPrompt)
}
}
} ?: deleteEncryptedDatabaseKey()
}
} ?: throw IODatabaseException()
} ?: throw Exception("AdvancedUnlockManager not initialized")
}
private fun initAdvancedUnlockMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mAllowAdvancedUnlockMenu = false
try {
when (biometricMode) {
Mode.BIOMETRIC_UNAVAILABLE -> initNotAvailable()
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired()
Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
Mode.WAIT_CREDENTIAL -> initWaitData()
Mode.STORE_CREDENTIAL -> initEncryptData()
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
}
} catch (e: Exception) {
onGenericException(e)
}
invalidateBiometricMenu()
}
}
private fun invalidateBiometricMenu() {
// Show fingerprint key deletion
if (!mAddBiometricMenuInProgress) {
mAddBiometricMenuInProgress = true
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
mAllowAdvancedUnlockMenu = containsCipher
&& (biometricMode != Mode.BIOMETRIC_UNAVAILABLE
&& biometricMode != Mode.KEY_MANAGER_UNAVAILABLE)
mAddBiometricMenuInProgress = false
activity?.invalidateOptionsMenu()
}
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun connect(databaseUri: Uri) {
showViews(true)
this.databaseFileUri = databaseUri
cipherDatabaseListener = object: CipherDatabaseAction.CipherDatabaseListener {
override fun onCipherDatabaseCleared() {
advancedUnlockManager?.closeBiometricPrompt()
checkUnlockAvailability()
}
}
cipherDatabaseAction.apply {
reloadPreferences()
cipherDatabaseListener?.let {
registerDatabaseListener(it)
}
}
checkUnlockAvailability()
}
@RequiresApi(Build.VERSION_CODES.M)
fun disconnect(hideViews: Boolean = true,
closePrompt: Boolean = true) {
this.databaseFileUri = null
// Close the biometric prompt
allowOpenBiometricPrompt = false
if (closePrompt)
advancedUnlockManager?.closeBiometricPrompt()
cipherDatabaseListener?.let {
cipherDatabaseAction.unregisterDatabaseListener(it)
}
biometricMode = Mode.BIOMETRIC_UNAVAILABLE
if (hideViews) {
showViews(false)
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun deleteEncryptedDatabaseKey() {
mAllowAdvancedUnlockMenu = false
advancedUnlockManager?.closeBiometricPrompt()
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
checkUnlockAvailability()
}
} ?: checkUnlockAvailability()
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
lifecycleScope.launch(Dispatchers.Main) {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
setAdvancedUnlockedMessageView(errString.toString())
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationFailed() {
lifecycleScope.launch(Dispatchers.Main) {
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationSucceeded() {
lifecycleScope.launch(Dispatchers.Main) {
when (biometricMode) {
Mode.BIOMETRIC_UNAVAILABLE -> {
}
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> {
}
Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> {
}
Mode.KEY_MANAGER_UNAVAILABLE -> {
}
Mode.WAIT_CREDENTIAL -> {
}
Mode.STORE_CREDENTIAL -> {
// newly store the entered password in encrypted way
mBuilderListener?.retrieveCredentialForEncryption()?.let { credential ->
advancedUnlockManager?.encryptData(credential)
}
}
Mode.EXTRACT_CREDENTIAL -> {
// retrieve the encrypted value from preferences
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
cipherDatabase?.encryptedValue?.let { value ->
advancedUnlockManager?.decryptData(value)
} ?: deleteEncryptedDatabaseKey()
}
} ?: run {
onAuthenticationError(-1, getString(R.string.error_database_uri_null))
}
}
}
}
}
override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {
databaseFileUri?.let { databaseUri ->
mBuilderListener?.onCredentialEncrypted(
CipherEncryptDatabase().apply {
this.databaseUri = databaseUri
this.credentialStorage = credentialDatabaseStorage
this.encryptedValue = encryptedValue
this.specParameters = ivSpec
}
)
}
}
override fun handleDecryptedResult(decryptedValue: ByteArray) {
// Load database directly with password retrieve
databaseFileUri?.let { databaseUri ->
mBuilderListener?.onCredentialDecrypted(
CipherDecryptDatabase().apply {
this.databaseUri = databaseUri
this.credentialStorage = credentialDatabaseStorage
this.decryptedValue = decryptedValue
}
)
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onUnrecoverableKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onInvalidKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onGenericException(e: Exception) {
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
setAdvancedUnlockedMessageView(errorMessage)
}
private fun showViews(show: Boolean) {
lifecycleScope.launch(Dispatchers.Main) {
if (show) {
if (mAdvancedUnlockInfoView?.visibility != View.VISIBLE)
mAdvancedUnlockInfoView?.showByFading()
}
else {
if (mAdvancedUnlockInfoView?.visibility == View.VISIBLE)
mAdvancedUnlockInfoView?.hideByFading()
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedTitleView(textId: Int) {
lifecycleScope.launch(Dispatchers.Main) {
mAdvancedUnlockInfoView?.setTitle(textId)
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedMessageView(textId: Int) {
lifecycleScope.launch(Dispatchers.Main) {
mAdvancedUnlockInfoView?.setMessage(textId)
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
lifecycleScope.launch(Dispatchers.Main) {
mAdvancedUnlockInfoView?.message = text
}
}
enum class Mode {
BIOMETRIC_UNAVAILABLE,
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED,
KEY_MANAGER_UNAVAILABLE,
WAIT_CREDENTIAL,
STORE_CREDENTIAL,
EXTRACT_CREDENTIAL
}
interface BuilderListener {
fun retrieveCredentialForEncryption(): ByteArray
fun conditionToStoreCredential(): Boolean
fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase)
fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase)
}
override fun onPause() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!keepConnection) {
// If close prompt, bug "user not authenticated in Android R"
disconnect(false)
advancedUnlockManager = null
}
}
super.onPause()
}
override fun onDestroyView() {
mAdvancedUnlockInfoView = null
super.onDestroyView()
}
override fun onDestroy() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
disconnect()
advancedUnlockManager = null
mBuilderListener = null
}
super.onDestroy()
}
override fun onDetach() {
mBuilderListener = null
super.onDetach()
}
companion object {
private val TAG = AdvancedUnlockFragment::class.java.name
}
}

View File

@@ -1,513 +0,0 @@
/*
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.biometric
import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.*
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.security.KeyStore
import java.security.UnrecoverableKeyException
import java.util.concurrent.Executors
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
@RequiresApi(api = Build.VERSION_CODES.M)
class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) {
private var keyStore: KeyStore? = null
private var keyGenerator: KeyGenerator? = null
private var cipher: Cipher? = null
private var biometricPrompt: BiometricPrompt? = null
private var authenticationCallback = object: BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
advancedUnlockCallback?.onAuthenticationSucceeded()
}
override fun onAuthenticationFailed() {
advancedUnlockCallback?.onAuthenticationFailed()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
advancedUnlockCallback?.onAuthenticationError(errorCode, errString)
}
}
var advancedUnlockCallback: AdvancedUnlockCallback? = null
private var isKeyManagerInit = false
private val biometricUnlockEnable = PreferencesUtil.isBiometricUnlockEnable(retrieveContext())
private val deviceCredentialUnlockEnable = PreferencesUtil.isDeviceCredentialUnlockEnable(retrieveContext())
val isKeyManagerInitialized: Boolean
get() {
if (!isKeyManagerInit) {
advancedUnlockCallback?.onGenericException(Exception("Biometric not initialized"))
}
return isKeyManagerInit
}
private fun isBiometricOperation(): Boolean {
return biometricUnlockEnable || isDeviceCredentialBiometricOperation()
}
// Since Android 30, device credential is also a biometric operation
private fun isDeviceCredentialOperation(): Boolean {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.R
&& deviceCredentialUnlockEnable
}
private fun isDeviceCredentialBiometricOperation(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& deviceCredentialUnlockEnable
}
init {
if (isDeviceSecure(retrieveContext())
&& (biometricUnlockEnable || deviceCredentialUnlockEnable)) {
try {
this.keyStore = KeyStore.getInstance(ADVANCED_UNLOCK_KEYSTORE)
this.keyGenerator = KeyGenerator.getInstance(ADVANCED_UNLOCK_KEY_ALGORITHM, ADVANCED_UNLOCK_KEYSTORE)
this.cipher = Cipher.getInstance(
ADVANCED_UNLOCK_KEY_ALGORITHM + "/"
+ ADVANCED_UNLOCK_BLOCKS_MODES + "/"
+ ADVANCED_UNLOCK_ENCRYPTION_PADDING)
isKeyManagerInit = (keyStore != null
&& keyGenerator != null
&& cipher != null)
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize the keystore", e)
isKeyManagerInit = false
advancedUnlockCallback?.onGenericException(e)
}
} else {
// really not much to do when no fingerprint support found
isKeyManagerInit = false
}
}
@Synchronized private fun getSecretKey(): SecretKey? {
if (!isKeyManagerInitialized) {
return null
}
try {
// Create new key if needed
keyStore?.let { keyStore ->
keyStore.load(null)
try {
if (!keyStore.containsAlias(ADVANCED_UNLOCK_KEYSTORE_KEY)) {
// Set the alias of the entry in Android KeyStore where the key will appear
// and the constrains (purposes) in the constructor of the Builder
keyGenerator?.init(
KeyGenParameterSpec.Builder(
ADVANCED_UNLOCK_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(ADVANCED_UNLOCK_BLOCKS_MODES)
.setEncryptionPaddings(ADVANCED_UNLOCK_ENCRYPTION_PADDING)
.apply {
// 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
if (biometricUnlockEnable) {
setUserAuthenticationRequired(true)
}
// 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()
}
} catch (e: Exception) {
Log.e(TAG, "Unable to create a key in keystore", e)
advancedUnlockCallback?.onGenericException(e)
}
return keyStore.getKey(ADVANCED_UNLOCK_KEYSTORE_KEY, null) as SecretKey?
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve the key in keystore", e)
advancedUnlockCallback?.onGenericException(e)
}
return null
}
@Synchronized fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) {
initEncryptData(actionIfCypherInit, true)
}
@Synchronized private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
firstLaunch: Boolean) {
if (!isKeyManagerInitialized) {
return
}
try {
getSecretKey()?.let { secretKey ->
cipher?.let { cipher ->
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
actionIfCypherInit.invoke(
AdvancedUnlockCryptoPrompt(
cipher,
R.string.advanced_unlock_prompt_store_credential_title,
R.string.advanced_unlock_prompt_store_credential_message,
isDeviceCredentialOperation(), isBiometricOperation())
)
}
}
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
if (firstLaunch) {
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
initEncryptData(actionIfCypherInit, false)
} else {
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize encrypt data", e)
advancedUnlockCallback?.onGenericException(e)
}
}
@Synchronized fun encryptData(value: ByteArray) {
if (!isKeyManagerInitialized) {
return
}
try {
val encrypted = cipher?.doFinal(value) ?: byteArrayOf()
// passes updated iv spec on to callback so this can be stored for decryption
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
advancedUnlockCallback?.handleEncryptedResult(encrypted, spec.iv)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to encrypt data", e)
advancedUnlockCallback?.onGenericException(e)
}
}
@Synchronized fun initDecryptData(ivSpecValue: ByteArray,
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
initDecryptData(ivSpecValue, actionIfCypherInit, true)
}
@Synchronized private fun initDecryptData(ivSpecValue: ByteArray,
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
firstLaunch: Boolean = true) {
if (!isKeyManagerInitialized) {
return
}
try {
// important to restore spec here that was used for decryption
val spec = IvParameterSpec(ivSpecValue)
getSecretKey()?.let { secretKey ->
cipher?.let { cipher ->
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
actionIfCypherInit.invoke(
AdvancedUnlockCryptoPrompt(
cipher,
R.string.advanced_unlock_prompt_extract_credential_title,
null,
isDeviceCredentialOperation(), isBiometricOperation())
)
}
}
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
if (firstLaunch) {
deleteKeystoreKey()
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
} else {
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
}
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
if (firstLaunch) {
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
} else {
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize decrypt data", e)
advancedUnlockCallback?.onGenericException(e)
}
}
@Synchronized fun decryptData(encryptedValue: ByteArray) {
if (!isKeyManagerInitialized) {
return
}
try {
// actual decryption here
cipher?.doFinal(encryptedValue)?.let { decrypted ->
advancedUnlockCallback?.handleDecryptedResult(decrypted)
}
} catch (badPaddingException: BadPaddingException) {
Log.e(TAG, "Unable to decrypt data", badPaddingException)
advancedUnlockCallback?.onInvalidKeyException(badPaddingException)
} catch (e: Exception) {
Log.e(TAG, "Unable to decrypt data", e)
advancedUnlockCallback?.onGenericException(e)
}
}
@Synchronized fun deleteKeystoreKey() {
try {
keyStore?.load(null)
keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
} catch (e: Exception) {
Log.e(TAG, "Unable to delete entry key in keystore", e)
advancedUnlockCallback?.onGenericException(e)
}
}
@Synchronized fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt,
deviceCredentialResultLauncher: ActivityResultLauncher<Intent>
) {
// Init advanced unlock prompt
if (biometricPrompt == null) {
biometricPrompt = BiometricPrompt(retrieveContext(),
Executors.newSingleThreadExecutor(),
authenticationCallback)
}
val promptTitle = retrieveContext().getString(cryptoPrompt.promptTitleId)
val promptDescription = cryptoPrompt.promptDescriptionId?.let { descriptionId ->
retrieveContext().getString(descriptionId)
} ?: ""
if (cryptoPrompt.isBiometricOperation) {
val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
setTitle(promptTitle)
if (promptDescription.isNotEmpty())
setDescription(promptDescription)
setConfirmationRequired(false)
if (isDeviceCredentialBiometricOperation()) {
setAllowedAuthenticators(DEVICE_CREDENTIAL)
} else {
setNegativeButtonText(retrieveContext().getString(android.R.string.cancel))
}
}.build()
biometricPrompt?.authenticate(
promptInfoExtractCredential,
BiometricPrompt.CryptoObject(cryptoPrompt.cipher))
}
else if (cryptoPrompt.isDeviceCredentialOperation) {
val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java)
@Suppress("DEPRECATION")
deviceCredentialResultLauncher.launch(
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription)
)
}
}
@Synchronized fun closeBiometricPrompt() {
biometricPrompt?.cancelAuthentication()
}
interface AdvancedUnlockErrorCallback {
fun onUnrecoverableKeyException(e: Exception)
fun onInvalidKeyException(e: Exception)
fun onGenericException(e: Exception)
}
interface AdvancedUnlockCallback : AdvancedUnlockErrorCallback {
fun onAuthenticationSucceeded()
fun onAuthenticationFailed()
fun onAuthenticationError(errorCode: Int, errString: CharSequence)
fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray)
fun handleDecryptedResult(decryptedValue: ByteArray)
}
companion object {
private val TAG = AdvancedUnlockManager::class.java.name
private const val ADVANCED_UNLOCK_KEYSTORE = "AndroidKeyStore"
private const val ADVANCED_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
private const val ADVANCED_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
@RequiresApi(api = Build.VERSION_CODES.M)
fun canAuthenticate(context: Context): Int {
return try {
BiometricManager.from(context).canAuthenticate(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
BIOMETRIC_STRONG or DEVICE_CREDENTIAL
} else {
BIOMETRIC_STRONG
}
)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
try {
BiometricManager.from(context).canAuthenticate(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
BIOMETRIC_WEAK or DEVICE_CREDENTIAL
} else {
BIOMETRIC_WEAK
}
)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
}
}
}
@RequiresApi(api = Build.VERSION_CODES.M)
fun isDeviceSecure(context: Context): Boolean {
val keyguardManager = ContextCompat.getSystemService(context, KeyguardManager::class.java)
return keyguardManager?.isDeviceSecure ?: false
}
@RequiresApi(api = Build.VERSION_CODES.M)
fun biometricUnlockSupported(context: Context): Boolean {
val biometricCanAuthenticate = try {
BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
try {
BiometricManager.from(context).canAuthenticate(BIOMETRIC_WEAK)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
}
}
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
)
}
@RequiresApi(api = Build.VERSION_CODES.M)
fun deviceCredentialUnlockSupported(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ContextCompat.getSystemService(context, KeyguardManager::class.java)?.apply {
return isDeviceSecure
}
}
return false
}
/**
* Remove entry key in keystore
*/
@RequiresApi(api = Build.VERSION_CODES.M)
fun deleteEntryKeyInKeystoreForBiometric(fragmentActivity: FragmentActivity,
advancedCallback: AdvancedUnlockErrorCallback) {
AdvancedUnlockManager{ fragmentActivity }.apply {
advancedUnlockCallback = object : AdvancedUnlockCallback {
override fun onAuthenticationSucceeded() {}
override fun onAuthenticationFailed() {}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {}
override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {}
override fun handleDecryptedResult(decryptedValue: ByteArray) {}
override fun onUnrecoverableKeyException(e: Exception) {
advancedCallback.onUnrecoverableKeyException(e)
}
override fun onInvalidKeyException(e: Exception) {
advancedCallback.onInvalidKeyException(e)
}
override fun onGenericException(e: Exception) {
advancedCallback.onGenericException(e)
}
}
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

@@ -0,0 +1,21 @@
package com.kunzisoft.keepass.biometric
import androidx.annotation.StringRes
import javax.crypto.Cipher
data class DeviceUnlockCryptoPrompt(
var type: DeviceUnlockCryptoPromptType,
var cipher: Cipher,
@StringRes var titleId: Int,
@StringRes var descriptionId: Int? = null,
var isDeviceCredentialOperation: Boolean,
var isBiometricOperation: Boolean
) {
fun isOldCredentialOperation(): Boolean {
return !isBiometricOperation && isDeviceCredentialOperation
}
}
enum class DeviceUnlockCryptoPromptType {
CREDENTIAL_ENCRYPTION, CREDENTIAL_DECRYPTION
}

View File

@@ -0,0 +1,363 @@
/*
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.biometric
import android.app.Activity
import android.app.KeyguardManager
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.view.DeviceUnlockView
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.showByFading
import com.kunzisoft.keepass.viewmodels.DeviceUnlockPromptMode
import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.concurrent.Executors
@RequiresApi(Build.VERSION_CODES.M)
class DeviceUnlockFragment: Fragment() {
private var mDeviceUnlockView: DeviceUnlockView? = null
private val mDeviceUnlockViewModel: DeviceUnlockViewModel by activityViewModels()
private var mBiometricPrompt: BiometricPrompt? = null
// Only to fix multiple fingerprint menu #332
private var mAllowDeviceUnlockMenu = false
private var mDeviceCredentialResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
mDeviceUnlockViewModel.onAuthenticationSucceeded()
} else {
setAuthenticationFailed()
}
mDeviceUnlockViewModel.biometricPromptClosed()
}
private var biometricAuthenticationCallback = object: BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
mDeviceUnlockViewModel.onAuthenticationSucceeded(result)
mDeviceUnlockViewModel.biometricPromptClosed()
}
override fun onAuthenticationFailed() {
setAuthenticationFailed()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
setAuthenticationError(errorCode, errString)
}
}
private val menuProvider: MenuProvider = object: MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
// biometric menu
if (mAllowDeviceUnlockMenu)
menuInflater.inflate(R.menu.device_unlock, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
when (menuItem.itemId) {
R.id.menu_keystore_remove_key ->
deleteEncryptedDatabaseKey()
}
return false
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
val rootView = inflater.inflate(R.layout.fragment_device_unlock, container, false)
mDeviceUnlockView = rootView.findViewById(R.id.device_unlock_view)
return rootView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Init device unlock prompt
mBiometricPrompt = BiometricPrompt(
this@DeviceUnlockFragment,
Executors.newSingleThreadExecutor(),
biometricAuthenticationCallback
)
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDeviceUnlockViewModel.uiState.collect { uiState ->
// Change mode
toggleDeviceCredentialMode(uiState.newDeviceUnlockMode)
// Prompt
manageDeviceCredentialPrompt(uiState.cryptoPromptState)
// Advanced menu
mAllowDeviceUnlockMenu = uiState.allowDeviceUnlockMenu
activity?.invalidateOptionsMenu()
}
}
}
}
fun cancelBiometricPrompt() {
lifecycleScope.launch(Dispatchers.Main) {
mBiometricPrompt?.cancelAuthentication()
}
}
private fun toggleDeviceCredentialMode(deviceUnlockMode: DeviceUnlockMode) {
lifecycleScope.launch(Dispatchers.Main) {
try {
when (deviceUnlockMode) {
DeviceUnlockMode.BIOMETRIC_UNAVAILABLE -> setNotAvailableMode()
DeviceUnlockMode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> setSecurityUpdateRequiredMode()
DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> setNotConfiguredMode()
DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE -> setKeyManagerNotAvailableMode()
DeviceUnlockMode.WAIT_CREDENTIAL -> setWaitCredentialMode()
DeviceUnlockMode.STORE_CREDENTIAL -> setStoreCredentialMode()
DeviceUnlockMode.EXTRACT_CREDENTIAL -> setExtractCredentialMode()
}
} catch (e: Exception) {
mDeviceUnlockViewModel.setException(e)
}
}
}
private fun manageDeviceCredentialPrompt(
state: DeviceUnlockPromptMode
) {
mDeviceUnlockViewModel.cryptoPrompt?.let { prompt ->
when (state) {
DeviceUnlockPromptMode.SHOW -> {
openPrompt(prompt)
mDeviceUnlockViewModel.promptShown()
}
DeviceUnlockPromptMode.CLOSE -> {
cancelBiometricPrompt()
mDeviceUnlockViewModel.biometricPromptClosed()
}
else -> {}
}
}
}
private fun openPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) {
lifecycleScope.launch(Dispatchers.Main) {
try {
val promptTitle = getString(cryptoPrompt.titleId)
val promptDescription = cryptoPrompt.descriptionId?.let { descriptionId ->
getString(descriptionId)
} ?: ""
if (cryptoPrompt.isBiometricOperation) {
mBiometricPrompt?.authenticate(
BiometricPrompt.PromptInfo.Builder().apply {
setTitle(promptTitle)
if (promptDescription.isNotEmpty())
setDescription(promptDescription)
setConfirmationRequired(false)
if (isDeviceCredentialBiometricOperation(context)) {
setAllowedAuthenticators(DEVICE_CREDENTIAL)
} else {
setNegativeButtonText(getString(android.R.string.cancel))
}
}.build(),
BiometricPrompt.CryptoObject(cryptoPrompt.cipher)
)
} else if (cryptoPrompt.isDeviceCredentialOperation) {
context?.let { context ->
@Suppress("DEPRECATION")
mDeviceCredentialResultLauncher?.launch(
ContextCompat.getSystemService(
context,
KeyguardManager::class.java
)?.createConfirmDeviceCredentialIntent(
promptTitle,
promptDescription
)
)
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to open prompt", e)
mDeviceUnlockViewModel.setException(e)
}
}
}
private fun setNotAvailableMode() {
showViews(false)
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener(null)
}
private fun openBiometricSetting() {
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener {
try {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
context?.startActivity(Intent(Settings.ACTION_BIOMETRIC_ENROLL))
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> {
@Suppress("DEPRECATION") context
?.startActivity(Intent(Settings.ACTION_FINGERPRINT_ENROLL))
}
else -> {
context?.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
}
}
} catch (e: Exception) {
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
context?.startActivity(Intent(Settings.ACTION_SETTINGS))
}
}
}
private fun setSecurityUpdateRequiredMode() {
showViews(true)
setDeviceUnlockedTitleView(R.string.biometric_security_update_required)
openBiometricSetting()
}
private fun setNotConfiguredMode() {
showViews(true)
setDeviceUnlockedTitleView(R.string.configure_biometric)
openBiometricSetting()
}
private fun setKeyManagerNotAvailableMode() {
showViews(true)
setDeviceUnlockedTitleView(R.string.keystore_not_accessible)
openBiometricSetting()
}
private fun setWaitCredentialMode() {
showViews(true)
setDeviceUnlockedTitleView(R.string.unavailable)
context?.let { context ->
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener {
mDeviceUnlockViewModel.setException(SecurityException(
context.getString(R.string.credential_before_click_device_unlock_button)
))
}
}
}
private fun setStoreCredentialMode() {
showViews(true)
setDeviceUnlockedTitleView(R.string.unlock_and_link_biometric)
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { _ ->
mDeviceUnlockViewModel.showPrompt()
}
}
private fun setExtractCredentialMode() {
showViews(true)
setDeviceUnlockedTitleView(R.string.unlock)
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { _ ->
mDeviceUnlockViewModel.showPrompt()
}
}
fun deleteEncryptedDatabaseKey() {
mDeviceUnlockViewModel.deleteEncryptedDatabaseKey()
}
private fun showViews(show: Boolean) {
if (show) {
if (mDeviceUnlockView?.visibility != View.VISIBLE)
mDeviceUnlockView?.showByFading()
}
else {
if (mDeviceUnlockView?.visibility == View.VISIBLE)
mDeviceUnlockView?.hideByFading()
}
}
private fun setDeviceUnlockedTitleView(textId: Int) {
mDeviceUnlockView?.setTitle(textId)
}
private fun setAuthenticationError(errorCode: Int, errString: CharSequence) {
mDeviceUnlockViewModel.biometricPromptClosed()
when (errorCode) {
BiometricPrompt.ERROR_CANCELED,
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
BiometricPrompt.ERROR_USER_CANCELED -> {
// No operation
Log.i(TAG, "$errString")
}
else -> {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
mDeviceUnlockViewModel.setException(SecurityException(errString.toString()))
}
}
}
private fun setAuthenticationFailed() {
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
mDeviceUnlockViewModel.setException(
SecurityException(getString(R.string.device_unlock_not_recognized))
)
}
override fun onPause() {
super.onPause()
cancelBiometricPrompt()
mDeviceUnlockViewModel.clear()
}
override fun onDestroyView() {
mDeviceUnlockView = null
super.onDestroyView()
}
companion object {
private val TAG = DeviceUnlockFragment::class.java.name
}
}

View File

@@ -0,0 +1,430 @@
/*
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.biometric
import android.app.KeyguardManager
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties
import android.util.Log
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.security.KeyStore
import java.security.UnrecoverableKeyException
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
@RequiresApi(api = Build.VERSION_CODES.M)
class DeviceUnlockManager(private var appContext: Context) {
private var keyStore: KeyStore? = null
private var keyGenerator: KeyGenerator? = null
private var cipher: Cipher? = null
private var biometricUnlockEnable = isBiometricUnlockEnable(appContext)
private var deviceCredentialUnlockEnable = isDeviceCredentialUnlockEnable(appContext)
init {
if (biometricUnlockEnable || deviceCredentialUnlockEnable) {
if (isDeviceSecure(appContext)) {
try {
this.keyStore = KeyStore.getInstance(DEVICE_UNLOCK_KEYSTORE)
this.keyGenerator = KeyGenerator.getInstance(
DEVICE_UNLOCK_KEY_ALGORITHM,
DEVICE_UNLOCK_KEYSTORE
)
this.cipher = Cipher.getInstance(
DEVICE_UNLOCK_KEY_ALGORITHM + "/"
+ DEVICE_UNLOCK_BLOCKS_MODES + "/"
+ DEVICE_UNLOCK_ENCRYPTION_PADDING
)
if (keyStore == null) {
throw SecurityException("Unable to initialize the keystore")
}
if (keyGenerator == null) {
throw SecurityException("Unable to initialize the key generator")
}
if (cipher == null) {
throw SecurityException("Unable to initialize the cipher")
}
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize the device unlock manager", e)
throw e
}
} else {
throw SecurityException("Device not secure enough")
}
}
}
@Synchronized private fun getSecretKey(): SecretKey? {
try {
// Create new key if needed
keyStore?.let { keyStore ->
keyStore.load(null)
try {
if (!keyStore.containsAlias(DEVICE_UNLOCK_KEYSTORE_KEY)) {
// Set the alias of the entry in Android KeyStore where the key will appear
// and the constrains (purposes) in the constructor of the Builder
keyGenerator?.init(
KeyGenParameterSpec.Builder(
DEVICE_UNLOCK_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(DEVICE_UNLOCK_BLOCKS_MODES)
.setEncryptionPaddings(DEVICE_UNLOCK_ENCRYPTION_PADDING)
.apply {
// 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
if (biometricUnlockEnable) {
setUserAuthenticationRequired(true)
}
// To store in the security chip
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
&& appContext.packageManager.hasSystemFeature(
PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
setIsStrongBoxBacked(true)
}
}
.build())
keyGenerator?.generateKey()
}
} catch (e: Exception) {
Log.e(TAG, "Unable to create a key in keystore", e)
throw e
}
return keyStore.getKey(DEVICE_UNLOCK_KEYSTORE_KEY, null) as SecretKey?
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve the key in keystore", e)
throw e
}
return null
}
@Synchronized fun initEncryptData(
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
) {
initEncryptData(true, actionIfCypherInit)
}
@Synchronized private fun initEncryptData(
firstLaunch: Boolean,
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
) {
try {
getSecretKey()?.let { secretKey ->
cipher?.let { cipher ->
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
actionIfCypherInit.invoke(
DeviceUnlockCryptoPrompt(
type = DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION,
cipher = cipher,
titleId = R.string.device_unlock_prompt_store_credential_title,
descriptionId = R.string.device_unlock_prompt_store_credential_message,
isDeviceCredentialOperation = isDeviceCredentialOperation(
deviceCredentialUnlockEnable
),
isBiometricOperation = isBiometricOperation(
biometricUnlockEnable, deviceCredentialUnlockEnable
)
)
)
}
}
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
throw unrecoverableKeyException
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
if (firstLaunch) {
deleteAllEntryKeysInKeystoreForBiometric(appContext)
initEncryptData(false, actionIfCypherInit)
} else {
throw invalidKeyException
}
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize encrypt data", e)
throw e
}
}
@Synchronized fun encryptData(
value: ByteArray,
cipher: Cipher?,
handleEncryptedResult: (encryptedValue: ByteArray, ivSpec: ByteArray) -> Unit
) {
try {
val encrypted = cipher?.doFinal(value) ?: byteArrayOf()
// passes updated iv spec on to callback so this can be stored for decryption
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
handleEncryptedResult.invoke(encrypted, spec.iv)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to encrypt data", e)
throw e
}
}
@Synchronized fun initDecryptData(
ivSpecValue: ByteArray,
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
) {
initDecryptData(ivSpecValue, true, actionIfCypherInit)
}
@Synchronized private fun initDecryptData(
ivSpecValue: ByteArray,
firstLaunch: Boolean = true,
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
) {
try {
// important to restore spec here that was used for decryption
val spec = IvParameterSpec(ivSpecValue)
getSecretKey()?.let { secretKey ->
cipher?.let { cipher ->
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
actionIfCypherInit.invoke(
DeviceUnlockCryptoPrompt(
type = DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION,
cipher = cipher,
titleId = R.string.device_unlock_prompt_extract_credential_title,
descriptionId = null,
isDeviceCredentialOperation = isDeviceCredentialOperation(
deviceCredentialUnlockEnable
),
isBiometricOperation = isBiometricOperation(
biometricUnlockEnable, deviceCredentialUnlockEnable
)
)
)
}
}
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
if (firstLaunch) {
deleteKeystoreKey()
initDecryptData(ivSpecValue, false, actionIfCypherInit)
} else {
throw unrecoverableKeyException
}
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
if (firstLaunch) {
deleteAllEntryKeysInKeystoreForBiometric(appContext)
initDecryptData(ivSpecValue, false, actionIfCypherInit)
} else {
throw invalidKeyException
}
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize decrypt data", e)
throw e
}
}
@Synchronized fun decryptData(
encryptedValue: ByteArray,
cipher: Cipher?,
handleDecryptedResult: (decryptedValue: ByteArray) -> Unit
) {
try {
// actual decryption here
cipher?.doFinal(encryptedValue)?.let { decrypted ->
handleDecryptedResult.invoke(decrypted)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to decrypt data", e)
throw e
}
}
@Synchronized fun deleteKeystoreKey() {
try {
keyStore?.load(null)
keyStore?.deleteEntry(DEVICE_UNLOCK_KEYSTORE_KEY)
} catch (e: Exception) {
Log.e(TAG, "Unable to delete entry key in keystore", e)
throw e
}
}
companion object {
private val TAG = DeviceUnlockManager::class.java.name
private const val DEVICE_UNLOCK_KEYSTORE = "AndroidKeyStore"
private const val DEVICE_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
private const val DEVICE_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
private const val DEVICE_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
private const val DEVICE_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
@RequiresApi(api = Build.VERSION_CODES.M)
fun canAuthenticate(context: Context): Int {
return try {
BiometricManager.from(context).canAuthenticate(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
BIOMETRIC_STRONG or DEVICE_CREDENTIAL
} else {
BIOMETRIC_STRONG
}
)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
try {
BiometricManager.from(context).canAuthenticate(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
BIOMETRIC_WEAK or DEVICE_CREDENTIAL
} else {
BIOMETRIC_WEAK
}
)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
}
}
}
fun isDeviceSecure(context: Context): Boolean {
return ContextCompat.getSystemService(context, KeyguardManager::class.java)
?.isDeviceSecure ?: false
}
fun biometricUnlockSupported(context: Context): Boolean {
val biometricCanAuthenticate = try {
BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
try {
BiometricManager.from(context).canAuthenticate(BIOMETRIC_WEAK)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
}
}
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
)
}
fun deviceCredentialUnlockSupported(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
(biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
)
} else {
true
}
}
/**
* Remove entry key in keystore
*/
fun deleteEntryKeyInKeystoreForBiometric(
appContext: Context
) {
DeviceUnlockManager(appContext).apply {
deleteKeystoreKey()
}
}
fun deleteAllEntryKeysInKeystoreForBiometric(appContext: Context) {
try {
deleteEntryKeyInKeystoreForBiometric(appContext)
} catch (e: Exception) {
Toast.makeText(appContext,
deviceUnlockError(e, appContext),
Toast.LENGTH_SHORT).show()
} finally {
CipherDatabaseAction.getInstance(appContext).deleteAll()
}
}
}
}
fun deviceUnlockError(error: Throwable, context: Context): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& (error is UnrecoverableKeyException
|| error is KeyPermanentlyInvalidatedException)) {
context.getString(R.string.device_unlock_invalid_key)
} else
error.cause?.localizedMessage
?: error.localizedMessage
?: error.toString()
}
fun isBiometricUnlockEnable(appContext: Context) =
PreferencesUtil.isBiometricUnlockEnable(appContext)
fun isDeviceCredentialUnlockEnable(appContext: Context) =
PreferencesUtil.isDeviceCredentialUnlockEnable(appContext)
private fun isBiometricOperation(
biometricUnlockEnable: Boolean,
deviceCredentialUnlockEnable: Boolean
): Boolean {
return biometricUnlockEnable
|| isDeviceCredentialBiometricOperation(deviceCredentialUnlockEnable)
}
// Since Android 30, device credential is also a biometric operation
private fun isDeviceCredentialOperation(
deviceCredentialUnlockEnable: Boolean
): Boolean {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.R
&& deviceCredentialUnlockEnable
}
private fun isDeviceCredentialBiometricOperation(
deviceCredentialUnlockEnable: Boolean
): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& deviceCredentialUnlockEnable
}
fun isDeviceCredentialBiometricOperation(context: Context?): Boolean {
if (context == null) {
return false
}
return isDeviceCredentialBiometricOperation(
isDeviceCredentialUnlockEnable(context)
)
}

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