Compare commits

...

1148 Commits

Author SHA1 Message Date
J-Jamet
c5cbba5971 Merge branch 'release/2.8.6' 2020-09-25 22:18:16 +02:00
J-Jamet
aea5493180 Small change when write inner random stream id 2020-09-25 21:40:58 +02:00
J-Jamet
431832e50e Downgrade constaintLayout to fix extra field 2020-09-25 21:00:04 +02:00
J-Jamet
daf239c93c Fix autolink #720 2020-09-25 20:41:24 +02:00
J-Jamet
2c669deae5 Add writeByte method in LittleEndianDataOutputStream 2020-09-25 19:34:02 +02:00
J-Jamet
81969de76e Write UInt size 2020-09-25 19:23:05 +02:00
J-Jamet
5a29e7a83e Replace strong tag 2020-09-25 18:57:45 +02:00
J-Jamet
9ed5aef40f Fix strings 2020-09-25 18:56:32 +02:00
J-Jamet
9e025b4329 Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2020-09-25 18:47:51 +02:00
J-Jamet
543da4fe24 Fix write custom data #651 2020-09-25 18:31:30 +02:00
J-Jamet
e82e50a57a Refactor VariantDictionary serializer 2020-09-25 18:16:23 +02:00
Martin
288d62d666 Translated using Weblate (Czech)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-09-25 17:40:58 +02:00
zeritti
8ed224eeab Translated using Weblate (Czech)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-09-25 17:40:58 +02:00
J-Jamet
22bafff943 Update CHANGELOG 2020-09-25 16:30:11 +02:00
J-Jamet
02403cf566 Fix dialog background #717 2020-09-25 16:28:48 +02:00
J-Jamet
eabbc6f037 Refactor binary write in header 2020-09-24 19:01:21 +02:00
J-Jamet
27e60edaf4 Fix check create button visibility 2020-09-24 18:43:43 +02:00
J-Jamet
f7d0b64dea Small changes 2020-09-24 18:07:58 +02:00
J-Jamet
b874484101 Refactor binary protection 2020-09-24 17:51:05 +02:00
Martin
0ee72db2e9 Translated using Weblate (Czech)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-09-24 17:00:09 +02:00
zeritti
4f07bac9ec Translated using Weblate (Czech)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-09-24 17:00:09 +02:00
Martin
80abaf70f2 Translated using Weblate (Czech)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-09-24 16:59:24 +02:00
zeritti
d6acbc2fb1 Translated using Weblate (Czech)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-09-24 16:59:24 +02:00
J-Jamet
caf93c019a Add Looper in Handler 2020-09-24 14:28:12 +02:00
J-Jamet
7044efa7c3 Upgrade compilation version 2020-09-24 12:17:55 +02:00
J-Jamet
5b4d8f971f Fix string 2020-09-24 12:12:27 +02:00
J-Jamet
75e8feb9b8 Change yes -> ok, no -> cancel 2020-09-24 12:10:44 +02:00
J-Jamet
5d59419a06 Fix autofill domain with Android 11 #702 2020-09-24 11:48:42 +02:00
J-Jamet
ff2e47bf70 Change version to 2.8.6 2020-09-24 09:25:31 +02:00
ssantos
4721ed7c15 Translated using Weblate (Portuguese)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2020-09-23 18:41:05 +02:00
Zidan Pragata
d9da328088 Translated using Weblate (Indonesian)
Currently translated at 44.3% (204 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2020-09-23 18:40:58 +02:00
Milo Ivir
2ee506ac06 Translated using Weblate (Croatian)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-09-23 18:40:58 +02:00
ssantos
16c4e4dc5b Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2020-09-23 18:40:57 +02:00
J-Jamet
28b8fb1b97 Update CHANGELOG 2020-09-23 17:16:30 +02:00
J-Jamet
c8bae9fba6 Fix binaries unique Ids #713 2020-09-23 17:15:45 +02:00
J-Jamet
920764ad33 Fix binaries unique Ids #713 2020-09-23 17:07:15 +02:00
J-Jamet
a45d114527 Add toString() for BinaryPool 2020-09-23 16:47:10 +02:00
J-Jamet
62cca09045 Change length binary method 2020-09-23 16:46:31 +02:00
J-Jamet
ea126b90e2 Revert "Revert "Early return in readHeaderField" #713"
This reverts commit 777a20182e.
2020-09-23 13:13:22 +02:00
J-Jamet
777a20182e Revert "Early return in readHeaderField" #713
This reverts commit f2150e3d85.
2020-09-23 12:21:06 +02:00
J-Jamet
df62a9d32b Binary dir as no backup 2020-09-23 11:35:20 +02:00
ssantos
47e211bf3c Added translation using Weblate (Portuguese) 2020-09-22 17:31:10 +02:00
ian Vatega
650ca3844e Added translation using Weblate (Esperanto) 2020-09-22 12:56:17 +02:00
J-Jamet
8de49fa027 Fix renaming custom field #709 2020-09-21 11:31:14 +02:00
J-Jamet
35b7633f4d Fix autofill recognition #712 and update version to 2.9 2020-09-21 10:56:09 +02:00
Oğuz Ersen
2c96a80280 Translated using Weblate (Turkish)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-09-20 12:39:55 +02:00
Eric
c7000c8ac6 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-09-20 12:39:55 +02:00
ihor_ck
1dca2b4387 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-09-20 12:39:54 +02:00
solokot
ccbe9ad21e Translated using Weblate (Russian)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-09-20 12:39:54 +02:00
Stephan Paternotte
1fe6f11ef4 Translated using Weblate (Dutch)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2020-09-20 12:39:54 +02:00
HARADA Hiroyuki
9d4ab60fc4 Translated using Weblate (Japanese)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-09-20 12:39:53 +02:00
zeritti
ea8c5ad6e1 Translated using Weblate (Czech)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-09-20 12:39:53 +02:00
Nikita Epifanov
63a0f5f91c Translated using Weblate (Russian)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-09-18 22:01:50 +02:00
J-Jamet
e8bacbdb6f Merge tag '2.8.5' into develop
2.8.5
2020-09-18 16:13:36 +02:00
J-Jamet
bcc8226ccc Merge branch 'release/2.8.5' 2020-09-18 16:13:25 +02:00
J-Jamet
ddec91a0c5 Upgrade to 2.8.5 2020-09-18 16:13:01 +02:00
J-Jamet
2c262bb29d Fix Base64 #708 2020-09-18 16:09:43 +02:00
HARADA Hiroyuki
ae0afb7b53 Translated using Weblate (Japanese)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-09-18 15:38:56 +02:00
HARADA Hiroyuki
84e7fe5b60 Translated using Weblate (Japanese)
Currently translated at 100.0% (460 of 460 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-09-18 15:08:01 +02:00
J-Jamet
ccb32b045a Merge tag '2.8.4' into develop
2.8.4
2020-09-18 14:05:31 +02:00
J-Jamet
b2e29ac4bd Merge branch 'release/2.8.4' 2020-09-18 14:05:24 +02:00
J-Jamet
7df6309a68 Update CHANGELOG 2020-09-18 13:57:05 +02:00
J-Jamet
a203ad9f64 Replace strong tag 2020-09-18 13:48:56 +02:00
J-Jamet
44d6e09337 Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into develop 2020-09-18 13:44:27 +02:00
J-Jamet
7c59ec019a Add credentials information and fix small error issue in keyfile 2020-09-18 13:35:32 +02:00
J-Jamet
b457f64ec8 Fix passwordEncodingDialogFragment leak 2020-09-18 13:12:25 +02:00
J-Jamet
e152e59a61 Fix dialog leak 2020-09-18 12:55:04 +02:00
J-Jamet
2c620ad69a Add dialog for empty keyfile #679 2020-09-18 12:24:43 +02:00
J-Jamet
c08b405fc2 Remove unused dialog 2020-09-18 12:09:22 +02:00
J-Jamet
585e39c591 Open database with empty key file #679 2020-09-18 11:35:57 +02:00
J-Jamet
60e857dba9 Fix title in setting after orientation change 2020-09-17 19:33:06 +02:00
J-Jamet
9a4aa2b08f Clear listeners during fragment onDetach() 2020-09-17 19:24:39 +02:00
J-Jamet
2900b08b70 Fix OTP title and username #707 2020-09-17 19:10:26 +02:00
J-Jamet
b0936563a2 Fix upload attachment multiple times 2020-09-17 18:09:37 +02:00
J-Jamet
3d327b245a Update CHANGELOG 2020-09-17 17:39:58 +02:00
J-Jamet
9a14de3448 Add corrupted attachment icon #691 2020-09-17 17:38:29 +02:00
HARADA Hiroyuki
59f83545cf Translated using Weblate (Japanese)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-09-17 16:24:03 +02:00
J-Jamet
d9da1ef085 Fix opening database with bad attachment #691 2020-09-17 15:59:48 +02:00
Vachan
279a27f740 Translated using Weblate (Malayalam)
Currently translated at 80.5% (364 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2020-09-17 09:53:01 +02:00
HARADA Hiroyuki
6ba822ee48 Translated using Weblate (Japanese)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-09-16 19:27:58 +02:00
J-Jamet
e4445949c8 Update CHANGELOG 2020-09-16 11:39:48 +02:00
J-Jamet
8178ff583d Merge branch 'feature/Fragment_Edit_Entry' into develop #686 2020-09-16 11:36:32 +02:00
J-Jamet
0e0e19a802 Delete extra field animation 2020-09-16 11:34:01 +02:00
J-Jamet
1e2e7d841f Fix OTP generation 2020-09-15 23:22:49 +02:00
J-Jamet
263c2f00eb Fix OTP generation 2020-09-15 22:48:35 +02:00
J-Jamet
39c0b57652 Fix expire date 2020-09-15 22:42:24 +02:00
J-Jamet
677baf549c Remove unused classes 2020-09-15 22:42:08 +02:00
J-Jamet
848478c28b Remove unused code 2020-09-15 22:02:35 +02:00
J-Jamet
98ad33a589 Fix concurrent modification 2020-09-15 21:59:16 +02:00
J-Jamet
3f33733f40 Fix expiry time 2020-09-15 21:50:55 +02:00
J-Jamet
cdc4ae4fb3 Fix date instant 2020-09-15 21:35:47 +02:00
J-Jamet
3edfa8a6ce Fix entry edit education 2020-09-15 21:25:07 +02:00
J-Jamet
6e99b667af Refactor edit fragment code 2020-09-15 20:16:40 +02:00
J-Jamet
23c8735568 Try to fix leak 2020-09-15 20:12:44 +02:00
J-Jamet
f39065044f Remove unused code 2020-09-15 19:50:37 +02:00
J-Jamet
7d2ae47b0f Fix last focus 2020-09-15 19:49:20 +02:00
J-Jamet
7247de6908 Refactor fragment edit entry, using EntryInfo 2020-09-15 19:04:20 +02:00
Zidan Pragata
4c2aef8504 Translated using Weblate (Javanese)
Currently translated at 5.9% (27 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/jv/
2020-09-15 17:36:14 +02:00
Zidan Pragata
ac8717cf1f Translated using Weblate (Indonesian)
Currently translated at 40.2% (182 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2020-09-15 17:36:13 +02:00
Stephan Paternotte
7761928064 Translated using Weblate (Dutch)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2020-09-15 17:36:13 +02:00
HARADA Hiroyuki
198047406b Translated using Weblate (Japanese)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-09-15 17:36:12 +02:00
J-Jamet
1d57309db9 EntryEditContentsFragment as EntryEditFragment 2020-09-15 16:56:51 +02:00
J-Jamet
07af9f36b2 Move temp entry in fragment 2020-09-15 16:50:04 +02:00
J-Jamet
d157fea9be Add fragment in edit entry 2020-09-15 14:04:48 +02:00
Zidan Pragata
7692caf622 Added translation using Weblate (Javanese) 2020-09-14 13:50:26 +02:00
J-Jamet
dc65a8823f Update CHANGELOG 2020-09-14 13:01:01 +02:00
J-Jamet
2210932fdf Fix app crash when unlocking database V1 without backup folder #692 2020-09-14 12:56:38 +02:00
J-Jamet
0cb1bf4b7f Show error when upload error 2020-09-14 12:28:02 +02:00
J-Jamet
44d175dd40 Remove unlinked data as database preference #684 2020-09-14 12:13:26 +02:00
J-Jamet
097feb6437 Move unlinked attachment callback in fragment 2020-09-14 11:17:33 +02:00
J-Jamet
250c7e5f20 Change string 2020-09-13 16:29:35 +02:00
J-Jamet
83740f77bb Fix attachment service listeners 2020-09-13 16:18:46 +02:00
J-Jamet
8267e13d22 Fix uploading attachment with same name 2020-09-13 15:47:05 +02:00
HARADA Hiroyuki
a7e9396f35 Translated using Weblate (Japanese)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-09-13 15:36:11 +02:00
J-Jamet
0f6c8601d2 Remove temp attachment if not used 2020-09-13 13:38:33 +02:00
J-Jamet
66e68092d5 Fix incomplete attachment deletion #684 2020-09-13 13:11:08 +02:00
J-Jamet
09616a594c Fix commit attachment layout 2020-09-13 10:36:51 +02:00
J-Jamet
7ae58ea7ea Change consumme by consume 2020-09-13 10:25:01 +02:00
Oğuz Ersen
9f1f85a3c4 Translated using Weblate (Turkish)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-09-11 23:44:02 +02:00
Vachan
9992cdfce0 Translated using Weblate (Malayalam)
Currently translated at 71.9% (325 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2020-09-11 12:36:11 +02:00
Milo Ivir
f5fa08ce94 Translated using Weblate (Croatian)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-09-11 12:36:10 +02:00
Andrew
67f70f7f85 Translated using Weblate (Russian)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-09-11 12:36:10 +02:00
Éfrit
840bd56a3d Translated using Weblate (French)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-09-11 12:36:10 +02:00
HARADA Hiroyuki
c1e2d31cfd Translated using Weblate (Japanese)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-09-09 18:05:42 +02:00
HARADA Hiroyuki
c35322d44e Translated using Weblate (Japanese)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-09-06 19:24:01 +02:00
Dhruvan Ganesh
9867e46c3f Translated using Weblate (Tamil)
Currently translated at 2.8% (13 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ta/
2020-09-06 14:36:08 +02:00
WaldiS
946a120038 Translated using Weblate (Polish)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-09-06 14:36:08 +02:00
Dhruvan Ganesh
2cd1a17aa2 Added translation using Weblate (Tamil) 2020-09-05 13:58:58 +02:00
J-Jamet
6e29fe2932 Fix education hint freeze #685 2020-09-04 10:55:16 +02:00
J-Jamet
e6c6bf6613 Rename version to 2.8.4 2020-09-04 09:57:23 +02:00
Eric
72c53169df Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-09-03 14:57:25 +02:00
ihor_ck
b78cce7b4f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-09-03 14:57:25 +02:00
solokot
c40499ec31 Translated using Weblate (Russian)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-09-03 14:57:25 +02:00
HARADA Hiroyuki
ee158e517e Translated using Weblate (Japanese)
Currently translated at 100.0% (452 of 452 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-09-03 14:57:24 +02:00
J-Jamet
70aebcf9aa Upgrade to 2.9 2020-09-02 13:46:02 +02:00
J-Jamet
b15870d441 Merge tag '2.8.3' into develop
2.8.3
2020-09-02 12:29:43 +02:00
J-Jamet
fe7074736a Merge branch 'release/2.8.3' 2020-09-02 12:29:36 +02:00
J-Jamet
69c523ffad Image button at 48dp 2020-09-02 12:21:18 +02:00
J-Jamet
6aeefdf43d Merge branch 'translations' into develop 2020-09-01 20:27:41 +02:00
J-Jamet
5f4c8be3d3 Replace strong tags 2020-09-01 20:27:19 +02:00
J-Jamet
3092e4c557 Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations
# Conflicts:
#	app/src/main/res/values-cs/strings.xml
#	app/src/main/res/values-da/strings.xml
#	app/src/main/res/values-de/strings.xml
#	app/src/main/res/values-hr/strings.xml
#	app/src/main/res/values-ja/strings.xml
#	app/src/main/res/values-pl/strings.xml
#	app/src/main/res/values-ru/strings.xml
#	app/src/main/res/values-tr/strings.xml
#	app/src/main/res/values-uk/strings.xml
#	app/src/main/res/values-zh-rCN/strings.xml
#	app/src/main/res/values-zh-rTW/strings.xml
2020-09-01 20:19:46 +02:00
J-Jamet
ea119068da Remove full file path setting 2020-09-01 19:19:15 +02:00
J-Jamet
14371ecf94 Educational hint for attachment 2020-09-01 19:12:25 +02:00
James Alison
45b0fcfe15 Translated using Weblate (Persian)
Currently translated at 57.3% (257 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fa/
2020-09-01 18:49:37 +02:00
behnam ghafari
00aa5f5586 Translated using Weblate (Persian)
Currently translated at 57.3% (257 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fa/
2020-09-01 18:49:37 +02:00
J-Jamet
79fd53fd4c Edit custom fields #675 2020-09-01 18:45:52 +02:00
J-Jamet
357ee3daf0 Fix styles color in Kitkat 2020-09-01 13:22:41 +02:00
J-Jamet
c2f7897f10 Information icon as home button 2020-09-01 12:43:29 +02:00
J-Jamet
75455c0c48 Change file modification info #667 2020-09-01 12:14:37 +02:00
J-Jamet
a394bb9f8e Remove modification date when not available 2020-09-01 12:00:08 +02:00
J-Jamet
2f5a846493 Change bottom bar color 2020-09-01 00:30:18 +02:00
J-Jamet
90376b361d Fix populate OTP 2020-09-01 00:30:03 +02:00
J-Jamet
229cf6bf5f Change purple background cardview 2020-09-01 00:19:00 +02:00
J-Jamet
bc46737353 Fix tab selection 2020-08-31 23:29:00 +02:00
J-Jamet
1db2243a2e Upgrade CHANGELOG 2020-08-31 22:16:28 +02:00
J-Jamet
7cf836b3cb Refactor backup methods for KDB database 2020-08-31 22:14:04 +02:00
J-Jamet
f7bbd295d6 Fix backup group 2020-08-31 21:45:39 +02:00
behnam ghafari
62dbd95b48 Added translation using Weblate (Persian) 2020-08-31 20:01:41 +02:00
HARADA Hiroyuki
df07e9c719 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-31 19:46:13 +02:00
J-Jamet
9aaf72726e Fix deletion for database V1 #394 2020-08-31 17:18:37 +02:00
J-Jamet
5289927619 Removes max lines in notes #676 2020-08-31 16:54:26 +02:00
J-Jamet
fa8c686f75 Revert EditTextVisibility #660 2020-08-31 16:35:02 +02:00
J-Jamet
df5f28b7c4 Smooth scroll when adding element and fix #660 2020-08-31 16:05:22 +02:00
HARADA Hiroyuki
280d8368fa Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-31 15:22:27 +02:00
HARADA Hiroyuki
80dbff1f21 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-31 15:11:27 +02:00
J-Jamet
7ee68a8481 Change item attachment focus 2020-08-31 13:35:38 +02:00
J-Jamet
ac8dd42c45 Change small code 2020-08-31 13:35:18 +02:00
J-Jamet
eed2148b2a Remove unused import 2020-08-28 21:49:15 +02:00
J-Jamet
dc5345b6d3 Fix database alias #670 2020-08-28 21:49:00 +02:00
J-Jamet
221af0b5bb Fix attachment history with the same name 2020-08-28 13:48:07 +02:00
J-Jamet
a10ccc1eb0 Second pass to fix attachment deleted in history 2020-08-28 13:12:54 +02:00
J-Jamet
b72d858480 First pass to fix attachment deleted in history 2020-08-28 11:08:30 +02:00
J-Jamet
60412cc90b Update CHANGELOG 2020-08-28 10:43:54 +02:00
J-Jamet
512ac87dc9 Fix attachment icon color 2020-08-28 10:38:48 +02:00
J-Jamet
d97020d1c5 Merge branch 'wiomoc-patch-1' into develop 2020-08-28 10:22:12 +02:00
Zidan Pragata
f82cb617ba Translated using Weblate (Indonesian)
Currently translated at 39.2% (176 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2020-08-28 09:00:49 +02:00
Small Ku
f79281a1a0 Translated using Weblate (Chinese (Traditional))
Currently translated at 55.5% (249 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2020-08-28 09:00:45 +02:00
HARADA Hiroyuki
7be1dbb78b Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-28 09:00:44 +02:00
C. Rüdinger
f875787799 Translated using Weblate (German)
Currently translated at 99.7% (447 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-08-28 09:00:44 +02:00
jan madsen
f82c208556 Translated using Weblate (Danish)
Currently translated at 94.8% (425 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-08-28 09:00:43 +02:00
Christoph Walcher
f2150e3d85 Early return in readHeaderField
In the old version the outer loop won't terminate if `EndOfHeader` is a zero sized field.
2020-08-28 03:09:23 +02:00
J-Jamet
ecc198e8a0 Fix toolbar appearance #669 2020-08-27 20:39:03 +02:00
J-Jamet
949bc58a80 Merge branch 'feature/File_Attachment' into develop #189 2020-08-27 19:14:51 +02:00
J-Jamet
7d79fff16f Disable compression by entry attachment in DatabaseV4 2020-08-27 19:04:58 +02:00
J-Jamet
3aeb678292 Fix binaries indexes in DatabaseV4 2020-08-27 18:45:56 +02:00
Zidan Pragata
160ac41bed Added translation using Weblate (Indonesian) 2020-08-27 16:41:50 +02:00
J-Jamet
9dfbcbe89c Not compress by default for database v4 2020-08-27 14:21:36 +02:00
J-Jamet
30da529348 Fix corruption in header 2020-08-27 13:57:34 +02:00
J-Jamet
dd8d114711 Change save binaries compression for database 3.1 & 4 2020-08-27 13:49:48 +02:00
J-Jamet
2191a4a848 Allow swipe notification when download completed 2020-08-27 10:27:45 +02:00
J-Jamet
8b9ea8d988 Fix remove oldest attachments 2020-08-27 10:16:37 +02:00
J-Jamet
46dda8567d Remove warnings and better gzip view implementation 2020-08-26 23:43:49 +02:00
J-Jamet
6953da4d9a Change containsAttachment method 2020-08-26 23:35:16 +02:00
J-Jamet
e987d6647e Fix header binary compression 2020-08-26 23:25:13 +02:00
J-Jamet
359d85727e Remove oldest attachments files when deleted from entries 2020-08-26 21:25:51 +02:00
J-Jamet
a994bf9dd8 Change string to Gzip 2020-08-26 20:35:45 +02:00
J-Jamet
14c4e095f6 Remove attachment path view 2020-08-26 20:33:13 +02:00
J-Jamet
59dce0e56f Restore unknown compression 2020-08-26 20:27:13 +02:00
J-Jamet
1f54a893a7 Move classes 2020-08-26 19:28:26 +02:00
J-Jamet
9489f1ee3d Rename removeUnlinkedAttachments 2020-08-26 19:25:30 +02:00
J-Jamet
dc3d720e8d Binary files as time 2020-08-26 19:20:37 +02:00
J-Jamet
efe30b598b Write only attachments in header
Remove unlinked attachments
Simpler compression
2020-08-26 18:51:25 +02:00
J-Jamet
42515bfb2d Encapsulate consume attachment action 2020-08-26 11:44:56 +02:00
J-Jamet
acb3657d95 Better compression - decompression implementation 2020-08-26 11:43:35 +02:00
J-Jamet
e7159c9d36 Try to fix decompression 2020-08-26 10:39:56 +02:00
J-Jamet
f3fdca368b Fix compression after download and upload attachment 2020-08-25 20:07:41 +02:00
J-Jamet
4ea3e08a45 Refactor EntryAttachment to Attachment 2020-08-25 19:28:41 +02:00
J-Jamet
1eebc72b21 Encapsulate binary methods 2020-08-25 19:20:58 +02:00
J-Jamet
9a91be7e36 Add attachment icon in entry list 2020-08-25 18:13:44 +02:00
J-Jamet
48476f9b88 Add attachments 2020-08-25 17:51:01 +02:00
J-Jamet
68c991eb9b Ask to replace file 2020-08-25 17:48:17 +02:00
J-Jamet
b51c77b01b Ask for big files 2020-08-25 17:19:58 +02:00
J-Jamet
57105db554 Allow only one attachment for Database KDB 2020-08-25 12:52:41 +02:00
J-Jamet
4bd3bdaddf Fix concurrent exception and upload the same file 2020-08-25 12:39:17 +02:00
J-Jamet
9cf59b8d73 Refactoring code 2020-08-25 12:27:43 +02:00
J-Jamet
a793b0bb42 Remove not compatible elements below KitKat 2020-08-25 12:00:17 +02:00
J-Jamet
4d9e2e1471 Upload progression 2020-08-25 11:37:11 +02:00
J-Jamet
1719887e55 Fix lock button after download or upload 2020-08-24 22:52:12 +02:00
J-Jamet
2be00aca9d Merge branch 'develop' into feature/File_Attachment 2020-08-24 22:33:05 +02:00
J-Jamet
6fd05c5ad7 Rotate arrow drawable 2020-08-24 22:32:40 +02:00
J-Jamet
65e404374f Change upload icon 2020-08-24 22:32:05 +02:00
J-Jamet
0b78731bb3 Strings for upload notification 2020-08-24 22:20:58 +02:00
J-Jamet
65d318ed88 Fix many upload issues 2020-08-24 21:54:21 +02:00
Milo Ivir
1bfcea55a9 Translated using Weblate (Croatian)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-08-24 16:06:20 +02:00
Eric
6780eb004d Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-08-24 16:06:20 +02:00
ihor_ck
0e12b2d021 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-08-24 16:06:20 +02:00
Andrew
1b356f87ec Translated using Weblate (Russian)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-08-24 16:06:20 +02:00
solokot
bf24d0bae1 Translated using Weblate (Russian)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-08-24 16:06:19 +02:00
WaldiS
97c831d4bb Translated using Weblate (Polish)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-08-24 16:06:19 +02:00
HARADA Hiroyuki
e3e48ffa6d Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-24 16:06:19 +02:00
zeritti
b678416122 Translated using Weblate (Czech)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-08-24 16:06:19 +02:00
J-Jamet
df722925fa Upload attachments #189 2020-08-24 13:03:59 +02:00
J-Jamet
4cbc0d9806 Merge branch 'develop' into feature/File_Attachment 2020-08-23 12:49:44 +02:00
J-Jamet
b0f3711b4e Remove previous allow lock code 2020-08-23 12:49:12 +02:00
J-Jamet
14ec6579b2 Merge branch 'develop' into feature/File_Attachment 2020-08-23 12:41:56 +02:00
J-Jamet
30bf039473 Fix last item visibility 2020-08-23 12:26:10 +02:00
Oğuz Ersen
33bea317b0 Translated using Weblate (Turkish)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-08-22 05:31:03 +02:00
Andrew
c6814dc05e Translated using Weblate (Russian)
Currently translated at 97.9% (439 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-08-22 05:31:03 +02:00
HARADA Hiroyuki
16808069ec Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-22 05:31:02 +02:00
J-Jamet
e813974e29 Fix file info issue 2020-08-21 20:17:05 +02:00
J-Jamet
7e0010b536 Images buttons width 36dp 2020-08-21 20:02:32 +02:00
J-Jamet
93171adcb3 Smaller download icon 2020-08-21 19:48:16 +02:00
J-Jamet
9501cc76a4 Notes and URL as EntryField 2020-08-21 19:43:54 +02:00
J-Jamet
5d7db046ac Encapsulate username and OTP as EntryField 2020-08-21 18:35:54 +02:00
J-Jamet
46c259bc3e Fix field delete button position 2020-08-21 18:24:41 +02:00
J-Jamet
3bacff91d3 Visibility button for each field, password as EntryField view 2020-08-21 17:31:15 +02:00
J-Jamet
bd79d483d2 Upgrade to 2.8.3 2020-08-21 16:16:35 +02:00
Hosted Weblate
16e31f4881 Merge branch 'origin/master' into Weblate. 2020-08-21 16:14:09 +02:00
ssantos
afa23c393d Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2020-08-21 16:14:08 +02:00
HARADA Hiroyuki
54d23cb781 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-21 16:14:08 +02:00
random r
c757e410e9 Translated using Weblate (Italian)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-08-21 16:14:08 +02:00
J-Jamet
39dd25567d Merge tag '2.8.2' into develop
2.8.2
2020-08-21 16:04:09 +02:00
J-Jamet
32cd998c2a Merge branch 'release/2.8.2' 2020-08-21 16:04:02 +02:00
J-Jamet
691fc6335e Fix warnings 2020-08-21 15:48:58 +02:00
J-Jamet
b0025d1416 Refactor assign history 2020-08-21 13:49:04 +02:00
J-Jamet
2467d8b0e7 Try to fix new extra field 2020-08-21 13:06:13 +02:00
J-Jamet
28993c53e7 Fix keyboard shown 2020-08-21 12:25:32 +02:00
J-Jamet
efdea870f0 Upgrade kotlin to 1.4.0 2020-08-20 17:45:28 +02:00
J-Jamet
b2995ec862 Replace <strong> tags 2020-08-20 17:08:54 +02:00
J-Jamet
2bcc84dbb2 Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2020-08-20 16:49:47 +02:00
J-Jamet
70cc98ce33 Change strings according to #550 2020-08-20 16:48:56 +02:00
J-Jamet
6e055f398d Change focus timestamp variable 2020-08-20 14:20:51 +02:00
J-Jamet
9f6234f032 Fix show button in long tap mode 2020-08-20 14:16:20 +02:00
J-Jamet
6135544b72 Fix extra field cursor after orientation change 2020-08-20 14:04:48 +02:00
HARADA Hiroyuki
5f32ec218b Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-19 04:10:17 +02:00
J-Jamet
3b9a884db2 Remove TODO 2020-08-18 18:08:26 +02:00
J-Jamet
ada6b85868 Fix entry extra field replacement 2020-08-18 18:06:16 +02:00
HARADA Hiroyuki
2f8a4f447c Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-18 18:04:30 +02:00
J-Jamet
9bd6499271 Fix small issue 2020-08-18 17:09:51 +02:00
J-Jamet
76fcc919ef Encapsulate putItems 2020-08-18 16:46:09 +02:00
J-Jamet
a382297edf Encapsulate adapters in AnimatedItemsAdapter 2020-08-18 16:33:48 +02:00
HARADA Hiroyuki
4987dfe4f6 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-18 16:22:12 +02:00
J-Jamet
d1a2e50b8d Edit extra fields as recyclerview 2020-08-18 16:12:18 +02:00
HARADA Hiroyuki
b9e44b6166 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-18 14:12:31 +02:00
HARADA Hiroyuki
af601edc94 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-16 23:32:56 +02:00
Alexander Ritter
6640dcf9cd Translated using Weblate (German)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-08-16 23:32:56 +02:00
J-Jamet
9b2734ed38 Fix populate extra field 2020-08-16 10:40:17 +02:00
J-Jamet
afcfce056f Fix collapse animation 2020-08-16 09:53:47 +02:00
J-Jamet
8f3af2f27b Fix small issues in kitkat 2020-08-15 21:52:39 +02:00
J-Jamet
706aae47b3 Fix small cardView issue 2020-08-15 21:27:35 +02:00
J-Jamet
d494295b21 Harmonize image reverse buttons 2020-08-15 21:06:15 +02:00
J-Jamet
c1600a253b Harmonize image buttons 2020-08-15 20:41:57 +02:00
J-Jamet
a3bc29ad8f Fix expand uri info view 2020-08-15 15:54:08 +02:00
J-Jamet
83225ed157 Fix real attachment deletion 2020-08-15 15:38:46 +02:00
J-Jamet
f13f6dc01f Fix attachments deletion and change entry edit layout 2020-08-15 15:28:25 +02:00
J-Jamet
2d2489443a New icon to create extra field and fix focus 2020-08-15 12:41:16 +02:00
J-Jamet
0e5b7fbfa2 Possibility to remove attachment 2020-08-15 12:01:08 +02:00
HARADA Hiroyuki
d16a8068f7 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-14 16:35:05 +02:00
J-Jamet
c5ef11febc Tint password generator color 2020-08-14 15:50:54 +02:00
J-Jamet
027f31447a Fix delegation 2020-08-14 15:48:18 +02:00
HARADA Hiroyuki
41d1a4e5fb Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-14 09:49:39 +02:00
J-Jamet
924e3191cb Fix entry extra field view 2020-08-14 00:11:08 +02:00
J-Jamet
ebdc6b8fd9 Fix entry extra field view 2020-08-13 19:37:55 +02:00
HARADA Hiroyuki
5c05128cd7 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-13 18:41:25 +02:00
J-Jamet
5bf5685a12 Refactor extra field 2020-08-13 17:43:40 +02:00
J-Jamet
f7391cb4c4 Refactor extra field 2020-08-13 17:33:03 +02:00
HARADA Hiroyuki
bad7bd8884 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-13 17:20:25 +02:00
HARADA Hiroyuki
e0524a1656 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-13 17:05:45 +02:00
HARADA Hiroyuki
f59af9baa3 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-13 15:47:06 +02:00
HARADA Hiroyuki
b36890ca82 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-13 15:26:39 +02:00
HARADA Hiroyuki
22703af08b Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-13 15:12:46 +02:00
HARADA Hiroyuki
c4108269b3 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-13 14:55:52 +02:00
HARADA Hiroyuki
94c9d090cf Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-13 09:35:13 +02:00
HARADA Hiroyuki
28b9235a43 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-13 08:16:50 +02:00
librada
3aebae5e15 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-12 20:02:06 +02:00
librada
2631cb75d6 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-12 17:46:07 +02:00
librada
07b8b4156f Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-12 15:49:43 +02:00
librada
7978967c1a Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-12 14:26:02 +02:00
librada
3f24ff4de3 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-12 14:13:53 +02:00
librada
7253dd82a6 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-12 13:41:00 +02:00
librada
2b8eb3ae7e Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-12 13:36:08 +02:00
librada
429eae71cd Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-12 13:00:40 +02:00
librada
e5c552defb Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-12 12:42:27 +02:00
librada
5c950a2e2c Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-12 12:31:54 +02:00
librada
577ff78189 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-12 12:11:12 +02:00
librada
3f3cde05f7 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-12 11:30:28 +02:00
sivemortenfan
b48f2c3276 Translated using Weblate (Malayalam)
Currently translated at 65.4% (293 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2020-08-12 10:59:27 +02:00
ssantos
8e818846f0 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2020-08-12 10:59:26 +02:00
librada
1554e37f8c Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-12 10:59:24 +02:00
librada
affaabd011 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-12 10:47:35 +02:00
librada
f34a8f991c Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-12 08:30:37 +02:00
J-Jamet
c2b14d610b Revert "Small duration change in animation"
This reverts commit 23155279ab.
2020-08-11 17:52:32 +02:00
J-Jamet
02693d0cbb Fix database opening after creation 2020-08-11 16:57:16 +02:00
J-Jamet
23155279ab Small duration change in animation 2020-08-11 16:16:53 +02:00
J-Jamet
2d23f7403d Fix expand icon rotation 2020-08-11 15:59:35 +02:00
J-Jamet
ae411c6fd5 default database button at right 2020-08-10 20:26:23 +02:00
J-Jamet
ab8d6075a9 Allow dark style in free version 2020-08-10 18:10:19 +02:00
J-Jamet
bc5ae29a67 Default database in database selection 2020-08-10 17:36:06 +02:00
librada
1a8aabc30c Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-10 17:20:04 +02:00
J-Jamet
8c51d7f713 Add animation when delete custom field 2020-08-10 12:59:09 +02:00
J-Jamet
8a1485e7ce Change themes 2020-08-10 11:16:04 +02:00
J-Jamet
614145431a Replace info icon 2020-08-10 10:13:00 +02:00
J-Jamet
db25f1999f Remove flickering on the first database list load 2020-08-10 10:02:53 +02:00
J-Jamet
4ed231b9bb Replace and animate database info expand icon 2020-08-10 00:03:23 +02:00
J-Jamet
25a5342c11 Fix database opening after creation 2020-08-09 23:27:49 +02:00
J-Jamet
c7202e3ca9 Fix database list animation 2020-08-09 23:19:01 +02:00
C. Rüdinger
89c2e94cea Translated using Weblate (German)
Currently translated at 99.5% (446 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-08-09 22:32:47 +02:00
Oliver
3dc46771b5 Translated using Weblate (German)
Currently translated at 99.5% (446 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-08-09 22:32:47 +02:00
J-Jamet
0eac4d4d7f Fix database list change 2020-08-09 20:59:34 +02:00
J-Jamet
a0ceb788db Upgrade dependencies 2020-08-09 18:39:00 +02:00
J-Jamet
98fb36d03a Try to fix notification lock 2020-08-09 15:46:39 +02:00
J-Jamet
a670006517 Bottom app bar with center FAB 2020-08-09 14:25:22 +02:00
J-Jamet
9cdbe67cd4 Rollback stop task service before opening, ViewModel implementation seems fix the previous crash 2020-08-08 15:58:25 +02:00
Vachan
bbe8af452c Translated using Weblate (Malayalam)
Currently translated at 65.1% (292 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2020-08-08 15:32:47 +02:00
librada
f5edf28ce1 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-08 15:32:46 +02:00
J-Jamet
8fc30f590b Update CHANGELOG 2020-08-08 13:36:39 +02:00
J-Jamet
e578f23ebe Fix field order by using LinkedHashMap #611 2020-08-08 13:34:28 +02:00
J-Jamet
99c488fc9e Upgrade CHANGELOG 2020-08-08 10:41:07 +02:00
J-Jamet
f6a710660d Merge branch 'feature/ViewModel' into develop 2020-08-08 10:36:39 +02:00
J-Jamet
a61744bb65 Fix icon color 2020-08-07 21:14:27 +02:00
J-Jamet
17c3078c24 Better thread callback encapsulation 2020-08-07 21:00:20 +02:00
J-Jamet
5fa7731b56 Fix ViewModel methods call 2020-08-07 20:33:06 +02:00
J-Jamet
c8e2be4d8c Fix minor variable name 2020-08-07 16:51:55 +02:00
J-Jamet
e3db613a07 Rename progress database task provider 2020-08-07 16:45:52 +02:00
J-Jamet
0f7839027f Start using ViewModel for internal database action 2020-08-07 16:41:28 +02:00
J-Jamet
31b322a108 Replace AsyncTask by Coroutines 2020-08-07 11:29:16 +02:00
J-Jamet
b3c0494618 Better file attachment download implementation 2020-08-06 16:59:15 +02:00
J-Jamet
78ddb0533d Attachment download as coroutine 2020-08-06 16:35:15 +02:00
Vachan
da2158e7f2 Translated using Weblate (Malayalam)
Currently translated at 62.9% (282 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2020-08-06 10:32:49 +02:00
Stephan Paternotte
d2a1efb6e7 Translated using Weblate (Dutch)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2020-08-06 10:32:46 +02:00
librada
98a880db2d Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-08-06 10:32:46 +02:00
J-Jamet
cb82ef8703 Fix bottom app bar color in Kitkat 2020-08-05 17:00:01 +02:00
J-Jamet
d56246767b Better focus implementation 2020-08-05 16:37:30 +02:00
J-Jamet
bb477984aa Fix icons in arabic language 2020-08-05 15:51:06 +02:00
J-Jamet
5a3e650e02 Fix RTL views 2020-08-05 15:23:49 +02:00
J-Jamet
3c96dd2fac Upgrade CHANGELOG 2020-08-05 14:47:21 +02:00
J-Jamet
da051e3ff3 Fix biometric view visibility 2020-08-05 14:44:47 +02:00
J-Jamet
d15b6323c2 Upgrade CHANGELOG 2020-08-05 14:23:29 +02:00
J-Jamet
ec5f8fe4a4 Merge branch 'feature/CustomFieldLayout' into develop 2020-08-05 14:19:59 +02:00
J-Jamet
71d84d76f8 Fix last entry in Magikeyboard memory #653 2020-08-05 14:19:37 +02:00
J-Jamet
d33ed52ec2 Adjust Pan for EntryEdit 2020-08-05 11:00:23 +02:00
J-Jamet
3a970544bb Max password field lines -> 10 2020-08-04 19:58:02 +02:00
J-Jamet
792ac3a2e8 Fix password field 2020-08-04 19:51:22 +02:00
J-Jamet
60bbc27401 Unique password field with password generator button 2020-08-04 19:40:14 +02:00
J-Jamet
cf7cbcb6e6 Focus OTP view after creation 2020-08-04 19:03:04 +02:00
J-Jamet
c126a8eba9 Fix custom field creation 2020-08-04 18:51:00 +02:00
J-Jamet
66a60d0357 Fix manifest 2020-08-04 18:06:51 +02:00
J-Jamet
1acdadd027 Fix styles 2020-08-04 16:28:36 +02:00
J-Jamet
200be9dadd Fix toolbar padding with lock button 2020-08-04 15:19:49 +02:00
J-Jamet
a73e2872a4 Lock button color 2020-08-04 12:14:59 +02:00
J-Jamet
ce6f7729c5 Lock button with toolbar 2020-08-04 11:38:54 +02:00
J-Jamet
c285411371 Rollback lock button 2020-08-04 10:38:49 +02:00
J-Jamet
46394c600e New entry edit layout and custom lock 2020-08-03 20:54:20 +02:00
J-Jamet
bea9cb3248 Fix custom field error 2020-08-03 19:38:08 +02:00
J-Jamet
1087dcd714 New UI for custom field 2020-08-03 18:45:50 +02:00
Vachan
0b63029b7e Translated using Weblate (Malayalam)
Currently translated at 27.0% (121 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2020-08-01 09:47:48 +02:00
J-Jamet
9679d24414 New UI for Biometric 2020-08-01 01:00:53 +02:00
J-Jamet
7947fd53e5 Change biometric button layout 2020-07-31 23:34:00 +02:00
J-Jamet
8dedf75565 Minor style fix 2020-07-31 22:54:44 +02:00
J-Jamet
b5b7c12b49 Fix bar color in splashscreen 2020-07-31 22:15:04 +02:00
J-Jamet
51f4e3cc3a Change CardView margin 2020-07-31 21:59:16 +02:00
J-Jamet
24b0315d2e Update CHANGELOG 2020-07-31 21:31:05 +02:00
J-Jamet
be446220eb Fix themes 2020-07-31 21:29:44 +02:00
Vachan
a3ef2d332e Added translation using Weblate (Malayalam) 2020-07-31 05:33:41 +02:00
Milo Ivir
ba6fe576e3 Translated using Weblate (Croatian)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-07-30 08:42:39 +02:00
abidin toumi
abcef38102 Translated using Weblate (Arabic)
Currently translated at 61.3% (275 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2020-07-30 08:42:39 +02:00
ihor_ck
d5780b2f30 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-07-30 08:42:38 +02:00
librada
f7e498a0a2 Translated using Weblate (Japanese)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-07-30 08:42:00 +02:00
Petri Salminen
51ac7ca2de Translated using Weblate (Finnish)
Currently translated at 67.4% (302 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fi/
2020-07-30 08:41:57 +02:00
Aman ALam
c94535f6b5 Translated using Weblate (Punjabi)
Currently translated at 57.8% (259 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pa/
2020-07-25 10:10:55 +02:00
Thomas
07457ae368 Translated using Weblate (Chinese (Traditional))
Currently translated at 51.7% (232 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2020-07-25 10:10:55 +02:00
WaldiS
4767fff08c Translated using Weblate (Polish)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-07-25 10:10:55 +02:00
librada
f0c3498ecc Translated using Weblate (Japanese)
Currently translated at 71.4% (320 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-07-25 10:10:43 +02:00
Rodrigo Saldaña
1eca52d0fe Translated using Weblate (Spanish)
Currently translated at 76.3% (342 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-07-25 10:10:40 +02:00
SeerLite
7fbac9ad2f Translated using Weblate (Spanish)
Currently translated at 76.3% (342 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-07-25 10:10:40 +02:00
J-Jamet
6fb80ed50b Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2020-07-24 09:50:36 +02:00
J-Jamet
47f63ac81b Fix themes for free version 2020-07-24 09:50:26 +02:00
J-Jamet
42cc0b28ba Merge branch 'feature/Merge_Action_tasks_#628' into develop 2020-07-23 13:49:43 +02:00
J-Jamet
993806f781 Refactor Database.getInstance() in Task service 2020-07-22 23:48:07 +02:00
J-Jamet
8a5af33aaa Fix crash 2020-07-22 23:20:16 +02:00
J-Jamet
a974e36e9e Merge Open and Task services 2020-07-22 22:43:21 +02:00
Allan Nordhøy
17bcb2b39e Translated using Weblate (Norwegian Bokmål)
Currently translated at 80.3% (360 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-07-21 19:41:54 +02:00
ssantos
cddf02d0c1 Translated using Weblate (Portuguese (Portugal))
Currently translated at 60.9% (273 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2020-07-21 19:41:54 +02:00
librada
75ff7ece37 Translated using Weblate (Japanese)
Currently translated at 54.6% (245 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-07-21 19:41:54 +02:00
jan madsen
ec2b407a20 Translated using Weblate (Danish)
Currently translated at 97.3% (436 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-07-21 19:41:53 +02:00
ssantos
1dc9d78e54 Translated using Weblate (Portuguese (Portugal))
Currently translated at 60.4% (271 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2020-07-19 03:37:27 +02:00
WaldiS
5742a75c9d Translated using Weblate (Polish)
Currently translated at 99.1% (444 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-07-19 03:37:27 +02:00
zeritti
b5e9ad6d7e Translated using Weblate (Czech)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-07-19 03:37:27 +02:00
Oğuz Ersen
6393025219 Translated using Weblate (Turkish)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-07-18 08:42:19 +02:00
Eric
9ab3e289bc Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-07-18 08:42:19 +02:00
ihor_ck
6454474886 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-07-18 08:42:19 +02:00
solokot
c5720a7a03 Translated using Weblate (Russian)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-07-18 08:42:18 +02:00
C. Rüdinger
41b15adc6d Translated using Weblate (German)
Currently translated at 98.8% (443 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-07-18 08:42:18 +02:00
J-Jamet
05b962e718 First commit to merge action tasks 2020-07-17 11:02:58 +02:00
Hosted Weblate
1f01ca7b85 Merge branch 'origin/master' into Weblate. 2020-07-17 10:31:51 +02:00
J-Jamet
5d3b4fa5ec Update CHANGELOG and fix descriptions by version 2020-07-17 10:26:26 +02:00
J-Jamet
754d2b2dd3 Merge tag '2.8.1' into develop
2.8.1
2020-07-17 10:12:30 +02:00
J-Jamet
ffad62e3dc Merge branch 'release/2.8.1' 2020-07-17 10:12:19 +02:00
J-Jamet
1c11e16565 Update fastlane README.md 2020-07-17 10:12:09 +02:00
J-Jamet
edc12990b4 Change switch keyboard strings 2020-07-17 10:00:07 +02:00
J-Jamet
12ea234d18 Fix coroutines exceptions #627 2020-07-17 09:32:49 +02:00
Milo Ivir
0461206a61 Translated using Weblate (Croatian)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-07-16 23:41:52 +02:00
Serdar Sağlam
663f9e3962 Translated using Weblate (Turkish)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-07-16 23:41:52 +02:00
Eric
34ee948c8e Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-07-16 23:41:51 +02:00
ihor_ck
1bb9c2e4fe Translated using Weblate (Ukrainian)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-07-16 23:41:51 +02:00
solokot
8ab18ce5cc Translated using Weblate (Russian)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-07-16 23:41:51 +02:00
WaldiS
71be16826e Translated using Weblate (Polish)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-07-16 23:41:51 +02:00
Oğuz Ersen
926c09d9df Translated using Weblate (Turkish)
Currently translated at 100.0% (448 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-07-15 04:50:15 +02:00
ihor_ck
66c065ae7f Translated using Weblate (Ukrainian)
Currently translated at 98.8% (443 of 448 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-07-15 04:50:15 +02:00
Hosted Weblate
083ed7775c Merge branch 'origin/master' into Weblate. 2020-07-14 18:02:48 +02:00
Thomas
5185452495 Translated using Weblate (Chinese (Traditional))
Currently translated at 50.4% (224 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2020-07-14 18:02:47 +02:00
J-Jamet
fa15f226ab Merge tag '2.8' into develop
2.8
2020-07-14 17:19:30 +02:00
J-Jamet
fea4da2a33 Merge branch 'release/2.8' 2020-07-14 17:19:23 +02:00
J-Jamet
055c933f4b Replace <strong> and </strong> strings 2020-07-14 17:09:52 +02:00
J-Jamet
9bcb867748 Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2020-07-14 17:07:07 +02:00
J-Jamet
26e3c03f5f Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2020-07-13 08:39:08 +02:00
J-Jamet
c195c3b2d1 Remove unexpected char #622 2020-07-13 08:38:50 +02:00
J-Jamet
f9e0aacfeb Fix clipboard notification after orientation change 2020-07-10 18:08:26 +02:00
J-Jamet
37fef66647 Improve recognition to reset app timeout #562 2020-07-10 17:52:58 +02:00
J-Jamet
9f99b67563 Improve recognition to reset app timeout #562 2020-07-10 17:46:14 +02:00
J-Jamet
fe902648ad Settings to back to the previous keyboard during database credentials and after form filling #601 2020-07-10 13:00:27 +02:00
J-Jamet
13b933cd0b Try to fix biometric prompt in other activities #602 2020-07-09 19:34:57 +02:00
abidin toumi
1d3b1d1d80 Translated using Weblate (Arabic)
Currently translated at 61.0% (271 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2020-07-08 09:41:52 +02:00
Stephan Paternotte
67a612af3a Translated using Weblate (Dutch)
Currently translated at 100.0% (444 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2020-07-08 09:41:52 +02:00
J-Jamet
a891683806 Fix close option menu error 2020-07-06 20:05:03 +02:00
J-Jamet
440a72fc42 Move timeout helper call in service 2020-07-06 19:59:11 +02:00
J-Jamet
696d2e5197 Fix RemoteServiceException 2020-07-06 19:55:17 +02:00
J-Jamet
2b17d56fc7 Allow open database during selection mode #608 2020-07-06 17:55:08 +02:00
J-Jamet
a410ef5d9f Fix education hint in selection mode #600 2020-07-06 15:59:14 +02:00
Aman ALam
fe94769541 Translated using Weblate (Punjabi)
Currently translated at 55.6% (247 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pa/
2020-07-05 02:41:49 +02:00
WaldiS
c63ae9c00c Translated using Weblate (Polish)
Currently translated at 100.0% (444 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-07-05 02:41:49 +02:00
J-Jamet
d5ece8d007 Upgrade CHANGELOG 2020-07-04 19:19:05 +02:00
J-Jamet
692a971dc0 Fix issue #615 and #612, action loading 2020-07-04 15:45:07 +02:00
J-Jamet
05b8370cc0 Rename saveView by validateButton 2020-07-04 11:58:06 +02:00
J-Jamet
b6111b35a2 Change clipboard strings and remove swipe to clear #605 2020-07-04 11:27:21 +02:00
J-Jamet
4d72687628 Change ACTION_OPEN_DOCUMENT and flags 2020-07-02 13:36:46 +02:00
J-Jamet
8f125983ce Change selection mode toolbar style 2020-07-02 13:10:32 +02:00
J-Jamet
4279825caa Fix search in recycle bin #613 2020-07-02 12:10:04 +02:00
J-Jamet
77ae3a4623 Merge branch 'feature/javaLang_ClassCastException' into develop 2020-07-02 11:59:26 +02:00
J-Jamet
4c222dbc54 Upgrade CHANGELOG 2020-07-02 10:49:24 +02:00
J-Jamet
4e0f93ee8a Merge branch 'develop' into feature/javaLang_ClassCastException 2020-07-02 10:44:03 +02:00
abidin toumi
e99f3e6627 Translated using Weblate (Arabic)
Currently translated at 55.4% (246 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2020-06-27 01:49:37 +02:00
Milo Ivir
f73877c34a Translated using Weblate (German)
Currently translated at 100.0% (444 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-06-25 16:41:46 +02:00
J-Jamet
abd3f12cae Improve OTP form recognition 2020-06-23 15:53:13 +02:00
J-Jamet
00117f5b7b Rename UnsignedInt methods 2020-06-23 14:34:02 +02:00
J-Jamet
d7d728f93e Rename UnsignedInt and UnsignedLong methods 2020-06-23 14:33:43 +02:00
J-Jamet
dc9217c4ec Remove unused java.lang import 2020-06-23 14:07:03 +02:00
Milo Ivir
95acb13b93 Translated using Weblate (Croatian)
Currently translated at 100.0% (444 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-06-23 12:41:52 +02:00
WaldiS
234cc00d9f Translated using Weblate (Polish)
Currently translated at 99.3% (441 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-06-23 12:41:48 +02:00
Oliver Cervera
d6ba164799 Translated using Weblate (Italian)
Currently translated at 98.6% (438 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-06-23 12:41:47 +02:00
S L
910aa03dc8 Translated using Weblate (Finnish)
Currently translated at 31.0% (138 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fi/
2020-06-23 12:41:46 +02:00
C. Rüdinger
a3e4a4c873 Translated using Weblate (German)
Currently translated at 99.3% (441 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-06-23 12:41:45 +02:00
solokot
2d7c843447 Translated using Weblate (Russian)
Currently translated at 100.0% (444 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-06-21 21:49:03 +02:00
Aman ALam
e342b45473 Translated using Weblate (Punjabi)
Currently translated at 54.5% (242 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pa/
2020-06-20 12:37:24 +02:00
Milo Ivir
f354bccd58 Translated using Weblate (Croatian)
Currently translated at 75.4% (335 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-06-20 12:37:17 +02:00
Oğuz Ersen
98073134db Translated using Weblate (Turkish)
Currently translated at 100.0% (444 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-06-20 12:37:16 +02:00
Eric
360666b00b Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (444 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-06-20 12:37:16 +02:00
ihor_ck
ce4c807870 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (444 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-06-20 12:37:15 +02:00
solokot
f2783bdac8 Translated using Weblate (Russian)
Currently translated at 100.0% (444 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-06-20 12:37:15 +02:00
Stephan Paternotte
875ed16c3b Translated using Weblate (Dutch)
Currently translated at 99.5% (442 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2020-06-20 12:37:14 +02:00
Kunzisoft
383ba56d1f Translated using Weblate (French)
Currently translated at 100.0% (444 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-06-20 12:37:14 +02:00
Oliver
45eb54e624 Translated using Weblate (German)
Currently translated at 99.3% (441 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-06-20 12:37:13 +02:00
zeritti
5aff4e2ed6 Translated using Weblate (Czech)
Currently translated at 100.0% (444 of 444 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-06-20 12:37:13 +02:00
Aman ALam
e73e47dd94 Added translation using Weblate (Punjabi) 2020-06-18 20:51:10 +02:00
Hosted Weblate
1c8ac5efbc Merge branch 'origin/master' into Weblate. 2020-06-18 15:49:42 +02:00
abidin toumi
90fa5e1ecd Translated using Weblate (Arabic)
Currently translated at 54.2% (235 of 433 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2020-06-18 15:49:42 +02:00
J-Jamet
348994917b Upgrade to 2.8 2020-06-18 15:31:15 +02:00
J-Jamet
60dbea1027 Merge tag '2.7' into develop
2.7
2020-06-18 14:59:36 +02:00
J-Jamet
dae19bbccf Merge branch 'release/2.7' 2020-06-18 14:59:28 +02:00
J-Jamet
c81f83887e Replace <strong> tags 2020-06-18 14:44:50 +02:00
J-Jamet
04e555dde9 Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2020-06-18 14:25:28 +02:00
J-Jamet
be94518e31 Upgrade CHANGELOG 2020-06-18 14:25:06 +02:00
J-Jamet
66bec1e08c Merge branch 'feature/Button_Block_Autofill' into develop 2020-06-18 14:14:58 +02:00
J-Jamet
f61ce10716 Fix action bar style 2020-06-17 23:23:57 +02:00
J-Jamet
b1b92b2995 Add only the current app id in blocklist 2020-06-17 23:06:04 +02:00
J-Jamet
bd9f2c4757 Check if the search element is blocked after Autofill popup click 2020-06-17 22:49:09 +02:00
abidin toumi
a3ead2153e Translated using Weblate (Arabic)
Currently translated at 51.2% (222 of 433 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2020-06-17 22:41:44 +02:00
J-Jamet
e12f008b92 Merge branch 'develop' into feature/Button_Block_Autofill 2020-06-17 22:16:08 +02:00
J-Jamet
d064ece0ff Fix view flickering 2020-06-17 22:13:18 +02:00
J-Jamet
379fbf68b1 Fix special mode title 2020-06-17 21:54:59 +02:00
J-Jamet
83783c1a88 Better special mode implementation 2020-06-17 21:49:20 +02:00
J-Jamet
c7e46205b3 Encapsulate special mode view and add blocking menu for autofill 2020-06-17 21:29:28 +02:00
J-Jamet
0c61f0ded2 Better search info title 2020-06-17 20:22:17 +02:00
J-Jamet
49e2ec0498 Toolbar as SpecialModeView, show title 2020-06-17 20:01:45 +02:00
ihor_ck
fb0a74c101 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (433 of 433 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-06-16 20:41:43 +02:00
Renann Prado
245a7ddfe9 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (433 of 433 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2020-06-14 23:19:30 +02:00
Stephan Paternotte
ca8874c2e1 Translated using Weblate (Dutch)
Currently translated at 99.5% (431 of 433 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2020-06-14 23:19:29 +02:00
Oliver
dbcd7c8e03 Translated using Weblate (German)
Currently translated at 100.0% (433 of 433 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-06-14 23:19:29 +02:00
J-Jamet
9cce63659e Fix selection mode and search with autofill 2020-06-13 23:18:39 +02:00
J-Jamet
be0bbab0c8 Add default values in Blocklists and input password as autofill hints 2020-06-13 17:01:59 +02:00
J-Jamet
7b6d3698c4 Add inputType as Hex 2020-06-13 15:20:24 +02:00
J-Jamet
56daca8b4f Fix default entry icon #589 2020-06-12 20:17:01 +02:00
J-Jamet
ed382d102e Fix backstack in selection mode, add cancel button #589 2020-06-12 19:57:01 +02:00
Filip Miletic
745de2502e Translated using Weblate (Croatian)
Currently translated at 71.5% (310 of 433 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-06-11 04:41:42 +02:00
zeritti
503a4b1374 Translated using Weblate (Czech)
Currently translated at 100.0% (433 of 433 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-06-11 04:41:42 +02:00
J-Jamet
0b71b2d659 Fix populate Magikeyboard when URL is present 2020-06-10 20:50:55 +02:00
J-Jamet
2ad244df94 Update CHANGELOG 2020-06-10 20:43:16 +02:00
J-Jamet
d0da4f03a6 Search through subdomains as parameter #584 2020-06-10 20:40:01 +02:00
J-Jamet
ab0cd4152a Fix search and autofill performance 2020-06-10 20:25:54 +02:00
J-Jamet
93d04bfe60 Fix crash 2020-06-10 20:20:18 +02:00
J-Jamet
b1ccb40bd3 Upgrade CHANGELOG 2020-06-10 19:08:19 +02:00
J-Jamet
d177001ea8 Upgrade CHANGELOG 2020-06-10 19:03:28 +02:00
J-Jamet
2f921897c7 Block list to blocklist 2020-06-10 18:58:40 +02:00
J-Jamet
1a0f7146ce Merge branch 'feature/Autofill_Blocklist' #571 2020-06-10 18:53:53 +02:00
J-Jamet
dff2386594 Fix shrink 2020-06-10 18:47:10 +02:00
J-Jamet
55cc782cc6 Keep list during orientation change 2020-06-10 18:33:22 +02:00
J-Jamet
6903099873 Better block list algorithm 2020-06-10 18:16:37 +02:00
J-Jamet
6e2d84be33 Dialog input much better 2020-06-10 18:02:52 +02:00
J-Jamet
9e542d0bbe Dialog input much better 2020-06-10 17:52:35 +02:00
J-Jamet
ade9af9ecd Block list verification 2020-06-10 16:43:23 +02:00
J-Jamet
59f11a1b26 Distinct block lists for AppId and WebDomain 2020-06-10 15:40:04 +02:00
Stephan Paternotte
cc1d6e2b47 Translated using Weblate (Dutch)
Currently translated at 99.5% (431 of 433 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2020-06-09 22:41:42 +02:00
J-Jamet
2655b1b3d1 Fix blocklist views 2020-06-09 20:15:27 +02:00
J-Jamet
053dd28f8c Rename to blocklist and fix persistent element 2020-06-09 20:07:34 +02:00
J-Jamet
65e4cf83d8 First commit for autofill blocklist 2020-06-09 18:35:40 +02:00
Oğuz Ersen
419099318c Translated using Weblate (Turkish)
Currently translated at 100.0% (433 of 433 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-06-08 02:41:43 +02:00
Allan Nordhøy
972edd3a30 Translated using Weblate (Norwegian Bokmål)
Currently translated at 80.3% (348 of 433 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-06-08 02:41:42 +02:00
Eric
cc4125e766 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (433 of 433 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-06-08 02:41:42 +02:00
ihor_ck
da40cc9830 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (433 of 433 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-06-08 02:41:41 +02:00
solokot
29d7e2dcfe Translated using Weblate (Russian)
Currently translated at 100.0% (433 of 433 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-06-06 11:58:57 +02:00
WaldiS
3d906fd582 Translated using Weblate (Polish)
Currently translated at 100.0% (433 of 433 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-06-06 11:58:56 +02:00
jan madsen
0cad43c18b Translated using Weblate (Danish)
Currently translated at 98.6% (427 of 433 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-06-06 11:58:56 +02:00
Hosted Weblate
c66b686a63 Merge branch 'origin/master' into Weblate. 2020-06-06 08:22:05 +02:00
J-Jamet
e2a1e3f327 Fix autofill for chrome 83.0.4103.96 #551 2020-06-05 22:56:00 +02:00
J-Jamet
2b5cf75a53 Update to 2.7 2020-06-05 15:46:25 +02:00
J-Jamet
1c350ac87b Merge tag '2.6' into develop
2.6
2020-06-05 14:31:39 +02:00
J-Jamet
ef6539909c Merge branch 'release/2.6' 2020-06-05 14:31:30 +02:00
J-Jamet
47d7c6fe65 Fix could not read credentials 2020-06-05 14:04:30 +02:00
J-Jamet
c94c5fbc95 Replace KeePass DX by KeePassDX 2020-06-05 13:20:09 +02:00
J-Jamet
63a833d114 Remove beta_dontask 2020-06-05 13:12:03 +02:00
J-Jamet
72162128e2 Fix strings 2020-06-05 12:56:48 +02:00
Kunzisoft
a8f712c335 Translated using Weblate (Telugu)
Currently translated at 1.8% (8 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/te/
2020-06-05 12:41:42 +02:00
Kunzisoft
ea8888a685 Translated using Weblate (Galician)
Currently translated at 6.7% (29 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2020-06-05 12:41:42 +02:00
Kunzisoft
2aecf69b67 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 14.5% (63 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nn/
2020-06-05 12:41:42 +02:00
Kunzisoft
1a4c24dd86 Translated using Weblate (Latvian)
Currently translated at 16.2% (70 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lv/
2020-06-05 12:41:41 +02:00
Kunzisoft
6cf2b45051 Translated using Weblate (Lithuanian)
Currently translated at 12.7% (55 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2020-06-05 12:41:41 +02:00
Kunzisoft
38dd2bdf6e Translated using Weblate (Finnish)
Currently translated at 20.1% (87 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fi/
2020-06-05 12:41:41 +02:00
Kunzisoft
8784f1da70 Translated using Weblate (Basque)
Currently translated at 15.7% (68 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2020-06-05 12:41:41 +02:00
Kunzisoft
b9208ea94e Translated using Weblate (Catalan)
Currently translated at 59.7% (258 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2020-06-05 12:41:41 +02:00
J-Jamet
dfb648b480 Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2020-06-05 11:31:40 +02:00
J-Jamet
f7af4f06ea Add short Toast after populate Magikeyboard 2020-06-03 19:24:05 +02:00
Destiny Li
d98f9eb62c Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-06-03 08:41:41 +02:00
Jörgen Sjöbom
fef1335f51 Translated using Weblate (Swedish)
Currently translated at 85.8% (371 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2020-06-03 08:41:41 +02:00
J. Lavoie
59f14cbdd4 Translated using Weblate (Italian)
Currently translated at 96.2% (416 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-06-03 08:41:40 +02:00
J. Lavoie
e8645c543f Translated using Weblate (French)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-06-03 08:41:39 +02:00
Filippo De Bortoli
b92f6177e3 Translated using Weblate (Italian)
Currently translated at 95.6% (413 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-06-02 07:41:30 +02:00
J. Lavoie
2888829a4f Translated using Weblate (Italian)
Currently translated at 95.6% (413 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-06-02 07:41:30 +02:00
J-Jamet
3d392ead19 Try to fix biometric prompt flickering 2020-06-01 21:16:58 +02:00
J-Jamet
a79b2bfa79 Merge branch 'develop' into TacoTheDank-develop 2020-06-01 13:59:56 +02:00
J-Jamet
15e1e2f02e Remove apache commons-io lib dependencies 2020-06-01 13:55:17 +02:00
J-Jamet
9e652803d0 Merge branch 'develop' of git://github.com/TacoTheDank/KeePassDX into TacoTheDank-develop 2020-06-01 12:07:10 +02:00
J-Jamet
7795fceb72 Merge branch 'mattiasduerrmeier-master' into develop 2020-05-31 16:32:05 +02:00
J-Jamet
9f01f26ea6 Merge branch 'master' of git://github.com/mattiasduerrmeier/KeePassDX into mattiasduerrmeier-master 2020-05-31 16:31:07 +02:00
TacoTheDank
abbc9a1d7a Update firamono font 2020-05-29 22:53:53 -04:00
TacoTheDank
2c15befb8e Update many dependencies 2020-05-29 22:35:53 -04:00
TacoTheDank
ba8ea84c4e Update Studio, use Kotlin JDK8 2020-05-29 21:25:59 -04:00
TacoTheDank
97912046da Update gradle wrapper 2020-05-29 21:21:57 -04:00
mattiasduerrmeier
43e99c23e3 Update strings.xml 2020-05-29 10:35:55 +02:00
mattiasduerrmeier
ffe14d75fc update : delete password description
I didn't quite understand what the delete password settings does, and I found the description not to be complete.
I think changing the description could make it clearer. 
I thought about adding "to an existing database", but I wanted to keep it short.
2020-05-28 15:40:25 +02:00
ButterflyOfFire
872ef66641 Translated using Weblate (Arabic)
Currently translated at 50.0% (216 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2020-05-28 10:41:37 +02:00
J-Jamet
6363862ec2 Fix Autofill for in-app purchases #529 2020-05-27 13:48:46 +02:00
J-Jamet
775a112e83 Fix Magikeyboard delete key 2020-05-27 13:01:06 +02:00
J-Jamet
7168904290 Fix node selection color 2020-05-27 12:28:58 +02:00
Stephan Paternotte
4213209b08 Translated using Weblate (Dutch)
Currently translated at 99.5% (430 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2020-05-27 08:41:37 +02:00
Oymate
5dce91b7f6 Translated using Weblate (Bengali (Bangladesh))
Currently translated at 4.6% (20 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bn_BD/
2020-05-25 23:17:27 +02:00
Eric
6d7fb9f87c Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-05-25 23:17:26 +02:00
ihor_ck
cc6c9dd8d1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-05-25 23:17:26 +02:00
Allan Nordhøy
21ebec52f3 Translated using Weblate (Greek)
Currently translated at 99.3% (429 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2020-05-25 23:17:26 +02:00
C. Rüdinger
11023ab225 Translated using Weblate (German)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-05-25 23:17:26 +02:00
scootergrisen
fe97b15905 Translated using Weblate (Danish)
Currently translated at 97.6% (422 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-05-25 23:17:25 +02:00
J-Jamet
de8738aa03 Default group icon for a new entry #293 2020-05-25 19:36:36 +02:00
J-Jamet
568bbf9126 Upgrade CHANGELOG 2020-05-25 19:06:57 +02:00
J-Jamet
a2481652da Better Autofill recognition #564 2020-05-25 19:03:33 +02:00
Aurel F
33e3d3272d Translated using Weblate (Romanian)
Currently translated at 92.8% (401 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2020-05-24 11:41:46 +02:00
Stephan Paternotte
b909651c29 Translated using Weblate (Dutch)
Currently translated at 99.5% (430 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2020-05-24 11:41:45 +02:00
J-Jamet
16794c5252 Merge branch 'feature/ShareTarget' into develop 2020-05-23 18:23:42 +02:00
J-Jamet
49ad270d88 Remove keyboard entry when no info to fill
Remove toast
2020-05-23 18:17:01 +02:00
J-Jamet
df9b6cb7e8 Revert "Fix back stack"
This reverts commit 797dc706e2.
2020-05-23 15:57:39 +02:00
J-Jamet
c6c4551928 Fix small string 2020-05-23 15:45:56 +02:00
J-Jamet
de84353eb0 Add MagikeyboardLauncherActivity 2020-05-23 15:34:04 +02:00
J-Jamet
6f05b80f34 Remove exported for EntrySelectionLauncherActivity 2020-05-23 15:02:45 +02:00
J-Jamet
797dc706e2 Fix back stack 2020-05-23 15:01:34 +02:00
J-Jamet
bdb615bcf9 Fix readonly and search after orientation change 2020-05-23 14:12:33 +02:00
J-Jamet
1b98717b0e Fix readonly and search after orientation change 2020-05-23 13:59:24 +02:00
J-Jamet
7bd1aedada Search improvement 2020-05-23 12:59:05 +02:00
J-Jamet
34bf8f9d1f Fix flickering during share 2020-05-23 10:48:36 +02:00
J-Jamet
b07f70e9fe Fix Share search without Magikeyboard 2020-05-22 22:01:10 +02:00
J-Jamet
e635788955 Add standard search 2020-05-22 13:22:09 +02:00
ihor_ck
bdb1cef3e5 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-05-22 11:58:34 +02:00
zeritti
e5a32e65c0 Translated using Weblate (Czech)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-05-22 11:58:33 +02:00
J-Jamet
5fc77922b4 Auto search entry by share to populate keyboard 2020-05-21 14:54:14 +02:00
J-Jamet
ce8add2895 Upgrade CHANGELOG 2020-05-21 12:29:28 +02:00
J-Jamet
88e1e5b770 Fix background theme color 2020-05-21 11:52:06 +02:00
J-Jamet
76ecbd3497 Revert NotificationService to keep notification in API23 2020-05-21 11:39:17 +02:00
J-Jamet
e33c9b932f LockReceiver in onCreate NotificationService 2020-05-20 20:00:12 +02:00
J-Jamet
038f6caa04 Fix notification hide 2020-05-20 19:01:19 +02:00
Oymate
36f5249d71 Translated using Weblate (Bengali (Bangladesh))
Currently translated at 0.9% (4 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bn_BD/
2020-05-20 17:41:38 +02:00
Oğuz Ersen
570702a5bd Translated using Weblate (Turkish)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-05-20 17:41:38 +02:00
Andrew
5d03c9c644 Translated using Weblate (English)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-05-20 17:41:38 +02:00
Dwhite Reeves
83906def4a Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (431 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-05-20 17:41:37 +02:00
ihor_ck
33e0f25fb1 Translated using Weblate (Ukrainian)
Currently translated at 99.7% (431 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-05-20 17:41:37 +02:00
solokot
2080de4139 Translated using Weblate (Russian)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-05-20 17:41:36 +02:00
WaldiS
decaca889c Translated using Weblate (Polish)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-05-20 17:41:36 +02:00
Anonymous
794a39032f Translated using Weblate (Finnish)
Currently translated at 20.1% (87 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fi/
2020-05-20 17:41:36 +02:00
Jami Kettunen
cc3cf17060 Translated using Weblate (Finnish)
Currently translated at 20.1% (87 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fi/
2020-05-20 17:41:35 +02:00
C. Rüdinger
b07da4bfa8 Translated using Weblate (German)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-05-20 17:41:35 +02:00
J-Jamet
4f52a4ee79 Fix biometric prompt - better code implementation #554 2020-05-20 17:30:50 +02:00
J-Jamet
cf77b999a3 Fix biometric prompt auto open after orientation change #554 2020-05-20 16:24:06 +02:00
J-Jamet
c9f062bdd9 Fix biometric prompt auto open after activity result #554 2020-05-20 15:27:34 +02:00
J-Jamet
12669f7ea0 Fix biometric prompt auto open after lock #554 2020-05-20 15:19:10 +02:00
J-Jamet
03451d2a6e Remove unused @JvmOverloads 2020-05-20 13:31:23 +02:00
J-Jamet
b5d7595f87 Try to fix service bug 2020-05-20 12:03:54 +02:00
J-Jamet
0d3d760a43 Try to fix custom field bug 2020-05-20 11:36:19 +02:00
J-Jamet
1cfc86e5ce Fix auto biometric prompt in background #554 2020-05-20 11:09:45 +02:00
J-Jamet
938343323d Shorter message for invalid credentials 2020-05-19 21:44:40 +02:00
Oymate
37112715c0 Added translation using Weblate (Bengali (Bangladesh)) 2020-05-19 17:08:25 +02:00
Andrew
bc80c10193 Translated using Weblate (Russian)
Currently translated at 99.7% (431 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-05-18 13:56:29 +02:00
J-Jamet
d989f48226 Fix unwanted opening of the dialog task #187 2020-05-17 14:19:52 +02:00
J-Jamet
45bcbb3a3d Fix entry not visually deleted in search #559 2020-05-16 19:06:30 +02:00
Oğuz Ersen
f4a74e0254 Translated using Weblate (Turkish)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-05-16 06:41:36 +02:00
Ashley Ouding
ace83cf68d Translated using Weblate (English)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-05-16 06:41:36 +02:00
Eric
209b9b9a6f Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-05-16 06:41:36 +02:00
ihor_ck
0d9dd6a33e Translated using Weblate (Ukrainian)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-05-16 06:41:35 +02:00
WaldiS
e4137a031e Translated using Weblate (Polish)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-05-16 06:41:35 +02:00
Anonymous
41e9ea7e4f Translated using Weblate (Japanese)
Currently translated at 44.4% (192 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-05-16 06:41:35 +02:00
librada
1a8b88973d Translated using Weblate (Japanese)
Currently translated at 44.4% (192 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2020-05-16 06:41:35 +02:00
zykure
3109e251b9 Translated using Weblate (German)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-05-16 06:41:34 +02:00
zeritti
452ab280f1 Translated using Weblate (Czech)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-05-16 06:41:33 +02:00
solokot
34ecfbb846 Translated using Weblate (Russian)
Currently translated at 99.7% (431 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-05-14 10:01:38 +02:00
Andrew
11fb97d275 Translated using Weblate (Russian)
Currently translated at 99.7% (431 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-05-14 10:01:38 +02:00
Hosted Weblate
4bb30d075b Merge branch 'origin/master' into Weblate. 2020-05-12 09:06:25 +02:00
Oğuz Ersen
f38af55c05 Translated using Weblate (Turkish)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-05-11 22:01:00 +02:00
Allan Nordhøy
8a007376cc Translated using Weblate (Norwegian Bokmål)
Currently translated at 78.0% (337 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-05-11 22:01:00 +02:00
J-Jamet
940a1dfc78 Upgrade to versionCode 34 2020-05-10 20:07:58 +02:00
J-Jamet
cbd64d1602 Merge tag '2.5' into develop
2.5
2020-05-10 19:32:48 +02:00
J-Jamet
955c9992d0 Merge branch 'release/2.5' 2020-05-10 19:32:37 +02:00
J-Jamet
87606818cd Rename ReadMe.md to README.md 2020-05-10 16:18:32 +02:00
J-Jamet
ffcd264bac Upgrade manually Readme with pull request #540 2020-05-10 16:17:41 +02:00
J-Jamet
04c70cf99d Update fastlane full description (En/Fr - Free/Pro) 2020-05-10 15:31:07 +02:00
J-Jamet
0e0b6fca07 Fix small issue 2020-05-10 11:48:56 +02:00
J-Jamet
b24a789352 Fix small string 2020-05-10 11:46:35 +02:00
J-Jamet
3d4f447037 Replace <strong> tags 2020-05-10 11:40:15 +02:00
J-Jamet
7aa52ca00a Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2020-05-10 11:27:58 +02:00
J-Jamet
a94c9050c5 Update CHANGELOG and full description 2020-05-10 11:05:23 +02:00
J-Jamet
9b18e71e12 Upgrade CHANGELOGS 2020-05-10 10:14:37 +02:00
J-Jamet
d5ff048bd0 Remove allow_copy_password_key from application preferences 2020-05-10 10:12:18 +02:00
Allan Nordhøy
76f44f2573 Translated using Weblate (Romanian)
Currently translated at 91.6% (396 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2020-05-10 02:13:06 +02:00
Allan Nordhøy
9988f1a76f Translated using Weblate (Norwegian Bokmål)
Currently translated at 77.7% (336 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-05-10 02:13:05 +02:00
Xiang Xu
1559376bc2 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-05-10 02:13:05 +02:00
ihor_ck
4d0f252acd Translated using Weblate (Ukrainian)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-05-10 02:13:05 +02:00
Allan Nordhøy
d5a84e66ad Translated using Weblate (Swedish)
Currently translated at 76.8% (332 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2020-05-10 02:12:59 +02:00
Retrial
1dfe3453f4 Translated using Weblate (Greek)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2020-05-10 02:12:59 +02:00
ihor_ck
a7dc9f31b9 Translated using Weblate (Ukrainian)
Currently translated at 53.7% (232 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-05-07 09:36:51 +02:00
76c72cfe
d275f18b3c Translated using Weblate (Swedish)
Currently translated at 76.8% (332 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2020-05-07 09:36:48 +02:00
WaldiS
d770429401 Translated using Weblate (Polish)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-05-07 09:36:47 +02:00
Retrial
501f0be99f Translated using Weblate (Greek)
Currently translated at 88.1% (381 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2020-05-07 09:36:47 +02:00
J-Jamet
a70122f873 Fix small error 2020-05-06 18:19:07 +02:00
J-Jamet
1e1bd15a06 Replace vararg for non-compatible devices 2020-05-06 18:14:41 +02:00
J-Jamet
164fb1f4f5 Fix small warning 2020-05-06 17:45:56 +02:00
J-Jamet
0206514bdb Rollback stylish super implementation 2020-05-06 17:33:12 +02:00
J-Jamet
ecf2b42fa4 Fix field reference 2020-05-06 16:47:47 +02:00
ihor_ck
df683aeea9 Translated using Weblate (Ukrainian)
Currently translated at 32.6% (141 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-05-05 21:12:01 +02:00
Anonymous
cdd05958f7 Translated using Weblate (Ukrainian)
Currently translated at 32.6% (141 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-05-05 21:11:59 +02:00
solokot
4ec5fc05fb Translated using Weblate (Russian)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-05-05 21:11:59 +02:00
Lucas Nunes
4019cafac1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2020-05-05 21:11:58 +02:00
J. Lavoie
4333e179d1 Translated using Weblate (Italian)
Currently translated at 95.3% (412 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-05-05 21:11:56 +02:00
Balázs Meskó
87d67be428 Translated using Weblate (Hungarian)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2020-05-05 21:11:55 +02:00
J. Lavoie
350863adff Translated using Weblate (French)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-05-05 21:11:55 +02:00
J. Lavoie
d125fc46a0 Translated using Weblate (German)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-05-05 21:11:55 +02:00
jan madsen
5f4c079071 Translated using Weblate (Danish)
Currently translated at 98.1% (424 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-05-05 21:11:55 +02:00
zeritti
5ec576a76e Translated using Weblate (Czech)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-05-05 21:11:54 +02:00
Kunzisoft
9edb00ebbb Translated using Weblate (French)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-05-04 20:29:35 +02:00
ihor_ck
59843e5464 Translated using Weblate (Ukrainian)
Currently translated at 15.0% (65 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-05-04 02:22:13 +02:00
solokot
4fe0ccf807 Translated using Weblate (Russian)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-05-02 20:32:07 +02:00
Anonymous
2ee012d3fe Translated using Weblate (Hungarian)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2020-05-02 16:11:02 +02:00
Balázs Meskó
161b4cb5f5 Translated using Weblate (Hungarian)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2020-05-02 16:11:02 +02:00
Anonymous
f9bee68d71 Translated using Weblate (Hungarian)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2020-05-02 16:10:34 +02:00
Balázs Meskó
e0d98aca1b Translated using Weblate (Hungarian)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2020-05-02 16:08:07 +02:00
sshshsjsjaja
d47518c00c Translated using Weblate (German)
Currently translated at 100.0% (432 of 432 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-05-02 15:55:55 +02:00
J-Jamet
58f97dc3ab Upgrade to versionCode 33 2020-05-01 13:48:54 +02:00
J-Jamet
c7706cb80c Merge tag '2.5RC2' into develop
2.5RC2
2020-05-01 13:29:00 +02:00
J-Jamet
49bcc877ef Merge branch 'release/2.5RC2' 2020-05-01 13:28:50 +02:00
J-Jamet
4b81dd552e Update screens 2020-05-01 13:11:31 +02:00
J-Jamet
ff63aaf3f3 Remove unused file types 2020-04-30 21:14:15 +02:00
J-Jamet
39cbd1477b Upgrade CHANGELOG 2020-04-30 18:19:37 +02:00
J-Jamet
3741cc558a Remove field reference URL 2020-04-30 18:02:12 +02:00
J-Jamet
2a314ca3c1 Merge branch 'translations' into develop 2020-04-30 17:51:50 +02:00
J-Jamet
10f9564825 Remove unused comment 2020-04-30 17:51:23 +02:00
J-Jamet
1ae87c9b18 Fix small string issues 2020-04-30 17:50:10 +02:00
J-Jamet
24301ba462 Fix string strong tag 2020-04-30 17:43:19 +02:00
J-Jamet
ff6481274f Fix small string bug 2020-04-30 17:36:43 +02:00
J-Jamet
866df585a2 Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2020-04-30 17:24:48 +02:00
J-Jamet
416bec04a1 Fix context 2020-04-30 17:07:52 +02:00
J-Jamet
f747d6725e Add flavor dimension 2020-04-30 17:03:43 +02:00
J-Jamet
8846918e55 Fix file manager dialog 2020-04-30 16:30:52 +02:00
J-Jamet
e347aefcd9 Manual merge of strings #460 2020-04-30 16:04:41 +02:00
J-Jamet
0a0676af3a Android backup management 2020-04-30 15:45:30 +02:00
Mik
7a69b63b4f Translated using Weblate (Italian)
Currently translated at 97.0% (421 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-04-30 03:32:39 +02:00
J-Jamet
fab5626741 Fix error message in password generator 2020-04-29 17:46:33 +02:00
J-Jamet
c6832d6478 Check permission in onResume 2020-04-29 16:56:47 +02:00
J-Jamet
f451597746 Add thread sleep 2020-04-29 16:35:17 +02:00
J-Jamet
a92e5b5156 Remove dialog task listener (issue dialog still in view) 2020-04-29 15:16:58 +02:00
J-Jamet
786b3b26ea Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2020-04-28 21:42:40 +02:00
J-Jamet
2e9fd2fd79 Merge branch 'feature/fix_opening' into develop 2020-04-28 21:42:18 +02:00
J-Jamet
3be2b9893b Move onActionFinish?.invoke(actionTask, result) 2020-04-26 20:00:47 +02:00
J-Jamet
7f6bed4f5f Replace EditText by TextInputEditText in InputLayout 2020-04-26 10:51:16 +02:00
J-Jamet
37322f284a Merge branch 'develop' into feature/fix_opening 2020-04-26 10:17:46 +02:00
J-Jamet
4106bb1792 Fix lock loop 2020-04-26 10:16:56 +02:00
J-Jamet
229db1242a Listener for task dialog 2020-04-26 09:10:18 +02:00
J-Jamet
8201b42135 Fix progress task dialog 2020-04-25 15:41:33 +02:00
J-Jamet
24818575dc Finish database task only if a binder is attach (to fix too fast opening) 2020-04-25 14:19:50 +02:00
J-Jamet
213050e7f2 Better task listener implementation 2020-04-24 22:09:46 +02:00
J-Jamet
1efaf4e3ea Kotlinized VariantDictionnary and fix database creation 2020-04-24 20:35:36 +02:00
J-Jamet
5532147992 Update CHANGELOG 2020-04-24 18:32:11 +02:00
J-Jamet
550b43094d Fix default username in existing entry #535 2020-04-24 18:30:17 +02:00
J-Jamet
5e831837c8 Update gradle kotlin 2020-04-24 18:20:07 +02:00
J-Jamet
480e88a088 Update gradle 2020-04-24 18:19:35 +02:00
J-Jamet
81cbcbe8af Upgrade TapTargetView lib to 1.13.0 2020-04-24 18:15:17 +02:00
J-Jamet
5649634809 Fix toolbar icon color after theme change 2020-04-24 17:55:44 +02:00
J-Jamet
69a253f738 Remove unused preference theme 2020-04-23 22:39:04 +02:00
J-Jamet
fbc8cfddb8 Fix purple style 2020-04-23 21:59:32 +02:00
J-Jamet
3e1024804c Try to fix preference icons 2020-04-23 21:37:24 +02:00
J-Jamet
cf0b51be00 Fix style purple 2020-04-23 17:53:44 +02:00
J-Jamet
5b295a2a8f Fix pref icon color for KitKat 2020-04-23 15:23:23 +02:00
Andrew
ebb3d7a149 Translated using Weblate (Russian)
Currently translated at 100.0% (434 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-04-23 15:11:22 +02:00
J-Jamet
73ab348d11 Fix keyfile name view 2020-04-23 14:59:21 +02:00
J-Jamet
8ce17086f8 Fix show lock button in setting orientation change 2020-04-23 14:51:32 +02:00
J-Jamet
45cffc93b1 Try to fix theme issue #509 2020-04-23 14:32:59 +02:00
J-Jamet
71cf4d5a34 Fix requireContext, requireArguments and import 2020-04-23 13:43:57 +02:00
J-Jamet
b79a4154af Fix deprecated FragmentManager 2020-04-23 13:35:42 +02:00
J-Jamet
ff818f8472 Upgrade Preference lib 2020-04-23 12:37:12 +02:00
Kunzisoft
b540d32ca3 Added translation using Weblate (Bulgarian) 2020-04-23 09:45:08 +02:00
J-Jamet
895a11d45f Fix opening database wait 2020-04-22 21:07:56 +02:00
J-Jamet
bdafae6132 Fix small warning 2020-04-22 20:50:59 +02:00
J-Jamet
f45d9ce6da Merge branch 'feature/UnsignedValues' into develop 2020-04-22 20:33:35 +02:00
J-Jamet
7456e2c8f5 Refactor key encryption methods and add unit tests 2020-04-22 20:23:23 +02:00
J-Jamet
fc51b50fe6 Fix download_progression string 2020-04-22 18:57:55 +02:00
J-Jamet
b31aa26975 Update CHANGELOG 2020-04-22 18:51:32 +02:00
J-Jamet
49801e1b14 Merge branch 'feature/KeeWeb_Database' into develop 2020-04-22 18:49:50 +02:00
J-Jamet
1453464cbd Remove unused code 2020-04-22 17:46:00 +02:00
J-Jamet
b9be8ff13d Fix KDBX header reader for KeeWeb database #533 2020-04-22 17:30:20 +02:00
J-Jamet
9663c3cadd Add UnsignedInt and UnsignedLong 2020-04-22 15:55:21 +02:00
J-Jamet
da5490bc0a Remove unused import 2020-04-22 15:37:40 +02:00
anonymous
df9b1b9fbb Translated using Weblate (Russian)
Currently translated at 100.0% (434 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-04-22 14:33:57 +02:00
zeritti
6a0a3ded13 Translated using Weblate (Czech)
Currently translated at 100.0% (434 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-04-21 19:11:21 +02:00
J-Jamet
ac7f4a08c3 Replace empty recycle bin icon 2020-04-20 19:12:16 +02:00
J-Jamet
e5ff5e3364 Update CHANGELOG 2020-04-20 19:09:24 +02:00
J-Jamet
f0ac19fcc1 Fix "Go" button #523 2020-04-20 19:07:09 +02:00
J-Jamet
ce23d34923 Fix #530 2020-04-20 18:41:57 +02:00
WaldiS
bdbba87f5a Translated using Weblate (Polish)
Currently translated at 100.0% (434 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-04-20 04:11:21 +02:00
Filippo De Bortoli
fa8ef0477b Translated using Weblate (Italian)
Currently translated at 89.1% (387 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-04-20 04:11:21 +02:00
Jeannette L
3e9ec4cfb6 Translated using Weblate (Italian)
Currently translated at 89.1% (387 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-04-20 04:11:20 +02:00
J-Jamet
ad1c9d8129 Merge branch 'feature/SearchParameters' into develop 2020-04-18 13:09:39 +02:00
J-Jamet
7aaac4c13c Encapsulate search and UUID hex string 2020-04-18 13:09:17 +02:00
J-Jamet
a6c71c3d54 StringUtil refactoring 2020-04-18 11:29:51 +02:00
J-Jamet
05714f38dc Add search in url for search info 2020-04-18 11:23:20 +02:00
J-Jamet
4ef5e4d8ae Refactor search parameters and Autofill search 2020-04-17 16:15:55 +02:00
Oğuz Ersen
5fd0e1d6fe Translated using Weblate (Turkish)
Currently translated at 100.0% (434 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-16 13:11:25 +02:00
Allan Nordhøy
97faf5bef3 Translated using Weblate (Norwegian Bokmål)
Currently translated at 79.0% (343 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-04-16 13:11:25 +02:00
Destiny Li
bd77128733 Translated using Weblate (English)
Currently translated at 100.0% (434 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-04-16 13:11:24 +02:00
Destiny Li
935debc6bf Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (434 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-04-16 13:11:24 +02:00
solokot
699ed19b6a Translated using Weblate (Russian)
Currently translated at 100.0% (434 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-04-16 13:11:24 +02:00
Jeannette L
126e5a94c2 Translated using Weblate (French)
Currently translated at 100.0% (434 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-04-16 13:11:23 +02:00
sshshsjsjaja
792bd36ea7 Translated using Weblate (German)
Currently translated at 100.0% (434 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-04-16 13:11:23 +02:00
jan madsen
04dfee1b43 Translated using Weblate (Danish)
Currently translated at 99.0% (430 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-04-16 13:11:23 +02:00
Carles Sadurní
6a08cfeea9 Translated using Weblate (Catalan)
Currently translated at 61.0% (265 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2020-04-16 13:11:22 +02:00
J-Jamet
29a0f6c9f6 Small amelioration 2020-04-15 20:06:14 +02:00
J-Jamet
e2bdf26d82 Fix autofill crash #520 2020-04-15 14:44:13 +02:00
Kunzisoft
1643949110 Translated using Weblate (French)
Currently translated at 99.7% (433 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-04-15 12:50:29 +02:00
J-Jamet
577dabd727 Better HashedBlockOutputStream implementation 2020-04-15 12:11:45 +02:00
J-Jamet
ed4bd0693f Fix small warning 2020-04-15 12:02:51 +02:00
J-Jamet
0f2982b34d Try to fix bad icon exception 2020-04-15 11:52:22 +02:00
C. Rüdinger
0954094800 Translated using Weblate (German)
Currently translated at 98.6% (428 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-04-14 23:35:52 +02:00
sshshsjsjaja
22dcfafc62 Translated using Weblate (German)
Currently translated at 98.6% (428 of 434 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-04-14 23:35:51 +02:00
J-Jamet
dd34051426 Update CHANGELOG 2020-04-14 20:28:25 +02:00
J-Jamet
6feaf2cb8a Update Autofill compatibility 2020-04-14 20:27:08 +02:00
J-Jamet
603f64ea92 Check TODOs 2020-04-14 20:23:50 +02:00
J-Jamet
568f9ab0d0 Check TODOs 2020-04-14 19:54:02 +02:00
J-Jamet
a2ff1c33e6 Update CHANGELOG 2020-04-14 19:33:29 +02:00
J-Jamet
84452e9fc0 Spongy Castle by Bouncy Castle replacement 2020-04-13 13:39:45 +02:00
J-Jamet
5a6ae453cf Upgrade strings comments 2020-04-13 10:03:49 +02:00
Hosted Weblate
cde3e3361d Merge branch 'origin/master' into Weblate. 2020-04-13 08:58:48 +02:00
Oğuz Ersen
25f01192a4 Translated using Weblate (Turkish)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-13 08:58:47 +02:00
Carles Sadurní
bacb08ec65 Translated using Weblate (Catalan)
Currently translated at 16.7% (72 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2020-04-13 08:58:47 +02:00
anonymous
36c905ee1c Translated using Weblate (Catalan)
Currently translated at 18.1% (78 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2020-04-12 22:44:08 +02:00
Carles Sadurní
6b9280534a Translated using Weblate (Catalan)
Currently translated at 18.1% (78 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2020-04-12 22:44:08 +02:00
J-Jamet
f94c6e850f Add History of each entry in description 2020-04-12 21:20:54 +02:00
J-Jamet
f4cecf6906 Upgrade to version 2.5 2020-04-12 20:51:02 +02:00
J-Jamet
a81d3766f8 Merge tag '2.5RC1' into develop
2.5RC1
2020-04-12 20:32:12 +02:00
J-Jamet
7182c2e66d Merge branch 'release/2.5RC1' 2020-04-12 20:32:03 +02:00
J-Jamet
503316bc70 Fix navigation bar color for light theme 2020-04-12 20:08:39 +02:00
J-Jamet
d5af59f2c7 Merge branch 'translations' into release/2.5RC1 2020-04-12 20:03:44 +02:00
J-Jamet
cb3ac1ad3a version 2.5RC1 2020-04-12 19:36:51 +02:00
J-Jamet
e161080e4c Fix special button 2020-04-11 17:36:24 +02:00
J-Jamet
ceab7f917b Upgrade CHANGELOG 2020-04-11 16:21:27 +02:00
J-Jamet
41d8066c4c Fix autofill bad recognition for key-value format #513 2020-04-11 16:16:50 +02:00
J-Jamet
e373cbd776 File manager explanation link 2020-04-11 12:23:46 +02:00
J-Jamet
05ea6b6b10 Change background color and fix bottom button 2020-04-10 20:37:56 +02:00
J-Jamet
28eed3ae71 New file selection layout 2020-04-10 20:15:08 +02:00
Mert Sezer
535c67ac9b Translated using Weblate (Turkish)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-10 16:09:45 +02:00
J-Jamet
68027a6e15 Linkfy notes #426 2020-04-10 13:22:26 +02:00
J-Jamet
12dea6b499 Add new autofill hint for username 2020-04-10 12:32:38 +02:00
J-Jamet
d8bd078a02 Fix custom fields duplication #257 2020-04-10 11:32:23 +02:00
J-Jamet
439bc109b0 Update CHANGELOG 2020-04-09 21:42:16 +02:00
J-Jamet
b1ec93ceb5 Add navigation bar color #510 2020-04-09 21:38:32 +02:00
J-Jamet
44b9aa0e48 Style black as separate file 2020-04-09 21:31:48 +02:00
J-Jamet
0a59063027 Add scrollbar in nodes list 2020-04-09 20:33:12 +02:00
Oğuz Ersen
5db4608abd Translated using Weblate (Turkish)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-09 06:45:37 +02:00
J-Jamet
6ece2aa6cb Add autofill web domain and app id 2020-04-08 20:50:28 +02:00
J-Jamet
95ee45f666 Merge branch 'feature/Autofill_Domain' into develop 2020-04-08 17:55:42 +02:00
J-Jamet
556e90b8d8 Fix autofill response 2020-04-08 17:17:29 +02:00
J-Jamet
42841e6247 Setting for autofill automatic search 2020-04-08 16:55:30 +02:00
J-Jamet
324c82248a Refactor magikeyboard settings and add autofill settings 2020-04-08 15:44:14 +02:00
J-Jamet
94f5a47918 Catch exception during autofill parse 2020-04-08 14:11:18 +02:00
J-Jamet
3f70990956 Better autofill workflow and add autofill icon for each entry 2020-04-08 13:02:15 +02:00
J-Jamet
d77635e572 Fix autofill auto search 2020-04-07 21:01:26 +02:00
solokot
0f26f1b751 Translated using Weblate (Russian)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-04-07 01:36:50 +02:00
J-Jamet
456bc22138 Encapsulate search methods and search without UI 2020-04-06 21:43:55 +02:00
J-Jamet
b6fe91e396 Fix small warning 2020-04-06 21:36:50 +02:00
J-Jamet
0fb4d26949 Add auto search info 2020-04-06 17:36:51 +02:00
J-Jamet
3b3583a416 Retrieve application id and domain 2020-04-06 11:41:16 +02:00
J-Jamet
cac1d576c8 Upgrade Autofill algorithm 2020-04-06 10:33:44 +02:00
J-Jamet
9cba0d0a48 Message to indicate file revoked 2020-04-06 10:12:09 +02:00
J-Jamet
3bf11e9dc0 Remove unused import 2020-04-06 09:41:02 +02:00
Fabio Iacovino
edbb160ac6 Translated using Weblate (Italian)
Currently translated at 90.4% (388 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-04-06 00:22:25 +02:00
WaldiS
ee111dc63c Translated using Weblate (Polish)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-04-05 17:58:26 +02:00
Filippo De Bortoli
b06edb756a Translated using Weblate (Italian)
Currently translated at 89.7% (385 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-04-05 17:13:40 +02:00
anonymous
fd745494e0 Translated using Weblate (Italian)
Currently translated at 89.7% (385 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-04-05 17:13:40 +02:00
Fabio Iacovino
702bf3f479 Translated using Weblate (Italian)
Currently translated at 89.7% (385 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-04-05 17:13:40 +02:00
Fabio Iacovino
6adc02a91f Translated using Weblate (Italian)
Currently translated at 85.3% (366 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-04-05 17:07:29 +02:00
anonymous
55bae5a130 Translated using Weblate (Italian)
Currently translated at 85.3% (366 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-04-05 17:07:29 +02:00
Filippo De Bortoli
1aae817b17 Translated using Weblate (Italian)
Currently translated at 85.3% (366 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-04-05 17:07:29 +02:00
Oğuz Ersen
8afc8c23fb Translated using Weblate (Turkish)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-05 03:01:39 +02:00
Allan Nordhøy
36e473b139 Translated using Weblate (Norwegian Bokmål)
Currently translated at 76.6% (329 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-04-05 03:01:38 +02:00
Allan Nordhøy
e529723f86 Translated using Weblate (English)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-04-05 03:01:38 +02:00
Dwhite Reeves
6c5112c142 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-04-05 03:01:38 +02:00
Allan Nordhøy
77ac68a603 Translated using Weblate (Dutch)
Currently translated at 77.6% (333 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2020-04-05 03:01:37 +02:00
Éfrit
e816f40872 Translated using Weblate (French)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-04-05 03:01:37 +02:00
zeritti
65313f114b Translated using Weblate (Czech)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-04-05 03:01:37 +02:00
J-Jamet
ef1f1342f5 Upgrade CHANGELOG 2020-04-04 18:49:22 +02:00
J-Jamet
9205fe6c08 Merge branch 'feature/Lock_Button' into develop 2020-04-04 18:43:03 +02:00
J-Jamet
75df3e81fe Fix style 2020-04-04 18:42:35 +02:00
J-Jamet
47fffbadb5 Setting to show the lock button 2020-04-04 18:25:48 +02:00
J-Jamet
78354473fa Fix lock button color and education screen 2020-04-04 17:53:04 +02:00
J-Jamet
8d7efb44b5 Change lock button color 2020-04-03 22:21:50 +02:00
J-Jamet
cab00b3d8c Change lock button color 2020-04-03 22:18:33 +02:00
Mert Sezer
8efab01336 Translated using Weblate (Turkish)
Currently translated at 99.5% (427 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-03 21:41:26 +02:00
Oğuz Ersen
66feb8beb4 Translated using Weblate (Turkish)
Currently translated at 99.5% (427 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-03 21:41:26 +02:00
Mert Sezer
ca4cccffeb Translated using Weblate (Turkish)
Currently translated at 99.3% (426 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-03 21:40:34 +02:00
Oğuz Ersen
51b1760c50 Translated using Weblate (Turkish)
Currently translated at 99.3% (426 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-03 21:40:33 +02:00
J-Jamet
42e1bda365 Change lock button color 2020-04-03 21:32:32 +02:00
J-Jamet
194b6b557a Add lock button elevation and border 2020-04-03 21:17:38 +02:00
Dwhite Reeves
e7bc439997 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-04-03 15:44:41 +02:00
Allan Nordhøy
ef76cce0ac Translated using Weblate (Romanian)
Currently translated at 95.3% (409 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2020-04-03 13:26:34 +02:00
Allan Nordhøy
63eec6d969 Translated using Weblate (Turkish)
Currently translated at 96.9% (416 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-03 13:26:34 +02:00
WaldiS
719ae74c06 Translated using Weblate (Polish)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-04-03 13:26:33 +02:00
Mert Sezer
37cf424eb8 Translated using Weblate (Turkish)
Currently translated at 96.9% (416 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-01 17:46:51 +02:00
anonymous
3a87f7ba9d Translated using Weblate (Turkish)
Currently translated at 96.9% (416 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-04-01 17:46:50 +02:00
J-Jamet
275428d825 First commit for new lock button 2020-04-01 14:57:31 +02:00
Pavel Borecki
7b9dac86ca Translated using Weblate (Czech)
Currently translated at 96.7% (415 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-04-01 03:28:42 +02:00
J-Jamet
46496ee2cc Merge branch 'damoasda-translation-update' into develop 2020-03-31 21:58:21 +02:00
J-Jamet
110cc402cc Add keyfile selection view 2020-03-31 21:48:51 +02:00
J-Jamet
587f006259 Long click to use GET_CONTENT 2020-03-31 19:46:43 +02:00
Moasda
ec45c0df81 Translated using Weblate (German)
Currently translated at 99.7% (428 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-30 22:24:51 +02:00
jan madsen
dc575aeca4 Translated using Weblate (Danish)
Currently translated at 98.3% (422 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-03-30 22:24:51 +02:00
Herbert Reiter
a3f790f000 Fix inconsistencies with translation arguments (e.g. %1$s), escape % character 2020-03-30 22:08:50 +02:00
Herbert Reiter
4e9e188d02 Add and fix German translations 2020-03-30 22:03:09 +02:00
J-Jamet
abb17efae4 Merge branch 'feature/File_Manager_Access' into develop 2020-03-30 18:17:24 +02:00
Hosted Weblate
16be990502 Merge branch 'origin/master' into Weblate. 2020-03-30 08:48:35 +02:00
anonymous
d1a496f9a3 Translated using Weblate (German)
Currently translated at 99.7% (428 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-30 03:17:18 +02:00
Allan Nordhøy
cd730fcfef Translated using Weblate (Norwegian Bokmål)
Currently translated at 76.9% (330 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-03-29 23:06:21 +02:00
Moasda
1b65bf665b Translated using Weblate (German)
Currently translated at 99.7% (428 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-29 23:06:21 +02:00
J-Jamet
6f4735790c Fix autofill for Android app #502 2020-03-29 22:00:24 +02:00
J-Jamet
040666f89d Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2020-03-29 20:19:42 +02:00
J-Jamet
227cb800c3 Merge branch 'damoasda-readme-fix' 2020-03-29 16:15:44 +02:00
Herbert Reiter
0052569a14 Fix FAQ link to wiki page 2020-03-29 15:41:49 +02:00
J-Jamet
f7561c4888 Upgrade to version 2.5beta31 2020-03-29 15:22:34 +02:00
Destiny Li
92200f19e7 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-03-29 14:09:36 +02:00
Éfrit
4c01f18a62 Translated using Weblate (French)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-03-29 14:09:36 +02:00
anonymous
a6ce3e49fe Translated using Weblate (German)
Currently translated at 99.5% (427 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-29 14:09:36 +02:00
Oliver
1b3a5d1bf6 Translated using Weblate (German)
Currently translated at 99.5% (427 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-29 14:09:35 +02:00
J-Jamet
db7bdc63c8 Fix #501 2020-03-28 18:13:09 +01:00
J-Jamet
7aca550f02 Merge branch 'develop' into feature/File_Manager_Access 2020-03-28 17:47:14 +01:00
J-Jamet
b60980b3fd Upgrade to 2.5beta31 2020-03-28 17:46:50 +01:00
J-Jamet
b578c2c584 Add write permission 2020-03-28 17:30:57 +01:00
solokot
fc45bd624e Translated using Weblate (Russian)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-27 20:06:43 +01:00
anonymous
17be3d9d2c Translated using Weblate (Russian)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-27 20:03:48 +01:00
solokot
5b3a38a7bc Translated using Weblate (Russian)
Currently translated at 100.0% (429 of 429 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-27 20:03:47 +01:00
Hosted Weblate
4403835d50 Merge branch 'origin/master' into Weblate. 2020-03-27 18:36:55 +01:00
C. Rüdinger
b7a3d3eb46 Translated using Weblate (German)
Currently translated at 99.5% (424 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-27 18:36:54 +01:00
J-Jamet
29846b22fe Merge tag '2.5beta30' into develop
2.5beta30
2020-03-27 18:13:28 +01:00
J-Jamet
32d235e8c7 Merge branch 'release/2.5beta30' 2020-03-27 18:13:21 +01:00
J-Jamet
cb982b3513 Fix strong tag 2020-03-27 17:53:42 +01:00
J-Jamet
d7ed6c26dd Merge branch 'develop' into translations 2020-03-27 17:49:29 +01:00
J-Jamet
8ff19f7e68 First string pass according to #460 2020-03-27 17:43:31 +01:00
J-Jamet
729e062c3a Add error when create database file 2020-03-27 17:12:54 +01:00
J-Jamet
7d0340ac07 Upgrade CHANGELOG 2020-03-27 16:57:35 +01:00
J-Jamet
01960e74c1 Fix check file ANR #494 2020-03-27 16:36:57 +01:00
anonymous
8e40250985 Translated using Weblate (German)
Currently translated at 96.4% (411 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-27 00:46:37 +01:00
C. Rüdinger
8ae2edb61a Translated using Weblate (German)
Currently translated at 96.4% (411 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-27 00:46:37 +01:00
J-Jamet
0baa7bcbf1 Change discard button 2020-03-26 16:36:17 +01:00
J-Jamet
fffee48918 Upgrade version to 2.5beta30 2020-03-26 14:50:20 +01:00
J-Jamet
515abb6e14 Better UUID view 2020-03-26 14:45:51 +01:00
J-Jamet
6c1c3ff87f New method to wait 1.5 seconds after screen turns off 2020-03-26 13:38:20 +01:00
J-Jamet
5b65575c7a Add comment to ignore autocomplete off 2020-03-26 12:57:24 +01:00
J-Jamet
0f258fc5f8 Upgrade autofill algorithm 2020-03-26 12:47:32 +01:00
anonymous
206bc661dc Translated using Weblate (German)
Currently translated at 96.2% (410 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-25 23:44:32 +01:00
J-Jamet
d0e35b109e Revert : Fix Nextcloud File Upload conflict #497 2020-03-25 22:48:15 +01:00
J-Jamet
61769c4f20 Fix Nextcloud File Upload conflict #497 2020-03-25 21:07:36 +01:00
Hosted Weblate
95778ee5f4 Merge branch 'origin/master' into Weblate. 2020-03-25 12:21:35 +01:00
anonymous
155030fdca Translated using Weblate (Italian)
Currently translated at 79.5% (339 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-25 12:21:35 +01:00
Filippo De Bortoli
98237ef76c Translated using Weblate (Italian)
Currently translated at 79.5% (339 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-25 12:21:34 +01:00
J-Jamet
f0a7b38199 Merge tag '2.5beta29' into develop
2.5beta29
2020-03-25 12:06:20 +01:00
J-Jamet
9fc5e6751b Merge branch 'release/2.5beta29' 2020-03-25 12:06:08 +01:00
J-Jamet
4c1630312b Add UUID color 2020-03-25 11:46:43 +01:00
J-Jamet
d397c5c996 Add UUID ref for entry 2020-03-25 11:22:19 +01:00
J-Jamet
f6c41b5a60 Replace the strong nodes 2020-03-24 20:49:33 +01:00
J-Jamet
06eb5c01c3 Update versions 2020-03-24 20:44:50 +01:00
J-Jamet
7df49f91e8 Merge branch 'release/2.5beta29' of github.com:Kunzisoft/KeePassDX into release/2.5beta29 2020-03-24 20:10:27 +01:00
J-Jamet
96dcbb0ce7 Merge branch 'develop' into release/2.5beta29 2020-03-24 20:09:47 +01:00
J-Jamet
5f828fb986 Fix setting 2020-03-24 20:09:30 +01:00
J-Jamet
533d663938 Merge branch 'translations' into develop 2020-03-24 19:44:38 +01:00
J-Jamet
ae788503a9 Fix html_about licence 2020-03-24 19:44:15 +01:00
anonymous
cf0acd9c73 Translated using Weblate (Italian)
Currently translated at 78.4% (334 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-24 11:42:45 +01:00
Filippo De Bortoli
0857f2f1cf Translated using Weblate (Italian)
Currently translated at 78.4% (334 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-24 11:42:43 +01:00
Ema Panz
c05d412bdb Translated using Weblate (Italian)
Currently translated at 71.1% (303 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-24 11:18:14 +01:00
anonymous
c8e0ce717d Translated using Weblate (Italian)
Currently translated at 71.1% (303 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-24 11:18:14 +01:00
J-Jamet
cc3485b201 Update Readme 2020-03-23 21:10:28 +01:00
J-Jamet
81ea7080c2 Show UUID as KeePass UUID 2020-03-21 20:25:37 +01:00
J-Jamet
76ff6f5ae0 Fix reference loop 2020-03-21 19:01:05 +01:00
J-Jamet
0c0d0b7a6f Encapsulate readonly for launch method 2020-03-21 17:23:17 +01:00
J-Jamet
ec8cf1f6b7 Move autofill setting up 2020-03-21 17:16:49 +01:00
J-Jamet
da44310d1b Fix focus validation button 2020-03-21 17:03:36 +01:00
J-Jamet
4bd9c84bb0 Add dialog to validate discard entry changes 2020-03-21 16:52:50 +01:00
J-Jamet
3b1269a770 Fix bind listeners 2020-03-21 16:12:07 +01:00
J-Jamet
7c986ccee8 Fix card_view_margin issue 2020-03-21 16:08:23 +01:00
J-Jamet
903bad8f36 Fix launch of open db service exception 2020-03-21 14:36:33 +01:00
J-Jamet
4b9577437c Hide add button when nodes are selected 2020-03-21 12:27:18 +01:00
J-Jamet
c316011fbc Show toast on invalid key exception 2020-03-21 12:16:45 +01:00
J-Jamet
3fb1f18c22 Education screen for OTP and entry edit menu 2020-03-19 19:47:51 +01:00
J-Jamet
53935058f5 Add OTP icon 2020-03-19 19:16:38 +01:00
J-Jamet
a3860c9581 Generate password icon as dice 2020-03-19 18:34:14 +01:00
J-Jamet
dc20899d26 Update CHANGELOG 2020-03-19 17:48:56 +01:00
J-Jamet
62ac3ddb75 Merge branch 'feature/Autofill_Improvement' into develop 2020-03-19 17:23:24 +01:00
J-Jamet
b792a61bf9 Improve hint "on" and "off" recognition 2020-03-19 15:18:06 +01:00
J-Jamet
aae9f9e1cb Warning when cancel autofill 2020-03-19 14:59:33 +01:00
J-Jamet
d098bf5e6a Upgrade Autofill algorithm 2020-03-19 14:35:22 +01:00
J-Jamet
b0e8a3ecd9 Add expiration datetime 2020-03-18 12:03:12 +01:00
J-Jamet
4efa684022 Merge branch 'feature/Edit_Expired_Date' into develop 2020-03-18 11:53:46 +01:00
J-Jamet
f2c8082990 Fix date picker kitkat issue 2020-03-18 11:53:21 +01:00
J-Jamet
1abba80045 Fix dialog theme 2020-03-18 11:45:52 +01:00
J-Jamet
68564a2b75 Merge branch 'develop' into feature/Edit_Expired_Date 2020-03-18 09:46:28 +01:00
J-Jamet
385b701b38 Fix cardview margin 2020-03-18 09:42:36 +01:00
Éfrit
11c9a1d707 Translated using Weblate (French)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-03-17 00:42:55 +01:00
J-Jamet
c5aef6b561 New entry edit tool menu style 2020-03-15 12:21:26 +01:00
J-Jamet
dfcf73cfd0 Merge branch 'develop' into feature/Edit_Expired_Date 2020-03-14 21:15:58 +01:00
J-Jamet
7fc9389700 Always show menu 2020-03-14 21:15:37 +01:00
J-Jamet
d1af7349bc Edit button as validation 2020-03-14 20:51:47 +01:00
J-Jamet
0fd955197d Expires selection 2020-03-14 20:16:36 +01:00
Destiny Li
cbf33507d1 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-03-14 17:37:29 +01:00
WaldiS
bc60a5d97e Translated using Weblate (Polish)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-03-14 17:37:27 +01:00
zeritti
57596b2991 Translated using Weblate (Czech)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-03-14 17:37:23 +01:00
solokot
43b3602a52 Translated using Weblate (Russian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-14 17:37:22 +01:00
J-Jamet
c09ec961b8 Merge branch 'feature/Entry_Edit_BottomBar' into develop 2020-03-14 11:00:47 +01:00
J-Jamet
d140b453b2 Merge branch 'feature/Entry_Edit_BottomBar' into develop 2020-03-14 10:36:52 +01:00
J-Jamet
9eb42636ec Change read icons 2020-03-13 21:58:32 +01:00
J-Jamet
ee2d663fce New icons for entry edit tools 2020-03-13 21:33:38 +01:00
J-Jamet
8e83615a22 Add Edit Toolbar menu on top 2020-03-13 17:06:09 +01:00
J-Jamet
0ff129c5ca Edit Entry with Card View 2020-03-13 11:06:56 +01:00
WaldiS
e49858439f Translated using Weblate (Polish)
Currently translated at 99.5% (424 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-03-12 20:33:30 +01:00
zeritti
3df07f7f47 Translated using Weblate (Czech)
Currently translated at 99.5% (424 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-03-12 20:33:30 +01:00
Aurel F
ff9e179593 Translated using Weblate (Romanian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2020-03-12 20:33:28 +01:00
J-Jamet
7539fee04b Add BottomBar in EntryEdit 2020-03-12 19:15:03 +01:00
J-Jamet
71a339a58f Suppress small warning 2020-03-12 17:15:17 +01:00
J-Jamet
9ef2ea016b Suppress deprecation for Keyboard 2020-03-12 17:14:46 +01:00
J-Jamet
de4936a16d Set nullable getSystemService 2020-03-12 17:05:53 +01:00
J-Jamet
574d2b8904 Wait 3 seconds before the lock after the screen turns off #59 2020-03-12 16:56:46 +01:00
J-Jamet
1f3f7634e7 Allow empty title in entries #423 2020-03-12 14:41:02 +01:00
J-Jamet
3c0725baff Fix remember key file option 2020-03-12 14:24:33 +01:00
J-Jamet
b0e1411012 Update CHANGELOG 2020-03-12 14:20:48 +01:00
J-Jamet
39daf4714d Merge branch 'feature/Delete_Registered_Keyfile_New_Credentials' into develop 2020-03-12 14:17:35 +01:00
J-Jamet
4706afa823 Fix credentials options 2020-03-12 14:16:30 +01:00
J-Jamet
25977d389d Add DAO command to delete registered keyfile 2020-03-12 13:18:36 +01:00
J-Jamet
f760110569 default_database_path_key in strings.xml 2020-03-12 09:28:46 +01:00
J-Jamet
133e78fe97 Update CHANGELOG 2020-03-12 09:20:29 +01:00
J-Jamet
d92e0c8620 Fix magikeyboard lock 2020-03-11 19:17:51 +01:00
J-Jamet
62fdb69d6b Fix small element 2020-03-11 18:56:45 +01:00
J-Jamet
def57c9fb2 Merge branch 'feature/Lock_Database' issue 483 2020-03-11 18:42:34 +01:00
J-Jamet
21c9c898c3 Add lock timer in service, notification remains lock to capture the broadcast 2020-03-11 18:30:11 +01:00
J-Jamet
1f03c922c2 Encapsulate lock broadcast 2020-03-11 16:46:44 +01:00
J-Jamet
3f6ae6bdac Fix node update #487 2020-03-11 12:42:18 +01:00
jan madsen
60615ee1eb Translated using Weblate (Danish)
Currently translated at 98.5% (420 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-03-10 20:23:08 +01:00
J-Jamet
92b0d1bfa9 Upgrade to version 2.5beta29 2020-03-10 19:02:37 +01:00
J-Jamet
237988dc1f Merge tag '2.5beta28' into develop
2.5beta28
2020-03-10 19:00:08 +01:00
J-Jamet
a846ec29ca Merge branch 'release/2.5beta28' 2020-03-10 18:59:59 +01:00
J-Jamet
4533e96bff Upgrade CHANGELOG 2020-03-10 18:48:57 +01:00
J-Jamet
0a401c3ac9 Fix strings 2020-03-10 18:41:35 +01:00
J-Jamet
468abaf077 Fix strings 2020-03-10 18:29:19 +01:00
J-Jamet
4ccf2f641c Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into develop 2020-03-10 18:14:14 +01:00
Dominik Baláž
34eb2785cf Translated using Weblate (Slovak)
Currently translated at 21.5% (92 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2020-03-10 17:21:26 +01:00
anonymous
09dbfe323e Translated using Weblate (Slovak)
Currently translated at 21.5% (92 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2020-03-10 17:21:26 +01:00
Destiny Li
1f06c5b425 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-03-10 11:33:21 +01:00
Destiny Li
b98e089f7a Translated using Weblate (Chinese (Traditional))
Currently translated at 48.3% (206 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2020-03-10 11:33:20 +01:00
Aurel F
a0ad06ed0a Translated using Weblate (Romanian)
Currently translated at 40.8% (174 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2020-03-10 11:33:18 +01:00
solokot
ec63365429 Translated using Weblate (Russian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-10 11:33:10 +01:00
J-Jamet
2cb85e4346 Update items after appearance change #452 2020-03-10 08:58:22 +01:00
J-Jamet
0d7c479c51 Merge branch 'feature/Migration_SDK_29' into develop 2020-03-10 08:40:53 +01:00
J-Jamet
5a6c21e662 Try to fix read only bug #480 2020-03-10 08:38:49 +01:00
J-Jamet
d6cadac98f Fix small warning 2020-03-08 13:22:11 +01:00
J-Jamet
dac2fc2c37 Use PreferenceManager from AndroidX 2020-03-08 13:10:16 +01:00
J-Jamet
0fb45cef0d Encapsulate preferences 2020-03-08 13:04:25 +01:00
anonymous
5ebdbd4003 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-03-08 12:08:21 +01:00
Destiny Li
b30f1023cb Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-03-08 12:08:21 +01:00
J-Jamet
e5f65a4d1e Fix % issue in string 2020-03-08 11:48:47 +01:00
J-Jamet
ab42a65aa4 Fix null issues 2020-03-08 11:41:13 +01:00
J-Jamet
e351456bfe First pass migration SDK 29 2020-03-08 11:03:21 +01:00
J-Jamet
452e68b08f Fix C cast warning 2020-03-08 10:52:29 +01:00
J-Jamet
d65beed7a1 Fix small warnings 2020-03-08 10:49:06 +01:00
J-Jamet
f5a5a0e8cb Add generated JSON for database 2020-03-08 10:41:52 +01:00
solokot
98380a0906 Translated using Weblate (Russian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-08 10:20:37 +01:00
J-Jamet
22fe7508f3 Fix doForEachChild warning 2020-03-08 10:16:31 +01:00
J-Jamet
c8e241fc76 Fix Room incremental warning 2020-03-08 09:50:32 +01:00
J-Jamet
2c943e00d0 Fix writeEnum warning 2020-03-08 09:50:07 +01:00
J-Jamet
7ddb83b72d Fix DateFormatter warning 2020-03-08 09:36:45 +01:00
Kunzisoft
50bac01699 Translated using Weblate (French)
Currently translated at 99.7% (425 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-03-08 09:33:23 +01:00
Aurel F
e0a92dfadd Translated using Weblate (Romanian)
Currently translated at 12.2% (52 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2020-03-08 09:33:22 +01:00
solokot
b50c951091 Translated using Weblate (Russian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-08 09:33:17 +01:00
Kunzisoft
2f589a95a9 Translated using Weblate (English)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-03-08 09:33:15 +01:00
J-Jamet
53eac86a95 Update AndroidX dependencies and fix small warning 2020-03-08 09:17:18 +01:00
J-Jamet
9412f8955e Update kotlin to 1.3.61 2020-03-08 09:07:14 +01:00
J-Jamet
71c98d82b1 Update gradle to 5.6.4 2020-03-08 09:00:56 +01:00
J-Jamet
42c1a925b4 Upgrade app to 2.54beta28 2020-03-08 08:59:41 +01:00
J-Jamet
1e84534ffd Merge tag '2.5beta27' into develop
2.5beta27
2020-03-07 14:41:37 +01:00
J-Jamet
b087733e37 Merge branch 'release/2.5beta27' 2020-03-07 14:41:29 +01:00
J-Jamet
675efe29ac Fix unused string 2020-03-07 14:31:08 +01:00
J-Jamet
9833af8225 Update french translations 2020-03-07 14:13:10 +01:00
J-Jamet
790c624571 Fix strong html 2020-03-07 14:06:50 +01:00
J-Jamet
67a70a8453 Merge branch 'translations' into develop 2020-03-07 13:57:47 +01:00
J-Jamet
64bb05e2dd Update CHANGELOG 2020-03-07 13:29:09 +01:00
J-Jamet
c111db6e73 Rename preference keys 2020-03-07 13:25:53 +01:00
J-Jamet
24c5915bd3 Setting to request a search when opening a database #220 2020-03-07 13:18:45 +01:00
J-Jamet
04c3717618 Remove screenshot from bug report 2020-03-07 12:41:57 +01:00
J-Jamet
78275a0984 Fix search empty list 2020-03-07 12:37:46 +01:00
J-Jamet
bd908ed10d Update CHANGELOG 2020-03-07 12:01:03 +01:00
J-Jamet
5473deec95 Show URL before username when title is empty #463 2020-03-07 11:58:47 +01:00
Ettore Atalan
653258afd2 Translated using Weblate (German)
Currently translated at 99.7% (418 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-07 03:33:06 +01:00
J-Jamet
00a2210eea Fix button menu color 2020-03-06 17:36:02 +01:00
J-Jamet
ec5688a013 Remove autofill setting for incompatible devices 2020-03-06 17:29:48 +01:00
J-Jamet
fc0c7b5708 Update CHANGELOG 2020-03-06 15:54:02 +01:00
J-Jamet
464f7ac486 Sorted list and comparator improvement 2020-03-06 15:46:08 +01:00
solokot
fb2457146c Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-29 19:33:06 +01:00
solokot
d90870838e Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-28 19:15:38 +01:00
Andrew
8c45f23291 Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-28 19:15:37 +01:00
Kunzisoft
315a3efa52 Added translation using Weblate (Romanian) 2020-02-27 14:01:57 +01:00
J-Jamet
28abb5ae6f Delete password from starting event #461 2020-02-27 13:26:41 +01:00
J-Jamet
bb78d89682 Try to fix aliased text #414 2020-02-27 13:17:01 +01:00
J-Jamet
fd36e19168 Update CHANGELOG 2020-02-27 12:57:31 +01:00
J-Jamet
58db516e44 Fix refresh appearance settings #452 #451 2020-02-27 12:54:47 +01:00
J-Jamet
2b875e94dc Add GZip string to attachment view if database is compressed 2020-02-27 12:38:16 +01:00
J-Jamet
0ba2447f55 Change about page 2020-02-27 10:16:43 +01:00
solokot
8e684d0d3a Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-25 23:32:46 +01:00
MrEnderfall
ce3c1d4685 Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-25 23:32:46 +01:00
J-Jamet
2cd77d47eb Move up remember keyfile locations setting 2020-02-23 13:43:09 +01:00
J-Jamet
ac9366e351 Update CHANGELOGS 2020-02-23 13:39:57 +01:00
J-Jamet
af356586f8 Merge branch 'feature/Check_Broken_Link' into develop 2020-02-23 13:33:56 +01:00
J-Jamet
7650db81a4 Hide broken database links 2020-02-23 13:15:09 +01:00
J-Jamet
8925c86afd Fix remember key file after press back 2020-02-23 12:49:40 +01:00
J-Jamet
8f1836009e Fix database and keyfile locations settings 2020-02-23 12:39:39 +01:00
J-Jamet
5fcf3f9b95 Fix erasing file 2020-02-23 10:47:28 +01:00
Kunzisoft
1c466d9e40 Translated using Weblate (French)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-02-22 17:33:01 +01:00
WaldiS
b97c2d9cbc Translated using Weblate (Polish)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-02-22 17:33:01 +01:00
zeritti
3d970e4967 Translated using Weblate (Czech)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-02-22 17:33:01 +01:00
J-Jamet
765c8f53dd Remove permission variable and rename preference 2020-02-20 19:53:49 +01:00
J-Jamet
4e81caeadf Remove unused external storage permission for Sdk > 18 2020-02-20 19:41:48 +01:00
J-Jamet
08b10e4d58 Update string 2020-02-20 19:35:52 +01:00
J-Jamet
f8594f72e8 Fix small elements 2020-02-20 19:31:44 +01:00
J-Jamet
24ebee07cd Fix small elements 2020-02-20 19:26:03 +01:00
J-Jamet
a06ebb0991 Force read only when file is not writable 2020-02-20 19:20:00 +01:00
J-Jamet
cd5d4498e7 Fix small element 2020-02-20 16:11:50 +01:00
J-Jamet
0bd62780c6 Check if uri is broken 2020-02-20 14:13:11 +01:00
J-Jamet
896f9327d6 Fix error messages #431 2020-02-20 11:58:00 +01:00
J-Jamet
cc83a99efe Try to fix #465 2020-02-20 09:06:11 +01:00
J Smith
b6da20fef7 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-02-19 23:53:44 +01:00
Kunzisoft
2912678559 Translated using Weblate (French)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-02-19 23:53:38 +01:00
WaldiS
409e870ef0 Translated using Weblate (Polish)
Currently translated at 99.5% (417 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-02-19 23:53:38 +01:00
Kunzisoft
3d84074a0a Translated using Weblate (English)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-02-19 23:53:37 +01:00
Oğuz Ersen
a717fdfed4 Translated using Weblate (Turkish)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-02-19 23:53:36 +01:00
J-Jamet
be1f68015b Merge branch 'master' into develop 2020-02-17 19:05:23 +01:00
WaldiS
2c29dcf1f6 Translated using Weblate (Polish)
Currently translated at 99.0% (415 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-02-17 00:50:57 +01:00
Filip Miletic
2b1173177f Translated using Weblate (Croatian)
Currently translated at 79.4% (333 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-02-17 00:50:55 +01:00
jan madsen
3c0f7dc79c Translated using Weblate (Danish)
Currently translated at 98.8% (414 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-02-17 00:50:42 +01:00
marzzzello
39d813bf3a Translated using Weblate (German)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-02-17 00:50:40 +01:00
Filip Miletic
5e2bc0d05b Translated using Weblate (Croatian)
Currently translated at 51.3% (215 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-02-14 04:18:25 +01:00
iLocIT!
ec751159ae Translated using Weblate (German)
Currently translated at 89.5% (375 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-02-14 04:18:02 +01:00
Filip Miletic
dda8b95f83 Translated using Weblate (Croatian)
Currently translated at 11.0% (46 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-02-13 19:31:33 +01:00
Filip Miletic
286012fe2a Added translation using Weblate (Croatian) 2020-02-13 11:04:02 +01:00
Jérémy JAMET
ab27299789 Update description for other KeePass app 2020-02-12 17:09:14 +01:00
Éfrit
dfcdd5aa88 Translated using Weblate (French)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-02-11 21:50:45 +01:00
Allan Nordhøy
a05ea52689 Translated using Weblate (Italian)
Currently translated at 72.8% (305 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-02-11 21:50:45 +01:00
zeritti
de12b5de5b Translated using Weblate (Czech)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-02-11 21:50:44 +01:00
solokot
57b03eaca4 Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-11 21:50:43 +01:00
Retrial
36bd00b760 Translated using Weblate (Greek)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2020-02-11 21:50:42 +01:00
Oğuz Ersen
4578a9974a Translated using Weblate (Turkish)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-02-11 21:50:33 +01:00
J-Jamet
d0371f58c6 Capture exception to avoid crash after clear clipboard 2020-02-08 17:39:08 +01:00
solokot
9f80457351 Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-08 16:39:00 +01:00
J-Jamet
56daf6f676 Remove JVMOverload in ClipboardHelper 2020-02-08 12:09:12 +01:00
J-Jamet
7f7b8d423b Merge branch 'translations' into develop 2020-02-08 11:34:35 +01:00
J-Jamet
031afc80cb Fix contribution strings 2020-02-08 11:32:56 +01:00
J-Jamet
b77e28b04d Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2020-02-08 11:27:36 +01:00
Allan Nordhøy
2f8c3fdcfe Translated using Weblate (Norwegian Bokmål)
Currently translated at 81.4% (341 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-02-08 11:27:15 +01:00
solokot
f501a87099 Translated using Weblate (Russian)
Currently translated at 99.8% (418 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-08 11:27:14 +01:00
Kunzisoft
d0ec5f26dd Translated using Weblate (English)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-02-08 11:27:13 +01:00
Mesut Akcan
97776e9329 Translated using Weblate (Turkish)
Currently translated at 98.3% (412 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-02-08 11:27:11 +01:00
Kunzisoft
a19356c49e Translated using Weblate (French)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-02-08 11:21:45 +01:00
J-Jamet
4f16918cf0 Remove unused strings 2020-02-08 11:19:45 +01:00
J-Jamet
1af9761144 Merge branch 'master' into develop 2020-02-07 22:14:20 +01:00
J-Jamet
d74e814c79 Fix fastlane data 2020-02-07 22:05:58 +01:00
J-Jamet
16d09bca6c Merge tag '2.5.0.0beta26' into develop
2.5.0.0beta26
2020-02-07 21:45:51 +01:00
477 changed files with 21682 additions and 9773 deletions

View File

@@ -20,9 +20,6 @@ Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
** Keepass Database **
- Created with: [e.g Windows KeePass 2.42]
- Version: [e.g. 2]

173
CHANGELOG
View File

@@ -1,4 +1,117 @@
KeepassDX (2.5.0.0beta26)
KeePassDX(2.8.6)
* Fix Autofill recognition #712
* Keep value after renaming custom field #709
* Prevent random binary bug #713
* Fix dialog background #717
* Better domain recognition for autofill #702
* Write custom data #651
* Fix autolink #720
KeePassDX(2.8.5)
* Fix Base 64 #708
KeePassDX(2.8.4)
* Fix incomplete attachment deletion #684
* Fix opening database v1 without backup folder #692
* Fix ANR during first entry education #685
* Entry edition as fragment and manual views to fix focus #686
* Fix opening database with corrupted attachment #691
* Manage empty keyfile #679
KeePassDX(2.8.3)
* Upload attachments
* Visibility button for each hidden field
* Fix read header file
* Fix deletion in KDB database
* Fix minor issues
KeePassDX(2.8.2)
* Fix themes / new UI
* Fix multiples notifications
* Fix entry in Magikeyboard memory
* Fix biometric view visibility
* Fix fields order
* Upgrade code with ViewModel and LiveData
KeePassDX(2.8.1)
* Capture exceptions in coroutines
KeePassDX(2.8)
* Fix TOTP period (> 60s)
* Fix searching in recycle bin
* Settings to back to the previous keyboard during database credentials and after form filling
* Improve action tasks
* Improve recognition to reset app timeout
* Fix minor issues
KeePassDX(2.7)
* Add blocklists for autofill
* Add autofill compatibility mode (usefull for Browser not compatible)
* Upgrade autofill recognition algorithm
* Setting to search through web subdomains
* Refactoring selection mode
KeePassDX(2.6)
* Share a web domain to automatically search for an entry
* Default group icon for a new entry
* Better autofill recognition
* Fix entry not visually deleted in search
* Fix hanged loading dialog
* Fix auto open biometric prompt if comes from background
* Minor fixes
KeePassDX(2.5)
* First stable version of KeePassDX
* Fork completely rewritten from the KeePassDroid project
* Fix small issues from the last Release Candidate
KeePassDX(2.5RC2)
* Replacement of Spongy Castle by Bouncy Castle
* Update Autofill compatibility
* Fix Magikeyboard "Go" action
* Fix KeeWeb database opening
* Fix default username
* Fix themes
* Fix small issues
KeePassDX(2.5RC1)
* Add write permission to keep compatibility with old file managers
* Fix autofill for apps
* Auto search for autofill
* New keyfile input
* Icon to hide keyfile input
* New lock button
* Setting to hide lock button in user interface
* Clickable links in notes
* Fix autofill for key-value pairs
KeePassDX(2.5beta30)
* Fix Lock after screen off (wait 1.5 seconds)
* Upgrade autofill algorithm
* Fix ANR during file verifications
KeePassDX(2.5beta29)
* Upgrade autofill algorithm
* Delete registered KeyFile after save new credentials
* Fix title and username entry view refresh after an update
* Fix database lock request (open notification always active)
* Allow empty title in entries
* Add expiration datetime
KeePassDX(2.5beta28)
* Fix read only database
* Upgrade to Android SDK 29
KeePassDX (2.5beta27)
* New setting to hide broken links
* Show URL when title is empty
* Setting to open search field at database opening
* Fix settings for database locations
* Fix error message when database file not writable
* Fix appearance refresh settings
* Sort optimization
KeePassDX (2.5.0.0beta26)
* Download attachments
* Change file size string format
* Prevent screenshot for all screen
@@ -11,7 +124,7 @@ KeepassDX (2.5.0.0beta26)
* Fix dates
* Fix UUID message for Database v1
KeepassDX (2.5.0.0beta25)
KeePassDX (2.5.0.0beta25)
* Setting for Recycle Bin
* Fix Recycle bin issues
* Fix TOTP
@@ -19,7 +132,7 @@ KeepassDX (2.5.0.0beta25)
* Fix update group
* Fix OOM
KeepassDX (2.5.0.0beta24)
KeePassDX (2.5.0.0beta24)
* Add OTP (HOTP / TOTP)
* Add settings (Color, Security, Master Key)
* Show history of each entry
@@ -29,7 +142,7 @@ KeepassDX (2.5.0.0beta24)
* Open/Save database as service / Add persistent notification
* Fix settings / edit group / small bugs
KeepassDX (2.5.0.0beta23)
KeePassDX (2.5.0.0beta23)
* New, more secure database creation workflow
* Recognize more database files
* Add alias for history files (WARNING: history is erased)
@@ -38,14 +151,14 @@ KeepassDX (2.5.0.0beta23)
* Fix OOM with KeyFile
* Fix small issues
KeepassDX (2.5.0.0beta22)
KeePassDX (2.5.0.0beta22)
* Rebuild code for actions
* Add UUID as entry view
* Fix bug with natural order
* Fix number of entries in databaseV1
* New entry views
KeepassDX (2.5.0.0beta21)
KeePassDX (2.5.0.0beta21)
* Fix nested groups no longer visible in V1 databases
* Improved data import algorithm for V1 databases
* Add natural database sort
@@ -53,10 +166,10 @@ KeepassDX (2.5.0.0beta21)
* Fix button disabled with only KeyFile
* Show the number of entries in a group
KeepassDX (2.5.0.0beta20)
KeePassDX (2.5.0.0beta20)
* Fix a major bug that displays an entry history
KeepassDX (2.5.0.0beta19)
KeePassDX (2.5.0.0beta19)
* Add lock button always visible
* New connection workflow
* Code refactored in Kotlin
@@ -67,7 +180,7 @@ KeepassDX (2.5.0.0beta19)
* Fix memory when load database
* Fix small bugs
KeepassDX (2.5.0.0beta18)
KeePassDX (2.5.0.0beta18)
* New recent databases views
* New information dialog
* Custom fields for the Magikeyboard
@@ -76,10 +189,10 @@ KeepassDX (2.5.0.0beta18)
* Fix memory when opening the database
* Memory management for attachments
KeepassDX (2.5.0.0beta17)
KeePassDX (2.5.0.0beta17)
* Fix font and search
KeepassDX (2.5.0.0beta16)
KeePassDX (2.5.0.0beta16)
* New search in a single fragment
* Search suggestions
* Added the display of usernames
@@ -87,20 +200,20 @@ KeepassDX (2.5.0.0beta16)
* Fix read-only mode
* Fix parcelable / toolbar / back
KeepassDX (2.5.0.0beta15)
KeePassDX (2.5.0.0beta15)
* Read only mode
* Best group recovery for the navigation fragment
* Fix copies in notifications
* Fix orientation
* Added translations
KeepassDX (2.5.0.0beta14)
KeePassDX (2.5.0.0beta14)
* Optimize all the memory with parcelables / fix search
KeepassDX (2.5.0.0beta13)
KeePassDX (2.5.0.0beta13)
* Fix memory issue with parcelable (crash in beta12 version)
KeepassDX (2.5.0.0beta12)
KeePassDX (2.5.0.0beta12)
* Added the Magikeyboard to fill the forms (settings still in development)
* Added move and copy for groups and entries
* New navigation in a single screen / new animations between activities
@@ -113,10 +226,10 @@ KeepassDX (2.5.0.0beta12)
* Fix the fingerprint recognition (WARNING : The keystore is reinit, you must delete the old keys)
* Fix small bugs
KeepassDX (2.5.0.0beta11)
KeePassDX (2.5.0.0beta11)
* Fix crash in beta10 version
KeepassDX (2.5.0.0beta10)
KeePassDX (2.5.0.0beta10)
* Dynamically change Algorithm and Key Derivation Function in settings
* Upgrade translations
* New red volcano theme, fix classic dark theme
@@ -124,7 +237,7 @@ KeepassDX (2.5.0.0beta10)
* Update fingerprint state with checkbox
* Fix bugs
KeepassDX (2.5.0.0beta9)
KeePassDX (2.5.0.0beta9)
* Education Screens to learn how to use the app
* New designs
* New custom font for character visibility
@@ -133,9 +246,9 @@ KeepassDX (2.5.0.0beta9)
* Change setting organisation
* Pro version
KeepassDX (2.5.0.0beta8)
KeePassDX (2.5.0.0beta8)
* Hide custom entries protected
* Best management of field references (https://keepass.info/help/base/fieldrefs.html)
* Best management of field references (https://KeePass.info/help/base/fieldrefs.html)
* Change database / default settings
* Add Autofill for search
* Add sorting by last access and by creation time
@@ -143,7 +256,7 @@ KeepassDX (2.5.0.0beta8)
* Refactor old code
* Fix bugs
KeepassDX (2.5.0.0beta7)
KeePassDX (2.5.0.0beta7)
* Rebuild Notifications
* Change links to https
* Add extended Ascii (ñæËÌÂÝÜ...)
@@ -152,10 +265,10 @@ KeepassDX (2.5.0.0beta7)
* Add setting to prevent the password copy
* Fix bugs
KeepassDX (2.5.0.0beta6)
KeePassDX (2.5.0.0beta6)
* Fix crash
KeepassDX (2.5.0.0beta5)
KeePassDX (2.5.0.0beta5)
* Autofill (Android O)
* Deletion for group
* New sorts with (Asc/Dsc, Groups before or after)
@@ -176,7 +289,7 @@ KeepassDX (2.5.0.0beta5)
* Fix many small bugs
* Add recycle bin setting (not yet accessible)
KeepassDX (2.5.0.0beta4)
KeePassDX (2.5.0.0beta4)
* Show only file name
* Setting for full path
* Add information for each database file
@@ -185,7 +298,7 @@ KeepassDX (2.5.0.0beta4)
* Delete view assignment for fingerprint opening
* Merge KeePassDroid 2.2.1
KeepassDX (2.5.0.0beta3)
KeePassDX (2.5.0.0beta3)
* New database workflow with new screens and folder selection
* Settings for default password generation
* Fingerprint dialog for explanations
@@ -196,17 +309,17 @@ KeepassDX (2.5.0.0beta3)
* Merge KeePassDroid 2.2.0.9
* Add corruption fix mode
KeepassDX (2.5.0.0beta2)
KeePassDX (2.5.0.0beta2)
* Remove libs for F-Droid
KeepassDX (2.5.0.0beta1)
* Fork KeepassDroid
KeePassDX (2.5.0.0beta1)
* Fork KeePassDroid
* Add Material Design
* Add Light and Night theme
* Min API is 14
* Solve bug for fingerprint
* Update French translation
* Change donation (see KeepassDroid to contribute on both projects)
* Change donation (see KeePassDroid to contribute on both projects)
KeePassDroid (2.2.1)
* Fix kdbx4 date corruption
@@ -467,7 +580,7 @@ KeePassDroid (1.9.10)
KeePassDroid (1.9.9)
* Go back to explicitly storing blank fields in the database
(works around bug in keepassx)
(works around bug in KeePassx)
* Add support for native code on MIPS architectures
* Adding Vibrate permission. On some devices notifications fail
without the vibrate permission.

View File

@@ -0,0 +1,177 @@
Terms of Service
This is the terms of service for the Android Backup Service.
1. Your relationship with Google
1.1 Your use of the Android Backup Service (referred to as the "Service" in this document) is subject to the terms of a legal agreement between you and Google. "Google" means Google LLC, whose principal place of business is at 1600 Amphitheatre Parkway, Mountain View, CA 94043, United States. This document explains how the agreement is made up, and sets out some of the terms of that agreement.
1.2 Unless otherwise agreed in writing with Google, your agreement with Google will always include, at a minimum, the terms and conditions set out in this document. These are referred to below as the "Terms".
1.3 The Terms form a legally binding agreement between you and Google in relation to your use of the Service. It is important that you take the time to read them carefully.
2. Accepting the Terms
2.1 In order to use the Service, you must first agree to the Terms. You may not use the Service if you do not accept the Terms.
2.2 You can accept the Terms by clicking to accept or agree to the Terms, where this option is made available to you by Google.
2.3 You may not use the Service and may not accept the Terms if you are not of legal age to form a binding contract with Google.
2.4 You represent that you have full power, capacity and authority to accept these Terms. If you are accepting on behalf of your employer or another entity, you represent that you have full legal authority to bind your employer or such entity to these Terms. If you don't have the legal authority to bind, please ensure that an authorized person from your entity consents to and accepts these Terms.
3. Provision of the Service by Google
3.1 Google has subsidiaries and affiliated legal entities around the world ("Subsidiaries and Affiliates"). Sometimes, these companies will be providing the Service to you on behalf of Google itself. You acknowledge and agree that Subsidiaries and Affiliates will be entitled to provide the Service to you.
3.2 Google is constantly innovating in order to provide the best possible experience for its users. You acknowledge and agree that the form and nature of the Service which Google provides may change from time to time without prior notice to you.
3.3 As part of this continuing innovation, you acknowledge and agree that Google may stop (permanently or temporarily) providing the Service (or any features within the Service) to you or to users generally at Google's sole discretion, without prior notice to you. You may stop using the Service at any time. You do not need to specifically inform Google when you stop using the Service.
3.4 You acknowledge and agree that if Google disables your Backup Service Key, you and the Android application(s) you developed ("Application(s)") may be prevented from accessing the Service and any content that is stored with the Service.
3.5 You acknowledge and agree that Google may set a fixed upper limit on the number of backup transmissions you may send or receive through the Service or on the amount of storage space used for the provision of the Service at any time, at Google's discretion. You agree to abide by any such fixed upper limits.
4. Use of the Service by you
4.1 In order to access the Service, you must have a unique application identifier ("Package Name") for your Application as described in the documentation for the Service.
4.2 After supplying Google with the Package Name and accepting the Terms, you will be issued an alphanumeric key ("Backup Service Key") assigned to you by Google that is uniquely associated with your Application. Your Application must include this Backup Service Key as described in the documentation for the Service.
4.3 There is currently no limit to the number of Backup Service Keys you may obtain in this manner provided that you use a different Package Name for each Backup Service Key you obtain. You agree that each Backup Service Key is only valid for Applications with the corresponding Package Name. You agree that Google may, in its sole discretion, impose a limit on the number of Backup Service Keys that may be obtained in the future. You agree that your continued use of any of the Backup Service Keys assigned by Google, or distribution of any Applications using such Backup Service Keys, constitutes your continued agreement to these Terms.
4.4 You agree to use the Service only for purposes that are permitted by (a) the Terms and (b) any applicable law, regulation, third-party terms of service, or generally accepted practices or guidelines in the relevant jurisdictions (including any laws regarding the export of data or software to and from the United States or other relevant countries).
4.5 You agree not to access (or attempt to access) any of the Service by any means other than through the interfaces, methods, and APIs that are provided by Google, unless you have been specifically allowed to do so in a separate agreement with Google.
4.6 You agree that you will not engage in any activity that interferes with or disrupts the Service (or the servers and networks which are connected to the Service), or the servers or networks of any third-party.
4.7 You agree that your use of the Service will be in compliance with any documentation guidelines provided by Google and that failure to comply with the documentation guidelines may result in the disabling of the Backup Service Key(s) for your Application(s).
4.8 Unless you have been specifically permitted to do so in a separate agreement with Google, you agree that you will not reproduce, duplicate, copy, sell, trade or resell (a) use of the Service, or (b) access to the Service.
4.9 You agree that you are solely responsible for (and that Google has no responsibility to you or to any third party for) your and your Application's use of the Service, any breach of your obligations under the Terms, and for the consequences (including any loss or damage which Google may suffer) of any such breach.
4.10 You agree that in your use of the Service, you and your Applications will protect the privacy and legal rights of users. You must provide legally adequate privacy notice and protection for users whose data your Applications back up to the Service. Further, your Application may only use that information for the limited purpose of backing up the data to the Service unless the user has given you permission for further use. If the user has not given you permission to back up information to the Service, you may not transmit such information to the Service.
4.11 You agree that you and your Applications will not transmit or store sensitive user information, such as user names, passwords, or credit card numbers, through the Service.
5. Security
5.1 You agree and understand that you are responsible for maintaining the security associated with any information you provide to access the Service as well as of the Backup Service Key(s) assigned to you by Google. You agree that only you are authorized to use the Backup Service Key(s) assigned to you.
5.2 Accordingly, you agree that you will be solely responsible to Google for all activities that occur in connection with your access to the Service, as well as the Backup Service Key.
5.3 If you become aware of any unauthorized use of your Backup Service Key(s) you agree to notify Google immediately.
6. Privacy and your personal information
6.1 For information about Google's data protection practices, please read Google's privacy policy at http://www.google.com/privacy.html. This policy explains how Google treats your personal information when you use the Service.
6.2 You agree to the use of your data in accordance with Google's privacy policies.
7. Content in the Service
7.1 You agree that you are solely responsible for (and that Google has no responsibility to you or to any third party for) any Content that you or your Applications transmit or store through the Service and for the consequences of your actions (including any loss or damage which Google may suffer) by doing so. You agree that you are solely responsible for (A) any Content that is transmitted through the Service by your Applications, and (B) any Content that Devices retrieve from the Service by virtue of your Applications. For purposes of the Terms, "Content" means information such as data, messages, settings information, written text, computer software, music, audio files or other sounds, photographs, videos or other images. "Device(s)" means device(s) powered by the Android operating system.
7.2 You agree that you will not transmit any Content through the Service that is copyrighted, protected by trade secret or otherwise subject to third party proprietary rights, including patent, privacy and publicity rights, unless you are the owner of such rights or have permission from their rightful owner to transmit the Content through the Service.
8. Proprietary rights
8.1 You acknowledge and agree that Google (or Google's licensors) own all legal right, title and interest in and to the Service, including any intellectual property rights which subsist in the Service (whether those rights happen to be registered or not, and wherever in the world those rights may exist).
8.2 Unless you have agreed otherwise in writing with Google, nothing in the Terms gives you a right to use any of Google's trade names, trademarks, service marks, logos, domain names, and other distinctive brand features.
8.3 If you have been given an explicit right to use any of these brand features in a separate written agreement with Google, then you agree that your use of such features shall be in compliance with that agreement, any applicable provisions of the Terms, and Google's brand feature use guidelines as updated from time to time. These guidelines can be viewed online at http://www.google.com/permissions/ guidelines.html (or such other URL as Google may provide for this purpose from time to time).
8.4 You agree that you shall not remove, obscure, or alter any proprietary rights notices (including copyright, trade mark notices) which may be affixed to or contained within the Service.
9. License from Google
9.1 Subject to terms and conditions of these Terms, Google gives you a personal, worldwide, royalty-free, non-assignable and non-exclusive license to use the Service as provided to you by Google. This license is for the sole purpose of enabling you to use and enjoy the benefit of the Service as provided by Google, in the manner permitted by the Terms.
9.2 You may not (and you may not permit anyone else to) copy, modify, create a derivative work of, reverse engineer, decompile or otherwise attempt to extract the source code from the Service or any part thereof, unless this is expressly permitted or required by law, or unless you have been specifically told that you may do so by Google, in writing.
9.3 Unless Google has given you specific written permission to do so, you may not assign (or grant a sub-license of) your rights to use the Service, grant a security interest in or over your rights to use the Service, or otherwise transfer any part of your rights to use the Service.
10. Your code
10.1 Google claims no ownership or control over any source code written by you to be used with the Service. You retain copyright and any other rights you already hold in this code, and you are responsible for protecting those rights, as appropriate.
11. Ending your relationship with Google
11.1 The Terms will continue to apply until terminated by either you or Google as set out below.
11.2 You may terminate your legal agreement with Google by discontinuing your use of the Service at any time.
11.3 Google may, at any time, terminate its legal agreement with you if:
(A) you have breached any provision of the Terms (or have acted in manner which clearly shows that you do not intend to, or are unable to comply with the provisions of the Terms); or
(B) Google is required to do so by law (for example, where the provision of the Service to you is, or becomes, unlawful); or
(C) Google is transitioning to no longer providing the Service; or
(D) your Application fails to meet the documentation guidelines provided by Google.
11.4 Nothing in this Section shall affect Google's rights regarding provision of the Service under Section 3 of the Terms.
11.5 When these Terms come to an end, all of the legal rights, obligations and liabilities that you and Google have benefited from, been subject to (or which have accrued over time whilst the Terms have been in force) or which are expressed to continue indefinitely, shall be unaffected by this cessation, and the provisions of Sections 12, 13 and Paragraph 16 shall continue to apply to such rights, obligations and liabilities indefinitely.
12. EXCLUSION OF WARRANTIES
12.1 NOTHING IN THESE TERMS, INCLUDING SECTIONS 12 AND 13, SHALL EXCLUDE OR LIMIT GOOGLE'S WARRANTY OR LIABILITY FOR LOSSES WHICH MAY NOT BE LAWFULLY EXCLUDED OR LIMITED BY APPLICABLE LAW. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR CONDITIONS OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR LOSS OR DAMAGE CAUSED BY NEGLIGENCE, BREACH OF CONTRACT OR BREACH OF IMPLIED TERMS, OR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, ONLY THE LIMITATIONS WHICH ARE LAWFUL IN YOUR JURISDICTION WILL APPLY TO YOU AND OUR LIABILITY WILL BE LIMITED TO THE MAXIMUM EXTENT PERMITTED BY LAW.
12.2 YOU EXPRESSLY UNDERSTAND AND AGREE THAT YOUR USE OF THE SERVICE IS AT YOUR SOLE RISK AND THAT THE SERVICE AND CONTENT ARE PROVIDED "AS IS" AND "AS AVAILABLE".
12.3 IN PARTICULAR, GOOGLE, ITS SUBSIDIARIES AND AFFILIATES, AND ITS LICENSORS DO NOT REPRESENT OR WARRANT TO YOU THAT:
(A) YOUR USE OF THE SERVICE WILL MEET YOUR REQUIREMENTS,
(B) YOUR USE OF THE SERVICE WILL BE UNINTERRUPTED, TIMELY, SECURE OR FREE FROM ERROR, AND
(C) THAT DEFECTS IN THE OPERATION OR FUNCTIONALITY OF ANY SOFTWARE PROVIDED TO YOU AS PART OF THE SERVICE WILL BE CORRECTED.
12.4 NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED BY YOU FROM GOOGLE OR THROUGH OR FROM THE SERVICE SHALL CREATE ANY WARRANTY NOT EXPRESSLY STATED IN THE TERMS.
12.5 GOOGLE FURTHER EXPRESSLY DISCLAIMS ALL WARRANTIES AND CONDITIONS OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
13. LIMITATION OF LIABILITY
13.1 SUBJECT TO OVERALL PROVISION IN PARAGRAPH 12.1 ABOVE, YOU EXPRESSLY UNDERSTAND AND AGREE THAT GOOGLE, ITS SUBSIDIARIES AND AFFILIATES, AND ITS LICENSORS SHALL NOT BE LIABLE TO YOU FOR:
(A) ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL CONSEQUENTIAL OR EXEMPLARY DAMAGES WHICH MAY BE INCURRED BY YOU, HOWEVER CAUSED AND UNDER ANY THEORY OF LIABILITY. THIS SHALL INCLUDE, BUT NOT BE LIMITED TO, ANY LOSS OF PROFIT (WHETHER INCURRED DIRECTLY OR INDIRECTLY), ANY LOSS OF GOODWILL OR BUSINESS REPUTATION, ANY LOSS OF DATA SUFFERED, COST OF PROCUREMENT OF SUBSTITUTE GOODS OR SERVICE, OR OTHER INTANGIBLE LOSS;
(B) ANY LOSS OR DAMAGE WHICH MAY BE INCURRED BY YOU, INCLUDING BUT NOT LIMITED TO LOSS OR DAMAGE AS A RESULT OF:
(I) ANY CHANGES WHICH GOOGLE MAY MAKE TO THE SERVICE, OR FOR ANY PERMANENT OR TEMPORARY CESSATION IN THE PROVISION OF THE SERVICE (OR ANY FEATURES WITHIN THE SERVICE);
(II) THE DELETION OF, CORRUPTION OF, OR FAILURE TO STORE, ANY CONTENT AND OTHER COMMUNICATIONS DATA MAINTAINED OR TRANSMITTED BY OR THROUGH YOUR USE OF THE SERVICE;
(III) YOUR FAILURE TO PROVIDE GOOGLE WITH ACCURATE ACCOUNT INFORMATION; OR
(IV) YOUR FAILURE TO KEEP YOUR PASSWORD OR ACCOUNT DETAILS SECURE AND CONFIDENTIAL.
13.2 THE LIMITATIONS ON GOOGLE'S LIABILITY TO YOU IN PARAGRAPH 13.1 ABOVE SHALL APPLY WHETHER OR NOT GOOGLE HAS BEEN ADVISED OF OR SHOULD HAVE BEEN AWARE OF THE POSSIBILITY OF ANY SUCH LOSSES ARISING.
14. Indemnification
14.1 You agree to hold harmless and indemnify Google, and its subsidiaries, affiliates, officers, agents, employees, or licensors from and against any third party claim arising from or in any way related to (a) your breach of the Terms, (b) your use of the Service, or (c) your violation of applicable laws, rules or regulations in connection with the Service, including any liability or expense arising from all claims, losses, damages (actual and consequential), suits, judgments, litigation costs and attorneys' fees, of every kind and nature. In such a case, Google will provide you with written notice of such claim, suit or action.
15. Changes to the Terms
15.1 Due to things like changes to the law or changes to functionality offered through the Service, Google may need to change these Terms from time to time. You should look at the Terms regularly. We'll post notice of the modified Terms within, or through, the Service. Once the modified Terms are posted, the changes will become effective immediately, and you are deemed to have accepted the modified Terms if you continue to use the Service. If you do not agree to the modified Terms for the Service, please stop using the Service.
16. General legal terms
16.1 The Terms constitute the whole legal agreement between you and Google and govern your use of the Service (but excluding any service which Google may provide to you under a separate written agreement), and completely replace any prior agreements between you and Google in relation to the Service.
16.2 You agree that Google may provide you with notices, including those regarding changes to the Terms, by email, regular mail, or postings on the Service.
16.3 You agree that if Google does not exercise or enforce any legal right or remedy which is contained in the Terms (or which Google has the benefit of under any applicable law), this will not be taken to be a formal waiver of Google's rights and that those rights or remedies will still be available to Google.
16.4 If any court of law, having the jurisdiction to decide on this matter, rules that any provision of these Terms is invalid, then that provision will be removed from the Terms without affecting the rest of the Terms. The remaining provisions of the Terms will continue to be valid and enforceable.
16.5 You acknowledge and agree that each member of the group of companies of which Google is the parent shall be third party beneficiaries to the Terms and that such other companies shall be entitled to directly enforce, and rely upon, any provision of the Terms which confers a benefit on (or rights in favor of) them. Other than this, no other person or company shall be third party beneficiaries to the Terms.
16.6 The Terms, and your relationship with Google under the Terms, shall be governed by the laws of the State of California without regard to its conflict of laws provisions. You and Google agree to submit to the exclusive jurisdiction of the courts located within the county of Santa Clara, California to resolve any legal matter arising from the Terms. Notwithstanding this, you agree that Google shall still be allowed to apply for injunctive remedies (or an equivalent type of urgent legal relief) in any jurisdiction.

91
README.md Normal file
View File

@@ -0,0 +1,91 @@
# 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 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, …).
- Allows opening and **copying URI / URL fields quickly**.
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
- Material design with **themes**.
- **Auto-Fill** and Integration.
- Field filling **keyboard**.
- **History** of each entry.
- Precise management of **settings**.
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
KeepassDX is **open source** and **ad-free**.
## What is KeePassDX?
An alternative to remembering an endless list of passwords manually. This is made more difficult by **using different passwords for each account**. If you use one password everywhere and security fails only one of those places, it grants access to your e-mail account, website, etc, and you may not know about it or notice, before bad things happen.
KeePassDX is a **password manager for Android**, which helps you **manage your passwords in a secure way**. You can put all your passwords in one database, locked with a **master key** and/or a **keyfile**. You **only have to remember one single master password and/or select the keyfile** to unlock the whole database. The databases are encrypted using the best and **most secure encryption algorithms** currently known.
## Small print?
KeePassDX is under **open source GPL3 license**, meaning you can use, study, change and share it at will. Copyleft ensures it stays that way.
From the full source, anyone can build, fork, and check whether for example the encryption algorithms are implemented correctly.
There is **no advertising**.
Do not worry, **the main features remain completely free**.
Optional visual styles are accessible after a contribution (and a congratulatory message (Ո‿Ո) ) or the purchase of an extended version to encourage contribution to the work of open source projects!
*If you contribute to the project and do not have access to the styles, do not hesitate to contact the author at [contact@kunzisoft.com](contact@kunzisoft.com).*
## Contributions
* Add features by making a **[pull request](https://help.github.com/articles/about-pull-requests/)**.
* Help to **[translate](https://hosted.weblate.org/projects/keepass-dx/strings/)** KeePassDX to your language (on [Weblate](https://hosted.weblate.org/projects/keepass-dx/) or by sending a [pull request](https://help.github.com/articles/about-pull-requests/)).
* **[Donate](https://www.kunzisoft.com/donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePassDX.
## 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.*
[<img src="https://f-droid.org/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
## Frequently Asked Questions
Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ)
## Other devices
- [KeePass](https://keepass.info/) (https://keepass.info/) is the original and official project for the desktop, with technical documentation for standardized database files. It is updated regularly with active maintenance (written in C#).
- [KeePassXC](https://keepassxc.org/) (https://keepassxc.org/) is an alternative integration of KeePass written in C++.
- [KeeWeb](https://keeweb.info/) (https://keeweb.info/) is a web version that is also compatible with KeePass files.
## License
Copyright © 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
This file is part of KeePassDX.
[KeePassDX](https://www.keepassdx.com) 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/>.
*This project is a fork of [KeepassDroid](https://github.com/bpellin/keepassdroid) by bpellin.*

View File

@@ -1,85 +0,0 @@
# 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 application allows to create keys and passwords in a secure way by integrating with the Android design standards.
<img 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...)
* Allows **fast copy** of fields and opening of URI / URL
* **Biometric recognition** for fast unlocking *(Fingerprint / Face unlock / ...)*
* **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA)
* Material design with **themes**
* **AutoFill** and Integration
* Field filling **keyboard**
* Precise management of **settings**
* Code written in **native language** *(Kotlin / Java / JNI / C)*
KeepassDX is **open source** and **ad-free**.
## What is KeePassDX?
Today you need to remember many passwords. You need a password for your e-mail account, your website's FTP password, online passwords (like website member account), etc. etc. etc. The list is endless. Also, you **should use different passwords for each account**. Because if you use only one password everywhere and someone gets this password you have a problem... A serious problem. The thief would have access to your e-mail account, website, etc. Unimaginable.
KeePassDX is a **free open source password manager for Android**, which helps you to **manage your passwords in a secure way**. You can put all your passwords in one database, which is locked with one **master key** or a **key file**. So you **only have to remember one single master password or select the key file** to unlock the whole database. The databases are encrypted using the best and **most secure encryption algorithms** currently known.
## Is it really free?
Yes, KeePassDX is under **free license (OSI certified)** and **without advertising**. You can have a look at its full source and check whether the encryption algorithms are implemented correctly.
*Note : If you access the application from a store, visual features may not be available to incentivize the contribution to the work of open source projects. These optional visuals are accessible after a donation (and a small congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free. If you contribute to the project and you do not have access to the themes, do not hesitate to contact me at [contact@kunzisoft.com](contact@kunzisoft.com), I will give you the procedure.*
## Contributions
You can contribute in different ways to help us on our work.
* Add features by a **[pull request](https://help.github.com/articles/about-pull-requests/)**.
* Help to **[translate](https://hosted.weblate.org/projects/keepass-dx/strings/)** into your language (By using [Weblate](https://hosted.weblate.org/projects/keepass-dx/) or with a manual [pull request](https://help.github.com/articles/about-pull-requests/))
* **[Donate](https://www.kunzisoft.com/donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePassDX
## Download
*We recommend the installation from [F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) which verifies that all libraries and application code are open source.*
[<img src="https://f-droid.org/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
## F.A.Q.
Other questions? You can read the [F.A.Q.](https://github.com/Kunzisoft/KeePassDX/wiki/F.A.Q.)
## Other devices
- [KeePass XC](https://keepassxc.org/) (https://keepassxc.org/) works with **GNU/Linux**, **Mac** and **Windows**, is updated regularly and under the terms of the GNU General Public License. This is the recommended version for computers.
- [KeePass](https://keepass.info/) (https://keepass.info/) is the historical project, with good technical documentation for standardized database files but only running on **Windows**.
## License
Copyright (c) 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
This file is part of KeePassDX.
[KeePassDX](https://www.keepassdx.com) 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/>.
*This project is a fork of [KeepassDroid](https://github.com/bpellin/keepassdroid) by bpellin.*

View File

@@ -4,21 +4,29 @@ apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 28
buildToolsVersion '28.0.3'
compileSdkVersion 30
buildToolsVersion '30.0.2'
defaultConfig {
applicationId "com.kunzisoft.keepass"
minSdkVersion 14
targetSdkVersion 28
versionCode = 26
versionName = "2.5.0.0beta26"
targetSdkVersion 30
versionCode = 42
versionName = "2.8.6"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
manifestPlaceholders = [ googleAndroidBackupAPIKey:"" ]
kapt {
arguments {
arg("room.incremental", "true")
arg("room.schemaLocation", "$projectDir/schemas".toString())
}
}
}
externalNativeBuild {
@@ -34,35 +42,36 @@ android {
}
}
dexOptions {
}
flavorDimensions "tier"
flavorDimensions "version"
productFlavors {
libre {
dimension "version"
applicationIdSuffix = ".libre"
buildConfigField "String", "BUILD_VERSION", "\"libre\""
buildConfigField "boolean", "FULL_VERSION", "true"
buildConfigField "boolean", "CLOSED_STORE", "false"
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
}
pro {
dimension "version"
applicationIdSuffix = ".pro"
buildConfigField "String", "BUILD_VERSION", "\"pro\""
buildConfigField "boolean", "FULL_VERSION", "true"
buildConfigField "boolean", "CLOSED_STORE", "true"
buildConfigField "String[]", "STYLES_DISABLED", "{}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIZiXvrQCzSV9LNI6-p7cjTKENZLHIrz_zaqZuQQ" ]
}
free {
dimension "version"
applicationIdSuffix = ".free"
buildConfigField "String", "BUILD_VERSION", "\"free\""
buildConfigField "boolean", "FULL_VERSION", "false"
buildConfigField "boolean", "CLOSED_STORE", "true"
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
}
}
@@ -76,38 +85,47 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
def spongycastleVersion = "1.58.0.0"
def room_version = "2.2.1"
def room_version = "2.2.5"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.cardview:cardview:1.0.0'
// WARNING: Bug with extra field
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.biometric:biometric:1.0.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.0.1'
// Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:1.3.1"
implementation 'androidx.fragment:fragment-ktx:1.2.5'
// WARNING: To upgrade with style, bug in edit text
implementation 'com.google.android.material:material:1.0.0'
// Database
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "com.madgag.spongycastle:core:$spongycastleVersion"
implementation "com.madgag.spongycastle:prov:$spongycastleVersion"
// Crypto
implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01'
// Time
implementation 'joda-time:joda-time:2.9.9'
implementation 'joda-time:joda-time:2.10.6'
// Color
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3'
// Education
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.12.0'
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
// Apache Commons Collections
implementation 'commons-collections:commons-collections:3.2.1'
implementation 'org.apache.commons:commons-io:1.3.2'
implementation 'commons-collections:commons-collections:3.2.2'
// Apache Commons Codec
implementation 'commons-codec:commons-codec:1.11'
implementation 'commons-codec:commons-codec:1.14'
// Icon pack
implementation project(path: ':icon-pack-classic')
implementation project(path: ':icon-pack-material')
// Tests
androidTestImplementation 'junit:junit:4.13'
}

View File

@@ -0,0 +1,84 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "56438e5f7372ef3e36e33b782aed245d",
"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, `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": "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, '56438e5f7372ef3e36e33b782aed245d')"
]
}
}

View File

@@ -26,11 +26,11 @@ import java.util.Random
import junit.framework.TestCase
import com.kunzisoft.keepass.crypto.finalkey.AndroidFinalKey
import com.kunzisoft.keepass.crypto.finalkey.NativeFinalKey
import com.kunzisoft.keepass.crypto.finalkey.AndroidAESKeyTransformer
import com.kunzisoft.keepass.crypto.finalkey.NativeAESKeyTransformer
class FinalKeyTest : TestCase() {
private var mRand: Random? = null
class AESKeyTest : TestCase() {
private lateinit var mRand: Random
@Throws(Exception::class)
override fun setUp() {
@@ -40,29 +40,28 @@ class FinalKeyTest : TestCase() {
}
@Throws(IOException::class)
fun testNativeAndroid() {
fun testAES() {
// Test both an old and an even number to test my flip variable
testNativeFinalKey(5)
testNativeFinalKey(6)
testAESFinalKey(5)
testAESFinalKey(6)
}
@Throws(IOException::class)
private fun testNativeFinalKey(rounds: Int) {
private fun testAESFinalKey(rounds: Long) {
val seed = ByteArray(32)
val key = ByteArray(32)
val nativeKey: ByteArray
val androidKey: ByteArray
val nativeKey: ByteArray?
val androidKey: ByteArray?
mRand!!.nextBytes(seed)
mRand!!.nextBytes(key)
mRand.nextBytes(seed)
mRand.nextBytes(key)
val aKey = AndroidFinalKey()
androidKey = aKey.transformMasterKey(seed, key, rounds.toLong())
val androidAESKey = AndroidAESKeyTransformer()
androidKey = androidAESKey.transformMasterKey(seed, key, rounds)
val nKey = NativeFinalKey()
nativeKey = nKey.transformMasterKey(seed, key, rounds.toLong())
val nativeAESKey = NativeAESKeyTransformer()
nativeKey = nativeAESKey.transformMasterKey(seed, key, rounds)
assertArrayEquals("Does not match", androidKey, nativeKey)
}
}

View File

@@ -80,6 +80,4 @@ class AESTest : TestCase() {
assertArrayEquals("Arrays differ on size: $dataSize", outAndroid, outNative)
}
}

View File

@@ -33,7 +33,7 @@ import junit.framework.TestCase
import com.kunzisoft.keepass.stream.HashedBlockInputStream
import com.kunzisoft.keepass.stream.HashedBlockOutputStream
class HashedBlock : TestCase() {
class HashedBlockTest : TestCase() {
@Throws(IOException::class)
fun testBlockAligned() {

View File

@@ -1,58 +0,0 @@
/*
* Copyright 2017 Brian Pellin, 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.tests.utils
import java.util.Locale
import com.kunzisoft.keepass.utils.StringUtil
import junit.framework.TestCase
class StringUtilTest : TestCase() {
private val text = "AbCdEfGhIj"
private val search = "BcDe"
private val badSearch = "Ed"
private val repText = "AbCtestingaBc"
private val repSearch = "ABc"
private val repSearchBad = "CCCCCC"
private val repNew = "12345"
private val repResult = "12345testing12345"
fun testIndexOfIgnoreCase1() {
assertEquals(1, StringUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH))
}
fun testIndexOfIgnoreCase2() {
assertEquals(-1f, StringUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH).toFloat(), 2f)
}
fun testIndexOfIgnoreCase3() {
assertEquals(-1, StringUtil.indexOfIgnoreCase(text, badSearch, Locale.ENGLISH))
}
fun testReplaceAllIgnoresCase1() {
assertEquals(repResult, StringUtil.replaceAllIgnoresCase(repText, repSearch, repNew, Locale.ENGLISH))
}
fun testReplaceAllIgnoresCase2() {
assertEquals(repText, StringUtil.replaceAllIgnoresCase(repText, repSearchBad, repNew, Locale.ENGLISH))
}
}

View File

@@ -0,0 +1,28 @@
package com.kunzisoft.keepass.tests.utils
import com.kunzisoft.keepass.utils.UnsignedInt
import junit.framework.TestCase
class UnsignedIntTest: TestCase() {
fun testUInt() {
val standardInt = UnsignedInt(15).toKotlinInt()
assertEquals(15, standardInt)
val unsignedInt = UnsignedInt(-1).toKotlinLong()
assertEquals(4294967295L, unsignedInt)
}
fun testMaxValue() {
val maxValue = UnsignedInt.MAX_VALUE.toKotlinLong()
assertEquals(4294967295L, maxValue)
val longValue = UnsignedInt.fromKotlinLong(4294967295L).toKotlinLong()
assertEquals(longValue, maxValue)
}
fun testLong() {
val longValue = UnsignedInt.fromKotlinLong(50L).toKotlinInt()
assertEquals(50, longValue)
val uIntLongValue = UnsignedInt.fromKotlinLong(4294967290).toKotlinLong()
assertEquals(4294967290, uIntLongValue)
}
}

View File

@@ -17,28 +17,29 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.tests
package com.kunzisoft.keepass.tests.utils
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.ULONG_MAX_VALUE
import com.kunzisoft.keepass.stream.*
import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.UnsignedLong
import junit.framework.TestCase
import org.junit.Assert.assertArrayEquals
import java.io.ByteArrayOutputStream
import java.util.*
class StringDatabaseKDBUtilsTest : TestCase() {
class ValuesTest : TestCase() {
fun testReadWriteLongZero() {
testReadWriteLong(0.toByte())
}
fun testReadWriteLongMax() {
testReadWriteLong(java.lang.Byte.MAX_VALUE)
testReadWriteLong(Byte.MAX_VALUE)
}
fun testReadWriteLongMin() {
testReadWriteLong(java.lang.Byte.MIN_VALUE)
testReadWriteLong(Byte.MIN_VALUE)
}
fun testReadWriteLongRnd() {
@@ -61,11 +62,11 @@ class StringDatabaseKDBUtilsTest : TestCase() {
}
fun testReadWriteIntMin() {
testReadWriteInt(java.lang.Byte.MIN_VALUE)
testReadWriteInt(Byte.MIN_VALUE)
}
fun testReadWriteIntMax() {
testReadWriteInt(java.lang.Byte.MAX_VALUE)
testReadWriteInt(Byte.MAX_VALUE)
}
private fun testReadWriteInt(value: Byte) {
@@ -77,11 +78,10 @@ class StringDatabaseKDBUtilsTest : TestCase() {
setArray(orig, value, 4)
val one = bytes4ToInt(orig)
val dest = intTo4Bytes(one)
val one = bytes4ToUInt(orig)
val dest = uIntTo4Bytes(one)
assertArrayEquals(orig, dest)
}
private fun setArray(buf: ByteArray, value: Byte, size: Int) {
@@ -103,11 +103,11 @@ class StringDatabaseKDBUtilsTest : TestCase() {
}
fun testReadWriteShortMin() {
testReadWriteShort(java.lang.Byte.MIN_VALUE)
testReadWriteShort(Byte.MIN_VALUE)
}
fun testReadWriteShortMax() {
testReadWriteShort(java.lang.Byte.MAX_VALUE)
testReadWriteShort(Byte.MAX_VALUE)
}
private fun testReadWriteShort(value: Byte) {
@@ -125,15 +125,15 @@ class StringDatabaseKDBUtilsTest : TestCase() {
}
fun testReadWriteByteMin() {
testReadWriteByte(java.lang.Byte.MIN_VALUE)
testReadWriteByte(Byte.MIN_VALUE)
}
fun testReadWriteByteMax() {
testReadWriteShort(java.lang.Byte.MAX_VALUE)
testReadWriteShort(Byte.MAX_VALUE)
}
private fun testReadWriteByte(value: Byte) {
val dest: Byte = uIntToByte(byteToUInt(value))
val dest: Byte = UnsignedInt(UnsignedInt.fromKotlinByte(value)).toKotlinByte()
assert(value == dest)
}
@@ -185,7 +185,7 @@ class StringDatabaseKDBUtilsTest : TestCase() {
val bos = ByteArrayOutputStream()
val leos = LittleEndianDataOutputStream(bos)
leos.writeLong(ULONG_MAX_VALUE)
leos.writeLong(UnsignedLong.MAX_VALUE)
leos.close()
val uLongMax = bos.toByteArray()

View File

@@ -8,10 +8,14 @@
android:normalScreens="true"
android:largeScreens="true"
android:anyDensity="true" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.USE_BIOMETRIC" />
<uses-permission
android:name="android.permission.VIBRATE"/>
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:label="@string/app_name"
@@ -19,17 +23,15 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:name="com.kunzisoft.keepass.app.App"
android:allowBackup="true"
android:fullBackupContent="@xml/backup"
android:fullBackupContent="@xml/backup_rules"
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
android:largeHeap="true"
android:resizeableActivity="true"
android:theme="@style/KeepassDXStyle.Night"
tools:targetApi="n">
<!-- TODO backup API Key -->
<meta-data
android:name="com.google.android.backup.api_key"
android:value="" />
android:value="${googleAndroidBackupAPIKey}" />
<activity
android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
android:theme="@style/KeepassDXStyle.SplashScreen"
@@ -122,33 +124,42 @@
android:configChanges="keyboardHidden" />
<activity
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustResize" />
android:windowSoftInputMode="adjustPan|stateAlwaysHidden" />
<!-- About and Settings -->
<activity
android:name="com.kunzisoft.keepass.activities.AboutActivity"
android:launchMode="singleTask"
android:label="@string/menu_about" />
<activity android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
android:label="@string/about" />
<activity
android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
<activity
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden" />
<activity android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
<activity android:name="com.kunzisoft.keepass.settings.SettingsAutofillActivity" />
<activity android:name="com.kunzisoft.keepass.magikeyboard.KeyboardLauncherActivity"
android:label="@string/keyboard_name"
android:exported="true">
<activity
android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
<activity
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
<activity
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
android:theme="@style/Theme.Transparent">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<activity android:name="com.kunzisoft.keepass.settings.MagikIMESettings"
<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">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>
<service
android:name="com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
android:enabled="true"

View File

@@ -21,15 +21,15 @@ package com.kunzisoft.keepass.activities
import android.content.pm.PackageManager.NameNotFoundException
import android.os.Bundle
import androidx.appcompat.widget.Toolbar
import android.text.method.LinkMovementMethod
import android.util.Log
import android.view.MenuItem
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 org.joda.time.DateTime
class AboutActivity : StylishActivity() {
@@ -40,7 +40,7 @@ class AboutActivity : StylishActivity() {
setContentView(R.layout.activity_about)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.title = getString(R.string.menu_about)
toolbar.title = getString(R.string.about)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
@@ -64,9 +64,17 @@ class AboutActivity : StylishActivity() {
val buildTextView = findViewById<TextView>(R.id.activity_about_build)
buildTextView.text = build
findViewById<TextView>(R.id.activity_about_licence_text).apply {
movementMethod = LinkMovementMethod.getInstance()
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
HtmlCompat.FROM_HTML_MODE_LEGACY)
}
val disclaimerText = findViewById<TextView>(R.id.disclaimer)
disclaimerText.text = getString(R.string.disclaimer_formal, DateTime().year)
findViewById<TextView>(R.id.activity_about_contribution_text).apply {
movementMethod = LinkMovementMethod.getInstance()
text = HtmlCompat.fromHtml(getString(R.string.html_about_contribution),
HtmlCompat.FROM_HTML_MODE_LEGACY)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {

View File

@@ -0,0 +1,117 @@
/*
* 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
import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.autofill.KeeAutofillService
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
@RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Build search param
val searchInfo = SearchInfo().apply {
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
}
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
if (assistStructure == null) {
setResult(Activity.RESULT_CANCELED)
finish()
} else if (!KeeAutofillService.searchAllowedFor(searchInfo.applicationId,
PreferencesUtil.applicationIdBlocklist(this))
|| !KeeAutofillService.searchAllowedFor(searchInfo.webDomain,
PreferencesUtil.webDomainBlocklist(this))) {
// If item not allowed, show a toast
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
setResult(Activity.RESULT_CANCELED)
finish()
} else {
// If database is open
SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(),
searchInfo,
{ items ->
// Items found
AutofillHelper.buildResponse(this, items)
finish()
},
{
// Show the database UI to select the entry
GroupActivity.launchForAutofillResult(this,
assistStructure,
false,
searchInfo)
},
{
// If database not open
FileDatabaseSelectActivity.launchForAutofillResult(this,
assistStructure,
searchInfo)
}
)
}
super.onCreate(savedInstanceState)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
super.onActivityResult(requestCode, resultCode, data)
}
companion object {
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
fun getAuthIntentSenderForResponse(context: Context,
searchInfo: SearchInfo? = null): IntentSender {
return PendingIntent.getActivity(context, 0,
// Doesn't work with Parcelable (don't know why?)
Intent(context, AutofillLauncherActivity::class.java).apply {
searchInfo?.let {
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
}
},
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
}
}
}

View File

@@ -25,6 +25,7 @@ import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.Menu
import android.view.MenuItem
@@ -34,24 +35,25 @@ import android.widget.ProgressBar
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.settings.SettingsAutofillActivity
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper
@@ -60,16 +62,19 @@ import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.createDocument
import com.kunzisoft.keepass.utils.onCreateDocumentResult
import com.kunzisoft.keepass.view.EntryContentsView
import com.kunzisoft.keepass.view.showActionError
import java.util.*
import kotlin.collections.HashMap
class EntryActivity : LockingActivity() {
private var coordinatorLayout: CoordinatorLayout? = null
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var titleIconView: ImageView? = null
private var historyView: View? = null
private var entryContentsView: EntryContentsView? = null
private var entryProgress: ProgressBar? = null
private var lockView: View? = null
private var toolbar: Toolbar? = null
private var mDatabase: Database? = null
@@ -83,10 +88,10 @@ class EntryActivity : LockingActivity() {
private var mShowPassword: Boolean = false
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAttachmentsToDownload: HashMap<Int, EntryAttachment> = HashMap()
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
private var clipboardHelper: ClipboardHelper? = null
private var firstLaunchOfActivity: Boolean = false
private var mFirstLaunchOfActivity: Boolean = false
private var iconColor: Int = 0
@@ -114,21 +119,30 @@ class EntryActivity : LockingActivity() {
invalidateOptionsMenu()
// Get views
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
titleIconView = findViewById(R.id.entry_icon)
historyView = findViewById(R.id.history_container)
entryContentsView = findViewById(R.id.entry_contents)
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
entryProgress = findViewById(R.id.entry_progress)
lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener {
lockAndExit()
}
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
// Init the clipboard helper
clipboardHelper = ClipboardHelper(this)
firstLaunchOfActivity = true
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
// Init attachment service binder manager
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
mProgressDialogThread?.onActionFinish = { actionTask, result ->
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
@@ -137,18 +151,27 @@ class EntryActivity : LockingActivity() {
finish()
}
}
// TODO Visual error for entry history
coordinatorLayout?.showActionError(result)
}
}
override fun onResume() {
super.onResume()
// Show the lock button
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
View.VISIBLE
} else {
View.GONE
}
// Get Entry from UUID
try {
val keyEntry: NodeId<UUID> = intent.getParcelableExtra(KEY_ENTRY)
mEntry = mDatabase?.getEntryById(keyEntry)
mEntryLastVersion = mEntry
val keyEntry: NodeId<UUID>? = intent.getParcelableExtra(KEY_ENTRY)
if (keyEntry != null) {
mEntry = mDatabase?.getEntryById(keyEntry)
mEntryLastVersion = mEntry
}
} catch (e: ClassCastException) {
Log.e(TAG, "Unable to retrieve the entry key")
}
@@ -178,7 +201,7 @@ class EntryActivity : LockingActivity() {
val entryInfo = entry.getEntryInfo(Database.getInstance())
// Manage entry copy to start notification if allowed
if (firstLaunchOfActivity) {
if (mFirstLaunchOfActivity) {
// Manage entry to launch copying notification if allowed
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
@@ -191,13 +214,13 @@ class EntryActivity : LockingActivity() {
mAttachmentFileBinderManager?.apply {
registerProgressTask()
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
override fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment) {
entryContentsView?.updateAttachmentDownloadProgress(attachment)
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
entryContentsView?.putAttachment(entryAttachmentState)
}
}
}
firstLaunchOfActivity = false
mFirstLaunchOfActivity = false
}
override fun onPause() {
@@ -219,14 +242,13 @@ class EntryActivity : LockingActivity() {
toolbar?.title = entryTitle
// Assign basic fields
entryContentsView?.assignUserName(entry.username)
entryContentsView?.assignUserNameCopyListener(View.OnClickListener {
entryContentsView?.assignUserName(entry.username) {
database.startManageEntry(entry)
clipboardHelper?.timeoutCopyToClipboard(entry.username,
getString(R.string.copy_field,
getString(R.string.copy_field,
getString(R.string.entry_user_name)))
database.stopManageEntry(entry)
})
}
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
@@ -253,23 +275,25 @@ class EntryActivity : LockingActivity() {
}
}
entryContentsView?.assignPassword(entry.password, allowCopyPasswordAndProtectedFields)
if (allowCopyPasswordAndProtectedFields) {
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
View.OnClickListener {
database.startManageEntry(entry)
clipboardHelper?.timeoutCopyToClipboard(entry.password,
getString(R.string.copy_field,
getString(R.string.copy_field,
getString(R.string.entry_password)))
database.stopManageEntry(entry)
})
}
} else {
// If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
entryContentsView?.assignPasswordCopyListener(showWarningClipboardDialogOnClickListener)
showWarningClipboardDialogOnClickListener
} else {
entryContentsView?.assignPasswordCopyListener(null)
null
}
}
entryContentsView?.assignPassword(entry.password,
allowCopyPasswordAndProtectedFields,
onPasswordCopyClickListener)
//Assign OTP field
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
@@ -283,24 +307,22 @@ class EntryActivity : LockingActivity() {
})
entryContentsView?.assignURL(entry.url)
entryContentsView?.assignComment(entry.notes)
entryContentsView?.assignNotes(entry.notes)
// Assign custom fields
if (entry.allowCustomFields()) {
if (mDatabase?.allowEntryCustomFields() == true) {
entryContentsView?.clearExtraFields()
for (element in entry.customFields.entries) {
val label = element.key
val value = element.value
entry.getExtraFields().forEach { field ->
val label = field.name
val value = field.protectedValue
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
if (allowCopyProtectedField) {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, View.OnClickListener {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
clipboardHelper?.timeoutCopyToClipboard(
value.toString(),
getString(R.string.copy_field, label)
)
})
}
} else {
// If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
@@ -311,28 +333,16 @@ class EntryActivity : LockingActivity() {
}
}
}
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
// Manage attachments
val attachments = entry.getAttachments()
val showAttachmentsView = attachments.isNotEmpty()
entryContentsView?.showAttachments(showAttachmentsView)
if (showAttachmentsView) {
entryContentsView?.assignAttachments(attachments)
entryContentsView?.onAttachmentClick { attachmentItem, _ ->
when (attachmentItem.downloadState) {
AttachmentState.NULL, AttachmentState.ERROR, AttachmentState.COMPLETE -> {
createDocument(this, attachmentItem.name)?.let { requestCode ->
mAttachmentsToDownload[requestCode] = attachmentItem
}
}
else -> {
// TODO Stop download
}
mDatabase?.binaryPool?.let { binaryPool ->
entryContentsView?.assignAttachments(entry.getAttachments(binaryPool).toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
createDocument(this, attachmentItem.name)?.let { requestCode ->
mAttachmentsToDownload[requestCode] = attachmentItem
}
}
}
entryContentsView?.refreshAttachments()
// Assign dates
entryContentsView?.assignCreationDate(entry.creationTime)
@@ -352,17 +362,9 @@ class EntryActivity : LockingActivity() {
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
taColorAccent.recycle()
}
val entryHistory = entry.getHistory()
// TODO isMainEntry = not an history
val showHistoryView = entryHistory.isNotEmpty()
entryContentsView?.showHistory(showHistoryView)
if (showHistoryView) {
entryContentsView?.assignHistory(entryHistory)
entryContentsView?.onHistoryClick { historyItem, position ->
launch(this, historyItem, mReadOnly, position)
}
entryContentsView?.assignHistory(entry.getHistory()) { historyItem, position ->
launch(this, historyItem, mReadOnly, position)
}
entryContentsView?.refreshHistory()
// Assign special data
entryContentsView?.assignUUID(entry.nodeId.id)
@@ -391,16 +393,6 @@ class EntryActivity : LockingActivity() {
}
}
private fun changeShowPasswordIcon(togglePassword: MenuItem?) {
if (mShowPassword) {
togglePassword?.setTitle(R.string.menu_hide_password)
togglePassword?.setIcon(R.drawable.ic_visibility_off_white_24dp)
} else {
togglePassword?.setTitle(R.string.menu_showpass)
togglePassword?.setIcon(R.drawable.ic_visibility_white_24dp)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
@@ -416,15 +408,6 @@ class EntryActivity : LockingActivity() {
menu.findItem(R.id.menu_edit)?.isVisible = false
}
val togglePassword = menu.findItem(R.id.menu_toggle_pass)
entryContentsView?.let {
if (it.isPasswordPresent || it.atLeastOneFieldProtectedPresent()) {
changeShowPasswordIcon(togglePassword)
} else {
togglePassword?.isVisible = false
}
}
val gotoUrl = menu.findItem(R.id.menu_goto_url)
gotoUrl?.apply {
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
@@ -440,38 +423,38 @@ class EntryActivity : LockingActivity() {
}
// Show education views
Handler().post { performedNextEducation(EntryActivityEducation(this), menu) }
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
return true
}
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
menu: Menu) {
val entryCopyEducationPerformed = entryContentsView?.isUserNamePresent == true
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
val entryCopyEducationPerformed = entryFieldCopyView != null
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
findViewById(R.id.entry_user_name_action_image),
entryFieldCopyView,
{
clipboardHelper?.timeoutCopyToClipboard(mEntry!!.username,
getString(R.string.copy_field,
getString(R.string.entry_user_name)))
val appNameString = getString(R.string.app_name)
clipboardHelper?.timeoutCopyToClipboard(appNameString,
getString(R.string.copy_field, appNameString))
},
{
// Launch autofill settings
startActivity(Intent(this@EntryActivity, SettingsAutofillActivity::class.java))
performedNextEducation(entryActivityEducation, menu)
})
if (!entryCopyEducationPerformed) {
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
// entryEditEducationPerformed
toolbar?.findViewById<View>(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
toolbar!!.findViewById(R.id.menu_edit),
{
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
},
{
// Open Keepass doc to create field references
startActivity(Intent(Intent.ACTION_VIEW,
UriUtil.parse(getString(R.string.field_references_url))))
})
menuEditView != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
menuEditView,
{
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
},
{
performedNextEducation(entryActivityEducation, menu)
}
)
}
}
@@ -481,12 +464,6 @@ class EntryActivity : LockingActivity() {
MenuUtil.onContributionItemSelected(this)
return true
}
R.id.menu_toggle_pass -> {
mShowPassword = !mShowPassword
changeShowPasswordIcon(item)
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
return true
}
R.id.menu_edit -> {
mEntry?.let {
EntryEditActivity.launch(this@EntryActivity, it)
@@ -506,7 +483,7 @@ class EntryActivity : LockingActivity() {
}
R.id.menu_restore_entry_history -> {
mEntryLastVersion?.let { mainEntry ->
mProgressDialogThread?.startDatabaseRestoreEntryHistory(
mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory(
mainEntry,
mEntryHistoryPosition,
!mReadOnly && mAutoSaveEnable)
@@ -514,39 +491,40 @@ class EntryActivity : LockingActivity() {
}
R.id.menu_delete_entry_history -> {
mEntryLastVersion?.let { mainEntry ->
mProgressDialogThread?.startDatabaseDeleteEntryHistory(
mProgressDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(
mainEntry,
mEntryHistoryPosition,
!mReadOnly && mAutoSaveEnable)
}
}
R.id.menu_lock -> {
lockAndExit()
return true
}
R.id.menu_save_database -> {
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
}
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
}
return super.onOptionsItemSelected(item)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(KEY_FIRST_LAUNCH_ACTIVITY, mFirstLaunchOfActivity)
}
override fun finish() {
// Transit data in previous Activity after an update
/*
TODO Slowdown when add entry as result
Intent intent = new Intent();
intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry);
onFinish(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
*/
Intent().apply {
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry)
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this)
}
super.finish()
}
companion object {
private val TAG = EntryActivity::class.java.name
private const val KEY_FIRST_LAUNCH_ACTIVITY = "KEY_FIRST_LAUNCH_ACTIVITY"
const val KEY_ENTRY = "KEY_ENTRY"
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"

View File

@@ -19,58 +19,89 @@
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.ScrollView
import android.widget.DatePicker
import android.widget.TimePicker
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.view.EntryEditContentsView
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.closeDatabase
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.showActionError
import com.kunzisoft.keepass.view.updateLockPaddingLeft
import org.joda.time.DateTime
import java.util.*
import kotlin.collections.ArrayList
class EntryEditActivity : LockingActivity(),
IconPickerDialogFragment.IconPickerListener,
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
GeneratePasswordDialogFragment.GeneratePasswordListener,
SetOTPDialogFragment.CreateOtpListener {
SetOTPDialogFragment.CreateOtpListener,
DatePickerDialog.OnDateSetListener,
TimePickerDialog.OnTimeSetListener,
FileTooBigDialogFragment.ActionChooseListener,
ReplaceFileDialogFragment.ActionChooseListener {
private var mDatabase: Database? = null
// Refs of an entry and group in database, are not modifiable
private var mEntry: Entry? = null
private var mParent: Group? = null
// New or copy of mEntry in the database to be modifiable
private var mNewEntry: Entry? = null
private var mIsNew: Boolean = false
// Views
private var coordinatorLayout: CoordinatorLayout? = null
private var scrollView: ScrollView? = null
private var entryEditContentsView: EntryEditContentsView? = null
private var saveView: View? = null
private var scrollView: NestedScrollView? = null
private var entryEditFragment: EntryEditFragment? = null
private var entryEditAddToolBar: Toolbar? = null
private var validateButton: View? = null
private var lockView: View? = null
// To manage attachments
private var mSelectFileHelper: SelectFileHelper? = null
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAllowMultipleAttachments: Boolean = false
private var mTempAttachments = ArrayList<Attachment>()
// Education
private var entryEditActivityEducation: EntryEditActivityEducation? = null
@@ -90,10 +121,13 @@ class EntryEditActivity : LockingActivity(),
scrollView = findViewById(R.id.entry_edit_scroll)
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
entryEditContentsView = findViewById(R.id.entry_edit_contents)
entryEditContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener {
lockAndExit()
}
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(entryEditContentsView)
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
@@ -101,6 +135,8 @@ class EntryEditActivity : LockingActivity(),
// Likely the app has been killed exit the activity
mDatabase = Database.getInstance()
var tempEntryInfo: EntryInfo? = null
// Entry is retrieve, it's an entry to update
intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let {
mIsNew = false
@@ -116,139 +152,210 @@ class EntryEditActivity : LockingActivity(),
entry.parent = mParent
}
}
// Create the new entry from the current one
if (savedInstanceState == null
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
mEntry?.let { entry ->
// Create a copy to modify
mNewEntry = Entry(entry).also { newEntry ->
// WARNING Remove the parent to keep memory with parcelable
newEntry.removeParent()
}
}
}
tempEntryInfo = mEntry?.getEntryInfo(mDatabase, true)
}
// Parent is retrieve, it's a new entry to create
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let {
mIsNew = true
// Create an empty new entry
if (savedInstanceState == null
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
mNewEntry = mDatabase?.createEntry()
}
mParent = mDatabase?.getGroupById(it)
// Add the default icon
mDatabase?.drawFactory?.let { iconFactory ->
entryEditContentsView?.setDefaultIcon(iconFactory)
// Add the default icon from parent if not a folder
val parentIcon = mParent?.icon
tempEntryInfo = mDatabase?.createEntry()?.getEntryInfo(mDatabase, true)
// Set default icon
if (parentIcon != null
&& parentIcon.iconId != IconImage.UNKNOWN_ID
&& parentIcon.iconId != IconImageStandard.FOLDER) {
tempEntryInfo?.icon = parentIcon
}
// Set default username
tempEntryInfo?.username = mDatabase?.defaultUsername ?: ""
}
// Build fragment to manage entry modification
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
if (entryEditFragment == null) {
entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo)
}
supportFragmentManager.beginTransaction()
.replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG)
.commit()
entryEditFragment?.apply {
drawFactory = mDatabase?.drawFactory
setOnDateClickListener = View.OnClickListener {
expiryTime.date.let { expiresDate ->
val dateTime = DateTime(expiresDate)
val defaultYear = dateTime.year
val defaultMonth = dateTime.monthOfYear-1
val defaultDay = dateTime.dayOfMonth
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
.show(supportFragmentManager, "DatePickerFragment")
}
}
setOnPasswordGeneratorClickListener = View.OnClickListener {
openPasswordGenerator()
}
// Add listener to the icon
setOnIconViewClickListener = View.OnClickListener {
IconPickerDialogFragment.launch(this@EntryEditActivity)
}
setOnRemoveAttachment = { attachment ->
mAttachmentFileBinderManager?.removeBinaryAttachment(attachment)
removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD))
}
setOnEditCustomField = { field ->
editCustomField(field)
}
}
// Retrieve the new entry after an orientation change
if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY)
// Retrieve temp attachments in case of deletion
if (savedInstanceState?.containsKey(TEMP_ATTACHMENTS) == true) {
mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments
}
// Close the activity if entry or parent can't be retrieve
if (mNewEntry == null || mParent == null) {
finish()
return
}
populateViewsWithEntry(mNewEntry!!)
// Assign title
title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry)
// Add listener to the icon
entryEditContentsView?.setOnIconViewClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) }
// Bottom Bar
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
entryEditAddToolBar?.apply {
menuInflater.inflate(R.menu.entry_edit, menu)
// Generate password button
entryEditContentsView?.setOnPasswordGeneratorClickListener { openPasswordGenerator() }
menu.findItem(R.id.menu_add_field).apply {
val allowCustomField = mDatabase?.allowEntryCustomFields() == true
isEnabled = allowCustomField
isVisible = allowCustomField
}
// Attachment not compatible below KitKat
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
menu.findItem(R.id.menu_add_attachment).isVisible = false
}
menu.findItem(R.id.menu_add_otp).apply {
val allowOTP = mDatabase?.allowOTP == true
isEnabled = allowOTP
// OTP not compatible below KitKat
isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
}
setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.menu_add_field -> {
addNewCustomField()
true
}
R.id.menu_add_attachment -> {
addNewAttachment(item)
true
}
R.id.menu_add_otp -> {
setupOTP()
true
}
else -> true
}
}
}
// To retrieve attachment
mSelectFileHelper = SelectFileHelper(this)
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
// Save button
saveView = findViewById(R.id.entry_edit_save)
saveView?.setOnClickListener { saveEntry() }
entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) {
addNewCustomField()
}
validateButton = findViewById(R.id.entry_edit_validate)
validateButton?.setOnClickListener { saveEntry() }
// Verify the education views
entryEditActivityEducation = EntryEditActivityEducation(this)
// Create progress dialog
mProgressDialogThread?.onActionFinish = { actionTask, result ->
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_CREATE_ENTRY_TASK,
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
if (result.isSuccess)
finish()
try {
if (result.isSuccess) {
var newNodes: List<Node> = ArrayList()
result.data?.getBundle(DatabaseTaskNotificationService.NEW_NODES_KEY)?.let { newNodesBundle ->
mDatabase?.let { database ->
newNodes = DatabaseTaskNotificationService.getListNodesFromBundle(database, newNodesBundle)
}
}
if (newNodes.size == 1) {
mEntry = newNodes[0] as Entry?
finish()
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve entry after database action", e)
}
}
}
coordinatorLayout?.showActionError(result)
}
}
// Show error
if (!result.isSuccess) {
result.message?.let { resultMessage ->
Snackbar.make(coordinatorLayout!!,
resultMessage,
Snackbar.LENGTH_LONG).asError().show()
override fun onResume() {
super.onResume()
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
View.VISIBLE
} else {
View.GONE
}
// Padding if lock button visible
entryEditAddToolBar?.updateLockPaddingLeft()
mAllowMultipleAttachments = mDatabase?.allowMultipleAttachments == true
mAttachmentFileBinderManager?.apply {
registerProgressTask()
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
when (entryAttachmentState.downloadState) {
AttachmentState.START -> {
entryEditFragment?.apply {
// When only one attachment is allowed
if (!mAllowMultipleAttachments) {
clearAttachments()
}
putAttachment(entryAttachmentState)
// Scroll to the attachment position
getAttachmentViewPosition(entryAttachmentState) {
scrollView?.smoothScrollTo(0, it.toInt())
}
}
}
AttachmentState.IN_PROGRESS -> {
entryEditFragment?.putAttachment(entryAttachmentState)
}
AttachmentState.COMPLETE -> {
entryEditFragment?.apply {
putAttachment(entryAttachmentState)
// Scroll to the attachment position
getAttachmentViewPosition(entryAttachmentState) {
scrollView?.smoothScrollTo(0, it.toInt())
}
}
}
AttachmentState.ERROR -> {
entryEditFragment?.removeAttachment(entryAttachmentState)
coordinatorLayout?.let {
Snackbar.make(it, R.string.error_file_not_create, Snackbar.LENGTH_LONG).asError().show()
}
}
else -> {}
}
}
}
}
}
private fun populateViewsWithEntry(newEntry: Entry) {
// Don't start the field reference manager, we want to see the raw ref
mDatabase?.stopManageEntry(newEntry)
override fun onPause() {
mAttachmentFileBinderManager?.unregisterProgressTask()
// Set info in temp parameters
temporarilySaveAndShowSelectedIcon(newEntry.icon)
// Set info in view
entryEditContentsView?.apply {
title = newEntry.title
username = if (newEntry.username.isEmpty()) mDatabase?.defaultUsername ?:"" else newEntry.username
url = newEntry.url
password = newEntry.password
notes = newEntry.notes
for (entry in newEntry.customFields.entries) {
post {
putCustomField(entry.key, entry.value)
}
}
}
}
private fun populateEntryWithViews(newEntry: Entry) {
mDatabase?.startManageEntry(newEntry)
newEntry.apply {
// Build info from view
entryEditContentsView?.let { entryView ->
removeAllFields()
title = entryView.title
username = entryView.username
url = entryView.url
password = entryView.password
notes = entryView.notes
entryView.customFields.forEach { customField ->
putExtraField(customField.name, customField.protectedValue)
}
}
}
mDatabase?.stopManageEntry(newEntry)
}
private fun temporarilySaveAndShowSelectedIcon(icon: IconImage) {
mNewEntry?.icon = icon
mDatabase?.drawFactory?.let { iconDrawFactory ->
entryEditContentsView?.setIcon(iconDrawFactory, icon)
}
super.onPause()
}
/**
@@ -259,34 +366,134 @@ class EntryEditActivity : LockingActivity(),
}
/**
* Add a new customized field view and scroll to bottom
* Add a new customized field
*/
private fun addNewCustomField() {
entryEditContentsView?.addEmptyCustomField()
EntryCustomFieldDialogFragment.getInstance().show(supportFragmentManager, "customFieldDialog")
}
private fun editCustomField(field: Field) {
EntryCustomFieldDialogFragment.getInstance(field).show(supportFragmentManager, "customFieldDialog")
}
override fun onNewCustomFieldApproved(newField: Field) {
entryEditFragment?.apply {
putExtraField(newField)
}
}
override fun onEditCustomFieldApproved(oldField: Field, newField: Field) {
entryEditFragment?.replaceExtraField(oldField, newField)
}
override fun onDeleteCustomFieldApproved(oldField: Field) {
entryEditFragment?.removeExtraField(oldField)
}
/**
* Add a new attachment
*/
private fun addNewAttachment(item: MenuItem) {
mSelectFileHelper?.selectFileOnClickViewListener?.onMenuItemClick(item)
}
override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) {
if (attachmentToUploadUri != null && fileName != null) {
buildNewAttachment(attachmentToUploadUri, fileName)
}
}
override fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?) {
startUploadAttachment(attachmentToUploadUri, attachment)
}
private fun startUploadAttachment(attachmentToUploadUri: Uri?, attachment: Attachment?) {
if (attachmentToUploadUri != null && attachment != null) {
// Start uploading in service
mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, attachment)
// Add in temp list
mTempAttachments.add(attachment)
}
}
private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) {
val compression = mDatabase?.compressionForNewEntry() ?: false
mDatabase?.buildNewBinary(UriUtil.getBinaryDir(this), compression)?.let { binaryAttachment ->
val entryAttachment = Attachment(fileName, binaryAttachment)
// Ask to replace the current attachment
if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) ||
entryEditFragment?.containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD)) == true) {
ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment)
.show(supportFragmentManager, "replacementFileFragment")
} else {
startUploadAttachment(attachmentToUploadUri, entryAttachment)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
uri?.let { attachmentToUploadUri ->
// TODO Async to get the name
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
documentFile.name?.let { fileName ->
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
.show(supportFragmentManager, "fileTooBigFragment")
} else {
buildNewAttachment(attachmentToUploadUri, fileName)
}
}
}
}
}
}
/**
* Set up OTP (HOTP or TOTP) and add it as extra field
*/
private fun setupOTP() {
// Retrieve the current otpElement if exists
// and open the dialog to set up the OTP
SetOTPDialogFragment.build(entryEditFragment?.getEntryInfo()?.otpModel)
.show(supportFragmentManager, "addOTPDialog")
}
/**
* Saves the new entry or update an existing entry in the database
*/
private fun saveEntry() {
// Get the temp entry
entryEditFragment?.getEntryInfo()?.let { newEntryInfo ->
// Launch a validation and show the error if present
if (entryEditContentsView?.isValid() == true) {
// Clone the entry
mNewEntry?.let { newEntry ->
if (mIsNew) {
// Create new one
mDatabase?.createEntry()
} else {
// Create a clone
Entry(mEntry!!)
}?.let { newEntry ->
// WARNING Add the parent previously deleted
newEntry.parent = mEntry?.parent
newEntry.setEntryInfo(mDatabase, newEntryInfo)
// Build info
newEntry.lastAccessTime = DateInstant()
newEntry.lastModificationTime = DateInstant()
populateEntryWithViews(newEntry)
// Delete temp attachment if not used
mTempAttachments.forEach {
mDatabase?.binaryPool?.let { binaryPool ->
if (!newEntry.getAttachments(binaryPool).contains(it)) {
mDatabase?.removeAttachmentIfNotUsed(it)
}
}
}
// Open a progress dialog and save entry
if (mIsNew) {
mParent?.let { parent ->
mProgressDialogThread?.startDatabaseCreateEntry(
mProgressDatabaseTaskProvider?.startDatabaseCreateEntry(
newEntry,
parent,
!mReadOnly && mAutoSaveEnable
@@ -294,7 +501,7 @@ class EntryEditActivity : LockingActivity(),
}
} else {
mEntry?.let { oldEntry ->
mProgressDialogThread?.startDatabaseUpdateEntry(
mProgressDatabaseTaskProvider?.startDatabaseUpdateEntry(
oldEntry,
newEntry,
!mReadOnly && mAutoSaveEnable
@@ -313,98 +520,151 @@ class EntryEditActivity : LockingActivity(),
// Save database not needed here
menu.findItem(R.id.menu_save_database)?.isVisible = false
MenuUtil.contributionMenuInflater(inflater, menu)
if (mDatabase?.allowOTP == true)
inflater.inflate(R.menu.entry_otp, menu)
entryEditActivityEducation?.let {
Handler().post { performedNextEducation(it) }
}
return true
}
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
val passwordView = entryEditContentsView?.generatePasswordView
val addNewFieldView = entryEditContentsView?.addNewFieldButton
val generatePasswordEducationPerformed = passwordView != null
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
passwordView,
{
openPasswordGenerator()
},
{
performedNextEducation(entryEditActivityEducation)
}
)
if (!generatePasswordEducationPerformed) {
// entryNewFieldEducationPerformed
mNewEntry != null && mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
entryEditActivityEducation?.let {
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
}
return super.onPrepareOptionsMenu(menu)
}
fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
if (entryEditFragment?.generatePasswordEducationPerformed(entryEditActivityEducation) != true) {
val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field)
val addNewFieldEducationPerformed = mDatabase?.allowEntryCustomFields() == true
&& addNewFieldView != null
&& addNewFieldView.isVisible
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
addNewFieldView,
{
addNewCustomField()
})
},
{
performedNextEducation(entryEditActivityEducation)
}
)
if (!addNewFieldEducationPerformed) {
val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment)
val addAttachmentEducationPerformed = attachmentView != null
&& attachmentView.isVisible
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
attachmentView,
{
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(attachmentView)
},
{
performedNextEducation(entryEditActivityEducation)
}
)
if (!addAttachmentEducationPerformed) {
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
setupOtpView != null
&& setupOtpView.isVisible
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
setupOtpView,
{
setupOTP()
}
)
}
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_lock -> {
lockAndExit()
return true
}
R.id.menu_save_database -> {
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
}
R.id.menu_contribute -> {
MenuUtil.onContributionItemSelected(this)
return true
}
R.id.menu_add_otp -> {
// Retrieve the current otpElement if exists
// and open the dialog to set up the OTP
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
.show(supportFragmentManager, "addOTPDialog")
return true
android.R.id.home -> {
onBackPressed()
}
android.R.id.home -> finish()
}
return super.onOptionsItemSelected(item)
}
override fun onOtpCreated(otpElement: OtpElement) {
var titleOTP: String? = null
var usernameOTP: String? = null
// Build a temp entry to get title and username (by ref)
entryEditFragment?.getEntryInfo()?.let { entryInfo ->
val entryTemp = mDatabase?.createEntry()
entryTemp?.setEntryInfo(mDatabase, entryInfo)
mDatabase?.startManageEntry(entryTemp)
titleOTP = entryTemp?.title
usernameOTP = entryTemp?.username
mDatabase?.stopManageEntry(mEntry)
}
// Update the otp field with otpauth:// url
val otpField = OtpEntryFields.buildOtpField(otpElement,
mEntry?.title, mEntry?.username)
entryEditContentsView?.putCustomField(otpField.name, otpField.protectedValue)
mEntry?.putExtraField(otpField.name, otpField.protectedValue)
val otpField = OtpEntryFields.buildOtpField(otpElement, titleOTP, usernameOTP)
mEntry?.putExtraField(Field(otpField.name, otpField.protectedValue))
entryEditFragment?.apply {
putExtraField(otpField)
}
}
override fun iconPicked(bundle: Bundle) {
IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon ->
temporarilySaveAndShowSelectedIcon(icon)
entryEditFragment?.icon = icon
}
}
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) {
entryEditFragment?.expiryTime?.date?.let { expiresDate ->
// Save the date
entryEditFragment?.expiryTime =
DateInstant(DateTime(expiresDate)
.withYear(year)
.withMonthOfYear(month + 1)
.withDayOfMonth(day)
.toDate())
// Launch the time picker
val dateTime = DateTime(expiresDate)
val defaultHour = dateTime.hourOfDay
val defaultMinute = dateTime.minuteOfHour
TimePickerFragment.getInstance(defaultHour, defaultMinute)
.show(supportFragmentManager, "TimePickerFragment")
}
}
}
override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) {
entryEditFragment?.expiryTime?.date?.let { expiresDate ->
// Save the date
entryEditFragment?.expiryTime =
DateInstant(DateTime(expiresDate)
.withHourOfDay(hours)
.withMinuteOfHour(minutes)
.toDate())
}
}
override fun onSaveInstanceState(outState: Bundle) {
mNewEntry?.let {
populateEntryWithViews(it)
outState.putParcelable(KEY_NEW_ENTRY, it)
}
outState.putParcelableArrayList(TEMP_ATTACHMENTS, mTempAttachments)
super.onSaveInstanceState(outState)
}
override fun acceptPassword(bundle: Bundle) {
bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID)?.let {
entryEditContentsView?.password = it
entryEditFragment?.password = it
}
entryEditActivityEducation?.let {
Handler().post { performedNextEducation(it) }
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
}
}
@@ -412,13 +672,22 @@ class EntryEditActivity : LockingActivity(),
// Do nothing here
}
override fun onBackPressed() {
AlertDialog.Builder(this)
.setMessage(R.string.discard_changes)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.discard) { _, _ ->
super@EntryEditActivity.onBackPressed()
}.create().show()
}
override fun finish() {
// Assign entry callback as a result in all case
try {
mNewEntry?.let {
mEntry?.let { entry ->
val bundle = Bundle()
val intentEntry = Intent()
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, mNewEntry)
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry)
intentEntry.putExtras(bundle)
if (mIsNew) {
setResult(ADD_ENTRY_RESULT_CODE, intentEntry)
@@ -442,7 +711,7 @@ class EntryEditActivity : LockingActivity(),
const val KEY_PARENT = "parent"
// SaveInstanceState
const val KEY_NEW_ENTRY = "new_entry"
const val TEMP_ATTACHMENTS = "TEMP_ATTACHMENTS"
// Keys for callback
const val ADD_ENTRY_RESULT_CODE = 31
@@ -450,6 +719,8 @@ class EntryEditActivity : LockingActivity(),
const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
const val ENTRY_EDIT_FRAGMENT_TAG = "ENTRY_EDIT_FRAGMENT_TAG"
/**
* Launch EntryEditActivity to update an existing entry
*

View File

@@ -0,0 +1,546 @@
/*
* 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
import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.applyFontVisibility
import com.kunzisoft.keepass.view.collapse
import com.kunzisoft.keepass.view.expand
class EntryEditFragment: StylishFragment() {
private lateinit var entryTitleLayoutView: TextInputLayout
private lateinit var entryTitleView: EditText
private lateinit var entryIconView: ImageView
private lateinit var entryUserNameView: EditText
private lateinit var entryUrlView: EditText
private lateinit var entryPasswordLayoutView: TextInputLayout
private lateinit var entryPasswordView: EditText
private lateinit var entryPasswordGeneratorView: View
private lateinit var entryExpiresCheckBox: CompoundButton
private lateinit var entryExpiresTextView: TextView
private lateinit var entryNotesView: EditText
private lateinit var extraFieldsContainerView: View
private lateinit var extraFieldsListView: ViewGroup
private lateinit var attachmentsContainerView: View
private lateinit var attachmentsListView: RecyclerView
private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter
private var fontInVisibility: Boolean = false
private var iconColor: Int = 0
private var expiresInstant: DateInstant = DateInstant.IN_ONE_MONTH
var drawFactory: IconDrawableFactory? = null
var setOnDateClickListener: View.OnClickListener? = null
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
var setOnIconViewClickListener: View.OnClickListener? = null
var setOnEditCustomField: ((Field) -> Unit)? = null
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
// Elements to modify the current entry
private var mEntryInfo = EntryInfo()
private var mLastFocusedEditField: FocusedEditField? = null
private var mExtraViewToRequestFocus: EditText? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
val rootView = inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_entry_edit_contents, container, false)
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
entryTitleLayoutView = rootView.findViewById(R.id.entry_edit_container_title)
entryTitleView = rootView.findViewById(R.id.entry_edit_title)
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
entryIconView.setOnClickListener {
setOnIconViewClickListener?.onClick(it)
}
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
entryUrlView = rootView.findViewById(R.id.entry_edit_url)
entryPasswordLayoutView = rootView.findViewById(R.id.entry_edit_container_password)
entryPasswordView = rootView.findViewById(R.id.entry_edit_password)
entryPasswordGeneratorView = rootView.findViewById(R.id.entry_edit_password_generator_button)
entryPasswordGeneratorView.setOnClickListener {
setOnPasswordGeneratorClickListener?.onClick(it)
}
entryExpiresCheckBox = rootView.findViewById(R.id.entry_edit_expires_checkbox)
entryExpiresTextView = rootView.findViewById(R.id.entry_edit_expires_text)
entryExpiresTextView.setOnClickListener {
if (entryExpiresCheckBox.isChecked)
setOnDateClickListener?.onClick(it)
}
entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
extraFieldsContainerView = rootView.findViewById(R.id.extra_fields_container)
extraFieldsListView = rootView.findViewById(R.id.extra_fields_list)
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
if (previousSize > 0 && newSize == 0) {
attachmentsContainerView.collapse(true)
} else if (previousSize == 0 && newSize == 1) {
attachmentsContainerView.expand(true)
}
}
attachmentsListView.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adapter = attachmentsAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
assignExpiresDateText()
}
// Retrieve the textColor to tint the icon
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
taIconColor?.recycle()
// Retrieve the new entry after an orientation change
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
else if (savedInstanceState?.containsKey(KEY_TEMP_ENTRY_INFO) == true) {
mEntryInfo = savedInstanceState.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
}
if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) {
mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD) ?: mLastFocusedEditField
}
populateViewsWithEntry()
return rootView
}
override fun onDetach() {
super.onDetach()
drawFactory = null
setOnDateClickListener = null
setOnPasswordGeneratorClickListener = null
setOnIconViewClickListener = null
setOnRemoveAttachment = null
setOnEditCustomField = null
}
fun getEntryInfo(): EntryInfo? {
populateEntryWithViews()
return mEntryInfo
}
fun generatePasswordEducationPerformed(entryEditActivityEducation: EntryEditActivityEducation): Boolean {
return entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
entryPasswordGeneratorView,
{
GeneratePasswordDialogFragment().show(parentFragmentManager, "PasswordGeneratorFragment")
},
{
try {
(activity as? EntryEditActivity?)?.performedNextEducation(entryEditActivityEducation)
} catch (ignore: Exception) {}
}
)
}
private fun populateViewsWithEntry() {
// Set info in view
icon = mEntryInfo.icon
title = mEntryInfo.title
username = mEntryInfo.username
url = mEntryInfo.url
password = mEntryInfo.password
expires = mEntryInfo.expires
expiryTime = mEntryInfo.expiryTime
notes = mEntryInfo.notes
assignExtraFields(mEntryInfo.customFields) { fields ->
setOnEditCustomField?.invoke(fields)
}
assignAttachments(mEntryInfo.attachments, StreamDirection.UPLOAD) { attachment ->
setOnRemoveAttachment?.invoke(attachment)
}
}
private fun populateEntryWithViews() {
// Icon already populate
mEntryInfo.title = title
mEntryInfo.username = username
mEntryInfo.url = url
mEntryInfo.password = password
mEntryInfo.expires = expires
mEntryInfo.expiryTime = expiryTime
mEntryInfo.notes = notes
mEntryInfo.customFields = getExtraFields()
mEntryInfo.otpModel = OtpEntryFields.parseFields { key ->
getExtraFields().firstOrNull { it.name == key }?.protectedValue?.toString()
}?.otpModel
mEntryInfo.attachments = getAttachments()
}
var title: String
get() {
return entryTitleView.text.toString()
}
set(value) {
entryTitleView.setText(value)
if (fontInVisibility)
entryTitleView.applyFontVisibility()
}
var icon: IconImage
get() {
return mEntryInfo.icon
}
set(value) {
mEntryInfo.icon = value
drawFactory?.let { drawFactory ->
entryIconView.assignDatabaseIcon(drawFactory, value, iconColor)
}
}
var username: String
get() {
return entryUserNameView.text.toString()
}
set(value) {
entryUserNameView.setText(value)
if (fontInVisibility)
entryUserNameView.applyFontVisibility()
}
var url: String
get() {
return entryUrlView.text.toString()
}
set(value) {
entryUrlView.setText(value)
if (fontInVisibility)
entryUrlView.applyFontVisibility()
}
var password: String
get() {
return entryPasswordView.text.toString()
}
set(value) {
entryPasswordView.setText(value)
if (fontInVisibility) {
entryPasswordView.applyFontVisibility()
}
}
private fun assignExpiresDateText() {
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
entryExpiresTextView.setOnClickListener(setOnDateClickListener)
expiresInstant.getDateTimeString(resources)
} else {
entryExpiresTextView.setOnClickListener(null)
resources.getString(R.string.never)
}
if (fontInVisibility)
entryExpiresTextView.applyFontVisibility()
}
var expires: Boolean
get() {
return entryExpiresCheckBox.isChecked
}
set(value) {
if (!value) {
expiresInstant = DateInstant.IN_ONE_MONTH
}
entryExpiresCheckBox.isChecked = value
assignExpiresDateText()
}
var expiryTime: DateInstant
get() {
return if (expires)
expiresInstant
else
DateInstant.NEVER_EXPIRE
}
set(value) {
if (expires)
expiresInstant = value
assignExpiresDateText()
}
var notes: String
get() {
return entryNotesView.text.toString()
}
set(value) {
entryNotesView.setText(value)
if (fontInVisibility)
entryNotesView.applyFontVisibility()
}
/* -------------
* Extra Fields
* -------------
*/
private var mExtraFieldsList: MutableList<Field> = ArrayList()
private var mOnEditButtonClickListener: ((item: Field)->Unit)? = null
private fun buildViewFromField(extraField: Field): View? {
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
val itemView: View? = inflater?.inflate(R.layout.item_entry_edit_extra_field, extraFieldsListView, false)
itemView?.id = View.NO_ID
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
extraFieldValueContainer?.isPasswordVisibilityToggleEnabled = extraField.protectedValue.isProtected
extraFieldValueContainer?.hint = extraField.name
extraFieldValueContainer?.id = View.NO_ID
val extraFieldValue: TextInputEditText? = itemView?.findViewById(R.id.entry_extra_field_value)
extraFieldValue?.apply {
if (extraField.protectedValue.isProtected) {
inputType = extraFieldValue.inputType or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
}
setText(extraField.protectedValue.toString())
if (fontInVisibility)
applyFontVisibility()
}
extraFieldValue?.id = View.NO_ID
extraFieldValue?.tag = "FIELD_VALUE_TAG"
if (mLastFocusedEditField?.field == extraField) {
mExtraViewToRequestFocus = extraFieldValue
}
val extraFieldEditButton: View? = itemView?.findViewById(R.id.entry_extra_field_edit)
extraFieldEditButton?.setOnClickListener {
mOnEditButtonClickListener?.invoke(extraField)
}
extraFieldEditButton?.id = View.NO_ID
return itemView
}
fun getExtraFields(): List<Field> {
mLastFocusedEditField = null
for (index in 0 until extraFieldsListView.childCount) {
val extraFieldValue: EditText = extraFieldsListView.getChildAt(index)
.findViewWithTag("FIELD_VALUE_TAG")
val extraField = mExtraFieldsList[index]
extraField.protectedValue.stringValue = extraFieldValue.text?.toString() ?: ""
if (extraFieldValue.isFocused) {
mLastFocusedEditField = FocusedEditField().apply {
field = extraField
cursorSelectionStart = extraFieldValue.selectionStart
cursorSelectionEnd = extraFieldValue.selectionEnd
}
}
}
return mExtraFieldsList
}
/**
* Remove all children and add new views for each field
*/
fun assignExtraFields(fields: List<Field>,
onEditButtonClickListener: ((item: Field)->Unit)?) {
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
// Reinit focused field
mExtraFieldsList.clear()
mExtraFieldsList.addAll(fields)
extraFieldsListView.removeAllViews()
fields.forEach {
extraFieldsListView.addView(buildViewFromField(it))
}
// Request last focus
mLastFocusedEditField?.let { focusField ->
mExtraViewToRequestFocus?.apply {
requestFocus()
setSelection(focusField.cursorSelectionStart,
focusField.cursorSelectionEnd)
}
}
mLastFocusedEditField = null
mOnEditButtonClickListener = onEditButtonClickListener
}
/**
* Update an extra field or create a new one if doesn't exists, the old value is lost
*/
fun putExtraField(extraField: Field) {
extraFieldsContainerView.visibility = View.VISIBLE
val oldField = mExtraFieldsList.firstOrNull { it.name == extraField.name }
oldField?.let {
val index = mExtraFieldsList.indexOf(oldField)
mExtraFieldsList.removeAt(index)
mExtraFieldsList.add(index, extraField)
extraFieldsListView.removeViewAt(index)
val newView = buildViewFromField(extraField)
extraFieldsListView.addView(newView, index)
newView?.requestFocus()
} ?: kotlin.run {
mExtraFieldsList.add(extraField)
val newView = buildViewFromField(extraField)
extraFieldsListView.addView(newView)
newView?.requestFocus()
}
}
/**
* Update an extra field and keep the old value
*/
fun replaceExtraField(oldExtraField: Field, newExtraField: Field) {
extraFieldsContainerView.visibility = View.VISIBLE
val index = mExtraFieldsList.indexOf(oldExtraField)
val oldValueEditText: EditText = extraFieldsListView.getChildAt(index)
.findViewWithTag("FIELD_VALUE_TAG")
val oldValue = oldValueEditText.text.toString()
val newExtraFieldWithOldValue = Field(newExtraField).apply {
this.protectedValue.stringValue = oldValue
}
mExtraFieldsList.removeAt(index)
mExtraFieldsList.add(index, newExtraFieldWithOldValue)
extraFieldsListView.removeViewAt(index)
val newView = buildViewFromField(newExtraFieldWithOldValue)
extraFieldsListView.addView(newView, index)
newView?.requestFocus()
}
fun removeExtraField(oldExtraField: Field) {
val previousSize = mExtraFieldsList.size
val index = mExtraFieldsList.indexOf(oldExtraField)
extraFieldsListView.getChildAt(index)?.let {
it.collapse(true) {
mExtraFieldsList.removeAt(index)
extraFieldsListView.removeViewAt(index)
val newSize = mExtraFieldsList.size
if (previousSize > 0 && newSize == 0) {
extraFieldsContainerView.collapse(true)
} else if (previousSize == 0 && newSize == 1) {
extraFieldsContainerView.expand(true)
}
}
}
}
/* -------------
* Attachments
* -------------
*/
fun getAttachments(): List<Attachment> {
return attachmentsAdapter.itemsList.map { it.attachment }
}
fun assignAttachments(attachments: List<Attachment>,
streamDirection: StreamDirection,
onDeleteItem: (attachment: Attachment)->Unit) {
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
attachmentsAdapter.onDeleteButtonClickListener = { item ->
onDeleteItem.invoke(item.attachment)
}
}
fun containsAttachment(): Boolean {
return !attachmentsAdapter.isEmpty()
}
fun containsAttachment(attachment: EntryAttachmentState): Boolean {
return attachmentsAdapter.contains(attachment)
}
fun putAttachment(attachment: EntryAttachmentState) {
attachmentsContainerView.visibility = View.VISIBLE
attachmentsAdapter.putItem(attachment)
}
fun removeAttachment(attachment: EntryAttachmentState) {
attachmentsAdapter.removeItem(attachment)
}
fun clearAttachments() {
attachmentsAdapter.clear()
}
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
attachmentsListView.postDelayed({
position.invoke(attachmentsContainerView.y
+ attachmentsListView.y
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
?: 0F)
)
}, 250)
}
override fun onSaveInstanceState(outState: Bundle) {
populateEntryWithViews()
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
super.onSaveInstanceState(outState)
}
companion object {
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
return EntryEditFragment().apply {
arguments = Bundle().apply {
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
}
}
}
}
}

View File

@@ -0,0 +1,130 @@
/*
* 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
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
/**
* Activity to search or select entry in database,
* Commonly used with Magikeyboard
*/
class EntrySelectionLauncherActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
var sharedWebDomain: String? = null
when (intent?.action) {
Intent.ACTION_SEND -> {
if ("text/plain" == intent.type) {
// Retrieve web domain
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
sharedWebDomain = Uri.parse(it).host
}
}
}
else -> {}
}
// Setting to integrate Magikeyboard
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
// Build search param
val searchInfo = SearchInfo().apply {
webDomain = sharedWebDomain
}
// If database is open
SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(),
searchInfo,
{ items ->
// Items found
if (searchShareForMagikeyboard) {
if (items.size == 1) {
// Automatically populate keyboard
val entryPopulate = items[0]
populateKeyboardAndMoveAppToBackground(this,
entryPopulate,
intent)
} else {
// Select the one we want
GroupActivity.launchForEntrySelectionResult(this,
true,
searchInfo)
}
} else {
GroupActivity.launch(this,
true,
searchInfo)
}
},
{
// Show the database UI to select the entry
if (searchShareForMagikeyboard) {
GroupActivity.launchForEntrySelectionResult(this,
false,
searchInfo)
} else {
GroupActivity.launch(this,
false,
searchInfo)
}
},
{
// If database not open
if (searchShareForMagikeyboard) {
FileDatabaseSelectActivity.launchForEntrySelectionResult(this,
searchInfo)
} else {
FileDatabaseSelectActivity.launch(this,
searchInfo)
}
}
)
finish()
super.onCreate(savedInstanceState)
}
}
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
entry: EntryInfo,
intent: Intent,
toast: Boolean = true) {
// Populate Magikeyboard with entry
MagikIME.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
// Consume the selection mode
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
activity.moveTaskToBack(true)
}

View File

@@ -22,20 +22,22 @@ package com.kunzisoft.keepass.activities
import android.annotation.SuppressLint
import android.app.Activity
import android.app.assist.AssistStructure
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.preference.PreferenceManager
import android.os.Looper
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
@@ -43,28 +45,36 @@ import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
import kotlinx.android.synthetic.main.activity_file_selection.*
import java.io.FileNotFoundException
class FileDatabaseSelectActivity : StylishActivity(),
class FileDatabaseSelectActivity : SpecialModeActivity(),
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
// Views
private var fileListContainer: View? = null
private var createButtonView: View? = null
private var coordinatorLayout: CoordinatorLayout? = null
private var createDatabaseButtonView: View? = null
private var openDatabaseButtonView: View? = null
private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels()
// Adapter to manage database history list
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
@@ -72,9 +82,9 @@ class FileDatabaseSelectActivity : StylishActivity(),
private var mDatabaseFileUri: Uri? = null
private var mOpenFileHelper: OpenFileHelper? = null
private var mSelectFileHelper: SelectFileHelper? = null
private var mProgressDialogThread: ProgressDialogThread? = null
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -82,28 +92,25 @@ class FileDatabaseSelectActivity : StylishActivity(),
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
setContentView(R.layout.activity_file_selection)
fileListContainer = findViewById(R.id.container_file_list)
coordinatorLayout = findViewById(R.id.activity_file_selection_coordinator_layout)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.title = ""
setSupportActionBar(toolbar)
// Create button
createButtonView = findViewById(R.id.create_database_button)
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
// There is an activity which can handle this intent.
createButtonView?.visibility = View.VISIBLE
}
else{
// No Activity found that can handle this intent.
createButtonView?.visibility = View.GONE
}
// Create database button
createDatabaseButtonView = findViewById(R.id.create_database_button)
createDatabaseButtonView?.setOnClickListener { createNewFile() }
createButtonView?.setOnClickListener { createNewFile() }
mOpenFileHelper = OpenFileHelper(this)
openDatabaseButtonView = findViewById(R.id.open_database_button)
openDatabaseButtonView?.setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
// Open database button
mSelectFileHelper = SelectFileHelper(this)
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
openDatabaseButtonView?.apply {
mSelectFileHelper?.selectFileOnClickViewListener?.let {
setOnClickListener(it)
setOnLongClickListener(it)
}
}
// History list
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
@@ -112,28 +119,25 @@ class FileDatabaseSelectActivity : StylishActivity(),
(fileDatabaseHistoryRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
// Construct adapter with listeners
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this)
mAdapterDatabaseHistory?.setOnDefaultDatabaseListener { databaseFile ->
databaseFilesViewModel.setDefaultDatabase(databaseFile)
}
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
UriUtil.parse(fileDatabaseHistoryEntityToOpen.databaseUri)?.let { databaseFileUri ->
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
launchPasswordActivity(
databaseFileUri,
UriUtil.parse(fileDatabaseHistoryEntityToOpen.keyFileUri))
fileDatabaseHistoryEntityToOpen.keyFileUri
)
}
updateFileListVisibility()
}
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
// Remove from app database
mFileDatabaseHistoryAction?.deleteFileDatabaseHistory(fileDatabaseHistoryToDelete) { fileHistoryDeleted ->
// Remove from adapter
fileHistoryDeleted?.let { databaseFileHistoryDeleted ->
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileHistoryDeleted)
mAdapterDatabaseHistory?.notifyDataSetChanged()
updateFileListVisibility()
}
}
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
true
}
mAdapterDatabaseHistory?.setOnSaveAliasListener { fileDatabaseHistoryWithNewAlias ->
mFileDatabaseHistoryAction?.addOrUpdateFileDatabaseHistory(fileDatabaseHistoryWithNewAlias)
// Update in app database
databaseFilesViewModel.updateDatabaseFile(fileDatabaseHistoryWithNewAlias)
}
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
@@ -141,13 +145,12 @@ class FileDatabaseSelectActivity : StylishActivity(),
if (!(savedInstanceState != null
&& savedInstanceState.containsKey(EXTRA_STAY)
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val databasePath = prefs.getString(PasswordActivity.KEY_DEFAULT_DATABASE_PATH, "")
val databasePath = PreferencesUtil.getDefaultDatabasePath(this)
UriUtil.parse(databasePath)?.let { databaseFileUri ->
launchPasswordActivityWithPath(databaseFileUri)
} ?: run {
Log.i(TAG, "Unable to launch Password Activity")
Log.i(TAG, "No default database to prepare")
}
}
@@ -157,15 +160,47 @@ class FileDatabaseSelectActivity : StylishActivity(),
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
}
// Observe list of databases
databaseFilesViewModel.databaseFilesLoaded.observe(this, Observer { databaseFiles ->
when (databaseFiles.databaseFileAction) {
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
}
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
}
GroupActivity.launch(this@FileDatabaseSelectActivity)
}
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
}
}
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
}
}
}
databaseFilesViewModel.consumeAction()
})
// Observe default database
databaseFilesViewModel.defaultDatabase.observe(this, Observer {
// Retrieve settings for default database
mAdapterDatabaseHistory?.setDefaultDatabase(it)
})
// Attach the dialog thread to this activity
mProgressDialogThread = ProgressDialogThread(this).apply {
onActionFinish = { actionTask, _ ->
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
onActionFinish = { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_CREATE_TASK -> {
// TODO Check
// mAdapterDatabaseHistory?.notifyDataSetChanged()
// updateFileListVisibility()
GroupActivity.launch(this@FileDatabaseSelectActivity)
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
val keyFileUri = result.data?.getParcelable<Uri?>(KEY_FILE_URI_KEY)
databaseFilesViewModel.addDatabaseFile(databaseUri, keyFileUri)
}
}
}
}
@@ -175,7 +210,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
/**
* Create a new file by calling the content provider
*/
@SuppressLint("InlinedApi")
private fun createNewFile() {
createDocument(this, getString(R.string.database_file_name_default) +
getString(R.string.database_file_extension_default), "application/x-keepass")
@@ -183,56 +217,76 @@ class FileDatabaseSelectActivity : StylishActivity(),
private fun fileNoFoundAction(e: FileNotFoundException) {
val error = getString(R.string.file_not_found_content)
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
coordinatorLayout?.let {
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
}
Log.e(TAG, error, e)
}
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
EntrySelectionHelper.doEntrySelectionAction(intent,
{
try {
PasswordActivity.launch(this@FileDatabaseSelectActivity,
databaseUri, keyFile)
databaseUri, keyFile,
searchInfo)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
// Remove the search info from intent
if (searchInfo != null) {
finish()
}
},
{
try {
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
databaseUri, keyFile)
finish()
databaseUri, keyFile,
searchInfo)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
finish()
},
{ assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
databaseUri, keyFile,
assistStructure)
assistStructure,
searchInfo)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
}
})
}
private fun launchGroupActivity(readOnly: Boolean) {
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
EntrySelectionHelper.doEntrySelectionAction(intent,
{
GroupActivity.launch(this@FileDatabaseSelectActivity, readOnly)
GroupActivity.launch(this@FileDatabaseSelectActivity,
false,
searchInfo,
readOnly)
},
{
GroupActivity.launchForKeyboardSelection(this@FileDatabaseSelectActivity, readOnly)
GroupActivity.launchForEntrySelectionResult(this@FileDatabaseSelectActivity,
false,
searchInfo,
readOnly)
// Do not keep history
finish()
},
{ assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity, assistStructure, readOnly)
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
assistStructure,
false,
searchInfo,
readOnly)
}
})
}
@@ -244,51 +298,43 @@ class FileDatabaseSelectActivity : StylishActivity(),
overridePendingTransition(0, 0)
}
private fun updateExternalStorageWarning() {
// To show errors
var warning = -1
val state = Environment.getExternalStorageState()
if (state == Environment.MEDIA_MOUNTED_READ_ONLY) {
warning = R.string.read_only_warning
} else if (state != Environment.MEDIA_MOUNTED) {
warning = R.string.warning_unmounted
}
val labelWarningView = findViewById<TextView>(R.id.label_warning)
if (warning != -1) {
labelWarningView.setText(warning)
labelWarningView.visibility = View.VISIBLE
} else {
labelWarningView.visibility = View.INVISIBLE
}
}
override fun onResume() {
val database = Database.getInstance()
if (database.loaded) {
launchGroupActivity(database.isReadOnly)
}
super.onResume()
updateExternalStorageWarning()
// Construct adapter with listeners
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
databaseFileHistoryList?.let {
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(it)
updateFileListVisibility()
mAdapterDatabaseHistory?.notifyDataSetChanged()
// Show open and create button or special mode
if (mSelectionMode) {
// Disable create button if in selection mode or request for autofill
createDatabaseButtonView?.visibility = View.GONE
} else {
if (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
}
}
// Register progress task
mProgressDialogThread?.registerProgressTask()
val database = Database.getInstance()
if (database.loaded) {
launchGroupActivity(database.isReadOnly)
} else {
// Construct adapter with listeners
if (PreferencesUtil.showRecentFiles(this)) {
databaseFilesViewModel.loadListOfDatabases()
} else {
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
mAdapterDatabaseHistory?.notifyDataSetChanged()
}
// Register progress task
mProgressDatabaseTaskProvider?.registerProgressTask()
}
}
override fun onPause() {
// Unregister progress task
mProgressDialogThread?.unregisterProgressTask()
mProgressDatabaseTaskProvider?.unregisterProgressTask()
super.onPause()
}
@@ -301,13 +347,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
}
private fun updateFileListVisibility() {
if (mAdapterDatabaseHistory?.itemCount == 0)
fileListContainer?.visibility = View.INVISIBLE
else
fileListContainer?.visibility = View.VISIBLE
}
override fun onAssignKeyDialogPositiveClick(
masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?) {
@@ -316,7 +355,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
mDatabaseFileUri?.let { databaseUri ->
// Create the new database
mProgressDialogThread?.startDatabaseCreate(
mProgressDatabaseTaskProvider?.startDatabaseCreate(
databaseUri,
masterPasswordChecked,
masterPassword,
@@ -344,8 +383,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
if (uri != null) {
launchPasswordActivityWithPath(uri)
}
@@ -357,29 +395,36 @@ class FileDatabaseSelectActivity : StylishActivity(),
if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment.getInstance(true)
.show(supportFragmentManager, "passwordDialog")
} else {
val error = getString(R.string.error_create_database)
coordinatorLayout?.let {
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
}
Log.e(TAG, error)
}
// else {
// TODO Show error
// }
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
MenuUtil.defaultMenuInflater(menuInflater, menu)
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
if (!mSelectionMode) {
MenuUtil.defaultMenuInflater(menuInflater, menu)
}
Handler(Looper.getMainLooper()).post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
return true
}
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
// If no recent files
val createDatabaseEducationPerformed = createButtonView != null && createButtonView!!.visibility == View.VISIBLE
val createDatabaseEducationPerformed =
createDatabaseButtonView != null && createDatabaseButtonView!!.visibility == View.VISIBLE
&& mAdapterDatabaseHistory != null
&& mAdapterDatabaseHistory!!.itemCount > 0
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
createButtonView!!,
createDatabaseButtonView!!,
{
createNewFile()
},
@@ -394,7 +439,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
openDatabaseButtonView!!,
{tapTargetView ->
tapTargetView?.let {
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
}
},
{}
@@ -403,6 +448,10 @@ class FileDatabaseSelectActivity : StylishActivity(),
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
}
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
}
@@ -414,18 +463,30 @@ class FileDatabaseSelectActivity : StylishActivity(),
/*
* -------------------------
* No Standard Launch, pass by PasswordActivity
* Launch only to standard search, else pass by PasswordActivity
* -------------------------
*/
fun launch(context: Context,
searchInfo: SearchInfo? = null) {
val intent = Intent(context, FileDatabaseSelectActivity::class.java)
searchInfo?.let {
intent.putExtra(KEY_SEARCH_INFO, it)
}
context.startActivity(intent)
}
/*
* -------------------------
* Keyboard Launch
* -------------------------
*/
fun launchForKeyboardSelection(activity: Activity) {
EntrySelectionHelper.startActivityForEntrySelection(activity, Intent(activity, FileDatabaseSelectActivity::class.java))
fun launchForEntrySelectionResult(activity: Activity,
searchInfo: SearchInfo? = null) {
EntrySelectionHelper.startActivityForEntrySelectionResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
searchInfo)
}
/*
@@ -435,10 +496,13 @@ class FileDatabaseSelectActivity : StylishActivity(),
*/
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure) {
fun launchForAutofillResult(activity: Activity,
assistStructure: AssistStructure,
searchInfo: SearchInfo? = null) {
AutofillHelper.startActivityForAutofillResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
assistStructure)
assistStructure,
searchInfo)
}
}
}

View File

@@ -28,10 +28,12 @@ import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
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.TextView
import androidx.annotation.RequiresApi
@@ -47,6 +49,7 @@ import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
@@ -61,7 +64,8 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.model.getSearchString
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
@@ -73,9 +77,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.view.AddNodeButtonView
import com.kunzisoft.keepass.view.ToolbarAction
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.*
class GroupActivity : LockingActivity(),
GroupEditDialogFragment.EditGroupListener,
@@ -87,13 +89,14 @@ class GroupActivity : LockingActivity(),
SortDialogFragment.SortSelectionListener {
// Views
private var rootContainerView: ViewGroup? = null
private var coordinatorLayout: CoordinatorLayout? = null
private var lockView: View? = null
private var toolbar: Toolbar? = null
private var searchTitleView: View? = null
private var toolbarAction: ToolbarAction? = null
private var iconView: ImageView? = null
private var numberChildrenView: TextView? = null
private var modeTitleView: TextView? = null
private var addNodeButtonView: AddNodeButtonView? = null
private var groupNameView: TextView? = null
@@ -101,6 +104,12 @@ class GroupActivity : LockingActivity(),
private var mListNodesFragment: ListNodesFragment? = null
private var mCurrentGroupIsASearch: Boolean = false
private var mRequestStartupSearch = true
private var actionNodeMode: ActionMode? = null
// To manage history in selection mode
private var mSelectionModeCountBackStack = 0
// Nodes
private var mRootGroup: Group? = null
@@ -114,15 +123,13 @@ class GroupActivity : LockingActivity(),
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isFinishing) {
return
}
mDatabase = Database.getInstance()
// Construct main view
setContentView(layoutInflater.inflate(R.layout.activity_group, null))
// Initialize views
rootContainerView = findViewById(R.id.activity_group_container_view)
coordinatorLayout = findViewById(R.id.group_coordinator)
iconView = findViewById(R.id.group_icon)
numberChildrenView = findViewById(R.id.group_numbers)
@@ -131,16 +138,27 @@ class GroupActivity : LockingActivity(),
searchTitleView = findViewById(R.id.search_title)
groupNameView = findViewById(R.id.group_name)
toolbarAction = findViewById(R.id.toolbar_action)
modeTitleView = findViewById(R.id.mode_title_view)
lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener {
lockAndExit()
}
toolbar?.title = ""
setSupportActionBar(toolbar)
// Retrieve the textColor to tint the icon
val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
mIconColor = taTextColor.getColor(0, Color.WHITE)
taTextColor.recycle()
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(addNodeButtonView)
resetAppTimeoutWhenViewFocusedOrChanged(rootContainerView)
// Retrieve elements after an orientation change
if (savedInstanceState != null) {
if (savedInstanceState.containsKey(REQUEST_STARTUP_SEARCH_KEY))
mRequestStartupSearch = savedInstanceState.getBoolean(REQUEST_STARTUP_SEARCH_KEY)
if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY))
mOldGroupToUpdate = savedInstanceState.getParcelable(OLD_GROUP_TO_UPDATE_KEY)
}
@@ -160,14 +178,6 @@ class GroupActivity : LockingActivity(),
return
}
// Update last access time.
mCurrentGroup?.touch(modified = false, touchParents = false)
// Retrieve the textColor to tint the icon
val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
mIconColor = taTextColor.getColor(0, Color.WHITE)
taTextColor.recycle()
var fragmentTag = LIST_NODES_FRAGMENT_TAG
if (mCurrentGroupIsASearch)
fragmentTag = SEARCH_FRAGMENT_TAG
@@ -184,6 +194,14 @@ class GroupActivity : LockingActivity(),
fragmentTag)
.commit()
// Update last access time.
mCurrentGroup?.touch(modified = false, touchParents = false)
// To relaunch the activity with ACTION_SEARCH
if (manageSearchInfoIntent(intent)) {
startActivity(intent)
}
// Add listeners to the add buttons
addNodeButtonView?.setAddGroupClickListener(View.OnClickListener {
GroupEditDialogFragment.build()
@@ -201,7 +219,7 @@ class GroupActivity : LockingActivity(),
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, database)
// Init dialog thread
mProgressDialogThread?.onActionFinish = { actionTask, result ->
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
var oldNodes: List<Node> = ArrayList()
result.data?.getBundle(OLD_NODES_KEY)?.let { oldNodesBundle ->
@@ -212,6 +230,8 @@ class GroupActivity : LockingActivity(),
newNodes = getListNodesFromBundle(database, newNodesBundle)
}
refreshSearchGroup()
when (actionTask) {
ACTION_DATABASE_UPDATE_GROUP_TASK -> {
if (result.isSuccess) {
@@ -251,15 +271,7 @@ class GroupActivity : LockingActivity(),
}
}
if (!result.isSuccess) {
coordinatorLayout?.let { coordinatorLayout ->
result.exception?.errorId?.let { errorId ->
Snackbar.make(coordinatorLayout, errorId, Snackbar.LENGTH_LONG).asError().show()
} ?: result.message?.let { message ->
Snackbar.make(coordinatorLayout, message, Snackbar.LENGTH_LONG).asError().show()
}
}
}
coordinatorLayout?.showActionError(result)
finishNodeAction()
@@ -274,11 +286,14 @@ class GroupActivity : LockingActivity(),
super.onNewIntent(intent)
intent?.let { intentNotNull ->
// To transform KEY_SEARCH_INFO in ACTION_SEARCH
manageSearchInfoIntent(intentNotNull)
Log.d(TAG, "setNewIntent: $intentNotNull")
setIntent(intentNotNull)
mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intentNotNull.action) {
// only one instance of search in backstack
openSearchGroup(retrieveCurrentGroup(intentNotNull, null))
deletePreviousSearchGroup()
openGroup(retrieveCurrentGroup(intentNotNull, null), true)
true
} else {
false
@@ -286,20 +301,34 @@ class GroupActivity : LockingActivity(),
}
}
private fun openSearchGroup(group: Group?) {
// Delete the previous search fragment
val searchFragment = supportFragmentManager.findFragmentByTag(SEARCH_FRAGMENT_TAG)
if (searchFragment != null) {
if (supportFragmentManager
.popBackStackImmediate(SEARCH_FRAGMENT_TAG,
FragmentManager.POP_BACK_STACK_INCLUSIVE))
supportFragmentManager.beginTransaction().remove(searchFragment).commit()
/**
* Transform the KEY_SEARCH_INFO in ACTION_SEARCH, return true if KEY_SEARCH_INFO was present
*/
private fun manageSearchInfoIntent(intent: Intent): Boolean {
// To relaunch the activity as ACTION_SEARCH
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
val autoSearch = intent.getBooleanExtra(AUTO_SEARCH_KEY, false)
if (searchInfo != null && autoSearch) {
intent.action = Intent.ACTION_SEARCH
intent.putExtra(SearchManager.QUERY, searchInfo.getSearchString(this))
return true
}
openGroup(group, true)
return false
}
private fun openChildGroup(group: Group) {
openGroup(group, false)
private fun deletePreviousSearchGroup() {
// Delete the previous search fragment
try {
val searchFragment = supportFragmentManager.findFragmentByTag(SEARCH_FRAGMENT_TAG)
if (searchFragment != null) {
if (supportFragmentManager
.popBackStackImmediate(SEARCH_FRAGMENT_TAG,
FragmentManager.POP_BACK_STACK_INCLUSIVE))
supportFragmentManager.beginTransaction().remove(searchFragment).commit()
}
} catch (exception: Exception) {
Log.e(TAG, "unable to remove previous search fragment", exception)
}
}
private fun openGroup(group: Group?, isASearch: Boolean) {
@@ -326,6 +355,12 @@ class GroupActivity : LockingActivity(),
fragmentTransaction.addToBackStack(fragmentTag)
fragmentTransaction.commit()
if (mSelectionMode)
mSelectionModeCountBackStack++
// Update last access time.
group?.touch(modified = false, touchParents = false)
mListNodesFragment = newListNodeFragment
mCurrentGroup = group
assignGroupViewElements()
@@ -339,9 +374,16 @@ class GroupActivity : LockingActivity(),
mOldGroupToUpdate?.let {
outState.putParcelable(OLD_GROUP_TO_UPDATE_KEY, it)
}
outState.putBoolean(REQUEST_STARTUP_SEARCH_KEY, mRequestStartupSearch)
super.onSaveInstanceState(outState)
}
private fun refreshSearchGroup() {
deletePreviousSearchGroup()
if (mCurrentGroupIsASearch)
openGroup(retrieveCurrentGroup(intent, null), true)
}
private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): Group? {
// Force read only if the database is like that
@@ -349,7 +391,9 @@ class GroupActivity : LockingActivity(),
// If it's a search
if (Intent.ACTION_SEARCH == intent.action) {
return mDatabase?.search(intent.getStringExtra(SearchManager.QUERY).trim { it <= ' ' })
val searchString = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
return mDatabase?.createVirtualGroupFromSearch(searchString,
PreferencesUtil.omitBackup(this))
}
// else a real group
else {
@@ -421,13 +465,6 @@ class GroupActivity : LockingActivity(),
// Assign number of children
refreshNumberOfChildren()
// Show selection mode message if needed
if (mSelectionMode) {
modeTitleView?.visibility = View.VISIBLE
} else {
modeTitleView?.visibility = View.GONE
}
// Show button if allowed
addNodeButtonView?.apply {
@@ -441,15 +478,29 @@ class GroupActivity : LockingActivity(),
enableAddGroup(addGroupEnabled)
enableAddEntry(addEntryEnabled)
if (isEnable)
if (actionNodeMode == null)
showButton()
}
}
override fun onCancelSpecialMode() {
// To remove the navigation history and
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
val fragmentManager = supportFragmentManager
if (mSelectionModeCountBackStack > 0) {
for (selectionMode in 0 .. mSelectionModeCountBackStack) {
fragmentManager.popBackStack()
}
}
// Reinit the counter for navigation history
mSelectionModeCountBackStack = 0
backToTheAppCaller()
}
private fun refreshNumberOfChildren() {
numberChildrenView?.apply {
if (PreferencesUtil.showNumberEntries(context)) {
text = mCurrentGroup?.getChildEntries(*Group.ChildFilter.getDefaults(context))?.size?.toString() ?: ""
text = mCurrentGroup?.getNumberOfChildEntries(Group.ChildFilter.getDefaults(context))?.toString() ?: ""
visibility = View.VISIBLE
} else {
visibility = View.GONE
@@ -458,13 +509,15 @@ class GroupActivity : LockingActivity(),
}
override fun onScrolled(dy: Int) {
addNodeButtonView?.hideButtonOnScrollListener(dy)
if (actionNodeMode == null)
addNodeButtonView?.hideOrShowButtonOnScrollListener(dy)
}
override fun onNodeClick(node: Node) {
when (node.type) {
Type.GROUP -> try {
openChildGroup(node as Group)
// Open child group
openGroup(node as Group, false)
} catch (e: ClassCastException) {
Log.e(TAG, "Node can't be cast in Group")
}
@@ -476,20 +529,19 @@ class GroupActivity : LockingActivity(),
EntryActivity.launch(this@GroupActivity, entryVersioned, mReadOnly)
},
{
rebuildListNodes()
// Populate Magikeyboard with entry
mDatabase?.let { database ->
MagikIME.addEntryAndLaunchNotificationIfAllowed(this@GroupActivity,
entryVersioned.getEntryInfo(database))
populateKeyboardAndMoveAppToBackground(this@GroupActivity,
entryVersioned.getEntryInfo(database),
intent)
}
// Consume the selection mode
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
moveTaskToBack(true)
},
{
// Build response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
mDatabase?.let { database ->
AutofillHelper.buildResponseWhenEntrySelected(this@GroupActivity,
AutofillHelper.buildResponse(this@GroupActivity,
entryVersioned.getEntryInfo(database))
}
}
@@ -501,22 +553,34 @@ class GroupActivity : LockingActivity(),
}
}
private var actionNodeMode: ActionMode? = null
private fun finishNodeAction() {
actionNodeMode?.finish()
actionNodeMode = null
}
override fun onNodeSelected(nodes: List<Node>): Boolean {
if (nodes.isNotEmpty()) {
if (actionNodeMode == null || toolbarAction?.getSupportActionModeCallback() == null) {
mListNodesFragment?.actionNodesCallback(nodes, this)?.let {
mListNodesFragment?.actionNodesCallback(nodes, this, object: ActionMode.Callback {
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return true
}
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return true
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
return false
}
override fun onDestroyActionMode(mode: ActionMode?) {
actionNodeMode = null
addNodeButtonView?.showButton()
}
})?.let {
actionNodeMode = toolbarAction?.startSupportActionMode(it)
}
} else {
actionNodeMode?.invalidate()
}
addNodeButtonView?.hideButton()
} else {
finishNodeAction()
}
@@ -567,7 +631,7 @@ class GroupActivity : LockingActivity(),
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
// Copy
mCurrentGroup?.let { newParent ->
mProgressDialogThread?.startDatabaseCopyNodes(
mProgressDatabaseTaskProvider?.startDatabaseCopyNodes(
nodes,
newParent,
!mReadOnly && mAutoSaveEnable
@@ -577,7 +641,7 @@ class GroupActivity : LockingActivity(),
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
// Move
mCurrentGroup?.let { newParent ->
mProgressDialogThread?.startDatabaseMoveNodes(
mProgressDatabaseTaskProvider?.startDatabaseMoveNodes(
nodes,
newParent,
!mReadOnly && mAutoSaveEnable
@@ -610,7 +674,8 @@ class GroupActivity : LockingActivity(),
if (database != null
&& database.isRecycleBinEnabled
&& database.recycleBin != mCurrentGroup) {
mProgressDialogThread?.startDatabaseDeleteNodes(
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
nodes,
!mReadOnly && mAutoSaveEnable
)
@@ -625,7 +690,7 @@ class GroupActivity : LockingActivity(),
}
override fun permanentlyDeleteNodes(nodes: List<Node>) {
mProgressDialogThread?.startDatabaseDeleteNodes(
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
nodes,
!mReadOnly && mAutoSaveEnable
)
@@ -633,10 +698,19 @@ class GroupActivity : LockingActivity(),
override fun onResume() {
super.onResume()
// Show the lock button
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
View.VISIBLE
} else {
View.GONE
}
// Refresh the elements
assignGroupViewElements()
// Refresh suggestions to change preferences
mSearchSuggestionAdapter?.reInit(this)
// Padding if lock button visible
toolbarAction?.updateLockPaddingLeft()
}
override fun onPause() {
@@ -654,8 +728,7 @@ class GroupActivity : LockingActivity(),
menu.findItem(R.id.menu_save_database)?.isVisible = false
}
if (!mSelectionMode) {
inflater.inflate(R.menu.default_menu, menu)
MenuUtil.contributionMenuInflater(inflater, menu)
MenuUtil.defaultMenuInflater(inflater, menu)
}
// Menu for recycle bin
@@ -666,13 +739,15 @@ class GroupActivity : LockingActivity(),
}
// Get the SearchView and set the searchable configuration
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager?
menu.findItem(R.id.menu_search)?.let {
val searchView = it.actionView as SearchView?
searchView?.apply {
setSearchableInfo(searchManager.getSearchableInfo(
ComponentName(this@GroupActivity, GroupActivity::class.java)))
(searchManager?.getSearchableInfo(
ComponentName(this@GroupActivity, GroupActivity::class.java)))?.let { searchableInfo ->
setSearchableInfo(searchableInfo)
}
setIconifiedByDefault(false) // Do not iconify the widget; expand it by default
suggestionsAdapter = mSearchSuggestionAdapter
setOnSuggestionListener(object : SearchView.OnSuggestionListener {
@@ -690,12 +765,19 @@ class GroupActivity : LockingActivity(),
}
})
}
// Expand the search view if defined in settings
if (mRequestStartupSearch
&& PreferencesUtil.automaticallyFocusSearch(this@GroupActivity)) {
// To request search only one time
mRequestStartupSearch = false
it.expandActionView()
}
}
super.onCreateOptionsMenu(menu)
// Launch education screen
Handler().post { performedNextEducation(GroupActivityEducation(this), menu) }
Handler(Looper.getMainLooper()).post { performedNextEducation(GroupActivityEducation(this), menu) }
return true
}
@@ -745,12 +827,11 @@ class GroupActivity : LockingActivity(),
if (!sortMenuEducationPerformed) {
// lockMenuEducationPerformed
toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_lock) != null
&& groupActivityEducation.checkAndPerformedLockMenuEducation(
toolbar!!.findViewById(R.id.menu_lock),
val lockButtonView = findViewById<View>(R.id.lock_button_icon)
lockButtonView != null
&& groupActivityEducation.checkAndPerformedLockMenuEducation(lockButtonView,
{
onOptionsItemSelected(menu.findItem(R.id.menu_lock))
lockAndExit()
},
{
performedNextEducation(groupActivityEducation, menu)
@@ -769,12 +850,8 @@ class GroupActivity : LockingActivity(),
R.id.menu_search ->
//onSearchRequested();
return true
R.id.menu_lock -> {
lockAndExit()
return true
}
R.id.menu_save_database -> {
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
return true
}
R.id.menu_empty_recycle_bin -> {
@@ -808,7 +885,7 @@ class GroupActivity : LockingActivity(),
// Not really needed here because added in runnable but safe
newGroup.parent = currentGroup
mProgressDialogThread?.startDatabaseCreateGroup(
mProgressDatabaseTaskProvider?.startDatabaseCreateGroup(
newGroup,
currentGroup,
!mReadOnly && mAutoSaveEnable
@@ -826,11 +903,11 @@ class GroupActivity : LockingActivity(),
removeChildren()
title = name
this.icon = icon // TODO custom icon
this.icon = icon // TODO custom icon #96
}
}
// If group updated save it in the database
mProgressDialogThread?.startDatabaseUpdateGroup(
mProgressDatabaseTaskProvider?.startDatabaseUpdateGroup(
oldGroupToUpdate,
updateGroup,
!mReadOnly && mAutoSaveEnable
@@ -855,24 +932,21 @@ class GroupActivity : LockingActivity(),
.iconPicked(bundle)
}
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
mListNodesFragment?.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom)
override fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters) {
mListNodesFragment?.onSortSelected(sortNodeEnum, sortNodeParameters)
}
override fun startActivity(intent: Intent) {
// Get the intent, verify the action and get the query
if (Intent.ACTION_SEARCH == intent.action) {
// manually launch the real search activity
val searchIntent = Intent(applicationContext, GroupActivity::class.java).apply {
// Add bundle of current intent
putExtras(this@GroupActivity.intent)
// manually launch the same search activity
val searchIntent = getIntent().apply {
// add query to the Intent Extras
action = Intent.ACTION_SEARCH
putExtra(SearchManager.QUERY, intent.getStringExtra(SearchManager.QUERY))
}
super.startActivity(searchIntent)
setIntent(searchIntent)
onNewIntent(searchIntent)
} else {
super.startActivity(intent)
}
@@ -900,16 +974,33 @@ class GroupActivity : LockingActivity(),
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
// Not directly get the entry from intent data but from database
mListNodesFragment?.rebuildList()
// Directly used the onActivityResult in fragment
mListNodesFragment?.onActivityResult(requestCode, resultCode, data)
}
private fun removeSearchInIntent(intent: Intent) {
private fun rebuildListNodes() {
mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment?
// to refresh fragment
mListNodesFragment?.rebuildList()
mCurrentGroup = mListNodesFragment?.mainGroup
// Remove search in intent
deletePreviousSearchGroup()
mCurrentGroupIsASearch = false
if (Intent.ACTION_SEARCH == intent.action) {
mCurrentGroupIsASearch = false
intent.action = Intent.ACTION_DEFAULT
intent.removeExtra(SearchManager.QUERY)
}
assignGroupViewElements()
}
private fun backToTheAppCaller() {
if (mAutofillSelection) {
// To get the app caller, only for autofill
super.onBackPressed()
} else {
// To move the app in background
moveTaskToBack(true)
}
}
override fun onBackPressed() {
@@ -917,24 +1008,23 @@ class GroupActivity : LockingActivity(),
finishNodeAction()
} else {
// Normal way when we are not in root
if (mRootGroup != null && mRootGroup != mCurrentGroup)
if (mRootGroup != null && mRootGroup != mCurrentGroup) {
super.onBackPressed()
// Else lock if needed
rebuildListNodes()
}
// Else in root, lock if needed
else {
intent.removeExtra(AUTO_SEARCH_KEY)
intent.removeExtra(KEY_SEARCH_INFO)
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
lockAndExit()
super.onBackPressed()
} else {
moveTaskToBack(true)
// To restore standard mode
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
backToTheAppCaller()
}
}
mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment
// to refresh fragment
mListNodesFragment?.rebuildList()
mCurrentGroup = mListNodesFragment?.mainGroup
removeSearchInIntent(intent)
assignGroupViewElements()
}
}
@@ -942,24 +1032,40 @@ class GroupActivity : LockingActivity(),
private val TAG = GroupActivity::class.java.name
private const val REQUEST_STARTUP_SEARCH_KEY = "REQUEST_STARTUP_SEARCH_KEY"
private const val GROUP_ID_KEY = "GROUP_ID_KEY"
private const val LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG"
private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"
private const val AUTO_SEARCH_KEY = "AUTO_SEARCH_KEY"
private fun buildAndLaunchIntent(context: Context, group: Group?, readOnly: Boolean,
intentBuildLauncher: (Intent) -> Unit) {
val checkTime = if (context is Activity)
TimeoutHelper.checkTimeAndLockIfTimeout(context)
else
TimeoutHelper.checkTime(context)
if (checkTime) {
val intent = Intent(context, GroupActivity::class.java)
if (group != null) {
intent.putExtra(GROUP_ID_KEY, group.nodeId)
}
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
intentBuildLauncher.invoke(intent)
private fun buildIntent(context: Context,
group: Group?,
readOnly: Boolean,
intentBuildLauncher: (Intent) -> Unit) {
val intent = Intent(context, GroupActivity::class.java)
if (group != null) {
intent.putExtra(GROUP_ID_KEY, group.nodeId)
}
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
intentBuildLauncher.invoke(intent)
}
private fun checkTimeAndBuildIntent(activity: Activity,
group: Group?,
readOnly: Boolean,
intentBuildLauncher: (Intent) -> Unit) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
buildIntent(activity, group, readOnly, intentBuildLauncher)
}
}
private fun checkTimeAndBuildIntent(context: Context,
group: Group?,
readOnly: Boolean,
intentBuildLauncher: (Intent) -> Unit) {
if (TimeoutHelper.checkTime(context)) {
buildIntent(context, group, readOnly, intentBuildLauncher)
}
}
@@ -968,11 +1074,15 @@ class GroupActivity : LockingActivity(),
* Standard Launch
* -------------------------
*/
@JvmOverloads
fun launch(context: Context, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
TimeoutHelper.recordTime(context)
buildAndLaunchIntent(context, null, readOnly) { intent ->
fun launch(context: Context,
autoSearch: Boolean = false,
searchInfo: SearchInfo? = null,
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
searchInfo?.let {
intent.putExtra(KEY_SEARCH_INFO, it)
}
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
context.startActivity(intent)
}
}
@@ -982,12 +1092,13 @@ class GroupActivity : LockingActivity(),
* Keyboard Launch
* -------------------------
*/
// TODO implement pre search to directly open the direct group
fun launchForKeyboardSelection(context: Context, readOnly: Boolean) {
TimeoutHelper.recordTime(context)
buildAndLaunchIntent(context, null, readOnly) { intent ->
EntrySelectionHelper.startActivityForEntrySelection(context, intent)
fun launchForEntrySelectionResult(context: Context,
autoSearch: Boolean = false,
searchInfo: SearchInfo? = null,
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
EntrySelectionHelper.startActivityForEntrySelectionResult(context, intent, searchInfo)
}
}
@@ -996,13 +1107,15 @@ class GroupActivity : LockingActivity(),
* Autofill Launch
* -------------------------
*/
// TODO implement pre search to directly open the direct group
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure, readOnly: Boolean) {
TimeoutHelper.recordTime(activity)
buildAndLaunchIntent(activity, null, readOnly) { intent ->
AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure)
fun launchForAutofillResult(activity: Activity,
assistStructure: AssistStructure,
autoSearch: Boolean = false,
searchInfo: SearchInfo? = null,
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) {
checkTimeAndBuildIntent(activity, null, readOnly) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure, searchInfo)
}
}
}

View File

@@ -21,9 +21,7 @@ package com.kunzisoft.keepass.activities
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.util.Log
@@ -54,7 +52,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
private var nodeClickListener: NodeClickListener? = null
private var onScrollListener: OnScrollListener? = null
private var listView: RecyclerView? = null
private var mNodesRecyclerView: RecyclerView? = null
var mainGroup: Group? = null
private set
private var mAdapter: NodeAdapter? = null
@@ -69,8 +67,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
private var notFoundView: View? = null
private var isASearchResult: Boolean = false
// Preferences for sorting
private var prefs: SharedPreferences? = null
private var readOnly: Boolean = false
get() {
@@ -101,6 +97,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}
}
override fun onDetach() {
nodeClickListener = null
onScrollListener = null
super.onDetach()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -155,7 +157,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
})
}
}
prefs = PreferenceManager.getDefaultSharedPreferences(context)
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -169,11 +170,17 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
// To apply theme
val rootView = inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_list_nodes, container, false)
listView = rootView.findViewById(R.id.nodes_list)
mNodesRecyclerView = rootView.findViewById(R.id.nodes_list)
notFoundView = rootView.findViewById(R.id.not_found_container)
mNodesRecyclerView?.apply {
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
layoutManager = LinearLayoutManager(context)
adapter = mAdapter
}
onScrollListener?.let { onScrollListener ->
listView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
mNodesRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
onScrollListener.onScrolled(dy)
@@ -181,8 +188,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
})
}
rebuildList()
return rootView
}
@@ -194,14 +199,14 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}
// Refresh data
mAdapter?.notifyDataSetChanged()
rebuildList()
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
// To show the " no search entry found "
listView?.visibility = View.GONE
mNodesRecyclerView?.visibility = View.GONE
notFoundView?.visibility = View.VISIBLE
} else {
listView?.visibility = View.VISIBLE
mNodesRecyclerView?.visibility = View.VISIBLE
notFoundView?.visibility = View.GONE
}
}
@@ -209,30 +214,27 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
fun rebuildList() {
// Add elements to the list
mainGroup?.let { mainGroup ->
mAdapter?.rebuildList(mainGroup)
}
listView?.apply {
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
layoutManager = LinearLayoutManager(context)
adapter = mAdapter
mAdapter?.apply {
rebuildList(mainGroup)
// To visually change the elements
if (PreferencesUtil.APPEARANCE_CHANGED) {
notifyDataSetChanged()
PreferencesUtil.APPEARANCE_CHANGED = false
}
}
}
}
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
// Toggle setting
prefs?.edit()?.apply {
putString(getString(R.string.sort_node_key), sortNodeEnum.name)
putBoolean(getString(R.string.sort_ascending_key), ascending)
putBoolean(getString(R.string.sort_group_before_key), groupsBefore)
putBoolean(getString(R.string.sort_recycle_bin_bottom_key), recycleBinBottom)
apply()
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
// Save setting
context?.let {
PreferencesUtil.saveNodeSort(it, sortNodeEnum, sortNodeParameters)
}
// Tell the adapter to refresh it's list
mAdapter?.notifyChangeSort(sortNodeEnum, ascending, groupsBefore)
mainGroup?.let { mainGroup ->
mAdapter?.rebuildList(mainGroup)
}
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
rebuildList()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@@ -270,14 +272,15 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}
fun actionNodesCallback(nodes: List<Node>,
menuListener: NodesActionMenuListener?) : ActionMode.Callback {
menuListener: NodesActionMenuListener?,
actionModeCallback: ActionMode.Callback) : ActionMode.Callback {
return object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
nodeActionSelectionMode = false
nodeActionPasteMode = PasteMode.UNDEFINED
return true
return actionModeCallback.onCreateActionMode(mode, menu)
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
@@ -307,7 +310,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
if (readOnly
|| isASearchResult
|| nodes.any { it.type == Type.GROUP }) {
// TODO COPY For Group
// TODO Copy For Group
menu?.removeItem(R.id.menu_copy)
menu?.removeItem(R.id.menu_move)
}
@@ -322,7 +325,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
// Add the number of items selected in title
mode?.title = nodes.size.toString()
return true
return actionModeCallback.onPrepareActionMode(mode, menu)
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
@@ -352,7 +355,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
nodeActionSelectionMode = false
returnValue
}
else -> false
else -> actionModeCallback.onActionItemClicked(mode, item)
}
}
@@ -362,6 +365,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
mAdapter?.unselectActionNodes()
nodeActionPasteMode = PasteMode.UNDEFINED
nodeActionSelectionMode = false
actionModeCallback.onDestroyActionMode(mode)
}
}
}
@@ -373,15 +377,11 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { newNode ->
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { changedNode ->
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
mAdapter?.addNode(newNode)
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
//mAdapter.updateLastNodeRegister(newNode);
mainGroup?.let { mainGroup ->
mAdapter?.rebuildList(mainGroup)
}
}
addNode(changedNode)
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE)
mAdapter?.notifyDataSetChanged()
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
@@ -17,25 +17,31 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.magikeyboard
package com.kunzisoft.keepass.activities
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.database.search.SearchHelper
class KeyboardLauncherActivity : AppCompatActivity() {
/**
* Activity to select entry in database and populate it in Magikeyboard
*/
class MagikeyboardLauncherActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
if (Database.getInstance().loaded && TimeoutHelper.checkTime(this))
GroupActivity.launchForKeyboardSelection(this, PreferencesUtil.enableReadOnlyDatabase(this))
else {
// Pass extra to get entry
FileDatabaseSelectActivity.launchForKeyboardSelection(this)
}
SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(),
null,
{},
{
GroupActivity.launchForEntrySelectionResult(this)
},
{
// Pass extra to get entry
FileDatabaseSelectActivity.launchForEntrySelectionResult(this)
}
)
finish()
super.onCreate(savedInstanceState)
}

View File

@@ -21,97 +21,104 @@ package com.kunzisoft.keepass.activities
import android.app.Activity
import android.app.assist.AssistStructure
import android.app.backup.BackupManager
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.preference.PreferenceManager
import android.os.Looper
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.*
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.widget.*
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar
import androidx.biometric.BiometricManager
import androidx.core.app.ActivityCompat
import androidx.lifecycle.Observer
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.OpenFileHelper
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.FileDatabaseInfo
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.closeDatabase
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import kotlinx.android.synthetic.main.activity_password.*
import java.io.FileNotFoundException
class PasswordActivity : StylishActivity() {
open class PasswordActivity : SpecialModeActivity() {
// Views
private var toolbar: Toolbar? = null
private var containerView: View? = null
private var filenameView: TextView? = null
private var passwordView: EditText? = null
private var keyFileView: EditText? = null
private var keyFileSelectionView: KeyFileSelectionView? = null
private var confirmButtonView: Button? = null
private var checkboxPasswordView: CompoundButton? = null
private var checkboxKeyFileView: CompoundButton? = null
private var checkboxDefaultDatabaseView: CompoundButton? = null
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
private var infoContainerView: ViewGroup? = null
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
private var mDatabaseFileUri: Uri? = null
private var mDatabaseKeyFileUri: Uri? = null
private var prefs: SharedPreferences? = null
private var mRememberKeyFile: Boolean = false
private var mOpenFileHelper: OpenFileHelper? = null
private var mSelectFileHelper: SelectFileHelper? = null
private var mPermissionAsked = false
private var readOnly: Boolean = false
private var mForceReadOnly: Boolean = false
set(value) {
infoContainerView?.visibility = if (value) {
readOnly = true
View.VISIBLE
} else {
View.GONE
}
field = value
}
private var mProgressDialogThread: ProgressDialogThread? = null
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
private var mAllowAutoOpenBiometricPrompt: Boolean = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
prefs = PreferenceManager.getDefaultSharedPreferences(this)
mRememberKeyFile = PreferencesUtil.rememberKeyFiles(this)
setContentView(R.layout.activity_password)
toolbar = findViewById(R.id.toolbar)
@@ -120,22 +127,26 @@ class PasswordActivity : StylishActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
containerView = findViewById(R.id.container)
confirmButtonView = findViewById(R.id.pass_ok)
confirmButtonView = findViewById(R.id.activity_password_open_button)
filenameView = findViewById(R.id.filename)
passwordView = findViewById(R.id.password)
keyFileView = findViewById(R.id.pass_keyfile)
keyFileSelectionView = findViewById(R.id.keyfile_selection)
checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
advancedUnlockInfoView = findViewById(R.id.biometric_info)
infoContainerView = findViewById(R.id.activity_password_info_container)
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
val browseView = findViewById<View>(R.id.open_database_button)
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
browseView.setOnClickListener(mOpenFileHelper!!.openFileOnClickViewListener)
mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
keyFileSelectionView?.apply {
mSelectFileHelper?.selectFileOnClickViewListener?.let {
setOnClickListener(it)
setOnLongClickListener(it)
}
}
passwordView?.setOnEditorActionListener(onEditorActionListener)
passwordView?.addTextChangedListener(object : TextWatcher {
@@ -148,23 +159,44 @@ class PasswordActivity : StylishActivity() {
checkboxPasswordView?.isChecked = true
}
})
keyFileView?.setOnEditorActionListener(onEditorActionListener)
keyFileView?.addTextChangedListener(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) {
if (editable.toString().isNotEmpty() && checkboxKeyFileView?.isChecked != true)
checkboxKeyFileView?.isChecked = true
}
})
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
enableOrNotTheConfirmationButton()
}
mProgressDialogThread = ProgressDialogThread(this).apply {
// If is a view intent
getUriFromIntent(intent)
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
}
if (savedInstanceState?.containsKey(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) == true) {
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
}
// Observe database file change
databaseFileViewModel.databaseFileLoaded.observe(this, Observer { databaseFile ->
// Force read only if the file does not exists
mForceReadOnly = databaseFile?.let {
!it.databaseFileExists
} ?: true
invalidateOptionsMenu()
// Post init uri with KeyFile only if needed
val keyFileUri =
if (mRememberKeyFile
&& (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
databaseFile?.keyFileUri
} else {
mDatabaseKeyFileUri
}
// Define title
filenameView?.text = databaseFile?.databaseAlias ?: ""
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
})
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
onActionFinish = { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> {
@@ -176,10 +208,9 @@ class PasswordActivity : StylishActivity() {
}
}
// Remove the password in view in all cases
removePassword()
if (result.isSuccess) {
mDatabaseKeyFileUri = null
clearCredentialsViews(true)
launchGroupActivity()
} else {
var resultError = ""
@@ -202,7 +233,7 @@ class PasswordActivity : StylishActivity() {
result.data?.let { resultData ->
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
keyFileUri = resultData.getParcelable(KEY_FILE_KEY)
keyFileUri = resultData.getParcelable(KEY_FILE_URI_KEY)
readOnly = resultData.getBoolean(READ_ONLY_KEY)
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
}
@@ -224,7 +255,7 @@ class PasswordActivity : StylishActivity() {
if (resultMessage != null && resultMessage.isNotEmpty()) {
resultError = "$resultError $resultMessage"
}
Log.e(TAG, resultError, resultException)
Log.e(TAG, resultError)
Snackbar.make(activity_password_coordinator_layout,
resultError,
Snackbar.LENGTH_LONG).asError().show()
@@ -235,19 +266,91 @@ class PasswordActivity : StylishActivity() {
}
}
private fun getUriFromIntent(intent: Intent?) {
// If is a view intent
val action = intent?.action
if (action != null
&& action == VIEW_INTENT) {
mDatabaseFileUri = intent.data
mDatabaseKeyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
} else {
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
}
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
getUriFromIntent(intent)
}
private fun launchGroupActivity() {
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
EntrySelectionHelper.doEntrySelectionAction(intent,
{
GroupActivity.launch(this@PasswordActivity, readOnly)
GroupActivity.launch(this@PasswordActivity,
true,
searchInfo,
readOnly)
// Finish activity if no search info
if (searchInfo != null) {
finish()
}
},
{
GroupActivity.launchForKeyboardSelection(this@PasswordActivity, readOnly)
SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(),
searchInfo,
{ items ->
// Response is build
if (items.size == 1) {
populateKeyboardAndMoveAppToBackground(this@PasswordActivity,
items[0],
intent)
} else {
// Select the one we want
GroupActivity.launchForEntrySelectionResult(this,
true,
searchInfo)
}
},
{
// Here no search info found, disable auto search
GroupActivity.launchForEntrySelectionResult(this@PasswordActivity,
false,
searchInfo,
readOnly)
},
{
// Simply close if database not opened, normally not happened
}
)
// Do not keep history
finish()
},
{ assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
GroupActivity.launchForAutofillResult(this@PasswordActivity, assistStructure, readOnly)
SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(),
searchInfo,
{ items ->
// Response is build
AutofillHelper.buildResponse(this, items)
finish()
},
{
// Here no search info found, disable auto search
GroupActivity.launchForAutofillResult(this@PasswordActivity,
assistStructure,
false,
searchInfo,
readOnly)
},
{
// Simply close if database not opened, normally not happened
finish()
}
)
}
})
}
@@ -263,108 +366,54 @@ class PasswordActivity : StylishActivity() {
}
override fun onResume() {
if (Database.getInstance().loaded)
launchGroupActivity()
// If the database isn't accessible make sure to clear the password field, if it
// was saved in the instance state
if (Database.getInstance().loaded) {
setEmptyViews()
}
// For check shutdown
super.onResume()
mProgressDialogThread?.registerProgressTask()
initUriFromIntent()
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
super.onSaveInstanceState(outState)
}
private fun initUriFromIntent() {
val databaseUri: Uri?
val keyFileUri: Uri?
// If is a view intent
val action = intent.action
if (action != null
&& action == VIEW_INTENT) {
databaseUri = intent.data
keyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
if (Database.getInstance().loaded) {
launchGroupActivity()
} else {
databaseUri = intent.getParcelableExtra(KEY_FILENAME)
keyFileUri = intent.getParcelableExtra(KEY_KEYFILE)
}
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
// Post init uri with KeyFile if needed
if (mRememberKeyFile && (keyFileUri == null || keyFileUri.toString().isEmpty())) {
// Retrieve KeyFile in a thread
databaseUri?.let { databaseUriNotNull ->
FileDatabaseHistoryAction.getInstance(applicationContext)
.getKeyFileUriByDatabaseUri(databaseUriNotNull) {
onPostInitUri(databaseUri, it)
}
// If the database isn't accessible make sure to clear the password field, if it
// was saved in the instance state
if (Database.getInstance().loaded) {
clearCredentialsViews()
}
} else {
onPostInitUri(databaseUri, keyFileUri)
mProgressDatabaseTaskProvider?.registerProgressTask()
// Back to previous keyboard is setting activated
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this)) {
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
}
// Don't allow auto open prompt if lock become when UI visible
mAllowAutoOpenBiometricPrompt = if (LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
false
else
mAllowAutoOpenBiometricPrompt
mDatabaseFileUri?.let { databaseFileUri ->
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
}
checkPermission()
}
}
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
mDatabaseFileUri = databaseFileUri
mDatabaseKeyFileUri = keyFileUri
// Define title
databaseFileUri?.let {
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
filenameView?.text = title
}
}
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
// Define Key File text
val keyUriString = keyFileUri?.toString() ?: ""
if (keyUriString.isNotEmpty() && mRememberKeyFile) { // Bug KeepassDX #18
populateKeyFileTextView(keyUriString)
if (mRememberKeyFile) {
populateKeyFileTextView(keyFileUri)
}
// Define listeners for default database checkbox and validate button
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
var newDefaultFileName: Uri? = null
if (isChecked) {
newDefaultFileName = databaseFileUri ?: newDefaultFileName
}
prefs?.edit()?.apply {
newDefaultFileName?.let {
putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString())
} ?: kotlin.run {
remove(KEY_DEFAULT_DATABASE_PATH)
}
apply()
}
val backupManager = BackupManager(this@PasswordActivity)
backupManager.dataChanged()
}
// Define listener for validate button
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
// Retrieve settings for default database
val defaultFilename = prefs?.getString(KEY_DEFAULT_DATABASE_PATH, "")
if (databaseFileUri != null
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
&& databaseFileUri == UriUtil.parse(defaultFilename)) {
checkboxDefaultDatabaseView?.isChecked = true
}
// If Activity is launch with a password and want to open directly
val intent = intent
val password = intent.getStringExtra(KEY_PASSWORD)
// Consume the intent extra password
intent.removeExtra(KEY_PASSWORD)
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
if (password != null) {
populatePasswordTextView(password)
@@ -376,7 +425,6 @@ class PasswordActivity : StylishActivity() {
var biometricInitialize = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
if (advancedUnlockedManager == null && databaseFileUri != null) {
advancedUnlockedManager = AdvancedUnlockedManager(this,
databaseFileUri,
@@ -403,10 +451,12 @@ class PasswordActivity : StylishActivity() {
}
})
}
advancedUnlockedManager?.isBiometricPromptAutoOpenEnable = mAllowAutoOpenBiometricPrompt
advancedUnlockedManager?.checkBiometricAvailability()
biometricInitialize = true
} else {
advancedUnlockedManager?.destroy()
advancedUnlockInfoView?.visibility = View.GONE
}
}
if (!biometricInitialize) {
@@ -430,10 +480,9 @@ class PasswordActivity : StylishActivity() {
}
}
private fun setEmptyViews() {
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
populatePasswordTextView(null)
// Bug KeepassDX #18
if (!mRememberKeyFile) {
if (clearKeyFile) {
populateKeyFileTextView(null)
}
}
@@ -450,34 +499,46 @@ class PasswordActivity : StylishActivity() {
}
}
private fun populateKeyFileTextView(text: String?) {
if (text == null || text.isEmpty()) {
keyFileView?.setText("")
private fun populateKeyFileTextView(uri: Uri?) {
if (uri == null || uri.toString().isEmpty()) {
keyFileSelectionView?.uri = null
if (checkboxKeyFileView?.isChecked == true)
checkboxKeyFileView?.isChecked = false
} else {
keyFileView?.setText(text)
keyFileSelectionView?.uri = uri
if (checkboxKeyFileView?.isChecked != true)
checkboxKeyFileView?.isChecked = true
}
}
override fun onPause() {
mProgressDialogThread?.unregisterProgressTask()
mProgressDatabaseTaskProvider?.unregisterProgressTask()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
advancedUnlockedManager?.destroy()
advancedUnlockedManager = null
}
// Reinit locking activity UI variable
LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
mAllowAutoOpenBiometricPrompt = true
super.onPause()
}
override fun onDestroy() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
advancedUnlockedManager?.destroy()
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
mDatabaseKeyFileUri?.let {
outState.putString(KEY_KEYFILE, it.toString())
}
super.onDestroy()
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
super.onSaveInstanceState(outState)
}
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
val password: String? = passwordView?.text?.toString()
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
val keyFile: Uri? = keyFileSelectionView?.uri
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity)
}
@@ -490,7 +551,7 @@ class PasswordActivity : StylishActivity() {
}
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
val keyFile: Uri? = keyFileSelectionView?.uri
verifyKeyFileCheckbox(keyFile)
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
}
@@ -499,18 +560,13 @@ class PasswordActivity : StylishActivity() {
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
}
private fun removePassword() {
passwordView?.setText("")
checkboxPasswordView?.isChecked = false
}
private fun loadDatabase(databaseFileUri: Uri?,
password: String?,
keyFileUri: Uri?,
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
removePassword()
clearCredentialsViews()
}
databaseFileUri?.let { databaseUri ->
@@ -531,7 +587,7 @@ class PasswordActivity : StylishActivity() {
readOnly: Boolean,
cipherDatabaseEntity: CipherDatabaseEntity?,
fixDuplicateUUID: Boolean) {
mProgressDialogThread?.startDatabaseLoad(
mProgressDatabaseTaskProvider?.startDatabaseLoad(
databaseUri,
password,
keyFile,
@@ -551,9 +607,15 @@ class PasswordActivity : StylishActivity() {
val inflater = menuInflater
// Read menu
inflater.inflate(R.menu.open_file, menu)
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
if (mSelectionMode || mForceReadOnly) {
menu.removeItem(R.id.menu_open_file_read_mode_key)
} else {
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
}
MenuUtil.defaultMenuInflater(inflater, menu)
if (!mSelectionMode) {
MenuUtil.defaultMenuInflater(inflater, menu)
}
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// biometric menu
@@ -562,15 +624,13 @@ class PasswordActivity : StylishActivity() {
super.onCreateOptionsMenu(menu)
launchEducation(menu) {
launchCheckPermission()
}
launchEducation(menu)
return true
}
// Check permission
private fun launchCheckPermission() {
private fun checkPermission() {
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
val permissions = arrayOf(writePermission)
if (Build.VERSION.SDK_INT >= 23
@@ -600,26 +660,25 @@ class PasswordActivity : StylishActivity() {
// To fix multiple view education
private var performedEductionInProgress = false
private fun launchEducation(menu: Menu, onEducationFinished: ()-> Unit) {
private fun launchEducation(menu: Menu) {
if (!performedEductionInProgress) {
performedEductionInProgress = true
// Show education views
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu, onEducationFinished) }
Handler(Looper.getMainLooper()).post { performedNextEducation(PasswordActivityEducation(this), menu) }
}
}
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
menu: Menu,
onEducationFinished: ()-> Unit) {
menu: Menu) {
val educationToolbar = toolbar
val unlockEducationPerformed = educationToolbar != null
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
educationToolbar,
{
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
performedNextEducation(passwordActivityEducation, menu)
},
{
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
performedNextEducation(passwordActivityEducation, menu)
})
if (!unlockEducationPerformed) {
val readOnlyEducationPerformed =
@@ -628,30 +687,25 @@ class PasswordActivity : StylishActivity() {
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
{
onOptionsItemSelected(menu.findItem(R.id.menu_open_file_read_mode_key))
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
performedNextEducation(passwordActivityEducation, menu)
},
{
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
performedNextEducation(passwordActivityEducation, menu)
})
if (!readOnlyEducationPerformed) {
val biometricCanAuthenticate = BiometricManager.from(this).canAuthenticate()
val biometricEducationPerformed =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& PreferencesUtil.isBiometricUnlockEnable(applicationContext)
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.unlockIconImageView != null
&& passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!,
{
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
performedNextEducation(passwordActivityEducation, menu)
},
{
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
performedNextEducation(passwordActivityEducation, menu)
})
if (!biometricEducationPerformed) {
onEducationFinished.invoke()
}
}
}
}
@@ -689,26 +743,32 @@ class PasswordActivity : StylishActivity() {
data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mAllowAutoOpenBiometricPrompt = false
// To get entry in result
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
var keyFileResult = false
mOpenFileHelper?.let {
mSelectFileHelper?.let {
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
if (uri != null) {
populateKeyFileTextView(uri.toString())
mDatabaseKeyFileUri = uri
populateKeyFileTextView(uri)
}
}
}
if (!keyFileResult) {
// this block if not a key file response
when (resultCode) {
LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> {
setEmptyViews()
Database.getInstance().closeAndClear(applicationContext.filesDir)
LockingActivity.RESULT_EXIT_LOCK -> {
clearCredentialsViews()
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
}
Activity.RESULT_CANCELED -> {
clearCredentialsViews()
}
}
}
@@ -718,19 +778,17 @@ class PasswordActivity : StylishActivity() {
private val TAG = PasswordActivity::class.java.name
const val KEY_DEFAULT_DATABASE_PATH = "KEY_DEFAULT_DATABASE_PATH"
private const val KEY_FILENAME = "fileName"
private const val KEY_KEYFILE = "keyFile"
private const val VIEW_INTENT = "android.intent.action.VIEW"
private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
private const val ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT = "ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT"
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
intentBuildLauncher: (Intent) -> Unit) {
val intent = Intent(activity, PasswordActivity::class.java)
@@ -750,8 +808,12 @@ class PasswordActivity : StylishActivity() {
fun launch(
activity: Activity,
databaseFile: Uri,
keyFile: Uri?) {
keyFile: Uri?,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
searchInfo?.let {
intent.putExtra(KEY_SEARCH_INFO, it)
}
activity.startActivity(intent)
}
}
@@ -766,9 +828,13 @@ class PasswordActivity : StylishActivity() {
fun launchForKeyboardResult(
activity: Activity,
databaseFile: Uri,
keyFile: Uri?) {
keyFile: Uri?,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
EntrySelectionHelper.startActivityForEntrySelection(activity, intent)
EntrySelectionHelper.startActivityForEntrySelectionResult(
activity,
intent,
searchInfo)
}
}
@@ -784,16 +850,18 @@ class PasswordActivity : StylishActivity() {
activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
assistStructure: AssistStructure?) {
assistStructure: AssistStructure?,
searchInfo: SearchInfo?) {
if (assistStructure != null) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
AutofillHelper.startActivityForAutofillResult(
activity,
intent,
assistStructure)
assistStructure,
searchInfo)
}
} else {
launch(activity, databaseFile, keyFile)
launch(activity, databaseFile, keyFile, searchInfo)
}
}
}

View File

@@ -25,17 +25,20 @@ import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import com.google.android.material.textfield.TextInputLayout
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.text.Editable
import android.text.SpannableStringBuilder
import android.text.TextWatcher
import android.view.View
import android.widget.CompoundButton
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView
class AssignMasterKeyDialogFragment : DialogFragment() {
@@ -51,13 +54,16 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
private var passwordRepeatTextInputLayout: TextInputLayout? = null
private var passwordRepeatView: TextView? = null
private var keyFileTextInputLayout: TextInputLayout? = null
private var keyFileCheckBox: CompoundButton? = null
private var keyFileView: TextView? = null
private var keyFileSelectionView: KeyFileSelectionView? = null
private var mListener: AssignPasswordDialogListener? = null
private var mOpenFileHelper: OpenFileHelper? = null
private var mSelectFileHelper: SelectFileHelper? = null
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
private var mNoKeyConfirmationDialog: AlertDialog? = null
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
private val passwordTextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
@@ -69,16 +75,6 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
}
}
private val keyFileTextWatcher = 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) {
keyFileCheckBox?.isChecked = true
}
}
interface AssignPasswordDialogListener {
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?)
@@ -96,6 +92,17 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
}
}
override fun onDetach() {
mListener = null
mEmptyPasswordConfirmationDialog?.dismiss()
mEmptyPasswordConfirmationDialog = null
mNoKeyConfirmationDialog?.dismiss()
mNoKeyConfirmationDialog = null
mEmptyKeyFileConfirmationDialog?.dismiss()
mEmptyKeyFileConfirmationDialog = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
@@ -110,24 +117,29 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
rootView = inflater.inflate(R.layout.fragment_set_password, null)
builder.setView(rootView)
.setTitle(R.string.assign_master_key)
// Add action buttons
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
val credentialsInfo: ImageView? = rootView?.findViewById(R.id.credentials_information)
credentialsInfo?.setOnClickListener {
UriUtil.gotoUrl(activity, 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)
keyFileTextInputLayout = rootView?.findViewById(R.id.keyfile_input_layout)
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
keyFileView = rootView?.findViewById(R.id.pass_keyfile)
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
mOpenFileHelper = OpenFileHelper(this)
rootView?.findViewById<View>(R.id.open_database_button)?.setOnClickListener { view ->
mOpenFileHelper?.openFileOnClickViewListener?.onClick(view) }
mSelectFileHelper = SelectFileHelper(this)
keyFileSelectionView?.apply {
setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
}
val dialog = builder.create()
@@ -139,7 +151,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
mMasterPassword = ""
mKeyFile = null
var error = verifyPassword() || verifyFile()
var error = verifyPassword() || verifyKeyFile()
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
error = true
if (allowNoMasterKey)
@@ -176,14 +188,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
// To check checkboxes if a text is present
passwordView?.addTextChangedListener(passwordTextWatcher)
keyFileView?.addTextChangedListener(keyFileTextWatcher)
}
override fun onPause() {
super.onPause()
passwordView?.removeTextChangedListener(passwordTextWatcher)
keyFileView?.removeTextChangedListener(keyFileTextWatcher)
}
private fun verifyPassword(): Boolean {
@@ -211,16 +221,16 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
return error
}
private fun verifyFile(): Boolean {
private fun verifyKeyFile(): Boolean {
var error = false
if (keyFileCheckBox != null
&& keyFileCheckBox!!.isChecked) {
UriUtil.parse(keyFileView?.text?.toString())?.let { uri ->
keyFileSelectionView?.uri?.let { uri ->
mKeyFile = uri
} ?: run {
error = true
keyFileTextInputLayout?.error = getString(R.string.error_nokeyfile)
keyFileSelectionView?.error = getString(R.string.error_nokeyfile)
}
}
return error
@@ -231,7 +241,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
val builder = AlertDialog.Builder(it)
builder.setMessage(R.string.warning_empty_password)
.setPositiveButton(android.R.string.ok) { _, _ ->
if (!verifyFile()) {
if (!verifyKeyFile()) {
mListener?.onAssignKeyDialogPositiveClick(
passwordCheckBox!!.isChecked, mMasterPassword,
keyFileCheckBox!!.isChecked, mKeyFile)
@@ -239,7 +249,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
}
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
builder.create().show()
mEmptyPasswordConfirmationDialog = builder.create()
mEmptyPasswordConfirmationDialog?.show()
}
}
@@ -254,19 +265,44 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
this@AssignMasterKeyDialogFragment.dismiss()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
builder.create().show()
mNoKeyConfirmationDialog = builder.create()
mNoKeyConfirmationDialog?.show()
}
}
private fun showEmptyKeyFileConfirmationDialog() {
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
}
mEmptyKeyFileConfirmationDialog = builder.create()
mEmptyKeyFileConfirmationDialog?.show()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
uri?.let { pathUri ->
keyFileCheckBox?.isChecked = true
keyFileView?.text = pathUri.toString()
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
keyFileSelectionView?.error = null
keyFileCheckBox?.isChecked = true
keyFileSelectionView?.uri = pathUri
if (lengthFile <= 0L) {
showEmptyKeyFileConfirmationDialog()
}
}
}
}
}

View File

@@ -0,0 +1,66 @@
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
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

@@ -46,6 +46,11 @@ class DeleteNodesDialogFragment : DialogFragment() {
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
arguments?.apply {
@@ -64,10 +69,10 @@ class DeleteNodesDialogFragment : DialogFragment() {
val builder = AlertDialog.Builder(activity)
builder.setMessage(getString(R.string.warning_permanently_delete_nodes))
builder.setPositiveButton(android.R.string.yes) { _, _ ->
builder.setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.permanentlyDeleteNodes(mNodesToDelete)
}
builder.setNegativeButton(android.R.string.no) { _, _ -> dismiss() }
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
// Create the AlertDialog object and return it
return builder.create()
}

View File

@@ -0,0 +1,182 @@
/*
* 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.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
import android.view.inputmethod.EditorInfo
import android.widget.Button
import android.widget.CompoundButton
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.Field
class EntryCustomFieldDialogFragment: DialogFragment() {
private var oldField: Field? = null
private var entryCustomFieldListener: EntryCustomFieldListener? = null
private var customFieldLabelContainer: TextInputLayout? = null
private var customFieldLabel: TextView? = null
private var customFieldDeleteButton: ImageView? = null
private var customFieldProtectionButton: CompoundButton? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
entryCustomFieldListener = context as EntryCustomFieldListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context.toString()
+ " must implement " + EntryCustomFieldListener::class.java.name)
}
}
override fun onDetach() {
entryCustomFieldListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_entry_new_field, null)
customFieldLabelContainer = root?.findViewById(R.id.entry_custom_field_label_container)
customFieldLabel = root?.findViewById(R.id.entry_custom_field_label)
customFieldDeleteButton = root?.findViewById(R.id.entry_custom_field_delete)
customFieldProtectionButton = root?.findViewById(R.id.entry_custom_field_protection)
oldField = arguments?.getParcelable(KEY_FIELD)
oldField?.let { oldCustomField ->
customFieldLabel?.text = oldCustomField.name
customFieldProtectionButton?.isChecked = oldCustomField.protectedValue.isProtected
customFieldDeleteButton?.visibility = View.VISIBLE
customFieldDeleteButton?.setOnClickListener {
entryCustomFieldListener?.onDeleteCustomFieldApproved(oldCustomField)
(dialog as AlertDialog?)?.dismiss()
}
} ?: run {
customFieldDeleteButton?.visibility = View.GONE
}
val builder = AlertDialog.Builder(activity)
builder.setView(root)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel) { _, _ -> }
val dialogCreated = builder.create()
customFieldLabel?.requestFocus()
customFieldLabel?.imeOptions = EditorInfo.IME_ACTION_DONE
customFieldLabel?.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
approveIfValid()
}
false
}
dialogCreated.window?.setSoftInputMode(SOFT_INPUT_STATE_VISIBLE)
return dialogCreated
}
return super.onCreateDialog(savedInstanceState)
}
override fun onResume() {
super.onResume()
// To prevent auto dismiss
val d = dialog as AlertDialog?
if (d != null) {
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
positiveButton.setOnClickListener {
approveIfValid()
}
}
}
private fun approveIfValid() {
if (isValid()) {
oldField?.let {
// New property with old value
entryCustomFieldListener?.onEditCustomFieldApproved(it,
Field(customFieldLabel?.text?.toString() ?: "",
ProtectedString(customFieldProtectionButton?.isChecked == true,
it.protectedValue.stringValue))
)
} ?: run {
entryCustomFieldListener?.onNewCustomFieldApproved(
Field(customFieldLabel?.text?.toString() ?: "",
ProtectedString(customFieldProtectionButton?.isChecked == true))
)
}
(dialog as AlertDialog?)?.dismiss()
}
}
private fun isValid(): Boolean {
return if (customFieldLabel?.text?.toString()?.isNotEmpty() != true) {
setError(R.string.error_string_key)
false
} else {
setError(null)
true
}
}
fun setError(@StringRes errorId: Int?) {
customFieldLabelContainer?.error = if (errorId == null) null else {
requireContext().getString(errorId)
}
}
interface EntryCustomFieldListener {
fun onNewCustomFieldApproved(newField: Field)
fun onEditCustomFieldApproved(oldField: Field, newField: Field)
fun onDeleteCustomFieldApproved(oldField: Field)
}
companion object {
private const val KEY_FIELD = "KEY_FIELD"
fun getInstance(): EntryCustomFieldDialogFragment {
return EntryCustomFieldDialogFragment()
}
fun getInstance(field: Field): EntryCustomFieldDialogFragment {
return EntryCustomFieldDialogFragment().apply {
arguments = Bundle().apply {
putParcelable(KEY_FIELD, field)
}
}
}
}
}

View File

@@ -28,7 +28,7 @@ import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.UriUtil
class BrowserDialogFragment : DialogFragment() {
class FileManagerDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
@@ -41,15 +41,8 @@ class BrowserDialogFragment : DialogFragment() {
val textDescription = root.findViewById<TextView>(R.id.file_manager_install_description)
textDescription.text = getString(R.string.file_manager_install_description)
val market = root.findViewById<Button>(R.id.file_manager_install_play_store)
market.setOnClickListener {
UriUtil.gotoUrl(context!!, R.string.file_manager_play_store)
dismiss()
}
val web = root.findViewById<Button>(R.id.file_manager_install_f_droid)
web.setOnClickListener {
UriUtil.gotoUrl(context!!, R.string.file_manager_f_droid)
root.findViewById<Button>(R.id.file_manager_button).setOnClickListener {
UriUtil.gotoUrl(requireContext(), R.string.file_manager_explanation_url)
dismiss()
}

View File

@@ -0,0 +1,97 @@
/*
* 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.net.Uri
import android.os.Bundle
import android.text.SpannableStringBuilder
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
/**
* Custom Dialog to confirm big file to upload
*/
class FileTooBigDialogFragment : DialogFragment() {
private var mActionChooseListener: ActionChooseListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
// Verify that the host activity implements the callback interface
try {
mActionChooseListener = context as ActionChooseListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + ActionChooseListener::class.java.name)
}
}
override fun onDetach() {
mActionChooseListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity)
builder.setMessage(SpannableStringBuilder().apply {
append(getString(R.string.warning_file_too_big))
append("\n\n")
append(getString(R.string.warning_sure_add_file))
})
builder.setPositiveButton(android.R.string.ok) { _, _ ->
mActionChooseListener?.onValidateUploadFileTooBig(
arguments?.getParcelable(KEY_FILE_URI),
arguments?.getString(KEY_FILE_NAME))
}
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
dismiss()
}
// Create the AlertDialog object and return it
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
interface ActionChooseListener {
fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?)
}
companion object {
const val MAX_WARNING_BINARY_FILE = 5242880
private const val KEY_FILE_URI = "KEY_FILE_URI"
private const val KEY_FILE_NAME = "KEY_FILE_NAME"
fun build(attachmentToUploadUri: Uri,
fileName: String): FileTooBigDialogFragment {
val fragment = FileTooBigDialogFragment()
fragment.arguments = Bundle().apply {
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
putString(KEY_FILE_NAME, fileName)
}
return fragment
}
}
}

View File

@@ -64,6 +64,11 @@ class GeneratePasswordDialogFragment : DialogFragment() {
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
@@ -167,7 +172,6 @@ class GeneratePasswordDialogFragment : DialogFragment() {
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,
@@ -178,6 +182,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
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) {
@@ -193,7 +198,6 @@ class GeneratePasswordDialogFragment : DialogFragment() {
}
companion object {
const val KEY_PASSWORD_ID = "KEY_PASSWORD_ID"
}
}

View File

@@ -73,7 +73,11 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
throw ClassCastException(context.toString()
+ " must implement " + GroupEditDialogFragment::class.java.name)
}
}
override fun onDetach() {
editGroupListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@@ -130,9 +134,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
}
iconButtonView?.setOnClickListener { _ ->
fragmentManager?.let {
IconPickerDialogFragment().show(it, "IconPickerDialogFragment")
}
IconPickerDialogFragment().show(parentFragmentManager, "IconPickerDialogFragment")
}
return builder.create()

View File

@@ -24,17 +24,18 @@ import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.core.widget.ImageViewCompat
import androidx.appcompat.app.AlertDialog
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.GridView
import android.widget.ImageView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.ImageViewCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.icons.IconPack
import com.kunzisoft.keepass.icons.IconPackChooser
@@ -56,11 +57,16 @@ class IconPickerDialogFragment : DialogFragment() {
}
}
override fun onDetach() {
iconPickerListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
iconPack = IconPackChooser.getSelectedIconPack(context!!)
iconPack = IconPackChooser.getSelectedIconPack(requireContext())
// Inflate and set the layout for the dialog
// Pass null as the parent view because its going in the dialog layout
@@ -112,7 +118,7 @@ class IconPickerDialogFragment : DialogFragment() {
// Retrieve the textColor to tint the icon
val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK)))
ta?.recycle()
ta.recycle()
}
}
@@ -132,7 +138,7 @@ class IconPickerDialogFragment : DialogFragment() {
return bundle.getParcelable(KEY_ICON_STANDARD)
}
fun launch(activity: StylishActivity) {
fun launch(activity: FragmentActivity) {
// Create an instance of the dialog fragment and show it
val dialog = IconPickerDialogFragment()
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")

View File

@@ -21,20 +21,51 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.AlertDialog
import android.app.Dialog
import android.content.DialogInterface
import android.content.Context
import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
class PasswordEncodingDialogFragment : DialogFragment() {
var positiveButtonClickListener: DialogInterface.OnClickListener? = null
private var mListener: Listener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as Listener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + Listener::class.java.name)
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
val masterPasswordChecked: Boolean = savedInstanceState?.getBoolean(MASTER_PASSWORD_CHECKED_KEY) ?: false
val masterPassword: String? = savedInstanceState?.getString(MASTER_PASSWORD_KEY)
val keyFileChecked: Boolean = savedInstanceState?.getBoolean(KEY_FILE_CHECKED_KEY) ?: false
val keyFile: Uri? = savedInstanceState?.getParcelable(KEY_FILE_URI_KEY)
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener)
builder.setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.onPasswordEncodingValidateListener(
databaseUri,
masterPasswordChecked,
masterPassword,
keyFileChecked,
keyFile
)
}
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
return builder.create()
@@ -42,5 +73,36 @@ class PasswordEncodingDialogFragment : DialogFragment() {
return super.onCreateDialog(savedInstanceState)
}
interface Listener {
fun onPasswordEncodingValidateListener(databaseUri: Uri?,
masterPasswordChecked: Boolean,
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?)
}
companion object {
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
private const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
private const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
private const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
private const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY"
fun getInstance(databaseUri: Uri,
masterPasswordChecked: Boolean,
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?): SortDialogFragment {
val fragment = SortDialogFragment()
fragment.arguments = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri)
putBoolean(MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
putString(MASTER_PASSWORD_KEY, masterPassword)
putBoolean(KEY_FILE_CHECKED_KEY, keyFileChecked)
putParcelable(KEY_FILE_URI_KEY, keyFile)
}
return fragment
}
}
}

View File

@@ -45,13 +45,13 @@ 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(context!!, R.string.app_pro_url)
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
}
} 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(context!!, R.string.contribution_url)
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
}
}
builder.setMessage(stringBuilder)

View File

@@ -0,0 +1,96 @@
/*
* 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.net.Uri
import android.os.Bundle
import android.text.SpannableStringBuilder
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Attachment
/**
* Custom Dialog to confirm big file to upload
*/
class ReplaceFileDialogFragment : DialogFragment() {
private var mActionChooseListener: ActionChooseListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
// Verify that the host activity implements the callback interface
try {
mActionChooseListener = context as ActionChooseListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + ActionChooseListener::class.java.name)
}
}
override fun onDetach() {
mActionChooseListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity)
builder.setMessage(SpannableStringBuilder().apply {
append(getString(R.string.warning_replace_file))
append("\n\n")
append(getString(R.string.warning_sure_add_file))
})
builder.setPositiveButton(android.R.string.ok) { _, _ ->
mActionChooseListener?.onValidateReplaceFile(
arguments?.getParcelable(KEY_FILE_URI),
arguments?.getParcelable(KEY_ENTRY_ATTACHMENT))
}
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
dismiss()
}
// Create the AlertDialog object and return it
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
interface ActionChooseListener {
fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?)
}
companion object {
private const val KEY_FILE_URI = "KEY_FILE_URI"
private const val KEY_ENTRY_ATTACHMENT = "KEY_ENTRY_ATTACHMENT"
fun build(attachmentToUploadUri: Uri,
attachment: Attachment): ReplaceFileDialogFragment {
val fragment = ReplaceFileDialogFragment()
fragment.arguments = Bundle().apply {
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
putParcelable(KEY_ENTRY_ATTACHMENT, attachment)
}
return fragment
}
}
}

View File

@@ -28,6 +28,7 @@ import android.text.TextWatcher
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
@@ -89,9 +90,9 @@ class SetOTPDialogFragment : DialogFragment() {
}
private var mSecretWellFormed = false
private var mCounterWellFormed = true
private var mPeriodWellFormed = true
private var mDigitsWellFormed = true
private var mCounterWellFormed = false
private var mPeriodWellFormed = false
private var mDigitsWellFormed = false
override fun onAttach(context: Context) {
super.onAttach(context)
@@ -106,6 +107,11 @@ class SetOTPDialogFragment : DialogFragment() {
}
}
override fun onDetach() {
mCreateOTPElementListener = null
super.onDetach()
}
@SuppressLint("ClickableViewAccessibility")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@@ -152,6 +158,28 @@ class SetOTPDialogFragment : DialogFragment() {
otpCounterTextView?.setOnTouchListener(mOnTouchListener)
otpDigitsTextView?.setOnTouchListener(mOnTouchListener)
// To manage focus
otpPeriodTextView?.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_NEXT) {
otpDigitsTextView?.requestFocus()
true
} else
false
}
otpCounterTextView?.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_NEXT) {
otpDigitsTextView?.requestFocus()
true
} else
false
}
otpCounterTextView?.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_NEXT) {
root?.requestFocus(View.FOCUS_DOWN)
true
} else
false
}
// HOTP / TOTP Type selection
val otpTypeArray = OtpType.values()
@@ -365,14 +393,26 @@ class SetOTPDialogFragment : DialogFragment() {
private fun upgradeParameters() {
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
.indexOf(mOtpElement.algorithm))
val secret = mOtpElement.getBase32Secret()
otpSecretTextView?.apply {
setText(mOtpElement.getBase32Secret())
setText(secret)
// Cursor at end
setSelection(this.text.length)
}
otpCounterTextView?.setText(mOtpElement.counter.toString())
otpPeriodTextView?.setText(mOtpElement.period.toString())
otpDigitsTextView?.setText(mOtpElement.digits.toString())
mSecretWellFormed = OtpElement.isValidBase32(secret)
val counter = mOtpElement.counter
otpCounterTextView?.setText(counter.toString())
mCounterWellFormed = OtpElement.isValidCounter(counter)
val period = mOtpElement.period
otpPeriodTextView?.setText(period.toString())
mPeriodWellFormed = OtpElement.isValidPeriod(period)
val digits = mOtpElement.digits
otpDigitsTextView?.setText(digits.toString())
mDigitsWellFormed = OtpElement.isValidDigits(digits)
}
override fun onSaveInstanceState(outState: Bundle) {

View File

@@ -54,6 +54,11 @@ class SortDialogFragment : DialogFragment() {
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
@@ -82,7 +87,9 @@ class SortDialogFragment : DialogFragment() {
builder.setView(rootView)
// Add action buttons
.setPositiveButton(android.R.string.ok
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) }
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum,
SortNodeEnum.SortNodeParameters(mAscending, mGroupsBefore, mRecycleBinBottom))
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
@@ -150,10 +157,7 @@ class SortDialogFragment : DialogFragment() {
}
interface SortSelectionListener {
fun onSortSelected(sortNodeEnum: SortNodeEnum,
ascending: Boolean,
groupsBefore: Boolean,
recycleBinBottom: Boolean)
fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters)
}
companion object {

View File

@@ -0,0 +1,62 @@
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
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

@@ -24,7 +24,6 @@ import android.os.Build
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.text.Html
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.widget.TextView

View File

@@ -52,7 +52,7 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_buy_pro), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.download) { _, _ ->
UriUtil.gotoUrl(context!!, R.string.app_pro_url)
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
}
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
}
@@ -61,7 +61,7 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
.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(context!!, R.string.contribution_url)
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
}
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
}

View File

@@ -24,15 +24,22 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.model.SearchInfo
object EntrySelectionHelper {
private const val EXTRA_ENTRY_SELECTION_MODE = "com.kunzisoft.keepass.extra.ENTRY_SELECTION_MODE"
private const val DEFAULT_ENTRY_SELECTION_MODE = false
// Key to retrieve search in intent
const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
fun startActivityForEntrySelection(context: Context, intent: Intent) {
fun startActivityForEntrySelectionResult(context: Context,
intent: Intent,
searchInfo: SearchInfo?) {
addEntrySelectionModeExtraInIntent(intent)
// only to avoid visible flickering when redirecting
searchInfo?.let {
intent.putExtra(KEY_SEARCH_INFO, it)
}
context.startActivity(intent)
}

View File

@@ -28,19 +28,20 @@ import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.util.Log
import android.view.MenuItem
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
import com.kunzisoft.keepass.utils.UriUtil
class OpenFileHelper {
class SelectFileHelper {
private var activity: Activity? = null
private var fragment: Fragment? = null
val openFileOnClickViewListener: OpenFileOnClickViewListener
get() = OpenFileOnClickViewListener()
val selectFileOnClickViewListener: SelectFileOnClickViewListener
get() = SelectFileOnClickViewListener()
constructor(context: Activity) {
this.activity = context
@@ -52,14 +53,25 @@ class OpenFileHelper {
this.fragment = context
}
inner class OpenFileOnClickViewListener : View.OnClickListener {
inner class SelectFileOnClickViewListener :
View.OnClickListener,
View.OnLongClickListener,
MenuItem.OnMenuItemClickListener {
override fun onClick(v: View) {
private fun onAbstractClick(longClick: Boolean = false) {
try {
try {
openActivityWithActionOpenDocument()
} catch(e: Exception) {
openActivityWithActionGetContent()
if (longClick) {
try {
openActivityWithActionGetContent()
} catch (e: Exception) {
openActivityWithActionOpenDocument()
}
} else {
try {
openActivityWithActionOpenDocument()
} catch (e: Exception) {
openActivityWithActionGetContent()
}
}
} catch (e: Exception) {
Log.e(TAG, "Enable to start the file picker activity", e)
@@ -68,17 +80,31 @@ class OpenFileHelper {
showBrowserDialog()
}
}
override fun onClick(v: View) {
onAbstractClick()
}
override fun onLongClick(v: View?): Boolean {
onAbstractClick(true)
return true
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
onAbstractClick()
return true
}
}
@SuppressLint("InlinedApi")
private fun openActivityWithActionOpenDocument() {
val intentOpenDocument = Intent(APP_ACTION_OPEN_DOCUMENT).apply {
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
if (fragment != null)
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
@@ -91,10 +117,10 @@ class OpenFileHelper {
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
if (fragment != null)
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
@@ -147,11 +173,10 @@ class OpenFileHelper {
*/
private fun showBrowserDialog() {
try {
val browserDialogFragment = BrowserDialogFragment()
if (fragment != null && fragment!!.fragmentManager != null)
browserDialogFragment.show(fragment!!.fragmentManager!!, "browserDialog")
else
browserDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
val fileManagerDialogFragment = FileManagerDialogFragment()
fragment?.let {
fileManagerDialogFragment.show(it.parentFragmentManager, "browserDialog")
} ?: fileManagerDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
} catch (e: Exception) {
Log.e(TAG, "Can't open BrowserDialog", e)
}
@@ -210,12 +235,6 @@ class OpenFileHelper {
private const val TAG = "OpenFileHelper"
private var APP_ACTION_OPEN_DOCUMENT: String = try {
Intent::class.java.getField("ACTION_OPEN_DOCUMENT").get(null) as String
} catch (e: Exception) {
"android.intent.action.OPEN_DOCUMENT"
}
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
private const val GET_CONTENT = 25745

View File

@@ -19,39 +19,21 @@
*/
package com.kunzisoft.keepass.activities.lock
import android.app.Activity
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.annotation.SuppressLint
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.*
abstract class LockingActivity : StylishActivity() {
companion object {
private const val TAG = "LockingActivity"
const val RESULT_EXIT_LOCK = 1450
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
}
abstract class LockingActivity : SpecialModeActivity() {
protected var mTimeoutEnable: Boolean = true
@@ -59,14 +41,17 @@ abstract class LockingActivity : StylishActivity() {
private var mExitLock: Boolean = false
// Force readOnly if Entry Selection mode
protected var mReadOnly: Boolean = false
protected var mReadOnly: Boolean
get() {
return field || mSelectionMode
return mReadOnlyToSave || mSelectionMode
}
protected var mSelectionMode: Boolean = false
set(value) {
mReadOnlyToSave = value
}
private var mReadOnlyToSave: Boolean = false
protected var mAutoSaveEnable: Boolean = true
var mProgressDialogThread: ProgressDialogThread? = null
var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
private set
override fun onCreate(savedInstanceState: Bundle?) {
@@ -81,18 +66,21 @@ abstract class LockingActivity : StylishActivity() {
}
if (mTimeoutEnable) {
mLockReceiver = LockReceiver()
val intentFilter = IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(LOCK_ACTION)
mLockReceiver = LockReceiver {
closeDatabase()
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
// Add onActivityForResult response
setResult(RESULT_EXIT_LOCK)
closeOptionsMenu()
finish()
}
registerReceiver(mLockReceiver, intentFilter)
registerLockReceiver(mLockReceiver)
}
mExitLock = false
mReadOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState, intent)
mProgressDialogThread = ProgressDialogThread(this)
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -108,10 +96,10 @@ abstract class LockingActivity : StylishActivity() {
override fun onResume() {
super.onResume()
mProgressDialogThread?.registerProgressTask()
mProgressDatabaseTaskProvider?.registerProgressTask()
// To refresh when back to normal workflow from selection workflow
mSelectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
mReadOnlyToSave = ReadOnlyHelper.retrieveReadOnlyFromIntent(intent)
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
invalidateOptionsMenu()
@@ -131,16 +119,19 @@ abstract class LockingActivity : StylishActivity() {
if (!mExitLock)
TimeoutHelper.recordTime(this)
}
LOCKING_ACTIVITY_UI_VISIBLE = true
}
override fun onSaveInstanceState(outState: Bundle) {
ReadOnlyHelper.onSaveInstanceState(outState, mReadOnly)
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
super.onSaveInstanceState(outState)
}
override fun onPause() {
mProgressDialogThread?.unregisterProgressTask()
LOCKING_ACTIVITY_UI_VISIBLE = false
mProgressDatabaseTaskProvider?.unregisterProgressTask()
super.onPause()
@@ -151,40 +142,32 @@ abstract class LockingActivity : StylishActivity() {
}
override fun onDestroy() {
unregisterLockReceiver(mLockReceiver)
super.onDestroy()
if (mLockReceiver != null)
unregisterReceiver(mLockReceiver)
}
inner class LockReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// If allowed, lock and exit
if (!TimeoutHelper.temporarilyDisableTimeout) {
intent.action?.let {
when (it) {
Intent.ACTION_SCREEN_OFF -> if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(this@LockingActivity)) {
lockAndExit()
}
LOCK_ACTION -> lockAndExit()
}
}
}
}
}
protected fun lockAndExit() {
lock()
sendBroadcast(Intent(LOCK_ACTION))
}
/**
* To reset the app timeout when a view is focused or changed
*/
@SuppressLint("ClickableViewAccessibility")
protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View?) {
views.forEach {
it?.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// Log.d(TAG, "View touched, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
}
}
false
}
it?.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
Log.d(TAG, "View focused, reset app timeout")
// Log.d(TAG, "View focused, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
}
}
@@ -205,24 +188,17 @@ abstract class LockingActivity : StylishActivity() {
super.onBackPressed()
}
}
}
fun Activity.lock() {
// Stop the Magikeyboard service
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
MagikIME.removeEntry(this)
companion object {
// Stop the notification service
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
private const val TAG = "LockingActivity"
Log.i(Activity::class.java.name, "Shutdown " + localClassName +
" after inactivity or manual lock")
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {
cancelAll()
const val RESULT_EXIT_LOCK = 1450
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
}
// Clear data
Database.getInstance().closeAndClear(applicationContext.filesDir)
// Add onActivityForResult response
setResult(LockingActivity.RESULT_EXIT_LOCK)
finish()
}

View File

@@ -0,0 +1,90 @@
package com.kunzisoft.keepass.activities.selection
import android.os.Build
import android.view.View
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.SpecialModeView
/**
* Activity to manage special mode (ie: selection mode)
*/
abstract class SpecialModeActivity : StylishActivity() {
protected var mSelectionMode: Boolean = false
protected var mAutofillSelection: Boolean = false
private var specialModeView: SpecialModeView? = null
open fun onCancelSpecialMode() {
onBackPressed()
}
override fun onResume() {
super.onResume()
mSelectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mAutofillSelection = AutofillHelper.retrieveAssistStructure(intent) != null
}
val searchInfo: SearchInfo? = intent.getParcelableExtra(EntrySelectionHelper.KEY_SEARCH_INFO)
// To show the selection mode
specialModeView = findViewById(R.id.special_mode_view)
specialModeView?.apply {
// Populate title
val typeModeId = if (mAutofillSelection)
R.string.autofill
else
R.string.magic_keyboard_title
title = "${resources.getString(R.string.selection_mode)} (${getString(typeModeId)})"
// Populate subtitle
subtitle = searchInfo?.getName(resources)
// Show the toolbar or not
visible = mSelectionMode
// Add back listener
onCancelButtonClickListener = View.OnClickListener {
onCancelSpecialMode()
}
// Create menu
menu.clear()
if (mAutofillSelection) {
menuInflater.inflate(R.menu.autofill, menu)
setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.menu_block_autofill -> {
blockAutofill(searchInfo)
}
}
true
}
}
}
}
fun blockAutofill(searchInfo: SearchInfo?) {
val webDomain = searchInfo?.webDomain
val applicationId = searchInfo?.applicationId
if (webDomain != null) {
PreferencesUtil.addWebDomainToBlocklist(this,
webDomain)
} else if (applicationId != null) {
PreferencesUtil.addApplicationIdToBlocklist(this,
applicationId)
}
onCancelSpecialMode()
Toast.makeText(this.applicationContext,
R.string.autofill_block_restart,
Toast.LENGTH_LONG).show()
}
}

View File

@@ -41,8 +41,9 @@ abstract class StylishActivity : AppCompatActivity() {
*/
override fun startActivity(intent: Intent) {
try {
if (intent.component != null && intent.component!!.shortClassName == ".HtcLinkifyDispatcherActivity") {
intent.component = null
intent.component?.let {
if (it.shortClassName == ".HtcLinkifyDispatcherActivity")
intent.component = null
}
super.startActivity(intent)
} catch (e: ActivityNotFoundException) {

View File

@@ -44,14 +44,21 @@ abstract class StylishFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// To fix status bar color
if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val window = activity!!.window
val attrColorPrimaryDark = intArrayOf(android.R.attr.colorPrimaryDark)
val taColorPrimaryDark = contextThemed?.theme?.obtainStyledAttributes(attrColorPrimaryDark)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val window = requireActivity().window
val defaultColor = Color.BLACK
window.statusBarColor = taColorPrimaryDark?.getColor(0, defaultColor) ?: defaultColor
taColorPrimaryDark?.recycle()
try {
val taStatusBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor))
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
taStatusBarColor?.recycle()
} catch (e: Exception) {}
try {
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
taNavigationBarColor?.recycle()
} catch (e: Exception) {}
}
return super.onCreateView(inflater, container, savedInstanceState)

View File

@@ -0,0 +1,126 @@
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.view.collapse
abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val context: Context)
: RecyclerView.Adapter<T>() {
protected val inflater: LayoutInflater = LayoutInflater.from(context)
var itemsList: MutableList<Item> = ArrayList()
private set
var onDeleteButtonClickListener: ((item: Item)->Unit)? = null
private var mItemToRemove: Item? = null
var onListSizeChangedListener: ((previousSize: Int, newSize: Int)->Unit)? = null
override fun getItemCount(): Int {
return itemsList.size
}
open fun assignItems(items: List<Item>) {
val previousSize = itemsList.size
itemsList.apply {
clear()
addAll(items)
}
notifyDataSetChanged()
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
}
open fun isEmpty(): Boolean {
return itemsList.isEmpty()
}
open fun contains(item: Item): Boolean {
return itemsList.contains(item)
}
open fun indexOf(item: Item): Int {
return itemsList.indexOf(item)
}
open fun putItem(item: Item) {
val previousSize = itemsList.size
if (itemsList.contains(item)) {
val index = itemsList.indexOf(item)
itemsList.removeAt(index)
itemsList.add(index, item)
notifyItemChanged(index)
} else {
itemsList.add(item)
notifyItemInserted(itemsList.indexOf(item))
}
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
}
/**
* Only replace [oldItem] by [newItem] if [oldItem] exists
*/
open fun replaceItem(oldItem: Item, newItem: Item) {
if (itemsList.contains(oldItem)) {
val index = itemsList.indexOf(oldItem)
itemsList.removeAt(index)
itemsList.add(index, newItem)
notifyItemChanged(index)
}
}
/**
* Only remove [item] if doesn't exists
*/
open fun removeItem(item: Item) {
if (itemsList.contains(item)) {
mItemToRemove = item
notifyItemChanged(itemsList.indexOf(item))
}
}
protected fun performDeletion(holder: T, item: Item): Boolean {
val effectivelyDeletionPerformed = mItemToRemove == item
if (effectivelyDeletionPerformed) {
holder.itemView.collapse(true) {
deleteItem(item)
}
}
return effectivelyDeletionPerformed
}
protected fun onBindDeleteButton(holder: T, deleteButton: View, item: Item, position: Int) {
deleteButton.apply {
visibility = View.VISIBLE
if (performDeletion(holder, item)) {
setOnClickListener(null)
} else {
setOnClickListener {
onDeleteButtonClickListener?.invoke(item)
mItemToRemove = item
notifyItemChanged(position)
}
}
}
}
private fun deleteItem(item: Item) {
val previousSize = itemsList.size
val position = itemsList.indexOf(item)
if (position >= 0) {
itemsList.removeAt(position)
notifyItemRemoved(position)
mItemToRemove = null
for (i in 0 until itemsList.size) {
notifyItemChanged(i)
}
}
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
}
fun clear() {
itemsList.clear()
notifyDataSetChanged()
}
}

View File

@@ -1,96 +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.adapters
import android.content.Context
import android.text.format.Formatter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachment
class EntryAttachmentsAdapter(val context: Context) : RecyclerView.Adapter<EntryAttachmentsAdapter.EntryBinariesViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
var entryAttachmentsList: MutableList<EntryAttachment> = ArrayList()
var onItemClickListener: ((item: EntryAttachment, position: Int)->Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder {
return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false))
}
override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) {
val entryAttachment = entryAttachmentsList[position]
holder.binaryFileTitle.text = entryAttachment.name
holder.binaryFileSize.text = Formatter.formatFileSize(context,
entryAttachment.binaryAttachment.length())
holder.binaryFileCompression.apply {
if (entryAttachment.binaryAttachment.isCompressed == true) {
text = CompressionAlgorithm.GZip.getName(context.resources)
visibility = View.VISIBLE
} else {
text = ""
visibility = View.GONE
}
}
holder.binaryFileProgress.apply {
visibility = when (entryAttachment.downloadState) {
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
}
progress = entryAttachment.downloadProgression
}
holder.itemView.setOnClickListener {
onItemClickListener?.invoke(entryAttachment, position)
}
}
override fun getItemCount(): Int {
return entryAttachmentsList.size
}
fun updateProgress(entryAttachment: EntryAttachment) {
val indexEntryAttachment = entryAttachmentsList.indexOfLast { current -> current.name == entryAttachment.name }
if (indexEntryAttachment != -1) {
entryAttachmentsList[indexEntryAttachment] = entryAttachment
notifyItemChanged(indexEntryAttachment)
}
}
fun clear() {
entryAttachmentsList.clear()
}
inner class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
}
}

View File

@@ -0,0 +1,148 @@
/*
* 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.adapters
import android.content.Context
import android.content.res.TypedArray
import android.graphics.Color
import android.text.format.Formatter
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.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
class EntryAttachmentsItemsAdapter(context: Context)
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
private var mTitleColor: Int
init {
// Get the primary text color of the theme
val typedValue = TypedValue()
context.theme.resolveAttribute(android.R.attr.textColorPrimary, typedValue, true)
val typedArray: TypedArray = context.obtainStyledAttributes(typedValue.data, intArrayOf(
android.R.attr.textColor))
mTitleColor = typedArray.getColor(0, -1)
typedArray.recycle()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder {
return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false))
}
override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) {
val entryAttachmentState = itemsList[position]
holder.itemView.visibility = View.VISIBLE
holder.binaryFileBroken.apply {
setColorFilter(Color.RED)
visibility = if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
View.VISIBLE
} else {
View.GONE
}
}
holder.binaryFileTitle.text = entryAttachmentState.attachment.name
if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
holder.binaryFileTitle.setTextColor(Color.RED)
} else {
holder.binaryFileTitle.setTextColor(mTitleColor)
}
holder.binaryFileSize.text = Formatter.formatFileSize(context,
entryAttachmentState.attachment.binaryAttachment.length())
holder.binaryFileCompression.apply {
if (entryAttachmentState.attachment.binaryAttachment.isCompressed) {
text = CompressionAlgorithm.GZip.getName(context.resources)
visibility = View.VISIBLE
} else {
text = ""
visibility = View.GONE
}
}
when (entryAttachmentState.streamDirection) {
StreamDirection.UPLOAD -> {
holder.binaryFileProgressIcon.isActivated = true
when (entryAttachmentState.downloadState) {
AttachmentState.START,
AttachmentState.IN_PROGRESS -> {
holder.binaryFileProgressContainer.visibility = View.VISIBLE
holder.binaryFileProgress.apply {
visibility = View.VISIBLE
progress = entryAttachmentState.downloadProgression
}
holder.binaryFileDeleteButton.apply {
visibility = View.GONE
setOnClickListener(null)
}
}
AttachmentState.NULL,
AttachmentState.ERROR,
AttachmentState.COMPLETE -> {
holder.binaryFileProgressContainer.visibility = View.GONE
holder.binaryFileProgress.visibility = View.GONE
holder.binaryFileDeleteButton.apply {
visibility = View.VISIBLE
onBindDeleteButton(holder, this, entryAttachmentState, position)
}
}
}
holder.itemView.setOnClickListener(null)
}
StreamDirection.DOWNLOAD -> {
holder.binaryFileProgressIcon.isActivated = false
holder.binaryFileProgressContainer.visibility = View.VISIBLE
holder.binaryFileDeleteButton.visibility = View.GONE
holder.binaryFileProgress.apply {
visibility = when (entryAttachmentState.downloadState) {
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
}
progress = entryAttachmentState.downloadProgression
}
holder.itemView.setOnClickListener {
onItemClickListener?.invoke(entryAttachmentState)
}
}
}
}
class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var binaryFileBroken: ImageView = itemView.findViewById(R.id.item_attachment_broken)
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
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 binaryFileDeleteButton: View = itemView.findViewById(R.id.item_attachment_delete_button)
}
}

View File

@@ -20,31 +20,35 @@
package com.kunzisoft.keepass.adapters
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 androidx.recyclerview.widget.RecyclerView
import android.util.TypedValue
import android.view.*
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import android.widget.ViewSwitcher
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryEntity
import com.kunzisoft.keepass.utils.FileDatabaseInfo
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.view.collapse
import com.kunzisoft.keepass.view.expand
class FileDatabaseHistoryAdapter(private val context: Context)
class FileDatabaseHistoryAdapter(context: Context)
: RecyclerView.Adapter<FileDatabaseHistoryAdapter.FileDatabaseHistoryViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var fileItemOpenListener: ((FileDatabaseHistoryEntity)->Unit)? = null
private var fileSelectClearListener: ((FileDatabaseHistoryEntity)->Boolean)? = null
private var saveAliasListener: ((FileDatabaseHistoryEntity)->Unit)? = null
private var defaultDatabaseListener: ((DatabaseFile?) -> Unit)? = null
private var fileItemOpenListener: ((DatabaseFile)->Unit)? = null
private var fileSelectClearListener: ((DatabaseFile)->Boolean)? = null
private var saveAliasListener: ((DatabaseFile)->Unit)? = null
private val listDatabaseFiles = ArrayList<FileDatabaseHistoryEntity>()
private val listDatabaseFiles = ArrayList<DatabaseFile>()
private var mExpandedPosition = -1
private var mPreviousExpandedPosition = -1
private var mDefaultDatabaseFile: DatabaseFile? = null
private var mExpandedDatabaseFile: DatabaseFile? = null
private var mPreviousExpandedDatabaseFile: DatabaseFile? = null
@ColorInt
private val defaultColor: Int
@@ -61,47 +65,74 @@ class FileDatabaseHistoryAdapter(private val context: Context)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileDatabaseHistoryViewHolder {
val view = inflater.inflate(R.layout.item_file_row, parent, false)
val view = inflater.inflate(R.layout.item_file_info, parent, false)
return FileDatabaseHistoryViewHolder(view)
}
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
// Get info from position
val fileHistoryEntity = listDatabaseFiles[position]
val fileDatabaseInfo = FileDatabaseInfo(context, fileHistoryEntity.databaseUri)
val databaseFile = listDatabaseFiles[position]
// Click item to open file
if (fileItemOpenListener != null)
holder.fileContainer.setOnClickListener {
fileItemOpenListener?.invoke(fileHistoryEntity)
holder.fileContainer.setOnClickListener {
fileItemOpenListener?.invoke(databaseFile)
}
// Default database
holder.defaultFileButton.apply {
this.isChecked = mDefaultDatabaseFile == databaseFile
setOnClickListener {
defaultDatabaseListener?.invoke(if (isChecked) databaseFile else null)
}
}
// File alias
holder.fileAlias.text = fileDatabaseInfo.retrieveDatabaseAlias(fileHistoryEntity.databaseAlias)
holder.fileAlias.text = databaseFile.databaseAlias
// File path
holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString())
holder.filePath.text = databaseFile.databaseDecodedPath
holder.filePreciseInfoContainer.visibility = if (fileDatabaseInfo.found()) {
// Modification
holder.fileModification.text = fileDatabaseInfo.getModificationString()
// Size
holder.fileSize.text = fileDatabaseInfo.getSizeString()
if (databaseFile.databaseFileExists) {
holder.fileInformationButton.clearColorFilter()
} else {
holder.fileInformationButton.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
}
View.VISIBLE
} else
View.GONE
// Modification
databaseFile.databaseLastModified?.let {
holder.fileModification.text = it
holder.fileModificationContainer.visibility = View.VISIBLE
} ?: run {
holder.fileModificationContainer.visibility = View.GONE
}
// Size
databaseFile.databaseSize?.let {
holder.fileSize.text = it
holder.fileSize.visibility = View.VISIBLE
} ?: run {
holder.fileSize.visibility = View.GONE
}
// Click on information
val isExpanded = position == mExpandedPosition
//This line hides or shows the layout in question
holder.fileExpandContainer.visibility = if (isExpanded) View.VISIBLE else View.GONE
val isExpanded = databaseFile == mExpandedDatabaseFile
// Hides or shows info
holder.fileExpandContainer.apply {
if (isExpanded) {
if (visibility != View.VISIBLE) {
visibility = View.VISIBLE
expand(true, resources.getDimensionPixelSize(R.dimen.item_file_info_height))
}
} else {
collapse(true)
}
}
// Save alias modification
holder.fileAliasCloseButton.setOnClickListener {
// Change the alias
fileHistoryEntity.databaseAlias = holder.fileAliasEdit.text.toString()
saveAliasListener?.invoke(fileHistoryEntity)
databaseFile.databaseAlias = holder.fileAliasEdit.text.toString()
saveAliasListener?.invoke(databaseFile)
// Finish save mode
holder.fileMainSwitcher.showPrevious()
@@ -116,20 +147,22 @@ class FileDatabaseHistoryAdapter(private val context: Context)
}
holder.fileDeleteButton.setOnClickListener {
fileSelectClearListener?.invoke(fileHistoryEntity)
fileSelectClearListener?.invoke(databaseFile)
}
if (isExpanded) {
mPreviousExpandedPosition = position
mPreviousExpandedDatabaseFile = databaseFile
}
holder.fileInformation.setOnClickListener {
mExpandedPosition = if (isExpanded) -1 else position
// Notify change
if (mPreviousExpandedPosition < itemCount)
notifyItemChanged(mPreviousExpandedPosition)
notifyItemChanged(position)
holder.fileInformationButton.apply {
animate().rotation(if (isExpanded) 180F else 0F).start()
setOnClickListener {
mExpandedDatabaseFile = if (isExpanded) null else databaseFile
// Notify change
val previousExpandedPosition = listDatabaseFiles.indexOf(mPreviousExpandedDatabaseFile)
notifyItemChanged(previousExpandedPosition)
val expandedPosition = listDatabaseFiles.indexOf(mExpandedDatabaseFile)
notifyItemChanged(expandedPosition)
}
}
// Refresh View / Close alias modification if not contains fileAlias
@@ -142,33 +175,72 @@ class FileDatabaseHistoryAdapter(private val context: Context)
return listDatabaseFiles.size
}
fun addDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<FileDatabaseHistoryEntity>) {
fun clearDatabaseFileHistoryList() {
listDatabaseFiles.clear()
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
}
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: FileDatabaseHistoryEntity) {
listDatabaseFiles.remove(fileDatabaseHistoryToDelete)
fun addDatabaseFileHistory(fileDatabaseHistoryToAdd: DatabaseFile) {
listDatabaseFiles.add(0, fileDatabaseHistoryToAdd)
notifyItemInserted(0)
}
fun setOnFileDatabaseHistoryOpenListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
fun updateDatabaseFileHistory(fileDatabaseHistoryToUpdate: DatabaseFile) {
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToUpdate)
if (listDatabaseFiles.remove(fileDatabaseHistoryToUpdate)) {
listDatabaseFiles.add(index, fileDatabaseHistoryToUpdate)
notifyItemChanged(index)
}
}
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: DatabaseFile) {
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToDelete)
if (listDatabaseFiles.remove(fileDatabaseHistoryToDelete)) {
notifyItemRemoved(index)
}
}
fun replaceAllDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<DatabaseFile>) {
if (listDatabaseFiles.isEmpty()) {
listFileDatabaseHistoryToAdd.forEach {
listDatabaseFiles.add(it)
notifyItemInserted(listDatabaseFiles.size)
}
} else {
listDatabaseFiles.clear()
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
notifyDataSetChanged()
}
}
fun setDefaultDatabase(databaseUri: Uri?) {
val defaultDatabaseFile = listDatabaseFiles.firstOrNull { it.databaseUri == databaseUri }
mDefaultDatabaseFile = defaultDatabaseFile
notifyDataSetChanged()
}
fun setOnDefaultDatabaseListener(listener: ((DatabaseFile?) -> Unit)?) {
this.defaultDatabaseListener = listener
}
fun setOnFileDatabaseHistoryOpenListener(listener : ((DatabaseFile)->Unit)?) {
this.fileItemOpenListener = listener
}
fun setOnFileDatabaseHistoryDeleteListener(listener : ((FileDatabaseHistoryEntity)->Boolean)?) {
fun setOnFileDatabaseHistoryDeleteListener(listener : ((DatabaseFile)->Boolean)?) {
this.fileSelectClearListener = listener
}
fun setOnSaveAliasListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
fun setOnSaveAliasListener(listener : ((DatabaseFile)->Unit)?) {
this.saveAliasListener = listener
}
inner class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)
var defaultFileButton: CompoundButton = itemView.findViewById(R.id.default_file_button)
var fileAlias: TextView = itemView.findViewById(R.id.file_alias)
var fileInformation: ImageView = itemView.findViewById(R.id.file_information)
var fileInformationButton: ImageView = itemView.findViewById(R.id.file_information_button)
var fileMainSwitcher: ViewSwitcher = itemView.findViewById(R.id.file_main_switcher)
var fileAliasEdit: EditText = itemView.findViewById(R.id.file_alias_edit)
@@ -178,7 +250,7 @@ class FileDatabaseHistoryAdapter(private val context: Context)
var fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
var filePath: TextView = itemView.findViewById(R.id.file_path)
var filePreciseInfoContainer: ViewGroup = itemView.findViewById(R.id.file_precise_info_container)
var fileModificationContainer: ViewGroup = itemView.findViewById(R.id.file_modification_container)
var fileModification: TextView = itemView.findViewById(R.id.file_modification)
var fileSize: TextView = itemView.findViewById(R.id.file_size)
}

View File

@@ -21,15 +21,13 @@ package com.kunzisoft.keepass.adapters
import android.content.Context
import android.graphics.Color
import android.graphics.Paint
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.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback
@@ -39,34 +37,34 @@ import com.kunzisoft.keepass.database.element.Entry
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.NodeVersionedInterface
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.setTextSize
import com.kunzisoft.keepass.view.strikeOut
import java.util.*
class NodeAdapter
/**
* Create node list adapter with contextMenu or not
* @param context Context to use
*/
(private val context: Context)
class NodeAdapter (private val context: Context)
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
private var nodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
private val nodeSortedListCallback: NodeSortedListCallback
private val nodeSortedList: SortedList<Node>
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var calculateViewTypeTextSize = Array(2) { true} // number of view type
private var textSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
private var prefTextSize: Float = 0F
private var subtextSize: Float = 0F
private var infoTextSize: Float = 0F
private var numberChildrenTextSize: Float = 0F
private var iconSize: Float = 0F
private var listSort: SortNodeEnum = SortNodeEnum.DB
private var ascendingSort: Boolean = true
private var groupsBeforeSort: Boolean = true
private var recycleBinBottomSort: Boolean = true
private var prefSizeMultiplier: Float = 0F
private var subtextDefaultDimension: Float = 0F
private var infoTextDefaultDimension: Float = 0F
private var numberChildrenTextDefaultDimension: Float = 0F
private var iconDefaultDimension: Float = 0F
private var showUserNames: Boolean = true
private var showNumberEntries: Boolean = true
private var entryFilters = arrayOf<Group.ChildFilter>()
@@ -76,7 +74,11 @@ class NodeAdapter
private val mDatabase: Database
@ColorInt
private val contentSelectionColor: Int
@ColorInt
private val iconGroupColor: Int
@ColorInt
private val iconEntryColor: Int
/**
@@ -87,27 +89,23 @@ class NodeAdapter
get() = nodeSortedList.size() <= 0
init {
this.infoTextDefaultDimension = context.resources.getDimension(R.dimen.list_medium_size_default)
this.subtextDefaultDimension = context.resources.getDimension(R.dimen.list_small_size_default)
this.numberChildrenTextDefaultDimension = context.resources.getDimension(R.dimen.list_tiny_size_default)
this.iconDefaultDimension = context.resources.getDimension(R.dimen.list_icon_size_default)
assignPreferences()
this.nodeSortedList = SortedList(Node::class.java, object : SortedListAdapterCallback<Node>(this) {
override fun compare(item1: Node, item2: Node): Int {
return listSort.getNodeComparator(ascendingSort, groupsBeforeSort, recycleBinBottomSort).compare(item1, item2)
}
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
return oldItem.type == newItem.type
&& oldItem.title == newItem.title
&& oldItem.icon == newItem.icon
}
override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
return item1 == item2
}
})
this.nodeSortedListCallback = NodeSortedListCallback()
this.nodeSortedList = SortedList(Node::class.java, nodeSortedListCallback)
// Database
this.mDatabase = Database.getInstance()
// Color of content selection
val taContentSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
this.contentSelectionColor = taContentSelectionColor.getColor(0, Color.WHITE)
taContentSelectionColor.recycle()
// Retrieve the color to tint the icon
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
this.iconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
@@ -118,17 +116,18 @@ class NodeAdapter
taTextColor.recycle()
}
private fun assignPreferences() {
this.prefTextSize = PreferencesUtil.getListTextSize(context)
this.infoTextSize = context.resources.getDimension(R.dimen.list_medium_size_default) * prefTextSize
this.subtextSize = context.resources.getDimension(R.dimen.list_small_size_default) * prefTextSize
this.numberChildrenTextSize = context.resources.getDimension(R.dimen.list_tiny_size_default) * prefTextSize
this.iconSize = context.resources.getDimension(R.dimen.list_icon_size_default) * prefTextSize
fun assignPreferences() {
this.prefSizeMultiplier = PreferencesUtil.getListTextSize(context)
notifyChangeSort(
PreferencesUtil.getListSort(context),
SortNodeEnum.SortNodeParameters(
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context),
PreferencesUtil.getRecycleBinBottomSort(context)
)
)
this.listSort = PreferencesUtil.getListSort(context)
this.ascendingSort = PreferencesUtil.getAscendingSort(context)
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context)
this.recycleBinBottomSort = PreferencesUtil.getRecycleBinBottomSort(context)
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
this.showNumberEntries = PreferencesUtil.showNumberEntries(context)
@@ -142,15 +141,24 @@ class NodeAdapter
* Rebuild the list by clear and build children from the group
*/
fun rebuildList(group: Group) {
this.nodeSortedList.clear()
assignPreferences()
try {
this.nodeSortedList.addAll(group.getChildren(*entryFilters))
} catch (e: Exception) {
Log.e(TAG, "Can't add node elements to the list", e)
Toast.makeText(context, "Can't add node elements to the list : " + e.message, Toast.LENGTH_LONG).show()
nodeSortedList.replaceAll(group.getFilteredChildren(entryFilters))
}
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
override fun compare(item1: Node, item2: Node): Int {
return nodeComparator!!.compare(item1, item2)
}
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
return oldItem.type == newItem.type
&& oldItem.title == newItem.title
&& oldItem.icon == newItem.icon
}
override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
return item1 == item2
}
notifyDataSetChanged()
}
fun contains(node: Node): Boolean {
@@ -261,10 +269,9 @@ class NodeAdapter
/**
* Notify a change sort of the list
*/
fun notifyChangeSort(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean) {
this.listSort = sortNodeEnum
this.ascendingSort = ascending
this.groupsBeforeSort = groupsBefore
fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
this.nodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
}
override fun getItemViewType(position: Int): Int {
@@ -282,24 +289,31 @@ class NodeAdapter
override fun onBindViewHolder(holder: NodeViewHolder, position: Int) {
val subNode = nodeSortedList.get(position)
// Node selection
holder.container.isSelected = actionNodesList.contains(subNode)
// Assign image
val iconColor = when (subNode.type) {
val iconColor = if (holder.container.isSelected)
contentSelectionColor
else when (subNode.type) {
Type.GROUP -> iconGroupColor
Type.ENTRY -> iconEntryColor
}
holder.imageIdentifier?.setColorFilter(iconColor)
holder.icon.apply {
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
// Relative size of the icon
layoutParams?.apply {
height = iconSize.toInt()
width = iconSize.toInt()
height = (iconDefaultDimension * prefSizeMultiplier).toInt()
width = (iconDefaultDimension * prefSizeMultiplier).toInt()
}
}
// Assign text
holder.text.apply {
text = subNode.title
setTextSize(textSizeUnit, infoTextSize)
setTextSize(textSizeUnit, infoTextDefaultDimension, prefSizeMultiplier)
strikeOut(subNode.isCurrentlyExpires)
}
// Add subText with username
@@ -320,10 +334,13 @@ class NodeAdapter
if (showUserNames && username.isNotEmpty()) {
visibility = View.VISIBLE
text = username
setTextSize(textSizeUnit, subtextSize)
setTextSize(textSizeUnit, subtextDefaultDimension, prefSizeMultiplier)
}
}
holder.attachmentIcon?.visibility =
if (entry.containsAttachment()) View.VISIBLE else View.GONE
mDatabase.stopManageEntry(entry)
}
@@ -332,9 +349,9 @@ class NodeAdapter
if (showNumberEntries) {
holder.numberChildren?.apply {
text = (subNode as Group)
.getChildEntries(*entryFilters)
.size.toString()
setTextSize(textSizeUnit, numberChildrenTextSize)
.getNumberOfChildEntries(entryFilters)
.toString()
setTextSize(textSizeUnit, numberChildrenTextDefaultDimension, prefSizeMultiplier)
visibility = View.VISIBLE
}
} else {
@@ -349,10 +366,7 @@ class NodeAdapter
holder.container.setOnLongClickListener {
nodeClickCallback?.onNodeLongClick(subNode) ?: false
}
holder.container.isSelected = actionNodesList.contains(subNode)
}
override fun getItemCount(): Int {
return nodeSortedList.size()
@@ -375,10 +389,12 @@ class NodeAdapter
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var container: View = itemView.findViewById(R.id.node_container)
var imageIdentifier: ImageView? = itemView.findViewById(R.id.node_image_identifier)
var icon: ImageView = itemView.findViewById(R.id.node_icon)
var text: TextView = itemView.findViewById(R.id.node_text)
var subText: TextView = itemView.findViewById(R.id.node_subtext)
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
}
companion object {

View File

@@ -28,8 +28,14 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.cursor.EntryCursorKDB
import com.kunzisoft.keepass.database.cursor.EntryCursorKDBX
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.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.strikeOut
@@ -38,9 +44,10 @@ class SearchEntryCursorAdapter(private val context: Context,
private val database: Database)
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
private val cursorInflater: LayoutInflater = context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private var displayUsername: Boolean = false
private val cursorInflater: LayoutInflater? = context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
private var mDisplayUsername: Boolean = false
private var mOmitBackup: Boolean = true
private val iconColor: Int
init {
@@ -53,12 +60,13 @@ class SearchEntryCursorAdapter(private val context: Context,
}
fun reInit(context: Context) {
this.displayUsername = PreferencesUtil.showUsernamesListEntries(context)
this.mDisplayUsername = PreferencesUtil.showUsernamesListEntries(context)
this.mOmitBackup = PreferencesUtil.omitBackup(context)
}
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
val view = cursorInflater.inflate(R.layout.item_search_entry, parent, false)
val view = cursorInflater!!.inflate(R.layout.item_search_entry, parent, false)
val viewHolder = ViewHolder()
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
@@ -69,8 +77,7 @@ class SearchEntryCursorAdapter(private val context: Context,
}
override fun bindView(view: View, context: Context, cursor: Cursor) {
database.getEntryFrom(cursor)?.let { currentEntry ->
getEntryFrom(cursor)?.let { currentEntry ->
val viewHolder = view.tag as ViewHolder
// Assign image
@@ -88,7 +95,7 @@ class SearchEntryCursorAdapter(private val context: Context,
// Assign subtitle
viewHolder.textViewSubTitle?.apply {
val entryUsername = currentEntry.username
text = if (displayUsername && entryUsername.isNotEmpty()) {
text = if (mDisplayUsername && entryUsername.isNotEmpty()) {
String.format("(%s)", entryUsername)
} else {
""
@@ -98,14 +105,48 @@ class SearchEntryCursorAdapter(private val context: Context,
}
}
private class ViewHolder {
internal var imageViewIcon: ImageView? = null
internal var textViewTitle: TextView? = null
internal var textViewSubTitle: TextView? = null
private fun getEntryFrom(cursor: Cursor): Entry? {
return database.createEntry()?.apply {
database.startManageEntry(this)
entryKDB?.let { entryKDB ->
(cursor as EntryCursorKDB).populateEntry(entryKDB, database.iconFactory)
}
entryKDBX?.let { entryKDBX ->
(cursor as EntryCursorKDBX).populateEntry(entryKDBX, database.iconFactory)
}
database.stopManageEntry(this)
}
}
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
return database.searchEntries(context, constraint.toString())
return searchEntries(context, constraint.toString())
}
private fun searchEntries(context: Context, query: String): Cursor? {
var cursorKDB: EntryCursorKDB? = null
var cursorKDBX: EntryCursorKDBX? = null
if (database.type == DatabaseKDB.TYPE)
cursorKDB = EntryCursorKDB()
if (database.type == DatabaseKDBX.TYPE)
cursorKDBX = EntryCursorKDBX()
val searchGroup = database.createVirtualGroupFromSearch(query,
mOmitBackup,
SearchHelper.MAX_SEARCH_ENTRY)
if (searchGroup != null) {
// Search in hide entries but not meta-stream
for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) {
entry.entryKDB?.let {
cursorKDB?.addEntry(it)
}
entry.entryKDBX?.let {
cursorKDBX?.addEntry(it)
}
}
}
return cursorKDB ?: cursorKDBX
}
fun getEntryFromPosition(position: Int): Entry? {
@@ -113,9 +154,14 @@ class SearchEntryCursorAdapter(private val context: Context,
val cursor = this.cursor
if (cursor.moveToFirst() && cursor.move(position)) {
pwEntry = database.getEntryFrom(cursor)
pwEntry = getEntryFrom(cursor)
}
return pwEntry
}
private class ViewHolder {
internal var imageViewIcon: ImageView? = null
internal var textViewTitle: TextView? = null
internal var textViewSubTitle: TextView? = null
}
}

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.app
import androidx.multidex.MultiDexApplication
import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.utils.UriUtil
class App : MultiDexApplication() {
@@ -33,7 +34,7 @@ class App : MultiDexApplication() {
}
override fun onTerminate() {
Database.getInstance().closeAndClear(applicationContext.filesDir)
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
super.onTerminate()
}
}

View File

@@ -13,8 +13,6 @@ package com.kunzisoft.keepass.app;
import android.os.Build;
import android.os.Process;
import com.kunzisoft.keepass.utils.StringUtil;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
@@ -66,7 +64,7 @@ public final class PRNGFixes {
private static boolean supportedOnThisDevice() {
// Blacklist on samsung devices
if (StringUtil.INSTANCE.indexOfIgnoreCase(Build.MANUFACTURER, "samsung", Locale.ENGLISH) >= 0) {
if (Build.MANUFACTURER.toLowerCase(Locale.ENGLISH).contains("samsung")) {
return false;
}

View File

@@ -32,7 +32,7 @@ class CipherDatabaseAction(applicationContext: Context) {
fun getCipherDatabase(databaseUri: Uri,
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
ActionDatabaseAsyncTask(
IOActionTask(
{
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
},
@@ -51,7 +51,7 @@ class CipherDatabaseAction(applicationContext: Context) {
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
cipherDatabaseResultListener: (() -> Unit)? = null) {
ActionDatabaseAsyncTask(
IOActionTask(
{
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
@@ -70,7 +70,7 @@ class CipherDatabaseAction(applicationContext: Context) {
fun deleteByDatabaseUri(databaseUri: Uri,
cipherDatabaseResultListener: (() -> Unit)? = null) {
ActionDatabaseAsyncTask(
IOActionTask(
{
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
},
@@ -81,7 +81,7 @@ class CipherDatabaseAction(applicationContext: Context) {
}
fun deleteAll() {
ActionDatabaseAsyncTask(
IOActionTask(
{
cipherDatabaseDao.deleteAll()
}

View File

@@ -21,31 +21,44 @@ package com.kunzisoft.keepass.app.database
import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.SingletonHolderParameter
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
class FileDatabaseHistoryAction(applicationContext: Context) {
class FileDatabaseHistoryAction(private val applicationContext: Context) {
private val databaseFileHistoryDao =
AppDatabase
.getDatabase(applicationContext)
.fileDatabaseHistoryDao()
fun getFileDatabaseHistory(databaseUri: Uri,
fileHistoryResultListener: (fileDatabaseHistoryResult: FileDatabaseHistoryEntity?) -> Unit) {
ActionDatabaseAsyncTask(
fun getDatabaseFile(databaseUri: Uri,
databaseFileResult: (DatabaseFile?) -> Unit) {
IOActionTask(
{
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
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.getModificationString(),
fileDatabaseInfo.getSizeString()
)
},
{
fileHistoryResultListener.invoke(it)
databaseFileResult.invoke(it)
}
).execute()
}
fun getKeyFileUriByDatabaseUri(databaseUri: Uri,
keyFileUriResultListener: (Uri?) -> Unit) {
ActionDatabaseAsyncTask(
IOActionTask(
{
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
},
@@ -59,61 +72,128 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
).execute()
}
fun getAllFileDatabaseHistories(fileHistoryResultListener: (fileDatabaseHistoryResult: List<FileDatabaseHistoryEntity>?) -> Unit) {
ActionDatabaseAsyncTask(
fun getDatabaseFileList(databaseFileListResult: (List<DatabaseFile>) -> Unit) {
IOActionTask(
{
databaseFileHistoryDao.getAll()
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.getModificationString(),
fileDatabaseInfo.getSizeString()
)
)
}
}
databaseFileListLoaded
},
{
fileHistoryResultListener.invoke(it)
}
).execute()
}
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null) {
addOrUpdateFileDatabaseHistory(FileDatabaseHistoryEntity(
databaseUri.toString(),
"",
keyFileUri?.toString(),
System.currentTimeMillis()
), true)
}
fun addOrUpdateFileDatabaseHistory(fileDatabaseHistory: FileDatabaseHistoryEntity, unmodifiedAlias: Boolean = false) {
ActionDatabaseAsyncTask(
{
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(fileDatabaseHistory.databaseUri)
if (unmodifiedAlias) {
fileDatabaseHistory.databaseAlias = fileDatabaseHistoryRetrieve?.databaseAlias ?: ""
}
// Update values if history element not yet in the database
if (fileDatabaseHistoryRetrieve == null) {
databaseFileHistoryDao.add(fileDatabaseHistory)
} else {
databaseFileHistoryDao.update(fileDatabaseHistory)
databaseFileList ->
databaseFileList?.let {
databaseFileListResult.invoke(it)
}
}
).execute()
}
fun deleteFileDatabaseHistory(fileDatabaseHistory: FileDatabaseHistoryEntity,
fileHistoryDeletedResult: (FileDatabaseHistoryEntity?) -> Unit) {
ActionDatabaseAsyncTask(
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null,
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
addOrUpdateDatabaseFile(DatabaseFile(
databaseUri,
keyFileUri
), databaseFileAddedOrUpdatedResult)
}
fun addOrUpdateDatabaseFile(databaseFileToAddOrUpdate: DatabaseFile,
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
IOActionTask(
{
databaseFileHistoryDao.delete(fileDatabaseHistory)
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()
)
// Update values if history element not yet in the database
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.getModificationString(),
fileDatabaseInfo.getSizeString()
)
}
},
{
if (it != null && it > 0)
fileHistoryDeletedResult.invoke(fileDatabaseHistory)
else
fileHistoryDeletedResult.invoke(null)
databaseFileAddedOrUpdatedResult?.invoke(it)
}
).execute()
}
fun deleteDatabaseFile(databaseFileToDelete: DatabaseFile,
databaseFileDeletedResult: (DatabaseFile?) -> Unit) {
IOActionTask(
{
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
)
} else {
null
}
}
}
},
{
databaseFileDeletedResult.invoke(it)
}
).execute()
}
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
IOActionTask(
{
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
}
).execute()
}
fun deleteAllKeyFiles() {
ActionDatabaseAsyncTask(
IOActionTask(
{
databaseFileHistoryDao.deleteAllKeyFiles()
}
@@ -121,7 +201,7 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
}
fun deleteAll() {
ActionDatabaseAsyncTask(
IOActionTask(
{
databaseFileHistoryDao.deleteAll()
}

View File

@@ -38,6 +38,9 @@ interface FileDatabaseHistoryDao {
@Delete
fun delete(fileDatabaseHistory: FileDatabaseHistoryEntity): Int
@Query("UPDATE file_database_history SET keyfile_uri=null WHERE database_uri = :databaseUriString")
fun deleteKeyFileByDatabaseUri(databaseUriString: String)
@Query("UPDATE file_database_history SET keyfile_uri=null")
fun deleteAllKeyFiles()

View File

@@ -19,21 +19,27 @@
*/
package com.kunzisoft.keepass.app.database
import android.os.AsyncTask
import kotlinx.coroutines.*
/**
* Private class to invoke each method in a separate thread
* Class to invoke action in a separate IO thread
*/
class ActionDatabaseAsyncTask<T>(
class IOActionTask<T>(
private val action: () -> T ,
private val afterActionDatabaseListener: ((T?) -> Unit)? = null
) : AsyncTask<Void, Void, T>() {
private val afterActionDatabaseListener: ((T?) -> Unit)? = null) {
override fun doInBackground(vararg args: Void?): T? {
return action.invoke()
}
private val mainScope = CoroutineScope(Dispatchers.Main)
override fun onPostExecute(result: T?) {
afterActionDatabaseListener?.invoke(result)
fun execute() {
mainScope.launch {
withContext(Dispatchers.IO) {
val asyncResult: Deferred<T?> = async {
action.invoke()
}
withContext(Dispatchers.Main) {
afterActionDatabaseListener?.invoke(asyncResult.await())
}
}
}
}
}

View File

@@ -26,15 +26,20 @@ import android.content.Intent
import android.os.Build
import android.service.autofill.Dataset
import android.service.autofill.FillResponse
import androidx.annotation.RequiresApi
import android.util.Log
import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue
import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.model.EntryInfo
import java.util.*
import com.kunzisoft.keepass.model.SearchInfo
@RequiresApi(api = Build.VERSION_CODES.O)
@@ -56,27 +61,44 @@ object AutofillHelper {
return String.format("%s (%s)", entryInfo.title, entryInfo.username)
if (entryInfo.title.isNotEmpty())
return entryInfo.title
if (entryInfo.username.isNotEmpty())
return entryInfo.username
if (entryInfo.url.isNotEmpty())
return entryInfo.url
if (entryInfo.username.isNotEmpty())
return entryInfo.username
return ""
}
private fun buildDataset(context: Context,
entryInfo: EntryInfo,
struct: StructureParser.Result): Dataset? {
internal fun addHeader(responseBuilder: FillResponse.Builder,
packageName: String,
webDomain: String?,
applicationId: String?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (webDomain != null) {
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_web_domain).apply {
setTextViewText(R.id.autofill_web_domain_text, webDomain)
})
} else if (applicationId != null) {
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_app_id).apply {
setTextViewText(R.id.autofill_app_id_text, applicationId)
})
}
}
}
internal fun buildDataset(context: Context,
entryInfo: EntryInfo,
struct: StructureParser.Result): Dataset? {
val title = makeEntryTitle(entryInfo)
val views = newRemoteViews(context.packageName, title)
val views = newRemoteViews(context, title, entryInfo.icon)
val builder = Dataset.Builder(views)
builder.setId(entryInfo.id)
struct.password.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.password)) }
val ids = ArrayList(struct.username)
if (entryInfo.username.contains("@") || struct.username.isEmpty())
ids.addAll(struct.email)
ids.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.username)) }
struct.usernameId?.let { usernameId ->
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
}
struct.passwordId?.let { password ->
builder.setValue(password, AutofillValue.forText(entryInfo.password))
}
return try {
builder.build()
@@ -87,31 +109,43 @@ object AutofillHelper {
}
/**
* Method to hit when right key is selected
* Build the Autofill response for one entry
*/
fun buildResponseWhenEntrySelected(activity: Activity, entryInfo: EntryInfo) {
var setResultOk = false
activity.intent?.extras?.let { extras ->
if (extras.containsKey(ASSIST_STRUCTURE)) {
activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure ->
StructureParser(structure).parse()?.let { result ->
// New Response
val responseBuilder = FillResponse.Builder()
val dataset = buildDataset(activity, entryInfo, result)
responseBuilder.addDataset(dataset)
val mReplyIntent = Intent()
Log.d(activity.javaClass.name, "Successed Autofill auth.")
mReplyIntent.putExtra(
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
responseBuilder.build())
setResultOk = true
activity.setResult(Activity.RESULT_OK, mReplyIntent)
fun buildResponse(activity: Activity, entryInfo: EntryInfo) {
buildResponse(activity, ArrayList<EntryInfo>().apply { add(entryInfo) })
}
/**
* Build the Autofill response for many entry
*/
fun buildResponse(activity: Activity, entriesInfo: List<EntryInfo>) {
if (entriesInfo.isEmpty()) {
activity.setResult(Activity.RESULT_CANCELED)
} else {
var setResultOk = false
activity.intent?.extras?.let { extras ->
if (extras.containsKey(ASSIST_STRUCTURE)) {
activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure ->
StructureParser(structure).parse()?.let { result ->
// New Response
val responseBuilder = FillResponse.Builder()
entriesInfo.forEach {
responseBuilder.addDataset(buildDataset(activity, it, result))
}
val mReplyIntent = Intent()
Log.d(activity.javaClass.name, "Successed Autofill auth.")
mReplyIntent.putExtra(
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
responseBuilder.build())
setResultOk = true
activity.setResult(Activity.RESULT_OK, mReplyIntent)
}
}
}
}
if (!setResultOk) {
Log.w(activity.javaClass.name, "Failed Autofill auth.")
activity.setResult(Activity.RESULT_CANCELED)
if (!setResultOk) {
Log.w(activity.javaClass.name, "Failed Autofill auth.")
activity.setResult(Activity.RESULT_CANCELED)
}
}
}
}
@@ -119,9 +153,15 @@ object AutofillHelper {
/**
* Utility method to start an activity with an Autofill for result
*/
fun startActivityForAutofillResult(activity: Activity, intent: Intent, assistStructure: AssistStructure) {
fun startActivityForAutofillResult(activity: Activity,
intent: Intent,
assistStructure: AssistStructure,
searchInfo: SearchInfo?) {
EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent)
intent.putExtra(ASSIST_STRUCTURE, assistStructure)
searchInfo?.let {
intent.putExtra(KEY_SEARCH_INFO, it)
}
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
}
@@ -140,9 +180,18 @@ object AutofillHelper {
}
}
private fun newRemoteViews(packageName: String, remoteViewsText: String): RemoteViews {
val presentation = RemoteViews(packageName, R.layout.item_autofill_service)
presentation.setTextViewText(R.id.text, remoteViewsText)
private fun newRemoteViews(context: Context,
remoteViewsText: String,
remoteViewsIcon: IconImage? = null): RemoteViews {
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
if (remoteViewsIcon != null) {
presentation.assignDatabaseIcon(context,
R.id.autofill_entry_icon,
Database.getInstance().drawFactory,
remoteViewsIcon,
ContextCompat.getColor(context, R.color.green))
}
return presentation
}
}

View File

@@ -1,70 +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.autofill
import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.os.Build
import android.os.Bundle
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
@RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
if (assistStructure != null) {
if (Database.getInstance().loaded && TimeoutHelper.checkTime(this))
GroupActivity.launchForAutofillResult(this, assistStructure, PreferencesUtil.enableReadOnlyDatabase(this))
else {
FileDatabaseSelectActivity.launchForAutofillResult(this, assistStructure)
}
} else {
setResult(Activity.RESULT_CANCELED)
finish()
}
super.onCreate(savedInstanceState)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
super.onActivityResult(requestCode, resultCode, data)
}
companion object {
fun getAuthIntentSenderForResponse(context: Context): IntentSender {
val intent = Intent(context, AutofillLauncherActivity::class.java)
return PendingIntent.getActivity(context, 0,
intent, PendingIntent.FLAG_CANCEL_CURRENT).intentSender
}
}
}

View File

@@ -22,31 +22,95 @@ package com.kunzisoft.keepass.autofill
import android.os.Build
import android.os.CancellationSignal
import android.service.autofill.*
import androidx.annotation.RequiresApi
import android.util.Log
import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
@RequiresApi(api = Build.VERSION_CODES.O)
class KeeAutofillService : AutofillService() {
override fun onFillRequest(request: FillRequest, cancellationSignal: CancellationSignal,
var applicationIdBlocklist: Set<String>? = null
var webDomainBlocklist: Set<String>? = null
override fun onCreate() {
super.onCreate()
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
}
override fun onFillRequest(request: FillRequest,
cancellationSignal: CancellationSignal,
callback: FillCallback) {
val fillContexts = request.fillContexts
val latestStructure = fillContexts[fillContexts.size - 1].structure
cancellationSignal.setOnCancelListener { Log.e(TAG, "Cancel autofill not implemented in this sample.") }
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
val responseBuilder = FillResponse.Builder()
// Check user's settings for authenticating Responses and Datasets.
val parseResult = StructureParser(latestStructure).parse()
parseResult?.allAutofillIds()?.let { autofillIds ->
if (listOf(*autofillIds).isNotEmpty()) {
StructureParser(latestStructure).parse()?.let { parseResult ->
// Build search info only if applicationId or webDomain are not blocked
if (searchAllowedFor(parseResult.applicationId, applicationIdBlocklist)
&& searchAllowedFor(parseResult.domain, webDomainBlocklist)) {
val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.domain
}
SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(),
searchInfo,
{ items ->
val responseBuilder = FillResponse.Builder()
AutofillHelper.addHeader(responseBuilder, packageName,
parseResult.domain, parseResult.applicationId)
items.forEach {
responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult))
}
callback.onSuccess(responseBuilder.build())
},
{
// Show UI if no search result
showUIForEntrySelection(parseResult, searchInfo, callback)
},
{
// Show UI if database not open
showUIForEntrySelection(parseResult, searchInfo, callback)
}
)
}
}
}
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
searchInfo: SearchInfo,
callback: FillCallback) {
parseResult.allAutofillIds().let { autofillIds ->
if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response.
val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this)
val presentation = RemoteViews(packageName, R.layout.item_autofill_service_unlock)
responseBuilder.setAuthentication(autofillIds, sender, presentation)
val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this,
searchInfo)
val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (!parseResult.domain.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
setTextViewText(R.id.autofill_web_domain_text, parseResult.domain)
}
} 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)
}
responseBuilder.setAuthentication(autofillIds, sender, remoteViewsUnlock)
callback.onSuccess(responseBuilder.build())
}
}
@@ -67,5 +131,18 @@ class KeeAutofillService : AutofillService() {
companion object {
private val TAG = KeeAutofillService::class.java.name
fun searchAllowedFor(element: String?, blockList: Set<String>?): Boolean {
element?.let { elementNotNull ->
if (blockList?.any { appIdBlocked ->
elementNotNull.contains(appIdBlocked)
} == true
) {
Log.d(TAG, "Autofill not allowed for $elementNotNull")
return false
}
}
return true
}
}
}

View File

@@ -20,8 +20,8 @@ package com.kunzisoft.keepass.autofill
import android.app.assist.AssistStructure
import android.os.Build
import androidx.annotation.RequiresApi
import android.text.InputType
import androidx.annotation.RequiresApi
import android.util.Log
import android.view.View
import android.view.autofill.AutofillId
@@ -35,74 +35,238 @@ import java.util.*
internal class StructureParser(private val structure: AssistStructure) {
private var result: Result? = null
private var usernameCandidate: AutofillId? = null
private var usernameNeeded = true
fun parse(): Result? {
result = Result()
result?.apply {
usernameCandidate = null
for (i in 0 until structure.windowNodeCount) {
val windowNode = structure.getWindowNodeAt(i)
title.add(windowNode.title)
windowNode.rootViewNode.webDomain?.let {
webDomain.add(it)
}
parseViewNode(windowNode.rootViewNode)
}
// If not explicit username field found, add the field just before password field.
if (username.isEmpty() && email.isEmpty()
&& password.isNotEmpty() && usernameCandidate != null)
username.add(usernameCandidate!!)
}
try {
result = Result()
result?.apply {
usernameCandidate = null
mainLoop@ for (i in 0 until structure.windowNodeCount) {
val windowNode = structure.getWindowNodeAt(i)
applicationId = windowNode.title.toString().split("/")[0]
Log.d(TAG, "Autofill applicationId: $applicationId")
return result
if (parseViewNode(windowNode.rootViewNode))
break@mainLoop
}
// If not explicit username field found, add the field just before password field.
if (usernameId == null && passwordId != null && usernameCandidate != null)
usernameId = usernameCandidate
}
// Return the result only if password field is retrieved
return if ((!usernameNeeded || result?.usernameId != null)
&& result?.passwordId != null)
result
else
null
} catch (e: Exception) {
return null
}
}
private fun parseViewNode(node: AssistStructure.ViewNode) {
val hints = node.autofillHints
val autofillId = node.autofillId
if (autofillId != null) {
if (hints != null && hints.isNotEmpty()) {
when {
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_USERNAME == it } -> result?.username?.add(autofillId)
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_EMAIL_ADDRESS == it } -> result?.email?.add(autofillId)
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_PASSWORD == it } -> result?.password?.add(autofillId)
else -> Log.d(TAG, "unsupported hints")
}
} else if (node.autofillType == View.AUTOFILL_TYPE_TEXT) {
val inputType = node.inputType
when {
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS > 0 -> result?.email?.add(autofillId)
inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD > 0 -> result?.password?.add(autofillId)
result?.password?.isEmpty() == true -> usernameCandidate = autofillId
}
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
// Get the domain of a web app
node.webDomain?.let { webDomain ->
if (webDomain.isNotEmpty()) {
result?.domain = webDomain
Log.d(TAG, "Autofill domain: $webDomain")
}
}
for (i in 0 until node.childCount)
parseViewNode(node.getChildAt(i))
// Only parse visible nodes
if (node.visibility == View.VISIBLE) {
if (node.autofillId != null
&& node.autofillType == View.AUTOFILL_TYPE_TEXT) {
// Parse methods
val hints = node.autofillHints
if (hints != null && hints.isNotEmpty()) {
if (parseNodeByAutofillHint(node))
return true
} else if (parseNodeByHtmlAttributes(node))
return true
else if (parseNodeByAndroidInput(node))
return true
}
// Recursive method to process each node
for (i in 0 until node.childCount) {
if (parseViewNode(node.getChildAt(i)))
return true
}
}
return false
}
private fun parseNodeByAutofillHint(node: AssistStructure.ViewNode): Boolean {
val autofillId = node.autofillId
node.autofillHints?.forEach {
when {
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
Log.d(TAG, "Autofill username hint")
}
it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
result?.passwordId = autofillId
Log.d(TAG, "Autofill password hint")
// Username not needed in this case
usernameNeeded = false
return true
}
// Ignore autocomplete="off"
// https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
it.equals("off", true) ||
it.equals("on", true) -> {
Log.d(TAG, "Autofill web hint")
return parseNodeByHtmlAttributes(node)
}
else -> Log.d(TAG, "Autofill unsupported hint $it")
}
}
return false
}
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
val autofillId = node.autofillId
val nodHtml = node.htmlInfo
when (nodHtml?.tag?.toLowerCase(Locale.ENGLISH)) {
"input" -> {
nodHtml.attributes?.forEach { pairAttribute ->
when (pairAttribute.first.toLowerCase(Locale.ENGLISH)) {
"type" -> {
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
"tel", "email" -> {
result?.usernameId = autofillId
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
"text" -> {
usernameCandidate = autofillId
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
"password" -> {
result?.passwordId = autofillId
Log.d(TAG, "Autofill password web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
return true
}
}
}
}
}
}
}
return false
}
private fun inputIsVariationType(inputType: Int, vararg type: Int): Boolean {
type.forEach {
if (inputType and InputType.TYPE_MASK_VARIATION == it)
return true
}
return false
}
private fun showHexInputType(inputType: Int): String {
return "0x${"%08x".format(inputType)}"
}
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
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) -> {
usernameCandidate = autofillId
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
}
inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_PASSWORD,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
result?.passwordId = autofillId
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
usernameNeeded = false
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)}")
}
}
}
InputType.TYPE_CLASS_NUMBER -> {
when {
inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
usernameCandidate = autofillId
Log.d(TAG, "Autofill usernale candidate android number type: ${showHexInputType(inputType)}")
}
inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
result?.passwordId = autofillId
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
usernameNeeded = false
return true
}
else -> {
Log.d(TAG, "Autofill unknown android number type: ${showHexInputType(inputType)}")
}
}
}
}
return false
}
@RequiresApi(api = Build.VERSION_CODES.O)
internal class Result {
val title: MutableList<CharSequence>
val webDomain: MutableList<String>
val username: MutableList<AutofillId>
val email: MutableList<AutofillId>
val password: MutableList<AutofillId>
var applicationId: String? = null
var domain: String? = null
set(value) {
if (field == null)
field = value
}
init {
title = ArrayList()
webDomain = ArrayList()
username = ArrayList()
email = ArrayList()
password = ArrayList()
}
var usernameId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var passwordId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
fun allAutofillIds(): Array<AutofillId> {
val all = ArrayList<AutofillId>()
all.addAll(username)
all.addAll(email)
all.addAll(password)
usernameId?.let {
all.add(it)
}
passwordId?.let {
all.add(it)
}
return all.toTypedArray()
}
}

View File

@@ -19,15 +19,11 @@
*/
package com.kunzisoft.keepass.backup
import android.annotation.SuppressLint
import android.app.backup.BackupAgentHelper
import android.app.backup.SharedPreferencesBackupHelper
@SuppressLint("NewApi")
class SettingsBackupAgent : BackupAgentHelper() {
//TODO Backup
override fun onCreate() {
val defaultPrefs = this.packageName + "_preferences"
val prefHelper = SharedPreferencesBackupHelper(this, defaultPrefs)

View File

@@ -26,6 +26,7 @@ import android.provider.Settings
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import android.widget.CompoundButton
import android.widget.TextView
import androidx.annotation.RequiresApi
@@ -52,7 +53,18 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
private var biometricMode: Mode = Mode.UNAVAILABLE
private var isBiometricPromptAutoOpenEnable = PreferencesUtil.isBiometricPromptAutoOpenEnable(context)
/**
* Manage setting to auto open biometric prompt
*/
private var biometricPromptAutoOpenPreference = PreferencesUtil.isBiometricPromptAutoOpenEnable(context)
var isBiometricPromptAutoOpenEnable: Boolean = true
get() {
return field && biometricPromptAutoOpenPreference
}
// 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 var cipherDatabaseAction = CipherDatabaseAction.getInstance(context.applicationContext)
@@ -73,6 +85,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
// biometric not supported (by API level or hardware) so keep option hidden
// or manually disable
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate()
allowOpenBiometricPrompt = true
if (!PreferencesUtil.isBiometricUnlockEnable(context)
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
@@ -206,7 +219,8 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
cryptoObject: BiometricPrompt.CryptoObject,
promptInfo: BiometricPrompt.PromptInfo) {
context.runOnUiThread {
biometricPrompt?.authenticate(promptInfo, cryptoObject)
if (allowOpenBiometricPrompt)
biometricPrompt?.authenticate(promptInfo, cryptoObject)
}
}
@@ -272,6 +286,9 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
}
fun destroy() {
// Close the biometric prompt
allowOpenBiometricPrompt = false
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
// Restore the checked listener
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
}
@@ -300,7 +317,6 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
loadDatabaseAfterRegisterCredentials.invoke(encryptedValue, ivSpec)
// TODO setAdvancedUnlockedMessageView(R.string.encrypted_value_stored)
}
override fun handleDecryptedResult(decryptedValue: String) {
@@ -313,12 +329,15 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
}
override fun onBiometricException(e: Exception) {
if (e.localizedMessage != null)
setAdvancedUnlockedMessageView(e.localizedMessage)
e.localizedMessage?.let {
setAdvancedUnlockedMessageView(it)
}
}
private fun showFingerPrintViews(show: Boolean) {
context.runOnUiThread { advancedUnlockInfoView?.hide = !show }
context.runOnUiThread {
advancedUnlockInfoView?.visibility = if (show) View.VISIBLE else View.GONE
}
}
private fun setAdvancedUnlockedTitleView(textId: Int) {

View File

@@ -60,7 +60,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
setTitle(context.getString(R.string.biometric_prompt_store_credential_title))
setDescription(context.getString(R.string.biometric_prompt_store_credential_message))
setConfirmationRequired(true)
// TODO device credential
// TODO device credential #102 #152
/*
if (keyguardManager?.isDeviceSecure == true)
setDeviceCredentialAllowed(true)
@@ -73,7 +73,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
setTitle(context.getString(R.string.biometric_prompt_extract_credential_title))
//setDescription(context.getString(R.string.biometric_prompt_extract_credential_message))
setConfirmationRequired(false)
// TODO device credential
// TODO device credential #102 #152
/*
if (keyguardManager?.isDeviceSecure == true)
setDeviceCredentialAllowed(true)
@@ -95,7 +95,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
// really not much to do when no fingerprint support found
isKeyManagerInit = false
} else {
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
try {
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)
@@ -271,6 +271,10 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
}
}
fun closeBiometricPrompt() {
biometricPrompt?.cancelAuthentication()
}
interface BiometricUnlockErrorCallback {
fun onInvalidKeyException(e: Exception)
fun onBiometricException(e: Exception)

View File

@@ -24,7 +24,7 @@ import com.kunzisoft.keepass.crypto.engine.AesEngine
import com.kunzisoft.keepass.crypto.engine.ChaCha20Engine
import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.crypto.engine.TwofishEngine
import org.spongycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.NoSuchAlgorithmException
import java.security.Security
import java.util.*
@@ -37,20 +37,10 @@ object CipherFactory {
private var blacklisted: Boolean = false
init {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
}
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class)
@JvmOverloads
fun getInstance(transformation: String, androidOverride: Boolean = false): Cipher {
// Return the native AES if it is possible
return if (!deviceBlacklisted() && !androidOverride && hasNativeImplementation(transformation) && NativeLib.loaded()) {
Cipher.getInstance(transformation, AESProvider())
} else {
Cipher.getInstance(transformation)
}
}
fun deviceBlacklisted(): Boolean {
if (!blacklistInit) {
blacklistInit = true
@@ -64,6 +54,16 @@ object CipherFactory {
return transformation == "AES/CBC/PKCS5Padding"
}
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class)
fun getInstance(transformation: String, androidOverride: Boolean = false): Cipher {
// Return the native AES if it is possible
return if (!deviceBlacklisted() && !androidOverride && hasNativeImplementation(transformation) && NativeLib.loaded()) {
Cipher.getInstance(transformation, AESProvider())
} else {
Cipher.getInstance(transformation)
}
}
/**
* Generate appropriate cipher based on KeePass 2.x UUID's
*/

View File

@@ -19,16 +19,18 @@
*/
package com.kunzisoft.keepass.crypto
enum class CrsAlgorithm constructor(val id: Int) {
import com.kunzisoft.keepass.utils.UnsignedInt
Null(0),
ArcFourVariant(1),
Salsa20(2),
ChaCha20(3);
enum class CrsAlgorithm constructor(val id: UnsignedInt) {
Null(UnsignedInt(0)),
ArcFourVariant(UnsignedInt(1)),
Salsa20(UnsignedInt(2)),
ChaCha20(UnsignedInt(3));
companion object {
fun fromId(num: Int): CrsAlgorithm? {
fun fromId(num: UnsignedInt): CrsAlgorithm? {
for (e in values()) {
if (e.id == num) {
return e

View File

@@ -74,12 +74,10 @@ object CryptoUtil {
return ret
}
@JvmOverloads
fun hashSha256(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
return hashGen("SHA-256", data, offset, count)
}
@JvmOverloads
fun hashSha512(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
return hashGen("SHA-512", data, offset, count)
}

View File

@@ -19,11 +19,11 @@
*/
package com.kunzisoft.keepass.crypto
import org.spongycastle.crypto.StreamCipher
import org.spongycastle.crypto.engines.ChaCha7539Engine
import org.spongycastle.crypto.engines.Salsa20Engine
import org.spongycastle.crypto.params.KeyParameter
import org.spongycastle.crypto.params.ParametersWithIV
import org.bouncycastle.crypto.StreamCipher
import org.bouncycastle.crypto.engines.ChaCha7539Engine
import org.bouncycastle.crypto.engines.Salsa20Engine
import org.bouncycastle.crypto.params.KeyParameter
import org.bouncycastle.crypto.params.ParametersWithIV
object StreamCipherFactory {

View File

@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.crypto.engine
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.stream.bytes16ToUuid
import org.spongycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException

View File

@@ -17,18 +17,20 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.crypto.finalkey;
package com.kunzisoft.keepass.crypto.finalkey
import com.kunzisoft.keepass.crypto.CipherFactory;
import com.kunzisoft.keepass.crypto.CipherFactory.deviceBlacklisted
public class FinalKeyFactory {
public static FinalKey createFinalKey() {
object AESKeyTransformerFactory : KeyTransformer() {
override fun transformMasterKey(seed: ByteArray?, key: ByteArray?, rounds: Long?): ByteArray? {
// Prefer the native final key implementation
if ( !CipherFactory.INSTANCE.deviceBlacklisted() && NativeFinalKey.available() ) {
return new NativeFinalKey();
val keyTransformer = if (!deviceBlacklisted()
&& NativeAESKeyTransformer.available()) {
NativeAESKeyTransformer()
} else {
// Fall back on the android crypto implementation
return new AndroidFinalKey();
AndroidAESKeyTransformer()
}
return keyTransformer.transformMasterKey(seed, key, rounds)
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* 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.crypto.finalkey
import java.io.IOException
import java.security.InvalidKeyException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import javax.crypto.Cipher
import javax.crypto.ShortBufferException
import javax.crypto.spec.SecretKeySpec
class AndroidAESKeyTransformer : KeyTransformer() {
@Throws(IOException::class)
override fun transformMasterKey(seed: ByteArray?, key: ByteArray?, rounds: Long?): ByteArray? {
val cipher: Cipher = try {
Cipher.getInstance("AES/ECB/NoPadding")
} catch (e: Exception) {
throw IOException("Unable to get the cipher", e)
}
try {
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(seed, "AES"))
} catch (e: InvalidKeyException) {
throw IOException("Unable to init the cipher", e)
}
if (key == null) {
throw IOException("Invalid key")
}
if (rounds == null) {
throw IOException("Invalid rounds")
}
// Encrypt key rounds times
val keyLength = key.size
val newKey = ByteArray(keyLength)
System.arraycopy(key, 0, newKey, 0, keyLength)
val destKey = ByteArray(keyLength)
for (i in 0 until rounds) {
try {
cipher.update(newKey, 0, newKey.size, destKey, 0)
System.arraycopy(destKey, 0, newKey, 0, newKey.size)
} catch (e: ShortBufferException) {
throw IOException("Short buffer", e)
}
}
// Hash the key
val messageDigest: MessageDigest = try {
MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) {
throw IOException("SHA-256 not implemented here: " + e.message)
}
messageDigest.update(newKey)
return messageDigest.digest()
}
}

View File

@@ -1,78 +0,0 @@
/*
* Copyright 2017 Brian Pellin, 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.crypto.finalkey;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.SecretKeySpec;
public class AndroidFinalKey extends FinalKey {
@Override
public byte[] transformMasterKey(byte[] pKeySeed, byte[] pKey, long rounds) throws IOException {
Cipher cipher;
try {
cipher = Cipher.getInstance("AES/ECB/NoPadding");
} catch (NoSuchAlgorithmException e) {
throw new IOException("NoSuchAlgorithm: " + e.getMessage());
} catch (NoSuchPaddingException e) {
throw new IOException("NoSuchPadding: " + e.getMessage());
}
try {
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(pKeySeed, "AES"));
} catch (InvalidKeyException e) {
throw new IOException("InvalidPasswordException: " + e.getMessage());
}
// Encrypt key rounds times
byte[] newKey = new byte[pKey.length];
System.arraycopy(pKey, 0, newKey, 0, pKey.length);
byte[] destKey = new byte[pKey.length];
for (int i = 0; i < rounds; i++) {
try {
cipher.update(newKey, 0, newKey.length, destKey, 0);
System.arraycopy(destKey, 0, newKey, 0, newKey.length);
} catch (ShortBufferException e) {
throw new IOException("Short buffer: " + e.getMessage());
}
}
// Hash the key
MessageDigest md = null;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
assert true;
throw new IOException("SHA-256 not implemented here: " + e.getMessage());
}
md.update(newKey);
return md.digest();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
@@ -17,10 +17,11 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.crypto.finalkey;
package com.kunzisoft.keepass.crypto.finalkey
import java.io.IOException;
import java.io.IOException
public abstract class FinalKey {
public abstract byte[] transformMasterKey(byte[] seed, byte[] key, long rounds) throws IOException;
}
abstract class KeyTransformer {
@Throws(IOException::class)
abstract fun transformMasterKey(seed: ByteArray?, key: ByteArray?, rounds: Long?): ByteArray?
}

View File

@@ -21,17 +21,20 @@ package com.kunzisoft.keepass.crypto.finalkey;
import com.kunzisoft.keepass.crypto.NativeLib;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
public class NativeFinalKey extends FinalKey {
public class NativeAESKeyTransformer extends KeyTransformer {
public static boolean available() {
return NativeLib.INSTANCE.init();
}
@Nullable
@Override
public byte[] transformMasterKey(byte[] seed, byte[] key, long rounds) throws IOException {
public byte[] transformMasterKey(@Nullable byte[] seed, @Nullable byte[] key, @Nullable Long rounds) throws IOException {
NativeLib.INSTANCE.init();
return nTransformMasterKey(seed, key, rounds);

View File

@@ -22,72 +22,69 @@ package com.kunzisoft.keepass.crypto.keyDerivation
import android.content.res.Resources
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.CryptoUtil
import com.kunzisoft.keepass.crypto.finalkey.FinalKeyFactory
import com.kunzisoft.keepass.crypto.finalkey.AESKeyTransformerFactory
import com.kunzisoft.keepass.stream.bytes16ToUuid
import java.io.IOException
import java.security.SecureRandom
import java.util.*
class AesKdf internal constructor() : KdfEngine() {
class AesKdf : KdfEngine() {
init {
uuid = CIPHER_UUID
}
override val defaultParameters: KdfParameters
get() {
return KdfParameters(uuid!!).apply {
setParamUUID()
setUInt32(PARAM_ROUNDS, DEFAULT_ROUNDS.toLong())
setUInt64(PARAM_ROUNDS, defaultKeyRounds)
}
}
override val defaultKeyRounds: Long
get() = DEFAULT_ROUNDS.toLong()
init {
uuid = CIPHER_UUID
}
override val defaultKeyRounds: Long = 6000L
override fun getName(resources: Resources): String {
return resources.getString(R.string.kdf_AES)
}
@Throws(IOException::class)
override fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray {
var currentMasterKey = masterKey
val rounds = p.getUInt64(PARAM_ROUNDS)
var seed = p.getByteArray(PARAM_SEED)
override fun transform(masterKey: ByteArray, kdfParameters: KdfParameters): ByteArray {
var seed = kdfParameters.getByteArray(PARAM_SEED)
if (seed != null && seed.size != 32) {
seed = CryptoUtil.hashSha256(seed)
}
var currentMasterKey = masterKey
if (currentMasterKey.size != 32) {
currentMasterKey = CryptoUtil.hashSha256(currentMasterKey)
}
if (seed.size != 32) {
seed = CryptoUtil.hashSha256(seed)
}
val rounds = kdfParameters.getUInt64(PARAM_ROUNDS)
val key = FinalKeyFactory.createFinalKey()
return key.transformMasterKey(seed, currentMasterKey, rounds)
return AESKeyTransformerFactory.transformMasterKey(seed, currentMasterKey, rounds) ?: ByteArray(0)
}
override fun randomize(p: KdfParameters) {
override fun randomize(kdfParameters: KdfParameters) {
val random = SecureRandom()
val seed = ByteArray(32)
random.nextBytes(seed)
p.setByteArray(PARAM_SEED, seed)
kdfParameters.setByteArray(PARAM_SEED, seed)
}
override fun getKeyRounds(p: KdfParameters): Long {
return p.getUInt64(PARAM_ROUNDS)
override fun getKeyRounds(kdfParameters: KdfParameters): Long {
return kdfParameters.getUInt64(PARAM_ROUNDS) ?: defaultKeyRounds
}
override fun setKeyRounds(p: KdfParameters, keyRounds: Long) {
p.setUInt64(PARAM_ROUNDS, keyRounds)
override fun setKeyRounds(kdfParameters: KdfParameters, keyRounds: Long) {
kdfParameters.setUInt64(PARAM_ROUNDS, keyRounds)
}
companion object {
private const val DEFAULT_ROUNDS = 6000
val CIPHER_UUID: UUID = bytes16ToUuid(
byteArrayOf(0xC9.toByte(),
0xD9.toByte(),
@@ -106,7 +103,7 @@ class AesKdf internal constructor() : KdfEngine() {
0x4F.toByte(),
0xEA.toByte()))
const val PARAM_ROUNDS = "R"
const val PARAM_SEED = "S"
const val PARAM_ROUNDS = "R" // UInt64
const val PARAM_SEED = "S" // Byte array
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.crypto.keyDerivation
import android.content.res.Resources
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.stream.bytes16ToUuid
import com.kunzisoft.keepass.utils.UnsignedInt
import java.io.IOException
import java.security.SecureRandom
import java.util.*
@@ -53,35 +54,49 @@ class Argon2Kdf internal constructor() : KdfEngine() {
}
@Throws(IOException::class)
override fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray {
override fun transform(masterKey: ByteArray, kdfParameters: KdfParameters): ByteArray {
val salt = p.getByteArray(PARAM_SALT)
val parallelism = p.getUInt32(PARAM_PARALLELISM).toInt()
val memory = p.getUInt64(PARAM_MEMORY)
val iterations = p.getUInt64(PARAM_ITERATIONS)
val version = p.getUInt32(PARAM_VERSION)
val secretKey = p.getByteArray(PARAM_SECRET_KEY)
val assocData = p.getByteArray(PARAM_ASSOC_DATA)
val salt = kdfParameters.getByteArray(PARAM_SALT)
val parallelism = kdfParameters.getUInt32(PARAM_PARALLELISM)?.let {
UnsignedInt(it)
}
val memory = kdfParameters.getUInt64(PARAM_MEMORY)?.div(MEMORY_BLOCK_SIZE)?.let {
UnsignedInt.fromKotlinLong(it)
}
val iterations = kdfParameters.getUInt64(PARAM_ITERATIONS)?.let {
UnsignedInt.fromKotlinLong(it)
}
val version = kdfParameters.getUInt32(PARAM_VERSION)?.let {
UnsignedInt(it)
}
val secretKey = kdfParameters.getByteArray(PARAM_SECRET_KEY)
val assocData = kdfParameters.getByteArray(PARAM_ASSOC_DATA)
return Argon2Native.transformKey(masterKey, salt, parallelism, memory, iterations,
secretKey, assocData, version)
return Argon2Native.transformKey(masterKey,
salt,
parallelism,
memory,
iterations,
secretKey,
assocData,
version)
}
override fun randomize(p: KdfParameters) {
override fun randomize(kdfParameters: KdfParameters) {
val random = SecureRandom()
val salt = ByteArray(32)
random.nextBytes(salt)
p.setByteArray(PARAM_SALT, salt)
kdfParameters.setByteArray(PARAM_SALT, salt)
}
override fun getKeyRounds(p: KdfParameters): Long {
return p.getUInt64(PARAM_ITERATIONS)
override fun getKeyRounds(kdfParameters: KdfParameters): Long {
return kdfParameters.getUInt64(PARAM_ITERATIONS) ?: defaultKeyRounds
}
override fun setKeyRounds(p: KdfParameters, keyRounds: Long) {
p.setUInt64(PARAM_ITERATIONS, keyRounds)
override fun setKeyRounds(kdfParameters: KdfParameters, keyRounds: Long) {
kdfParameters.setUInt64(PARAM_ITERATIONS, keyRounds)
}
override val minKeyRounds: Long
@@ -90,12 +105,12 @@ class Argon2Kdf internal constructor() : KdfEngine() {
override val maxKeyRounds: Long
get() = MAX_ITERATIONS
override fun getMemoryUsage(p: KdfParameters): Long {
return p.getUInt64(PARAM_MEMORY)
override fun getMemoryUsage(kdfParameters: KdfParameters): Long {
return kdfParameters.getUInt64(PARAM_MEMORY) ?: defaultMemoryUsage
}
override fun setMemoryUsage(p: KdfParameters, memory: Long) {
p.setUInt64(PARAM_MEMORY, memory)
override fun setMemoryUsage(kdfParameters: KdfParameters, memory: Long) {
kdfParameters.setUInt64(PARAM_MEMORY, memory)
}
override val defaultMemoryUsage: Long
@@ -107,21 +122,23 @@ class Argon2Kdf internal constructor() : KdfEngine() {
override val maxMemoryUsage: Long
get() = MAX_MEMORY
override fun getParallelism(p: KdfParameters): Int {
return p.getUInt32(PARAM_PARALLELISM).toInt() // TODO Verify
override fun getParallelism(kdfParameters: KdfParameters): Long {
return kdfParameters.getUInt32(PARAM_PARALLELISM)?.let {
UnsignedInt(it).toKotlinLong()
} ?: defaultParallelism
}
override fun setParallelism(p: KdfParameters, parallelism: Int) {
p.setUInt32(PARAM_PARALLELISM, parallelism.toLong())
override fun setParallelism(kdfParameters: KdfParameters, parallelism: Long) {
kdfParameters.setUInt32(PARAM_PARALLELISM, UnsignedInt.fromKotlinLong(parallelism))
}
override val defaultParallelism: Int
get() = DEFAULT_PARALLELISM.toInt()
override val defaultParallelism: Long
get() = DEFAULT_PARALLELISM.toKotlinLong()
override val minParallelism: Int
override val minParallelism: Long
get() = MIN_PARALLELISM
override val maxParallelism: Int
override val maxParallelism: Long
get() = MAX_PARALLELISM
companion object {
@@ -152,23 +169,24 @@ class Argon2Kdf internal constructor() : KdfEngine() {
private const val PARAM_SECRET_KEY = "K" // byte[]
private const val PARAM_ASSOC_DATA = "A" // byte[]
private const val MIN_VERSION: Long = 0x10
private const val MAX_VERSION: Long = 0x13
private val MIN_VERSION = UnsignedInt(0x10)
private val MAX_VERSION = UnsignedInt(0x13)
private const val MIN_SALT = 8
private const val MAX_SALT = Integer.MAX_VALUE
private val MAX_SALT = UnsignedInt.MAX_VALUE.toKotlinLong()
private const val MIN_ITERATIONS: Long = 1
private const val MIN_ITERATIONS: Long = 1L
private const val MAX_ITERATIONS = 4294967295L
private const val MIN_MEMORY = (1024 * 8).toLong()
private const val MAX_MEMORY = Integer.MAX_VALUE.toLong()
private val MAX_MEMORY = UnsignedInt.MAX_VALUE.toKotlinLong()
private const val MEMORY_BLOCK_SIZE: Long = 1024L
private const val MIN_PARALLELISM = 1
private const val MAX_PARALLELISM = (1 shl 24) - 1
private const val MIN_PARALLELISM: Long = 1L
private const val MAX_PARALLELISM: Long = ((1 shl 24) - 1).toLong()
private const val DEFAULT_ITERATIONS: Long = 2
private const val DEFAULT_ITERATIONS: Long = 2L
private const val DEFAULT_MEMORY = (1024 * 1024).toLong()
private const val DEFAULT_PARALLELISM: Long = 2
private val DEFAULT_PARALLELISM = UnsignedInt(2)
}
}

View File

@@ -20,20 +20,29 @@
package com.kunzisoft.keepass.crypto.keyDerivation;
import com.kunzisoft.keepass.crypto.NativeLib;
import com.kunzisoft.keepass.utils.UnsignedInt;
import java.io.IOException;
public class Argon2Native {
public static byte[] transformKey(byte[] password, byte[] salt, int parallelism,
long memory, long iterations, byte[] secretKey,
byte[] associatedData, long version) throws IOException {
public static byte[] transformKey(byte[] password, byte[] salt, UnsignedInt parallelism,
UnsignedInt memory, UnsignedInt iterations, byte[] secretKey,
byte[] associatedData, UnsignedInt version) throws IOException {
NativeLib.INSTANCE.init();
return nTransformMasterKey(password, salt, parallelism, memory, iterations, secretKey, associatedData, version);
return nTransformMasterKey(
password,
salt,
parallelism.toKotlinInt(),
memory.toKotlinInt(),
iterations.toKotlinInt(),
secretKey,
associatedData,
version.toKotlinInt());
}
private static native byte[] nTransformMasterKey(byte[] password, byte[] salt, int parallelism,
long memory, long iterations, byte[] secretKey,
byte[] associatedData, long version) throws IOException;
int memory, int iterations, byte[] secretKey,
byte[] associatedData, int version) throws IOException;
}

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.crypto.keyDerivation
import com.kunzisoft.keepass.utils.ObjectNameResource
import com.kunzisoft.keepass.utils.UnsignedInt
import java.io.IOException
import java.io.Serializable
@@ -33,17 +34,17 @@ abstract class KdfEngine : ObjectNameResource, Serializable {
abstract val defaultParameters: KdfParameters
@Throws(IOException::class)
abstract fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray
abstract fun transform(masterKey: ByteArray, kdfParameters: KdfParameters): ByteArray
abstract fun randomize(p: KdfParameters)
abstract fun randomize(kdfParameters: KdfParameters)
/*
* ITERATIONS
*/
abstract fun getKeyRounds(p: KdfParameters): Long
abstract fun getKeyRounds(kdfParameters: KdfParameters): Long
abstract fun setKeyRounds(p: KdfParameters, keyRounds: Long)
abstract fun setKeyRounds(kdfParameters: KdfParameters, keyRounds: Long)
abstract val defaultKeyRounds: Long
@@ -51,51 +52,51 @@ abstract class KdfEngine : ObjectNameResource, Serializable {
get() = 1
open val maxKeyRounds: Long
get() = Int.MAX_VALUE.toLong()
get() = UnsignedInt.MAX_VALUE.toKotlinLong()
/*
* MEMORY
*/
open fun getMemoryUsage(p: KdfParameters): Long {
return UNKNOWN_VALUE.toLong()
open fun getMemoryUsage(kdfParameters: KdfParameters): Long {
return UNKNOWN_VALUE
}
open fun setMemoryUsage(p: KdfParameters, memory: Long) {
open fun setMemoryUsage(kdfParameters: KdfParameters, memory: Long) {
// Do nothing by default
}
open val defaultMemoryUsage: Long
get() = UNKNOWN_VALUE.toLong()
get() = UNKNOWN_VALUE
open val minMemoryUsage: Long
get() = 1
open val maxMemoryUsage: Long
get() = Int.MAX_VALUE.toLong()
get() = UnsignedInt.MAX_VALUE.toKotlinLong()
/*
* PARALLELISM
*/
open fun getParallelism(p: KdfParameters): Int {
open fun getParallelism(kdfParameters: KdfParameters): Long {
return UNKNOWN_VALUE
}
open fun setParallelism(p: KdfParameters, parallelism: Int) {
open fun setParallelism(kdfParameters: KdfParameters, parallelism: Long) {
// Do nothing by default
}
open val defaultParallelism: Int
open val defaultParallelism: Long
get() = UNKNOWN_VALUE
open val minParallelism: Int
get() = 1
open val minParallelism: Long
get() = 1L
open val maxParallelism: Int
get() = Int.MAX_VALUE
open val maxParallelism: Long
get() = UnsignedInt.MAX_VALUE.toKotlinLong()
companion object {
const val UNKNOWN_VALUE = -1
const val UNKNOWN_VALUE: Long = -1L
}
}

View File

@@ -19,17 +19,23 @@
*/
package com.kunzisoft.keepass.crypto.keyDerivation
import com.kunzisoft.keepass.stream.LittleEndianDataInputStream
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
import com.kunzisoft.keepass.stream.bytes16ToUuid
import com.kunzisoft.keepass.stream.uuidTo16Bytes
import com.kunzisoft.keepass.utils.VariantDictionary
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.*
class KdfParameters internal constructor(val uuid: UUID) : VariantDictionary() {
class KdfParameters: VariantDictionary {
val uuid: UUID
constructor(uuid: UUID): super() {
this.uuid = uuid
}
constructor(uuid: UUID, d: VariantDictionary): super(d) {
this.uuid = uuid
}
fun setParamUUID() {
setByteArray(PARAM_UUID, uuidTo16Bytes(uuid))
@@ -41,26 +47,17 @@ class KdfParameters internal constructor(val uuid: UUID) : VariantDictionary() {
@Throws(IOException::class)
fun deserialize(data: ByteArray): KdfParameters? {
val bis = ByteArrayInputStream(data)
val lis = LittleEndianDataInputStream(bis)
val dictionary = VariantDictionary.deserialize(data)
val d = deserialize(lis) ?: return null
val uuidBytes = dictionary.getByteArray(PARAM_UUID) ?: return null
val uuid = bytes16ToUuid(uuidBytes)
val uuid = bytes16ToUuid(d.getByteArray(PARAM_UUID))
val kdfP = KdfParameters(uuid)
kdfP.copyTo(d)
return kdfP
return KdfParameters(uuid, dictionary)
}
@Throws(IOException::class)
fun serialize(kdf: KdfParameters): ByteArray {
val bos = ByteArrayOutputStream()
val los = LittleEndianDataOutputStream(bos)
serialize(kdf, los)
return bos.toByteArray()
fun serialize(kdfParameters: KdfParameters): ByteArray {
return VariantDictionary.serialize(kdfParameters)
}
}

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.action
import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.utils.UriUtil
@@ -36,7 +37,7 @@ open class AssignPasswordInDatabaseRunnable (
: SaveDatabaseRunnable(context, database, true) {
private var mMasterPassword: String? = null
protected var mKeyFile: Uri? = null
protected var mKeyFileUri: Uri? = null
private var mBackupKey: ByteArray? = null
@@ -44,7 +45,7 @@ open class AssignPasswordInDatabaseRunnable (
if (withMasterPassword)
this.mMasterPassword = masterPassword
if (withKeyFile)
this.mKeyFile = keyFile
this.mKeyFileUri = keyFile
}
override fun onStartRun() {
@@ -54,7 +55,7 @@ open class AssignPasswordInDatabaseRunnable (
mBackupKey = ByteArray(database.masterKey.size)
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFile)
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFileUri)
database.retrieveMasterKey(mMasterPassword, uriInputStream)
} catch (e: Exception) {
erase(mBackupKey)
@@ -70,6 +71,9 @@ open class AssignPasswordInDatabaseRunnable (
// Erase the biometric
CipherDatabaseAction.getInstance(context)
.deleteByDatabaseUri(mDatabaseUri)
// Erase the register keyfile
FileDatabaseHistoryAction.getInstance(context)
.deleteKeyFileByDatabaseUri(mDatabaseUri)
if (!result.isSuccess) {
// Erase the current master key

View File

@@ -24,6 +24,9 @@ import android.net.Uri
import android.util.Log
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.closeDatabase
class CreateDatabaseRunnable(context: Context,
private val mDatabase: Database,
@@ -33,7 +36,8 @@ class CreateDatabaseRunnable(context: Context,
withMasterPassword: Boolean,
masterPassword: String?,
withKeyFile: Boolean,
keyFile: Uri?)
keyFile: Uri?,
private val createDatabaseResult: ((Result) -> Unit)?)
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile) {
override fun onStartRun() {
@@ -41,26 +45,36 @@ class CreateDatabaseRunnable(context: Context,
// Create new database record
mDatabase.apply {
createData(mDatabaseUri, databaseName, rootName)
// Set Database state
loaded = true
}
} catch (e: Exception) {
mDatabase.closeAndClear()
mDatabase.closeAndClear(UriUtil.getBinaryDir(context))
setError(e)
}
super.onStartRun()
}
override fun onFinishRun() {
super.onFinishRun()
override fun onActionRun() {
super.onActionRun()
if (result.isSuccess) {
// Add database to recent files
FileDatabaseHistoryAction.getInstance(context.applicationContext)
.addOrUpdateDatabaseUri(mDatabaseUri, mKeyFile)
if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context.applicationContext)
.addOrUpdateDatabaseUri(mDatabaseUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mKeyFileUri else null)
}
// Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context)
} else {
Log.e("CreateDatabaseRunnable", "Unable to create the database")
}
}
override fun onFinishRun() {
super.onFinishRun()
createDatabaseResult?.invoke(result)
}
}

View File

@@ -27,10 +27,11 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.closeDatabase
class LoadDatabaseRunnable(private val context: Context,
private val mDatabase: Database,
@@ -39,17 +40,14 @@ class LoadDatabaseRunnable(private val context: Context,
private val mKey: Uri?,
private val mReadonly: Boolean,
private val mCipherEntity: CipherDatabaseEntity?,
private val mOmitBackup: Boolean,
private val mFixDuplicateUUID: Boolean,
private val progressTaskUpdater: ProgressTaskUpdater?,
private val mDuplicateUuidAction: ((Result) -> Unit)?)
private val mLoadDatabaseResult: ((Result) -> Unit)?)
: ActionRunnable() {
private val cacheDirectory = context.applicationContext.filesDir
override fun onStartRun() {
// Clear before we load
mDatabase.closeAndClear(cacheDirectory)
mDatabase.closeAndClear(UriUtil.getBinaryDir(context))
}
override fun onActionRun() {
@@ -57,31 +55,23 @@ class LoadDatabaseRunnable(private val context: Context,
mDatabase.loadData(mUri, mPass, mKey,
mReadonly,
context.contentResolver,
cacheDirectory,
mOmitBackup,
UriUtil.getBinaryDir(context),
mFixDuplicateUUID,
progressTaskUpdater)
}
catch (e: DuplicateUuidDatabaseException) {
mDuplicateUuidAction?.invoke(result)
setError(e)
}
catch (e: LoadDatabaseException) {
setError(e)
}
}
override fun onFinishRun() {
if (result.isSuccess) {
// Save keyFile in app database
val rememberKeyFile = PreferencesUtil.rememberKeyFiles(context)
if (rememberKeyFile) {
var keyUri = mKey
if (!rememberKeyFile) {
keyUri = null
}
if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context)
.addOrUpdateDatabaseUri(mUri, keyUri)
.addOrUpdateDatabaseUri(mUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mKey else null)
}
// Register the biometric
@@ -90,10 +80,14 @@ class LoadDatabaseRunnable(private val context: Context,
.addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called
}
// Start the opening notification
DatabaseOpenNotificationService.startIfAllowed(context)
// Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context)
} else {
mDatabase.closeAndClear(cacheDirectory)
mDatabase.closeAndClear(UriUtil.getBinaryDir(context))
}
}
override fun onFinishRun() {
mLoadDatabaseResult?.invoke(result)
}
}

View File

@@ -23,13 +23,13 @@ import android.content.*
import android.content.Context.BIND_ABOVE_CLIENT
import android.content.Context.BIND_NOT_FOREGROUND
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
@@ -45,6 +45,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
@@ -52,6 +53,8 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DESCRIPTION_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENCRYPTION_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ITERATIONS_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK
@@ -59,56 +62,61 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.retrieveProgressDialog
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import java.util.*
import kotlin.collections.ArrayList
class ProgressDialogThread(private val activity: FragmentActivity) {
class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
var onActionFinish: ((actionTask: String,
result: ActionRunnable.Result) -> Unit)? = null
private var intentDatabaseTask = Intent(activity, DatabaseTaskNotificationService::class.java)
private var intentDatabaseTask = Intent(activity.applicationContext, DatabaseTaskNotificationService::class.java)
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
private var serviceConnection: ServiceConnection? = null
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
TimeoutHelper.temporarilyDisableTimeout(activity)
startOrUpdateDialog(titleId, messageId, warningId)
startDialog(titleId, messageId, warningId)
}
override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) {
TimeoutHelper.temporarilyDisableTimeout(activity)
startOrUpdateDialog(titleId, messageId, warningId)
updateDialog(titleId, messageId, warningId)
}
override fun onStopAction(actionTask: String, result: ActionRunnable.Result) {
onActionFinish?.invoke(actionTask, result)
// Remove the progress task
ProgressTaskDialogFragment.stop(activity)
TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(activity)
stopDialog()
}
}
private fun startOrUpdateDialog(titleId: Int?, messageId: Int?, warningId: Int?) {
var progressTaskDialogFragment = retrieveProgressDialog(activity)
private fun startDialog(titleId: Int? = null,
messageId: Int? = null,
warningId: Int? = null) {
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = ProgressTaskDialogFragment.build()
ProgressTaskDialogFragment.start(activity, progressTaskDialogFragment)
progressTaskDialogFragment = activity.supportFragmentManager
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
}
progressTaskDialogFragment.apply {
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = ProgressTaskDialogFragment()
progressTaskDialogFragment?.show(activity.supportFragmentManager, PROGRESS_TASK_DIALOG_TAG)
}
updateDialog(titleId, messageId, warningId)
}
private fun updateDialog(titleId: Int?, messageId: Int?, warningId: Int?) {
progressTaskDialogFragment?.apply {
titleId?.let {
updateTitle(it)
}
@@ -121,12 +129,16 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
}
}
@Synchronized
private fun stopDialog() {
progressTaskDialogFragment?.dismissAllowingStateLoss()
progressTaskDialogFragment = null
}
private fun initServiceConnection() {
if (serviceConnection == null) {
serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder).apply {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
addActionTaskListener(actionTaskListener)
getService().checkAction()
}
@@ -140,7 +152,6 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
}
}
@Synchronized
private fun bindService() {
initServiceConnection()
serviceConnection?.let {
@@ -151,7 +162,6 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
/**
* Unbind the service and assign null to the service connection to check if already unbind or not
*/
@Synchronized
private fun unBindService() {
serviceConnection?.let {
activity.unbindService(it)
@@ -159,22 +169,21 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
serviceConnection = null
}
@Synchronized
fun registerProgressTask() {
ProgressTaskDialogFragment.stop(activity)
stopDialog()
// Register a database task receiver to stop loading dialog when service finish the task
databaseTaskBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
activity.runOnUiThread {
when (intent?.action) {
DATABASE_START_TASK_ACTION -> {
// Bind to the service when is starting
bindService()
}
DATABASE_STOP_TASK_ACTION -> {
unBindService()
}
when (intent?.action) {
DATABASE_START_TASK_ACTION -> {
// Bind to the service when is starting
bindService()
}
DATABASE_STOP_TASK_ACTION -> {
// Remove the progress task
stopDialog()
unBindService()
}
}
}
@@ -190,9 +199,8 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
bindService()
}
@Synchronized
fun unregisterProgressTask() {
ProgressTaskDialogFragment.stop(activity)
stopDialog()
mBinder?.removeActionTaskListener(actionTaskListener)
mBinder = null
@@ -206,19 +214,12 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
}
}
@Synchronized
private fun start(bundle: Bundle? = null, actionTask: String) {
activity.stopService(intentDatabaseTask)
if (bundle != null)
intentDatabaseTask.putExtras(bundle)
activity.runOnUiThread {
intentDatabaseTask.action = actionTask
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.startForegroundService(intentDatabaseTask)
} else {
activity.startService(intentDatabaseTask)
}
}
intentDatabaseTask.action = actionTask
activity.startService(intentDatabaseTask)
}
/*
@@ -237,7 +238,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
}
, ACTION_DATABASE_CREATE_TASK)
}
@@ -251,7 +252,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
@@ -270,7 +271,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
}
, ACTION_DATABASE_ASSIGN_PASSWORD_TASK)
}
@@ -462,6 +463,13 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
}
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
}
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
newMaxHistoryItems: Int,
save: Boolean) {
@@ -534,12 +542,12 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
}
fun startDatabaseSaveParallelism(oldParallelism: Int,
newParallelism: Int,
fun startDatabaseSaveParallelism(oldParallelism: Long,
newParallelism: Long,
save: Boolean) {
start(Bundle().apply {
putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism)
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)

View File

@@ -1,6 +1,6 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
@@ -17,19 +17,24 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.settings
package com.kunzisoft.keepass.database.action
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
class SettingsAutofillActivity : SettingsActivity() {
class RemoveUnlinkedDataDatabaseRunnable (
context: Context,
database: Database,
saveDatabase: Boolean)
: SaveDatabaseRunnable(context, database, saveDatabase) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mTimeoutEnable = false
}
override fun onActionRun() {
try {
database.removeUnlinkedAttachments()
} catch (e: Exception) {
setError(e)
}
override fun retrieveMainFragment(): Fragment {
return NestedSettingsFragment.newInstance(NestedSettingsFragment.Screen.FORM_FILLING)
super.onActionRun()
}
}

View File

@@ -34,7 +34,7 @@ class DeleteEntryHistoryDatabaseRunnable (
override fun onStartRun() {
try {
mainEntry.removeEntryFromHistory(entryHistoryPosition)
database.removeEntryHistory(mainEntry, entryHistoryPosition)
} catch (e: Exception) {
setError(e)
}

View File

@@ -64,6 +64,10 @@ class DeleteNodesRunnable(context: Context,
} else {
database.deleteEntry(currentNode)
}
// Remove the oldest attachments
currentNode.getAttachments(database.binaryPool).forEach {
database.removeAttachmentIfNotUsed(it)
}
}
}
}

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.database.action.node
import android.content.Context
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.Node
@@ -40,16 +41,34 @@ class UpdateEntryRunnable constructor(
// WARNING : Re attribute parent removed in entry edit activity to save memory
mNewEntry.addParentFrom(mOldEntry)
// Build oldest attachments
val oldEntryAttachments = mOldEntry.getAttachments(database.binaryPool, true)
val newEntryAttachments = mNewEntry.getAttachments(database.binaryPool, true)
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
// Not use equals because only check name
newEntryAttachments.forEach { newAttachment ->
oldEntryAttachments.forEach { oldAttachment ->
if (oldAttachment.name == newAttachment.name
&& oldAttachment.binaryAttachment == newAttachment.binaryAttachment)
attachmentsToRemove.remove(oldAttachment)
}
}
// Update entry with new values
mOldEntry.updateWith(mNewEntry)
mNewEntry.touch(modified = true, touchParents = true)
// Create an entry history (an entry history don't have history)
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
database.removeOldestEntryHistory(mOldEntry)
database.removeOldestEntryHistory(mOldEntry, database.binaryPool)
// Only change data in index
database.updateEntry(mOldEntry)
// Remove oldest attachments
attachmentsToRemove.forEach {
database.removeAttachmentIfNotUsed(it)
}
}
override fun nodeFinish(): ActionNodesValues {

View File

@@ -0,0 +1,69 @@
/*
* 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.database.element
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
data class Attachment(var name: String,
var binaryAttachment: BinaryAttachment) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString() ?: "",
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(name)
parcel.writeParcelable(binaryAttachment, flags)
}
override fun describeContents(): Int {
return 0
}
override fun toString(): String {
return "$name at $binaryAttachment"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Attachment) return false
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
return name.hashCode()
}
companion object CREATOR : Parcelable.Creator<Attachment> {
override fun createFromParcel(parcel: Parcel): Attachment {
return Attachment(parcel)
}
override fun newArray(size: Int): Array<Attachment?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -20,18 +20,12 @@
package com.kunzisoft.keepass.database.element
import android.content.ContentResolver
import android.content.Context
import android.content.res.Resources
import android.database.Cursor
import android.net.Uri
import android.util.Log
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.cursor.EntryCursorKDB
import com.kunzisoft.keepass.database.cursor.EntryCursorKDBX
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.*
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdInt
@@ -48,14 +42,15 @@ import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.stream.readBytes4ToInt
import com.kunzisoft.keepass.stream.readBytes4ToUInt
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.SingletonHolder
import com.kunzisoft.keepass.utils.UriUtil
import org.apache.commons.io.FileUtils
import java.io.*
import java.util.*
import kotlin.collections.ArrayList
class Database {
@@ -74,6 +69,13 @@ class Database {
val drawFactory = IconDrawableFactory()
var loaded = false
set(value) {
field = value
loadTimestamp = if (field) System.currentTimeMillis() else null
}
var loadTimestamp: Long? = null
private set
val iconFactory: IconImageFactory
get() {
@@ -137,6 +139,9 @@ class Database {
val version: String
get() = mDatabaseKDB?.version ?: mDatabaseKDBX?.version ?: "-"
val type: Class<*>?
get() = mDatabaseKDB?.javaClass ?: mDatabaseKDBX?.javaClass
val allowDataCompression: Boolean
get() = mDatabaseKDBX != null
@@ -151,6 +156,17 @@ class Database {
}
}
fun compressionForNewEntry(): Boolean {
if (mDatabaseKDB != null)
return false
// Default compression not necessary if stored in header
mDatabaseKDBX?.let {
return it.compressionAlgorithm == CompressionAlgorithm.GZip
&& it.kdbxVersion.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()
}
return false
}
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm) {
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
@@ -205,7 +221,6 @@ class Database {
var numberKeyEncryptionRounds: Long
get() = mDatabaseKDB?.numberKeyEncryptionRounds ?: mDatabaseKDBX?.numberKeyEncryptionRounds ?: 0
@Throws(NumberFormatException::class)
set(numberRounds) {
mDatabaseKDB?.numberKeyEncryptionRounds = numberRounds
mDatabaseKDBX?.numberKeyEncryptionRounds = numberRounds
@@ -213,13 +228,13 @@ class Database {
var memoryUsage: Long
get() {
return mDatabaseKDBX?.memoryUsage ?: return KdfEngine.UNKNOWN_VALUE.toLong()
return mDatabaseKDBX?.memoryUsage ?: return KdfEngine.UNKNOWN_VALUE
}
set(memory) {
mDatabaseKDBX?.memoryUsage = memory
}
var parallelism: Int
var parallelism: Long
get() = mDatabaseKDBX?.parallelism ?: KdfEngine.UNKNOWN_VALUE
set(parallelism) {
mDatabaseKDBX?.parallelism = parallelism
@@ -263,14 +278,14 @@ class Database {
}
/**
* Determine if RecycleBin is available or not for this version of database
* @return true if RecycleBin available
* Determine if a configurable RecycleBin is available or not for this version of database
* @return true if a configurable RecycleBin available
*/
val allowRecycleBin: Boolean
val allowConfigurableRecycleBin: Boolean
get() = mDatabaseKDBX != null
var isRecycleBinEnabled: Boolean
// TODO #394 isRecycleBinEnabled mDatabaseKDB
// Backup is always enabled in KDB database
get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false
set(value) {
mDatabaseKDBX?.isRecycleBinEnabled = value
@@ -288,12 +303,12 @@ class Database {
}
fun ensureRecycleBinExists(resources: Resources) {
mDatabaseKDB?.ensureRecycleBinExists()
mDatabaseKDB?.ensureBackupExists()
mDatabaseKDBX?.ensureRecycleBinExists(resources)
}
fun removeRecycleBin() {
// TODO #394 delete backup mDatabaseKDB?.removeRecycleBin()
// Don't allow remove backup in KDB
mDatabaseKDBX?.removeRecycleBin()
}
@@ -310,6 +325,8 @@ class Database {
fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
setDatabaseKDBX(DatabaseKDBX(databaseName, rootName))
this.fileUri = databaseUri
// Set Database state
this.loaded = true
}
@Throws(LoadDatabaseException::class)
@@ -317,7 +334,6 @@ class Database {
readOnly: Boolean,
contentResolver: ContentResolver,
cacheDirectory: File,
omitBackup: Boolean,
fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
@@ -338,7 +354,10 @@ class Database {
}
// Load Data, pass Uris as InputStreams
databaseInputStream = BufferedInputStream(UriUtil.getUriInputStream(contentResolver, uri))
val databaseStream = UriUtil.getUriInputStream(contentResolver, uri)
?: throw IOException("Database input stream cannot be retrieve")
databaseInputStream = BufferedInputStream(databaseStream)
if (!databaseInputStream.markSupported()) {
throw IOException("Input stream does not support mark.")
}
@@ -347,8 +366,8 @@ class Database {
databaseInputStream.mark(10)
// Get the file directory to save the attachments
val sig1 = databaseInputStream.readBytes4ToInt()
val sig2 = databaseInputStream.readBytes4ToInt()
val sig1 = databaseInputStream.readBytes4ToUInt()
val sig2 = databaseInputStream.readBytes4ToUInt()
// Return to the start
databaseInputStream.reset()
@@ -376,14 +395,12 @@ class Database {
else -> throw SignatureDatabaseException()
}
this.mSearchHelper = SearchHelper(omitBackup)
this.mSearchHelper = SearchHelper()
loaded = true
} catch (e: LoadDatabaseException) {
Log.e("KPD", "Database::loadData", e)
throw e
} catch (e: Exception) {
Log.e("KPD", "Database::loadData", e)
throw FileNotFoundDatabaseException()
} finally {
keyFileInputStream?.close()
@@ -391,60 +408,66 @@ class Database {
}
}
fun isGroupSearchable(group: Group, isOmitBackup: Boolean): Boolean {
return mDatabaseKDB?.isGroupSearchable(group.groupKDB, isOmitBackup) ?:
mDatabaseKDBX?.isGroupSearchable(group.groupKDBX, isOmitBackup) ?:
fun isGroupSearchable(group: Group, omitBackup: Boolean): Boolean {
return mDatabaseKDB?.isGroupSearchable(group.groupKDB, omitBackup) ?:
mDatabaseKDBX?.isGroupSearchable(group.groupKDBX, omitBackup) ?:
false
}
@JvmOverloads
fun search(str: String, max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.search(this, str, max)
fun createVirtualGroupFromSearch(searchQuery: String,
omitBackup: Boolean,
max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
searchQuery, SearchParameters(), omitBackup, max)
}
fun searchEntries(context: Context, query: String): Cursor? {
var cursorKDB: EntryCursorKDB? = null
var cursorKDBX: EntryCursorKDBX? = null
if (mDatabaseKDB != null)
cursorKDB = EntryCursorKDB()
if (mDatabaseKDBX != null)
cursorKDBX = EntryCursorKDBX()
val searchResult = search(query, SearchHelper.MAX_SEARCH_ENTRY)
if (searchResult != null) {
// Search in hide entries but not meta-stream
for (entry in searchResult.getChildEntries(*Group.ChildFilter.getDefaults(context))) {
entry.entryKDB?.let {
cursorKDB?.addEntry(it)
}
entry.entryKDBX?.let {
cursorKDBX?.addEntry(it)
}
}
}
return cursorKDB ?: cursorKDBX
fun createVirtualGroupFromSearchInfo(searchInfoString: String,
omitBackup: Boolean,
max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
searchInfoString, SearchParameters().apply {
searchInTitles = false
searchInUserNames = false
searchInPasswords = false
searchInUrls = true
searchInNotes = true
searchInOther = true
searchInUUIDs = false
searchInTags = false
ignoreCase = true
}, omitBackup, max)
}
fun getEntryFrom(cursor: Cursor): Entry? {
val iconFactory = mDatabaseKDB?.iconFactory ?: mDatabaseKDBX?.iconFactory ?: IconImageFactory()
return createEntry()?.apply {
startManageEntry(this)
mDatabaseKDB?.let {
entryKDB?.let { entryKDB ->
(cursor as EntryCursorKDB).populateEntry(entryKDB, iconFactory)
}
}
mDatabaseKDBX?.let {
entryKDBX?.let { entryKDBX ->
(cursor as EntryCursorKDBX).populateEntry(entryKDBX, iconFactory)
}
}
stopManageEntry(this)
val binaryPool: BinaryPool
get() {
return mDatabaseKDBX?.binaryPool ?: BinaryPool()
}
val allowMultipleAttachments: Boolean
get() {
if (mDatabaseKDB != null)
return false
if (mDatabaseKDBX != null)
return true
return false
}
fun buildNewBinary(cacheDirectory: File,
compressed: Boolean = false,
protected: Boolean = false): BinaryAttachment? {
return mDatabaseKDB?.buildNewBinary(cacheDirectory)
?: mDatabaseKDBX?.buildNewBinary(cacheDirectory, compressed, protected)
}
fun removeAttachmentIfNotUsed(attachment: Attachment) {
// No need in KDB database because unique attachment by entry
// Don't clear to fix upload multiple times
mDatabaseKDBX?.removeUnlinkedAttachment(attachment.binaryAttachment, false)
}
fun removeUnlinkedAttachments() {
// No check in database KDB because unique attachment by entry
mDatabaseKDBX?.removeUnlinkedAttachments(true)
}
@Throws(DatabaseOutputException::class)
@@ -492,11 +515,12 @@ class Database {
} else {
var outputStream: OutputStream? = null
try {
outputStream = contentResolver.openOutputStream(uri)
val pmo =
mDatabaseKDB?.let { DatabaseOutputKDB(it, outputStream) }
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, outputStream) }
pmo?.output()
outputStream = contentResolver.openOutputStream(uri, "rwt")
outputStream?.let { definedOutputStream ->
val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
databaseOutput?.output()
}
} catch (e: Exception) {
throw IOException(e)
} finally {
@@ -513,7 +537,9 @@ class Database {
mDatabaseKDBX?.clearCache()
// In all cases, delete all the files in the temp dir
try {
FileUtils.cleanDirectory(filesDirectory)
filesDirectory?.let { directory ->
cleanDirectory(directory)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to clear the directory cache.", e)
}
@@ -524,6 +550,17 @@ class Database {
this.loaded = false
}
private fun cleanDirectory(directory: File) {
directory.listFiles()?.let { files ->
for (file in files) {
if (file.isDirectory) {
cleanDirectory(file)
}
file.delete()
}
}
}
fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile)
?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile)
@@ -786,18 +823,25 @@ class Database {
}
}
fun startManageEntry(entry: Entry) {
fun startManageEntry(entry: Entry?) {
mDatabaseKDBX?.let {
entry.startToManageFieldReferences(it)
entry?.startToManageFieldReferences(it)
}
}
fun stopManageEntry(entry: Entry) {
fun stopManageEntry(entry: Entry?) {
mDatabaseKDBX?.let {
entry.stopToManageFieldReferences()
entry?.stopToManageFieldReferences()
}
}
/**
* @return true if database allows custom field
*/
fun allowEntryCustomFields(): Boolean {
return mDatabaseKDBX != null
}
/**
* Remove oldest history for each entry if more than max items or max memory
*/
@@ -805,7 +849,7 @@ class Database {
rootGroup?.doForEachChildAndForIt(
object : NodeHandler<Entry>() {
override fun operate(node: Entry): Boolean {
removeOldestEntryHistory(node)
removeOldestEntryHistory(node, binaryPool)
return true
}
},
@@ -813,34 +857,19 @@ class Database {
override fun operate(node: Group): Boolean {
return true
}
})
}
fun removeEachEntryHistory() {
rootGroup?.doForEachChildAndForIt(
object : NodeHandler<Entry>() {
override fun operate(node: Entry): Boolean {
node.removeAllHistory()
return true
}
},
object : NodeHandler<Group>() {
override fun operate(node: Group): Boolean {
return true
}
})
}
)
}
/**
* Remove oldest history if more than max items or max memory
*/
fun removeOldestEntryHistory(entry: Entry) {
fun removeOldestEntryHistory(entry: Entry, binaryPool: BinaryPool) {
mDatabaseKDBX?.let {
val maxItems = historyMaxItems
if (maxItems >= 0) {
while (entry.getHistory().size > maxItems) {
entry.removeOldestEntryFromHistory()
removeOldestEntryHistory(entry)
}
}
@@ -849,11 +878,10 @@ class Database {
while (true) {
var historySize: Long = 0
for (entryHistory in entry.getHistory()) {
historySize += entryHistory.getSize()
historySize += entryHistory.getSize(binaryPool)
}
if (historySize > maxSize) {
entry.removeOldestEntryFromHistory()
removeOldestEntryHistory(entry)
} else {
break
}
@@ -862,6 +890,22 @@ class Database {
}
}
private fun removeOldestEntryHistory(entry: Entry) {
entry.removeOldestEntryFromHistory()?.let {
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
removeAttachmentIfNotUsed(attachmentToRemove)
}
}
}
fun removeEntryHistory(entry: Entry, entryHistoryPosition: Int) {
entry.removeEntryFromHistory(entryHistoryPosition)?.let {
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
removeAttachmentIfNotUsed(attachmentToRemove)
}
}
}
companion object : SingletonHolder<Database>(::Database) {
private val TAG = Database::class.java.name

View File

@@ -23,6 +23,8 @@ import android.content.res.Resources
import android.os.Parcel
import android.os.Parcelable
import androidx.core.os.ConfigurationCompat
import org.joda.time.Duration
import org.joda.time.Instant
import java.text.SimpleDateFormat
import java.util.*
@@ -46,7 +48,7 @@ class DateInstant : Parcelable {
}
constructor(string: String) {
jDate = dateFormat.parse(string)
jDate = dateFormat.parse(string) ?: jDate
}
constructor() {
@@ -95,6 +97,7 @@ class DateInstant : Parcelable {
companion object {
val NEVER_EXPIRE = neverExpire
val IN_ONE_MONTH = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
private val dateFormat = SimpleDateFormat.getDateTimeInstance()
private val neverExpire: DateInstant
@@ -121,7 +124,7 @@ class DateInstant : Parcelable {
}
}
private fun isSameDate(d1: Date?, d2: Date?): Boolean {
private fun isSameDate(d1: Date, d2: Date): Boolean {
val cal1 = Calendar.getInstance()
cal1.time = d1
cal1.set(Calendar.MILLISECOND, 0)
@@ -142,7 +145,7 @@ class DateInstant : Parcelable {
fun getDateTimeString(resources: Resources, date: Date): String {
return java.text.DateFormat.getDateTimeInstance(
java.text.DateFormat.MEDIUM,
java.text.DateFormat.MEDIUM,
java.text.DateFormat.SHORT,
ConfigurationCompat.getLocales(resources.configuration)[0])
.format(date)
}

View File

@@ -26,17 +26,24 @@ import java.util.UUID
class DeletedObject {
var uuid: UUID = DatabaseVersioned.UUID_ZERO
var deletionTime: Date? = null
get() = if (field == null) {
Date(System.currentTimeMillis())
} else field
private var mDeletionTime: Date? = null
fun getDeletionTime(): Date {
if (mDeletionTime == null) {
mDeletionTime = Date(System.currentTimeMillis())
}
return mDeletionTime!!
}
fun setDeletionTime(deletionTime: Date) {
this.mDeletionTime = deletionTime
}
constructor()
@JvmOverloads
constructor(uuid: UUID, deletionTime: Date = Date()) {
this.uuid = uuid
this.deletionTime = deletionTime
this.mDeletionTime = deletionTime
}
override fun equals(other: Any?): Boolean {

View File

@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.database.element
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.BinaryPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
@@ -32,9 +33,6 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.otp.OtpElement
@@ -160,6 +158,12 @@ class Entry : Node, EntryVersionedInterface<Group> {
return contained ?: false
}
override fun nodeIndexInParentForNaturalOrder(): Int {
return entryKDB?.nodeIndexInParentForNaturalOrder()
?: entryKDBX?.nodeIndexInParentForNaturalOrder()
?: -1
}
override var creationTime: DateInstant
get() = entryKDB?.creationTime ?: entryKDBX?.creationTime ?: DateInstant()
set(value) {
@@ -241,13 +245,13 @@ class Entry : Node, EntryVersionedInterface<Group> {
"$PMS_TAN_ENTRY $username"
} else {
if (title.isEmpty())
if (username.isEmpty())
if (url.isEmpty())
nodeId.toString()
if (url.isEmpty())
if (username.isEmpty())
nodeId.toString()
else
url
username
else
username
url
else
title
}
@@ -279,66 +283,90 @@ class Entry : Node, EntryVersionedInterface<Group> {
}
/**
* Retrieve custom fields to show, key is the label, value is the value of field (protected or not)
* Retrieve extra fields to show, key is the label, value is the value of field (protected or not)
* @return Map of label/value
*/
val customFields: HashMap<String, ProtectedString>
get() = entryKDBX?.customFields ?: HashMap()
/**
* To redefine if version of entry allow custom field,
* @return true if entry allows custom field
*/
fun allowCustomFields(): Boolean {
return entryKDBX?.allowCustomFields() ?: false
}
fun removeAllFields() {
entryKDBX?.removeAllFields()
fun getExtraFields(): List<Field> {
val extraFields = ArrayList<Field>()
entryKDBX?.let {
for (field in it.customFields) {
extraFields.add(Field(field.key, field.value))
}
}
return extraFields
}
/**
* Update or add an extra field to the list (standard or custom)
* @param label Label of field, must be unique
* @param value Value of field
*/
fun putExtraField(label: String, value: ProtectedString) {
entryKDBX?.putExtraField(label, value)
fun putExtraField(field: Field) {
entryKDBX?.putExtraField(field.name, field.protectedValue)
}
fun getOtpElement(): OtpElement? {
return OtpEntryFields.parseFields { key ->
customFields[key]?.toString()
private fun addExtraFields(fields: List<Field>) {
fields.forEach {
putExtraField(it)
}
}
fun startToManageFieldReferences(db: DatabaseKDBX) {
entryKDBX?.startToManageFieldReferences(db)
private fun removeAllFields() {
entryKDBX?.removeAllFields()
}
fun getOtpElement(): OtpElement? {
entryKDBX?.let {
return OtpEntryFields.parseFields { key ->
it.customFields[key]?.toString()
}
}
return null
}
fun startToManageFieldReferences(database: DatabaseKDBX) {
entryKDBX?.startToManageFieldReferences(database)
}
fun stopToManageFieldReferences() {
entryKDBX?.stopToManageFieldReferences()
}
fun getAttachments(): ArrayList<EntryAttachment> {
val attachments = ArrayList<EntryAttachment>()
val binaryDescriptionKDB = entryKDB?.binaryDescription ?: ""
val binaryKDB = entryKDB?.binaryData
if (binaryKDB != null) {
attachments.add(EntryAttachment(binaryDescriptionKDB, binaryKDB))
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> {
val attachments = ArrayList<Attachment>()
entryKDB?.getAttachment()?.let {
attachments.add(it)
}
val actionEach = object : (Map.Entry<String, BinaryAttachment>)->Unit {
override fun invoke(mapEntry: Map.Entry<String, BinaryAttachment>) {
attachments.add(EntryAttachment(mapEntry.key, mapEntry.value))
}
entryKDBX?.getAttachments(binaryPool, inHistory)?.let {
attachments.addAll(it)
}
entryKDBX?.binaries?.forEach(actionEach)
return attachments
}
fun containsAttachment(): Boolean {
return entryKDB?.containsAttachment() == true
|| entryKDBX?.containsAttachment() == true
}
private fun addAttachments(binaryPool: BinaryPool, attachments: List<Attachment>) {
attachments.forEach {
putAttachment(it, binaryPool)
}
}
private fun removeAttachment(attachment: Attachment) {
entryKDB?.removeAttachment(attachment)
entryKDBX?.removeAttachment(attachment)
}
private fun removeAllAttachments() {
entryKDB?.removeAttachment()
entryKDBX?.removeAttachments()
}
private fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
entryKDB?.putAttachment(attachment)
entryKDBX?.putAttachment(attachment, binaryPool)
}
fun getHistory(): ArrayList<Entry> {
val history = ArrayList<Entry>()
val entryKDBXHistory = entryKDBX?.history ?: ArrayList()
@@ -354,20 +382,22 @@ class Entry : Node, EntryVersionedInterface<Group> {
}
}
fun removeEntryFromHistory(position: Int) {
entryKDBX?.removeEntryFromHistory(position)
fun removeEntryFromHistory(position: Int): Entry? {
entryKDBX?.removeEntryFromHistory(position)?.let {
return Entry(it)
}
return null
}
fun removeAllHistory() {
entryKDBX?.removeAllHistory()
fun removeOldestEntryFromHistory(): Entry? {
entryKDBX?.removeOldestEntryFromHistory()?.let {
return Entry(it)
}
return null
}
fun removeOldestEntryFromHistory() {
entryKDBX?.removeOldestEntryFromHistory()
}
fun getSize(): Long {
return entryKDBX?.size ?: 0L
fun getSize(binaryPool: BinaryPool): Long {
return entryKDBX?.getSize(binaryPool) ?: 0L
}
fun containsCustomData(): Boolean {
@@ -390,25 +420,54 @@ class Entry : Node, EntryVersionedInterface<Group> {
database?.stopManageEntry(this)
else
database?.startManageEntry(this)
entryInfo.id = nodeId.toString()
entryInfo.title = title
entryInfo.icon = icon
entryInfo.username = username
entryInfo.password = password
entryInfo.expires = expires
entryInfo.expiryTime = expiryTime
entryInfo.url = url
entryInfo.notes = notes
for (entry in customFields.entries) {
entryInfo.customFields.add(
Field(entry.key, entry.value))
}
entryInfo.customFields = getExtraFields()
// Add otpElement to generate token
entryInfo.otpModel = getOtpElement()?.otpModel
// Replace parameter fields by generated OTP fields
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
if (!raw) {
// Replace parameter fields by generated OTP fields
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
}
database?.binaryPool?.let { binaryPool ->
entryInfo.attachments = getAttachments(binaryPool)
}
if (!raw)
database?.stopManageEntry(this)
return entryInfo
}
fun setEntryInfo(database: Database?, newEntryInfo: EntryInfo) {
database?.startManageEntry(this)
removeAllFields()
removeAllAttachments()
// NodeId stay as is
title = newEntryInfo.title
icon = newEntryInfo.icon
username = newEntryInfo.username
password = newEntryInfo.password
expires = newEntryInfo.expires
expiryTime = newEntryInfo.expiryTime
url = newEntryInfo.url
notes = newEntryInfo.notes
addExtraFields(newEntryInfo.customFields)
database?.binaryPool?.let { binaryPool ->
addAttachments(binaryPool, newEntryInfo.attachments)
}
database?.stopManageEntry(this)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

View File

@@ -28,6 +28,7 @@ import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.*
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.*
import kotlin.collections.ArrayList
@@ -187,6 +188,12 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
return contained ?: false
}
override fun nodeIndexInParentForNaturalOrder(): Int {
return groupKDB?.nodeIndexInParentForNaturalOrder()
?: groupKDBX?.nodeIndexInParentForNaturalOrder()
?: -1
}
override var creationTime: DateInstant
get() = groupKDB?.creationTime ?: groupKDBX?.creationTime ?: DateInstant()
set(value) {
@@ -225,62 +232,66 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
override val isCurrentlyExpires: Boolean
get() = groupKDB?.isCurrentlyExpires ?: groupKDBX?.isCurrentlyExpires ?: false
override fun getChildGroups(): MutableList<Group> {
val children = ArrayList<Group>()
groupKDB?.getChildGroups()?.forEach {
children.add(Group(it))
}
groupKDBX?.getChildGroups()?.forEach {
children.add(Group(it))
}
return children
override fun getChildGroups(): List<Group> {
return groupKDB?.getChildGroups()?.map {
Group(it)
} ?:
groupKDBX?.getChildGroups()?.map {
Group(it)
} ?:
ArrayList()
}
override fun getChildEntries(): MutableList<Entry> {
// To cal function with vararg
return getChildEntries(*emptyArray<ChildFilter>())
override fun getChildEntries(): List<Entry> {
return groupKDB?.getChildEntries()?.map {
Entry(it)
} ?:
groupKDBX?.getChildEntries()?.map {
Entry(it)
} ?:
ArrayList()
}
fun getChildEntries(vararg filter: ChildFilter): MutableList<Entry> {
val children = ArrayList<Entry>()
val withoutMetaStream = filter.contains(ChildFilter.META_STREAM)
val showExpiredEntries = !filter.contains(ChildFilter.EXPIRED)
groupKDB?.getChildEntries()?.forEach {
val entryToAddAsChild = Entry(it)
if ((!withoutMetaStream || (withoutMetaStream && !entryToAddAsChild.isMetaStream))
&& (!entryToAddAsChild.isCurrentlyExpires or showExpiredEntries))
children.add(entryToAddAsChild)
}
groupKDBX?.getChildEntries()?.forEach {
val entryToAddAsChild = Entry(it)
if (!entryToAddAsChild.isCurrentlyExpires or showExpiredEntries)
children.add(entryToAddAsChild)
fun getChildEntriesInfo(database: Database): List<EntryInfo> {
val entriesInfo = ArrayList<EntryInfo>()
getChildEntries().forEach { entry ->
entriesInfo.add(entry.getEntryInfo(database))
}
return entriesInfo
}
return children
fun getFilteredChildEntries(filters: Array<ChildFilter>): List<Entry> {
val withoutMetaStream = filters.contains(ChildFilter.META_STREAM)
val showExpiredEntries = !filters.contains(ChildFilter.EXPIRED)
return groupKDB?.getChildEntries()?.filter {
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream))
&& (!it.isCurrentlyExpires or showExpiredEntries)
}?.map {
Entry(it)
} ?:
groupKDBX?.getChildEntries()?.filter {
!it.isCurrentlyExpires or showExpiredEntries
}?.map {
Entry(it)
} ?:
ArrayList()
}
fun getNumberOfChildEntries(filters: Array<ChildFilter> = emptyArray()): Int {
return getFilteredChildEntries(filters).size
}
/**
* Filter entries and return children
* @return List of direct children (one level below) as NodeVersioned
*/
fun getChildren(vararg filter: ChildFilter): List<Node> {
val children = ArrayList<Node>()
children.addAll(getChildGroups())
fun getChildren(): List<Node> {
return getChildGroups() + getChildEntries()
}
groupKDB?.let {
children.addAll(getChildEntries(*filter))
}
groupKDBX?.let {
// No MetasStream in V4
children.addAll(getChildEntries(*filter))
}
return children
fun getFilteredChildren(filters: Array<ChildFilter>): List<Node> {
return getChildGroups() + getFilteredChildEntries(filters)
}
override fun addChildGroup(group: Group) {

View File

@@ -20,149 +20,198 @@
package com.kunzisoft.keepass.database.element
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
import com.kunzisoft.keepass.database.element.node.Type
import java.util.*
enum class SortNodeEnum {
DB, TITLE, USERNAME, CREATION_TIME, LAST_MODIFY_TIME, LAST_ACCESS_TIME;
fun getNodeComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean): Comparator<Node> {
fun <G: GroupVersionedInterface<G, *>> getNodeComparator(sortNodeParameters: SortNodeParameters)
: Comparator<NodeVersionedInterface<G>> {
return when (this) {
DB -> NodeNaturalComparator(ascending, groupsBefore, false) // Force false because natural order contains recycle bin
TITLE -> NodeTitleComparator(ascending, groupsBefore, recycleBinBottom)
USERNAME -> NodeUsernameComparator(ascending, groupsBefore, recycleBinBottom)
CREATION_TIME -> NodeCreationComparator(ascending, groupsBefore, recycleBinBottom)
LAST_MODIFY_TIME -> NodeLastModificationComparator(ascending, groupsBefore, recycleBinBottom)
LAST_ACCESS_TIME -> NodeLastAccessComparator(ascending, groupsBefore, recycleBinBottom)
DB -> NodeNaturalComparator(sortNodeParameters) // Force false because natural order contains recycle bin
TITLE -> NodeTitleComparator(sortNodeParameters)
USERNAME -> NodeUsernameComparator(sortNodeParameters)
CREATION_TIME -> NodeCreationComparator(sortNodeParameters)
LAST_MODIFY_TIME -> NodeLastModificationComparator(sortNodeParameters)
LAST_ACCESS_TIME -> NodeLastAccessComparator(sortNodeParameters)
}
}
abstract class NodeComparator(var ascending: Boolean, var groupsBefore: Boolean, var recycleBinBottom: Boolean) : Comparator<Node> {
data class SortNodeParameters(var ascending: Boolean = true,
var groupsBefore: Boolean = true,
var recycleBinBottom: Boolean = true)
abstract fun compareBySpecificOrder(object1: Node, object2: Node): Int
abstract class NodeComparator
<
G: GroupVersionedInterface<*, *>,
T: NodeVersionedInterface<G>
>(var sortNodeParameters: SortNodeParameters)
: Comparator<T> {
private fun specificOrderOrHashIfEquals(object1: Node, object2: Node): Int {
val database = Database.getInstance()
abstract fun compareBySpecificOrder(object1: T, object2: T): Int
private fun specificOrderOrHashIfEquals(object1: T, object2: T): Int {
val specificOrderComp = compareBySpecificOrder(object1, object2)
return if (specificOrderComp == 0) {
object1.hashCode() - object2.hashCode()
} else if (!ascending) -specificOrderComp else specificOrderComp // If descending, revert
return when {
specificOrderComp == 0 -> object1.hashCode() - object2.hashCode()
sortNodeParameters.ascending -> specificOrderComp
else -> -specificOrderComp
}
}
override fun compare(object1: Node, object2: Node): Int {
override fun compare(object1: T, object2: T): Int {
if (object1 == object2)
return 0
if (object1.type == Type.GROUP) {
return if (object2.type == Type.GROUP) {
// RecycleBin at end of groups
val database = Database.getInstance()
if (database.isRecycleBinEnabled && recycleBinBottom) {
if (database.recycleBin == object1)
return 1
if (database.recycleBin == object2)
return -1
when (object1.type) {
Type.GROUP -> {
when (object2.type) {
Type.GROUP -> {
// RecycleBin at end of groups
if (database.isRecycleBinEnabled && sortNodeParameters.recycleBinBottom) {
if (database.recycleBin == object1)
return 1
if (database.recycleBin == object2)
return -1
}
return specificOrderOrHashIfEquals(object1, object2)
}
Type.ENTRY -> {
return if (sortNodeParameters.groupsBefore)
-1
else
1
}
}
specificOrderOrHashIfEquals(object1, object2)
} else if (object2.type == Type.ENTRY) {
if (groupsBefore)
-1
else
1
} else {
-1
}
} else if (object1.type == Type.ENTRY) {
return if (object2.type == Type.ENTRY) {
specificOrderOrHashIfEquals(object1, object2)
} else if (object2.type == Type.GROUP) {
if (groupsBefore)
1
else
-1
} else {
-1
Type.ENTRY -> {
return when (object2.type) {
Type.GROUP -> {
if (sortNodeParameters.groupsBefore)
1
else
-1
}
Type.ENTRY -> {
specificOrderOrHashIfEquals(object1, object2)
}
}
}
}
// Type not known
return -1
}
}
/**
* Comparator of node by natural database placement
*/
class NodeNaturalComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
class NodeNaturalComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
return object1.nodePositionInParent.compareTo(object2.nodePositionInParent)
override fun compareBySpecificOrder(object1: T, object2: T): Int {
return object1.nodeIndexInParentForNaturalOrder()
.compareTo(object2.nodeIndexInParentForNaturalOrder())
}
}
/**
* Comparator of Node by Title
*/
class NodeTitleComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
class NodeTitleComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
return object1.title.compareTo(object2.title, ignoreCase = true)
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val titleCompare = object1.title.compareTo(object2.title, ignoreCase = true)
return if (titleCompare == 0)
NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
titleCompare
}
}
/**
* Comparator of Node by Username, Groups by title
*/
class NodeUsernameComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
class NodeUsernameComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) {
override fun compareBySpecificOrder(object1: T, object2: T): Int {
return if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) {
// To get username if it's a ref
return (object1 as Entry).getEntryInfo(Database.getInstance()).username
.compareTo((object2 as Entry).getEntryInfo(Database.getInstance()).username,
val usernameCompare = (object1 as Entry).getEntryInfo(database).username
.compareTo((object2 as Entry).getEntryInfo(database).username,
ignoreCase = true)
if (usernameCompare == 0)
NodeTitleComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
usernameCompare
} else {
NodeTitleComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
}
return NodeTitleComparator(ascending, groupsBefore, recycleBinBottom).compare(object1, object2)
}
}
/**
* Comparator of node by creation
*/
class NodeCreationComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
class NodeCreationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
return object1.creationTime.date
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val creationCompare = object1.creationTime.date
.compareTo(object2.creationTime.date)
return if (creationCompare == 0)
NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
creationCompare
}
}
/**
* Comparator of node by last modification
*/
class NodeLastModificationComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
class NodeLastModificationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
return object1.lastModificationTime.date
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val lastModificationCompare = object1.lastModificationTime.date
.compareTo(object2.lastModificationTime.date)
return if (lastModificationCompare == 0)
NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
lastModificationCompare
}
}
/**
* Comparator of node by last access
*/
class NodeLastAccessComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
class NodeLastAccessComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
return object1.lastAccessTime.date
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val lastAccessCompare = object1.lastAccessTime.date
.compareTo(object2.lastAccessTime.date)
return if (lastAccessCompare == 0)
NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
lastAccessCompare
}
}
}

View File

@@ -0,0 +1,207 @@
/*
* Copyright 2018 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.database.element.database
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.stream.readBytes
import java.io.*
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
class BinaryAttachment : Parcelable {
private var dataFile: File? = null
var isCompressed: Boolean = false
private set
var isProtected: Boolean = false
private set
var isCorrupted: Boolean = false
fun length(): Long {
return dataFile?.length() ?: 0
}
/**
* Empty protected binary
*/
constructor()
constructor(dataFile: File, compressed: Boolean = false, protected: Boolean = false) {
this.dataFile = dataFile
this.isCompressed = compressed
this.isProtected = protected
}
private constructor(parcel: Parcel) {
parcel.readString()?.let {
dataFile = File(it)
}
isCompressed = parcel.readByte().toInt() != 0
isProtected = parcel.readByte().toInt() != 0
isCorrupted = parcel.readByte().toInt() != 0
}
@Throws(IOException::class)
fun getInputDataStream(): InputStream {
return when {
length() > 0 -> FileInputStream(dataFile!!)
else -> ByteArrayInputStream(ByteArray(0))
}
}
@Throws(IOException::class)
fun getUnGzipInputDataStream(): InputStream {
return if (isCompressed)
GZIPInputStream(getInputDataStream())
else
getInputDataStream()
}
@Throws(IOException::class)
fun getOutputDataStream(): OutputStream {
return when {
dataFile != null -> FileOutputStream(dataFile!!)
else -> throw IOException("Unable to write in an unknown file")
}
}
@Throws(IOException::class)
fun getGzipOutputDataStream(): OutputStream {
return if (isCompressed) {
GZIPOutputStream(getOutputDataStream())
} else {
getOutputDataStream()
}
}
@Throws(IOException::class)
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
dataFile?.let { concreteDataFile ->
// To compress, create a new binary with file
if (!isCompressed) {
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
GZIPOutputStream(FileOutputStream(fileBinaryCompress)).use { outputStream ->
getInputDataStream().use { inputStream ->
inputStream.readBytes(bufferSize) { buffer ->
outputStream.write(buffer)
}
}
}
// Remove unGzip file
if (concreteDataFile.delete()) {
if (fileBinaryCompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = true
}
}
}
}
}
@Throws(IOException::class)
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
dataFile?.let { concreteDataFile ->
if (isCompressed) {
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
FileOutputStream(fileBinaryDecompress).use { outputStream ->
getUnGzipInputDataStream().use { inputStream ->
inputStream.readBytes(bufferSize) { buffer ->
outputStream.write(buffer)
}
}
}
// Remove gzip file
if (concreteDataFile.delete()) {
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = false
}
}
}
}
}
@Throws(IOException::class)
fun clear() {
if (dataFile != null && !dataFile!!.delete())
throw IOException("Unable to delete temp file " + dataFile!!.absolutePath)
}
override fun equals(other: Any?): Boolean {
if (this === other)
return true
if (other == null || javaClass != other.javaClass)
return false
if (other !is BinaryAttachment)
return false
var sameData = false
if (dataFile != null && dataFile == other.dataFile)
sameData = true
return isCompressed == other.isCompressed
&& isProtected == other.isProtected
&& isCorrupted == other.isCorrupted
&& sameData
}
override fun hashCode(): Int {
var result = 0
result = 31 * result + if (isCompressed) 1 else 0
result = 31 * result + if (isProtected) 1 else 0
result = 31 * result + if (isCorrupted) 1 else 0
result = 31 * result + dataFile!!.hashCode()
return result
}
override fun toString(): String {
return dataFile.toString()
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(dataFile?.absolutePath)
dest.writeByte((if (isCompressed) 1 else 0).toByte())
dest.writeByte((if (isProtected) 1 else 0).toByte())
dest.writeByte((if (isCorrupted) 1 else 0).toByte())
}
companion object {
private val TAG = BinaryAttachment::class.java.name
@JvmField
val CREATOR: Parcelable.Creator<BinaryAttachment> = object : Parcelable.Creator<BinaryAttachment> {
override fun createFromParcel(parcel: Parcel): BinaryAttachment {
return BinaryAttachment(parcel)
}
override fun newArray(size: Int): Array<BinaryAttachment?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -19,52 +19,137 @@
*/
package com.kunzisoft.keepass.database.element.database
import android.util.SparseArray
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import java.io.IOException
class BinaryPool {
private val pool = SparseArray<BinaryAttachment>()
private val pool = LinkedHashMap<Int, BinaryAttachment>()
/**
* To get a binary by the pool key (ref attribute in entry)
*/
operator fun get(key: Int): BinaryAttachment? {
return pool[key]
}
fun put(key: Int, value: BinaryAttachment) {
pool.put(key, value)
/**
* To linked a binary with a pool key, if the pool key doesn't exists, create an unused one
*/
fun put(key: Int?, value: BinaryAttachment) {
if (key == null)
put(value)
else
pool[key] = value
}
fun doForEachBinary(action: (key: Int, binary: BinaryAttachment) -> Unit) {
for (i in 0 until pool.size()) {
action.invoke(i, pool.get(pool.keyAt(i)))
/**
* To put a [binaryAttachment] in the pool,
* if already exists, replace the current one,
* else add it with a new key
*/
fun put(binaryAttachment: BinaryAttachment): Int {
var key = findKey(binaryAttachment)
if (key == null) {
key = findUnusedKey()
}
pool[key] = binaryAttachment
return key
}
/**
* Remove a binary from the pool, the file is not deleted
*/
@Throws(IOException::class)
fun clear() {
doForEachBinary { _, binary ->
binary.clear()
fun remove(binaryAttachment: BinaryAttachment) {
findKey(binaryAttachment)?.let {
pool.remove(it)
}
pool.clear()
// Don't clear attachment here because a file can be used in many BinaryAttachment
}
fun add(fileBinary: BinaryAttachment) {
if (findKey(fileBinary) == null) {
pool.put(findUnusedKey(), fileBinary)
}
}
fun findUnusedKey(): Int {
var unusedKey = pool.size()
while (get(unusedKey) != null)
/**
* Utility method to find an unused key in the pool
*/
private fun findUnusedKey(): Int {
var unusedKey = 0
while (pool[unusedKey] != null)
unusedKey++
return unusedKey
}
fun findKey(pb: BinaryAttachment): Int? {
for (i in 0 until pool.size()) {
if (pool.get(pool.keyAt(i)) == pb) return i
/**
* Return key of [binaryAttachmentToRetrieve] or null if not found
*/
private fun findKey(binaryAttachmentToRetrieve: BinaryAttachment): Int? {
val contains = pool.containsValue(binaryAttachmentToRetrieve)
return if (!contains)
null
else {
for ((key, binary) in pool) {
if (binary == binaryAttachmentToRetrieve) {
return key
}
}
return null
}
return null
}
/**
* Utility method to order binaries and solve index problem in database v4
*/
private fun orderedBinaries(): List<KeyBinary> {
val keyBinaryList = ArrayList<KeyBinary>()
for ((key, binary) in pool) {
keyBinaryList.add(KeyBinary(key, binary))
}
return keyBinaryList
}
/**
* To register a binary with a ref corresponding to an ordered index
*/
fun getBinaryIndexFromKey(key: Int): Int? {
val index = orderedBinaries().indexOfFirst { it.key == key }
return if (index < 0)
null
else
index
}
/**
* Different from doForEach, provide an ordered index to each binary
*/
fun doForEachOrderedBinary(action: (index: Int, keyBinary: KeyBinary) -> Unit) {
orderedBinaries().forEachIndexed(action)
}
/**
* To do an action on each binary in the pool
*/
fun doForEachBinary(action: (binary: BinaryAttachment) -> Unit) {
pool.values.forEach { action.invoke(it) }
}
@Throws(IOException::class)
fun clear() {
doForEachBinary {
it.clear()
}
pool.clear()
}
override fun toString(): String {
val stringBuffer = StringBuffer()
for ((key, value) in pool) {
if (stringBuffer.isNotEmpty())
stringBuffer.append(", {$key:$value}")
else
stringBuffer.append("{$key:$value}")
}
return stringBuffer.toString()
}
/**
* Utility data class to order binaries
*/
data class KeyBinary(val key: Int, val binary: BinaryAttachment)
}

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