Compare commits

..

245 Commits
2.7 ... 2.8.2

Author SHA1 Message Date
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
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
202 changed files with 5168 additions and 2383 deletions

View File

@@ -1,3 +1,22 @@
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)

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass"
minSdkVersion 14
targetSdkVersion 29
versionCode = 35
versionName = "2.7"
versionCode = 38
versionName = "2.8.2"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
@@ -50,7 +50,7 @@ android {
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 {
@@ -69,7 +69,7 @@ android {
buildConfigField "String", "BUILD_VERSION", "\"free\""
buildConfigField "boolean", "FULL_VERSION", "false"
buildConfigField "boolean", "CLOSED_STORE", "true"
buildConfigField "String[]", "STYLES_DISABLED", "{}"
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
}
@@ -95,13 +95,15 @@ def room_version = "2.2.5"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.0.1'
implementation 'androidx.core:core-ktx:1.2.0'
// Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:1.3.1"
implementation 'androidx.fragment:fragment-ktx:1.2.5'
// To upgrade with style
implementation 'com.google.android.material:material:1.0.0'
// Database

View File

@@ -6,23 +6,23 @@ import junit.framework.TestCase
class UnsignedIntTest: TestCase() {
fun testUInt() {
val standardInt = UnsignedInt(15).toInt()
val standardInt = UnsignedInt(15).toKotlinInt()
assertEquals(15, standardInt)
val unsignedInt = UnsignedInt(-1).toLong()
val unsignedInt = UnsignedInt(-1).toKotlinLong()
assertEquals(4294967295L, unsignedInt)
}
fun testMaxValue() {
val maxValue = UnsignedInt.MAX_VALUE.toLong()
val maxValue = UnsignedInt.MAX_VALUE.toKotlinLong()
assertEquals(4294967295L, maxValue)
val longValue = UnsignedInt.fromLong(4294967295L).toLong()
val longValue = UnsignedInt.fromKotlinLong(4294967295L).toKotlinLong()
assertEquals(longValue, maxValue)
}
fun testLong() {
val longValue = UnsignedInt.fromLong(50L).toInt()
val longValue = UnsignedInt.fromKotlinLong(50L).toKotlinInt()
assertEquals(50, longValue)
val uIntLongValue = UnsignedInt.fromLong(4294967290).toLong()
val uIntLongValue = UnsignedInt.fromKotlinLong(4294967290).toKotlinLong()
assertEquals(4294967290, uIntLongValue)
}
}

View File

@@ -35,11 +35,11 @@ class ValuesTest : TestCase() {
}
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() {
@@ -62,11 +62,11 @@ class ValuesTest : 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) {
@@ -103,11 +103,11 @@ class ValuesTest : 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 ValuesTest : 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 = UnsignedInt(UnsignedInt.fromByte(value)).toByte()
val dest: Byte = UnsignedInt(UnsignedInt.fromKotlinByte(value)).toKotlinByte()
assert(value == dest)
}

View File

@@ -124,8 +124,7 @@
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"
@@ -161,10 +160,6 @@
</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

@@ -89,7 +89,7 @@ class EntryActivity : LockingActivity() {
private var mAttachmentsToDownload: HashMap<Int, EntryAttachment> = HashMap()
private var clipboardHelper: ClipboardHelper? = null
private var firstLaunchOfActivity: Boolean = false
private var mFirstLaunchOfActivity: Boolean = false
private var iconColor: Int = 0
@@ -130,14 +130,17 @@ class EntryActivity : LockingActivity() {
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 -> {
@@ -196,7 +199,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
@@ -215,7 +218,7 @@ class EntryActivity : LockingActivity() {
}
}
firstLaunchOfActivity = false
mFirstLaunchOfActivity = false
}
override fun onPause() {
@@ -306,11 +309,7 @@ class EntryActivity : LockingActivity() {
// Assign custom fields
if (entry.allowCustomFields()) {
entryContentsView?.clearExtraFields()
for (element in entry.customFields.entries) {
val label = element.key
val value = element.value
for ((label, value) in entry.customFields) {
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
if (allowCopyProtectedField) {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, View.OnClickListener {
@@ -332,25 +331,18 @@ class EntryActivity : LockingActivity() {
entryContentsView?.setHiddenPasswordStyle(!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
entryContentsView?.assignAttachments(entry.getAttachments()) { attachmentItem ->
when (attachmentItem.downloadState) {
AttachmentState.NULL, AttachmentState.ERROR, AttachmentState.COMPLETE -> {
createDocument(this, attachmentItem.name)?.let { requestCode ->
mAttachmentsToDownload[requestCode] = attachmentItem
}
}
else -> {
// TODO Stop download
}
}
}
entryContentsView?.refreshAttachments()
// Assign dates
entryContentsView?.assignCreationDate(entry.creationTime)
@@ -370,16 +362,9 @@ class EntryActivity : LockingActivity() {
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
taColorAccent.recycle()
}
val entryHistory = entry.getHistory()
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)
@@ -520,7 +505,7 @@ class EntryActivity : LockingActivity() {
}
R.id.menu_restore_entry_history -> {
mEntryLastVersion?.let { mainEntry ->
mProgressDialogThread?.startDatabaseRestoreEntryHistory(
mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory(
mainEntry,
mEntryHistoryPosition,
!mReadOnly && mAutoSaveEnable)
@@ -528,20 +513,25 @@ class EntryActivity : LockingActivity() {
}
R.id.menu_delete_entry_history -> {
mEntryLastVersion?.let { mainEntry ->
mProgressDialogThread?.startDatabaseDeleteEntryHistory(
mProgressDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(
mainEntry,
mEntryHistoryPosition,
!mReadOnly && mAutoSaveEnable)
}
}
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
@@ -555,6 +545,8 @@ class EntryActivity : LockingActivity() {
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

@@ -31,7 +31,6 @@ import android.view.View
import android.widget.DatePicker
import android.widget.TimePicker
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.ActionMenuView
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.widget.NestedScrollView
@@ -45,7 +44,10 @@ import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.model.FocusedEditField
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
@@ -57,11 +59,13 @@ import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.view.EntryEditContentsView
import com.kunzisoft.keepass.view.showActionError
import com.kunzisoft.keepass.view.updateLockPaddingLeft
import org.joda.time.DateTime
import java.util.*
class EntryEditActivity : LockingActivity(),
IconPickerDialogFragment.IconPickerListener,
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
GeneratePasswordDialogFragment.GeneratePasswordListener,
SetOTPDialogFragment.CreateOtpListener,
DatePickerDialog.OnDateSetListener,
@@ -80,10 +84,12 @@ class EntryEditActivity : LockingActivity(),
private var coordinatorLayout: CoordinatorLayout? = null
private var scrollView: NestedScrollView? = null
private var entryEditContentsView: EntryEditContentsView? = null
private var entryEditAddToolBar: ActionMenuView? = null
private var saveView: View? = null
private var entryEditAddToolBar: Toolbar? = null
private var validateButton: View? = null
private var lockView: View? = null
private var mFocusedEditExtraField: FocusedEditField? = null
// Education
private var entryEditActivityEducation: EntryEditActivityEducation? = null
@@ -114,6 +120,9 @@ class EntryEditActivity : LockingActivity(),
.show(supportFragmentManager, "DatePickerFragment")
}
}
entryEditContentsView?.entryPasswordGeneratorView?.setOnClickListener {
openPasswordGenerator()
}
lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener {
@@ -121,7 +130,7 @@ class EntryEditActivity : LockingActivity(),
}
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(entryEditContentsView)
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
@@ -146,8 +155,7 @@ class EntryEditActivity : LockingActivity(),
}
// Create the new entry from the current one
if (savedInstanceState == null
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
if (savedInstanceState?.containsKey(KEY_NEW_ENTRY) != true) {
mEntry?.let { entry ->
// Create a copy to modify
mNewEntry = Entry(entry).also { newEntry ->
@@ -162,8 +170,7 @@ class EntryEditActivity : LockingActivity(),
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let {
mIsNew = true
// Create an empty new entry
if (savedInstanceState == null
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
if (savedInstanceState?.containsKey(KEY_NEW_ENTRY) != true) {
mNewEntry = mDatabase?.createEntry()
}
mParent = mDatabase?.getGroupById(it)
@@ -181,11 +188,14 @@ class EntryEditActivity : LockingActivity(),
}
// Retrieve the new entry after an orientation change
if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
if (savedInstanceState?.containsKey(KEY_NEW_ENTRY) == true) {
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY)
}
if (savedInstanceState?.containsKey(EXTRA_FIELD_FOCUSED_ENTRY) == true) {
mFocusedEditExtraField = savedInstanceState.getParcelable(EXTRA_FIELD_FOCUSED_ENTRY)
}
// Close the activity if entry or parent can't be retrieve
if (mNewEntry == null || mParent == null) {
finish()
@@ -205,6 +215,12 @@ class EntryEditActivity : LockingActivity(),
entryEditAddToolBar?.apply {
menuInflater.inflate(R.menu.entry_edit, menu)
menu.findItem(R.id.menu_add_field).apply {
val allowLock = PreferencesUtil.showLockDatabaseButton(context)
isEnabled = allowLock
isVisible = allowLock
}
menu.findItem(R.id.menu_add_field).apply {
val allowCustomField = mNewEntry?.allowCustomFields() == true
isEnabled = allowCustomField
@@ -219,10 +235,6 @@ class EntryEditActivity : LockingActivity(),
setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.menu_generate_password -> {
openPasswordGenerator()
true
}
R.id.menu_add_field -> {
addNewCustomField()
true
@@ -237,14 +249,14 @@ class EntryEditActivity : LockingActivity(),
}
// Save button
saveView = findViewById(R.id.entry_edit_validate)
saveView?.setOnClickListener { saveEntry() }
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 -> {
@@ -264,6 +276,9 @@ class EntryEditActivity : LockingActivity(),
} else {
View.GONE
}
// Padding if lock button visible
entryEditAddToolBar?.updateLockPaddingLeft()
}
private fun populateViewsWithEntry(newEntry: Entry) {
@@ -286,10 +301,11 @@ class EntryEditActivity : LockingActivity(),
if (expires)
expiresDate = newEntry.expiryTime
notes = newEntry.notes
for (entry in newEntry.customFields.entries) {
post {
putCustomField(entry.key, entry.value)
}
assignExtraFields(newEntry.customFields.mapTo(ArrayList()) {
Field(it.key, it.value)
}, mFocusedEditExtraField)
assignAttachments(newEntry.getAttachments()) { attachment ->
newEntry.removeAttachment(attachment)
}
}
}
@@ -310,10 +326,11 @@ class EntryEditActivity : LockingActivity(),
if (entryView.expires) {
expiryTime = entryView.expiresDate
}
notes = entryView. notes
entryView.customFields.forEach { customField ->
notes = entryView.notes
entryView.getExtraField().forEach { customField ->
putExtraField(customField.name, customField.protectedValue)
}
mFocusedEditExtraField = entryView.getExtraFieldFocused()
}
}
@@ -335,12 +352,18 @@ 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")
}
override fun onNewCustomFieldApproved(label: String, protection: Boolean) {
entryEditContentsView?.putExtraField(Field(label, ProtectedString(protection)))
}
override fun onNewCustomFieldCanceled(label: String, protection: Boolean) {}
private fun setupOTP() {
// Retrieve the current otpElement if exists
// and open the dialog to set up the OTP
@@ -352,7 +375,6 @@ class EntryEditActivity : LockingActivity(),
* Saves the new entry or update an existing entry in the database
*/
private fun saveEntry() {
// Launch a validation and show the error if present
if (entryEditContentsView?.isValid() == true) {
// Clone the entry
@@ -369,7 +391,7 @@ class EntryEditActivity : LockingActivity(),
// Open a progress dialog and save entry
if (mIsNew) {
mParent?.let { parent ->
mProgressDialogThread?.startDatabaseCreateEntry(
mProgressDatabaseTaskProvider?.startDatabaseCreateEntry(
newEntry,
parent,
!mReadOnly && mAutoSaveEnable
@@ -377,7 +399,7 @@ class EntryEditActivity : LockingActivity(),
}
} else {
mEntry?.let { oldEntry ->
mProgressDialogThread?.startDatabaseUpdateEntry(
mProgressDatabaseTaskProvider?.startDatabaseUpdateEntry(
oldEntry,
newEntry,
!mReadOnly && mAutoSaveEnable
@@ -405,7 +427,7 @@ class EntryEditActivity : LockingActivity(),
}
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
val passwordGeneratorView: View? = entryEditAddToolBar?.findViewById(R.id.menu_generate_password)
val passwordGeneratorView: View? = entryEditContentsView?.entryPasswordGeneratorView
val generatePasswordEducationPerformed = passwordGeneratorView != null
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
passwordGeneratorView,
@@ -445,7 +467,7 @@ class EntryEditActivity : LockingActivity(),
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_save_database -> {
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
}
R.id.menu_contribute -> {
MenuUtil.onContributionItemSelected(this)
@@ -463,7 +485,7 @@ class EntryEditActivity : LockingActivity(),
// Update the otp field with otpauth:// url
val otpField = OtpEntryFields.buildOtpField(otpElement,
mEntry?.title, mEntry?.username)
entryEditContentsView?.putCustomField(otpField.name, otpField.protectedValue)
entryEditContentsView?.putExtraField(otpField)
mEntry?.putExtraField(otpField.name, otpField.protectedValue)
}
@@ -512,6 +534,10 @@ class EntryEditActivity : LockingActivity(),
outState.putParcelable(KEY_NEW_ENTRY, it)
}
mFocusedEditExtraField?.let {
outState.putParcelable(EXTRA_FIELD_FOCUSED_ENTRY, it)
}
super.onSaveInstanceState(outState)
}
@@ -569,6 +595,7 @@ class EntryEditActivity : LockingActivity(),
// SaveInstanceState
const val KEY_NEW_ENTRY = "new_entry"
const val EXTRA_FIELD_FOCUSED_ENTRY = "EXTRA_FIELD_FOCUSED_ENTRY"
// Keys for callback
const val ADD_ENTRY_RESULT_CODE = 31

View File

@@ -32,9 +32,11 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
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
@@ -48,14 +50,17 @@ 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
@@ -65,10 +70,11 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
// Views
private var coordinatorLayout: CoordinatorLayout? = null
private var fileManagerExplanationButton: View? = null
private var databaseButtonsContainerView: View? = 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
@@ -78,7 +84,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
private var mOpenFileHelper: OpenFileHelper? = null
private var mProgressDialogThread: ProgressDialogThread? = null
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -97,8 +103,6 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
}
databaseButtonsContainerView = findViewById(R.id.database_buttons_container)
// Create database button
createDatabaseButtonView = findViewById(R.id.create_database_button)
createDatabaseButtonView?.setOnClickListener { createNewFile() }
@@ -120,26 +124,25 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
(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
)
}
}
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
// Remove from app database
mFileDatabaseHistoryAction?.deleteFileDatabaseHistory(fileDatabaseHistoryToDelete) { fileHistoryDeleted ->
// Remove from adapter
fileHistoryDeleted?.let { databaseFileHistoryDeleted ->
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileHistoryDeleted)
mAdapterDatabaseHistory?.notifyDataSetChanged()
}
}
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
true
}
mAdapterDatabaseHistory?.setOnSaveAliasListener { fileDatabaseHistoryWithNewAlias ->
mFileDatabaseHistoryAction?.addOrUpdateFileDatabaseHistory(fileDatabaseHistoryWithNewAlias)
// Update in app database
databaseFilesViewModel.updateDatabaseFile(fileDatabaseHistoryWithNewAlias)
}
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
@@ -162,12 +165,47 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
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 -> {
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)
}
}
}
}
@@ -271,8 +309,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
// Show open and create button or special mode
if (mSelectionMode) {
// Disable buttons if in selection mode or request for autofill
databaseButtonsContainerView?.visibility = View.GONE
// 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.
@@ -281,7 +319,6 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
// No Activity found that can handle this intent.
createDatabaseButtonView?.visibility = View.GONE
}
databaseButtonsContainerView?.visibility = View.VISIBLE
}
val database = Database.getInstance()
@@ -290,34 +327,20 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
} else {
// Construct adapter with listeners
if (PreferencesUtil.showRecentFiles(this)) {
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
databaseFileHistoryList?.let { historyList ->
val hideBrokenLocations = PreferencesUtil.hideBrokenLocations(this@FileDatabaseSelectActivity)
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(
// Show only uri accessible
historyList.filter {
if (hideBrokenLocations) {
FileDatabaseInfo(this@FileDatabaseSelectActivity,
it.databaseUri).exists
} else
true
})
mAdapterDatabaseHistory?.notifyDataSetChanged()
}
}
databaseFilesViewModel.loadListOfDatabases()
} else {
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
mAdapterDatabaseHistory?.notifyDataSetChanged()
}
// Register progress task
mProgressDialogThread?.registerProgressTask()
mProgressDatabaseTaskProvider?.registerProgressTask()
}
}
override fun onPause() {
// Unregister progress task
mProgressDialogThread?.unregisterProgressTask()
mProgressDatabaseTaskProvider?.unregisterProgressTask()
super.onPause()
}
@@ -338,7 +361,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
mDatabaseFileUri?.let { databaseUri ->
// Create the new database
mProgressDialogThread?.startDatabaseCreate(
mProgressDatabaseTaskProvider?.startDatabaseCreate(
databaseUri,
masterPasswordChecked,
masterPassword,
@@ -366,8 +389,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
if (uri != null) {
launchPasswordActivityWithPath(uri)
}
@@ -403,11 +425,12 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
// If no recent files
val createDatabaseEducationPerformed = createDatabaseButtonView != null && createDatabaseButtonView!!.visibility == View.VISIBLE
val createDatabaseEducationPerformed =
createDatabaseButtonView != null && createDatabaseButtonView!!.visibility == View.VISIBLE
&& mAdapterDatabaseHistory != null
&& mAdapterDatabaseHistory!!.itemCount > 0
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
createDatabaseButtonView!!,
createDatabaseButtonView!!,
{
createNewFile()
},

View File

@@ -32,6 +32,7 @@ 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
@@ -87,6 +88,7 @@ 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
@@ -103,6 +105,8 @@ class GroupActivity : LockingActivity(),
private var mCurrentGroupIsASearch: Boolean = false
private var mRequestStartupSearch = true
private var actionNodeMode: ActionMode? = null
// To manage history in selection mode
private var mSelectionModeCountBackStack = 0
@@ -118,15 +122,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)
@@ -150,7 +152,7 @@ class GroupActivity : LockingActivity(),
taTextColor.recycle()
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(addNodeButtonView)
resetAppTimeoutWhenViewFocusedOrChanged(rootContainerView)
// Retrieve elements after an orientation change
if (savedInstanceState != null) {
@@ -216,7 +218,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 ->
@@ -389,7 +391,8 @@ class GroupActivity : LockingActivity(),
// If it's a search
if (Intent.ACTION_SEARCH == intent.action) {
val searchString = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
return mDatabase?.createVirtualGroupFromSearch(searchString)
return mDatabase?.createVirtualGroupFromSearch(searchString,
PreferencesUtil.omitBackup(this))
}
// else a real group
else {
@@ -474,7 +477,8 @@ class GroupActivity : LockingActivity(),
enableAddGroup(addGroupEnabled)
enableAddEntry(addEntryEnabled)
showButton()
if (actionNodeMode == null)
showButton()
}
}
@@ -504,7 +508,8 @@ class GroupActivity : LockingActivity(),
}
override fun onScrolled(dy: Int) {
addNodeButtonView?.hideButtonOnScrollListener(dy)
if (actionNodeMode == null)
addNodeButtonView?.hideOrShowButtonOnScrollListener(dy)
}
override fun onNodeClick(node: Node) {
@@ -547,18 +552,28 @@ class GroupActivity : LockingActivity(),
}
}
private var actionNodeMode: ActionMode? = null
private fun finishNodeAction() {
actionNodeMode?.finish()
actionNodeMode = null
addNodeButtonView?.showButton()
}
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 {
@@ -615,7 +630,7 @@ class GroupActivity : LockingActivity(),
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
// Copy
mCurrentGroup?.let { newParent ->
mProgressDialogThread?.startDatabaseCopyNodes(
mProgressDatabaseTaskProvider?.startDatabaseCopyNodes(
nodes,
newParent,
!mReadOnly && mAutoSaveEnable
@@ -625,7 +640,7 @@ class GroupActivity : LockingActivity(),
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
// Move
mCurrentGroup?.let { newParent ->
mProgressDialogThread?.startDatabaseMoveNodes(
mProgressDatabaseTaskProvider?.startDatabaseMoveNodes(
nodes,
newParent,
!mReadOnly && mAutoSaveEnable
@@ -659,7 +674,7 @@ class GroupActivity : LockingActivity(),
&& database.isRecycleBinEnabled
&& database.recycleBin != mCurrentGroup) {
mProgressDialogThread?.startDatabaseDeleteNodes(
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
nodes,
!mReadOnly && mAutoSaveEnable
)
@@ -674,7 +689,7 @@ class GroupActivity : LockingActivity(),
}
override fun permanentlyDeleteNodes(nodes: List<Node>) {
mProgressDialogThread?.startDatabaseDeleteNodes(
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
nodes,
!mReadOnly && mAutoSaveEnable
)
@@ -693,6 +708,8 @@ class GroupActivity : LockingActivity(),
assignGroupViewElements()
// Refresh suggestions to change preferences
mSearchSuggestionAdapter?.reInit(this)
// Padding if lock button visible
toolbarAction?.updateLockPaddingLeft()
}
override fun onPause() {
@@ -833,7 +850,7 @@ class GroupActivity : LockingActivity(),
//onSearchRequested();
return true
R.id.menu_save_database -> {
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
return true
}
R.id.menu_empty_recycle_bin -> {
@@ -867,7 +884,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
@@ -889,7 +906,7 @@ class GroupActivity : LockingActivity(),
}
}
// If group updated save it in the database
mProgressDialogThread?.startDatabaseUpdateGroup(
mProgressDatabaseTaskProvider?.startDatabaseUpdateGroup(
oldGroupToUpdate,
updateGroup,
!mReadOnly && mAutoSaveEnable

View File

@@ -266,14 +266,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 {
@@ -318,7 +319,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 {
@@ -348,7 +349,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
nodeActionSelectionMode = false
returnValue
}
else -> false
else -> actionModeCallback.onActionItemClicked(mode, item)
}
}
@@ -358,6 +359,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
mAdapter?.unselectActionNodes()
nodeActionPasteMode = PasteMode.UNDEFINED
nodeActionSelectionMode = false
actionModeCallback.onDestroyActionMode(mode)
}
}
}

View File

@@ -21,7 +21,6 @@ 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.pm.PackageManager
import android.net.Uri
@@ -34,10 +33,12 @@ import android.util.Log
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
@@ -48,10 +49,9 @@ import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
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
@@ -60,16 +60,15 @@ 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.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.*
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
@@ -83,11 +82,12 @@ open class PasswordActivity : SpecialModeActivity() {
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
@@ -107,7 +107,7 @@ open class PasswordActivity : SpecialModeActivity() {
field = value
}
private var mProgressDialogThread: ProgressDialogThread? = null
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
private var mAllowAutoOpenBiometricPrompt: Boolean = true
@@ -129,12 +129,12 @@ open class PasswordActivity : SpecialModeActivity() {
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)
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
keyFileSelectionView?.apply {
@@ -165,12 +165,34 @@ open class PasswordActivity : SpecialModeActivity() {
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)
}
mProgressDialogThread = ProgressDialogThread(this).apply {
// 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 -> {
@@ -207,7 +229,7 @@ open class PasswordActivity : SpecialModeActivity() {
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)
}
@@ -229,7 +251,7 @@ open class PasswordActivity : SpecialModeActivity() {
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()
@@ -353,7 +375,12 @@ open class PasswordActivity : SpecialModeActivity() {
clearCredentialsViews()
}
mProgressDialogThread?.registerProgressTask()
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)
@@ -361,72 +388,23 @@ open class PasswordActivity : SpecialModeActivity() {
else
mAllowAutoOpenBiometricPrompt
initUriFromIntent()
mDatabaseFileUri?.let { databaseFileUri ->
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
}
checkPermission()
}
}
private fun initUriFromIntent() {
/*
// "canXrite" doesn't work with Google Drive, don't really know why?
mForceReadOnly = mDatabaseFileUri?.let {
!FileDatabaseInfo(this, it).canWrite
} ?: false
*/
mForceReadOnly = mDatabaseFileUri?.let {
!FileDatabaseInfo(this, it).exists
} ?: true
// Post init uri with KeyFile if needed
if (mRememberKeyFile && (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
// Retrieve KeyFile in a thread
mDatabaseFileUri?.let { databaseUri ->
FileDatabaseHistoryAction.getInstance(applicationContext)
.getKeyFileUriByDatabaseUri(databaseUri) {
onPostInitUri(databaseUri, it)
}
}
} else {
onPostInitUri(mDatabaseFileUri, mDatabaseKeyFileUri)
}
}
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
// Define title
databaseFileUri?.let {
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
filenameView?.text = title
}
}
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
// Define Key File text
if (mRememberKeyFile) {
populateKeyFileTextView(keyFileUri)
}
// Define listeners for default database checkbox and validate button
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
var newDefaultFileUri: Uri? = null
if (isChecked) {
newDefaultFileUri = databaseFileUri ?: newDefaultFileUri
}
PreferencesUtil.saveDefaultDatabasePath(this, newDefaultFileUri)
val backupManager = BackupManager(this@PasswordActivity)
backupManager.dataChanged()
}
// Define listener for validate button
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
// Retrieve settings for default database
val defaultFilename = PreferencesUtil.getDefaultDatabasePath(this)
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)
@@ -443,7 +421,6 @@ open class PasswordActivity : SpecialModeActivity() {
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,
@@ -475,6 +452,7 @@ open class PasswordActivity : SpecialModeActivity() {
biometricInitialize = true
} else {
advancedUnlockedManager?.destroy()
advancedUnlockInfoView?.visibility = View.GONE
}
}
if (!biometricInitialize) {
@@ -530,7 +508,7 @@ open class PasswordActivity : SpecialModeActivity() {
}
override fun onPause() {
mProgressDialogThread?.unregisterProgressTask()
mProgressDatabaseTaskProvider?.unregisterProgressTask()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
advancedUnlockedManager?.destroy()
@@ -605,7 +583,7 @@ open class PasswordActivity : SpecialModeActivity() {
readOnly: Boolean,
cipherDatabaseEntity: CipherDatabaseEntity?,
fixDuplicateUUID: Boolean) {
mProgressDialogThread?.startDatabaseLoad(
mProgressDatabaseTaskProvider?.startDatabaseLoad(
databaseUri,
password,
keyFile,

View File

@@ -0,0 +1,139 @@
/*
* 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.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
import android.view.inputmethod.EditorInfo
import android.widget.Button
import android.widget.CompoundButton
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
class EntryCustomFieldDialogFragment: DialogFragment() {
private var entryCustomFieldListener: EntryCustomFieldListener? = null
private var customFieldLabelContainer: TextInputLayout? = null
private var customFieldLabel: TextView? = 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 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)
customFieldProtectionButton = root?.findViewById(R.id.entry_custom_field_protection)
val builder = AlertDialog.Builder(activity)
builder.setView(root)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel) { _, _ ->
entryCustomFieldListener?.onNewCustomFieldCanceled(
customFieldLabel?.text.toString(),
customFieldProtectionButton?.isChecked == true
)
}
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()) {
entryCustomFieldListener?.onNewCustomFieldApproved(
customFieldLabel?.text.toString(),
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(label: String, protection: Boolean)
fun onNewCustomFieldCanceled(label: String, protection: Boolean)
}
companion object {
fun getInstance(): EntryCustomFieldDialogFragment {
return EntryCustomFieldDialogFragment()
}
}
}

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)
@@ -152,6 +153,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 +388,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

@@ -89,13 +89,13 @@ class OpenFileHelper {
@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)
@@ -108,10 +108,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)
@@ -226,12 +226,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,14 +19,15 @@
*/
package com.kunzisoft.keepass.activities.lock
import android.annotation.SuppressLint
import android.content.Intent
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.ReadOnlyHelper
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
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.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
@@ -50,7 +51,7 @@ abstract class LockingActivity : SpecialModeActivity() {
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?) {
@@ -71,6 +72,7 @@ abstract class LockingActivity : SpecialModeActivity() {
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
// Add onActivityForResult response
setResult(RESULT_EXIT_LOCK)
closeOptionsMenu()
finish()
}
registerLockReceiver(mLockReceiver)
@@ -78,7 +80,7 @@ abstract class LockingActivity : SpecialModeActivity() {
mExitLock = false
mProgressDialogThread = ProgressDialogThread(this)
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -94,7 +96,7 @@ abstract class LockingActivity : SpecialModeActivity() {
override fun onResume() {
super.onResume()
mProgressDialogThread?.registerProgressTask()
mProgressDatabaseTaskProvider?.registerProgressTask()
// To refresh when back to normal workflow from selection workflow
mReadOnlyToSave = ReadOnlyHelper.retrieveReadOnlyFromIntent(intent)
@@ -129,7 +131,7 @@ abstract class LockingActivity : SpecialModeActivity() {
override fun onPause() {
LOCKING_ACTIVITY_UI_VISIBLE = false
mProgressDialogThread?.unregisterProgressTask()
mProgressDatabaseTaskProvider?.unregisterProgressTask()
super.onPause()
@@ -151,11 +153,21 @@ abstract class LockingActivity : SpecialModeActivity() {
/**
* 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)
}
}

View File

@@ -46,12 +46,19 @@ abstract class StylishFragment : Fragment() {
// To fix status bar color
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val window = requireActivity().window
val attrColorPrimaryDark = intArrayOf(android.R.attr.colorPrimaryDark)
val taColorPrimaryDark = contextThemed?.theme?.obtainStyledAttributes(attrColorPrimaryDark)
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,85 @@
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 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)
}
fun onBindDeleteButton(holder: T, deleteButton: View, item: Item, position: Int) {
deleteButton.apply {
visibility = View.VISIBLE
if (mItemToRemove == item) {
holder.itemView.collapse(true) {
deleteItem(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

@@ -21,7 +21,6 @@ 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
@@ -33,11 +32,10 @@ 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>() {
class EntryAttachmentsItemsAdapter(context: Context, private val editable: Boolean)
: AnimatedItemsAdapter<EntryAttachment, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
private val inflater: LayoutInflater = LayoutInflater.from(context)
var entryAttachmentsList: MutableList<EntryAttachment> = ArrayList()
var onItemClickListener: ((item: EntryAttachment, position: Int)->Unit)? = null
var onItemClickListener: ((item: EntryAttachment)->Unit)? = null
private val mDatabase = Database.getInstance()
@@ -46,8 +44,9 @@ class EntryAttachmentsAdapter(val context: Context) : RecyclerView.Adapter<Entry
}
override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) {
val entryAttachment = entryAttachmentsList[position]
val entryAttachment = itemsList[position]
holder.itemView.visibility = View.VISIBLE
holder.binaryFileTitle.text = entryAttachment.name
holder.binaryFileSize.text = Formatter.formatFileSize(context,
entryAttachment.binaryAttachment.length())
@@ -61,40 +60,43 @@ class EntryAttachmentsAdapter(val context: Context) : RecyclerView.Adapter<Entry
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
if (editable) {
holder.binaryFileProgressContainer.visibility = View.GONE
holder.binaryFileDeleteButton.apply {
visibility = View.VISIBLE
onBindDeleteButton(holder, this, entryAttachment, position)
}
} else {
holder.binaryFileProgressContainer.visibility = View.VISIBLE
holder.binaryFileDeleteButton.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)
}
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 }
val indexEntryAttachment = itemsList.indexOfLast { current -> current.name == entryAttachment.name }
if (indexEntryAttachment != -1) {
entryAttachmentsList[indexEntryAttachment] = entryAttachment
itemsList[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 binaryFileProgressContainer: View = itemView.findViewById(R.id.item_attachment_progress_container)
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
var binaryFileDeleteButton: View = itemView.findViewById(R.id.item_attachment_delete_button)
}
}

View File

@@ -0,0 +1,186 @@
/*
* 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.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.core.widget.doOnTextChanged
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.model.FocusedEditField
import com.kunzisoft.keepass.view.EditTextSelectable
import com.kunzisoft.keepass.view.applyFontVisibility
class EntryExtraFieldsItemsAdapter(context: Context)
: AnimatedItemsAdapter<Field, EntryExtraFieldsItemsAdapter.EntryExtraFieldViewHolder>(context) {
var applyFontVisibility = false
set(value) {
field = value
notifyDataSetChanged()
}
private var mValueViewInputType: Int = 0
private var mLastFocusedEditField = FocusedEditField()
private var mLastFocusedTimestamp: Long = 0L
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryExtraFieldViewHolder {
val view = EntryExtraFieldViewHolder(
inflater.inflate(R.layout.item_entry_edit_extra_field, parent, false)
)
mValueViewInputType = view.extraFieldValue.inputType
return view
}
override fun onBindViewHolder(holder: EntryExtraFieldViewHolder, position: Int) {
val extraField = itemsList[position]
holder.itemView.visibility = View.VISIBLE
if (extraField.protectedValue.isProtected) {
holder.extraFieldValueContainer.isPasswordVisibilityToggleEnabled = true
holder.extraFieldValue.inputType = EditorInfo.TYPE_TEXT_VARIATION_PASSWORD or mValueViewInputType
} else {
holder.extraFieldValueContainer.isPasswordVisibilityToggleEnabled = false
holder.extraFieldValue.inputType = mValueViewInputType
}
holder.extraFieldValueContainer.hint = extraField.name
holder.extraFieldValue.apply {
setText(extraField.protectedValue.toString())
// To Fix focus in RecyclerView
setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
setFocusField(extraField, selectionStart, selectionEnd)
} else {
// request focus on last text focused
if (focusedTimestampNotExpired()) {
requestFocusField(this, extraField, false)
} else {
removeFocusField(extraField)
}
}
}
addOnSelectionChangedListener(object: EditTextSelectable.OnSelectionChangedListener {
override fun onSelectionChanged(start: Int, end: Int) {
mLastFocusedEditField.apply {
cursorSelectionStart = start
cursorSelectionEnd = end
}
}
})
requestFocusField(this, extraField, true)
doOnTextChanged { text, _, _, _ ->
extraField.protectedValue.stringValue = text.toString()
}
if (applyFontVisibility)
applyFontVisibility()
}
holder.extraFieldDeleteButton.apply {
onBindDeleteButton(holder, this, extraField, position)
}
}
fun assignItems(items: List<Field>, focusedEditField: FocusedEditField?) {
focusedEditField?.let {
setFocusField(it, true)
}
super.assignItems(items)
}
override fun putItem(item: Field) {
setFocusField(mLastFocusedEditField.apply {
field = item
cursorSelectionStart = -1
cursorSelectionEnd = -1
}, true)
super.putItem(item)
}
private fun setFocusField(field: Field,
selectionStart: Int,
selectionEnd: Int,
force: Boolean = false) {
mLastFocusedEditField.apply {
this.field = field
this.cursorSelectionStart = selectionStart
this.cursorSelectionEnd = selectionEnd
}
setFocusField(mLastFocusedEditField, force)
}
private fun setFocusField(field: FocusedEditField, force: Boolean = false) {
mLastFocusedEditField = field
mLastFocusedTimestamp = if (force) 0L else System.currentTimeMillis()
}
private fun removeFocusField(field: Field? = null) {
if (field == null || mLastFocusedEditField.field == field) {
mLastFocusedEditField.destroy()
}
}
private fun requestFocusField(editText: EditText, field: Field, setSelection: Boolean) {
if (field == mLastFocusedEditField.field) {
editText.apply {
post {
if (setSelection) {
setEditTextSelection(editText)
}
requestFocus()
}
}
}
}
private fun setEditTextSelection(editText: EditText) {
try {
var newCursorPositionStart = mLastFocusedEditField.cursorSelectionStart
var newCursorPositionEnd = mLastFocusedEditField.cursorSelectionEnd
// Cursor at end if 0 or less
if (newCursorPositionStart < 0 || newCursorPositionEnd < 0) {
newCursorPositionStart = (editText.text?:"").length
newCursorPositionEnd = newCursorPositionStart
}
editText.setSelection(newCursorPositionStart, newCursorPositionEnd)
} catch (ignoredException: Exception) {}
}
private fun focusedTimestampNotExpired(): Boolean {
return mLastFocusedTimestamp == 0L || (mLastFocusedTimestamp + FOCUS_TIMESTAMP) > System.currentTimeMillis()
}
fun getFocusedField(): FocusedEditField {
return mLastFocusedEditField
}
class EntryExtraFieldViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var extraFieldValueContainer: TextInputLayout = itemView.findViewById(R.id.entry_extra_field_value_container)
var extraFieldValue: EditTextSelectable = itemView.findViewById(R.id.entry_extra_field_value)
var extraFieldDeleteButton: View = itemView.findViewById(R.id.entry_extra_field_delete)
}
companion object {
// time to focus element when a keyboard appears
private const val FOCUS_TIMESTAMP = 400L
}
}

View File

@@ -22,31 +22,33 @@ 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
@@ -63,35 +65,41 @@ 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
if (fileDatabaseInfo.exists) {
holder.fileInformation.clearColorFilter()
if (databaseFile.databaseFileExists) {
holder.fileInformationButton.clearColorFilter()
} else {
holder.fileInformation.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
holder.fileInformationButton.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
}
// Modification
fileDatabaseInfo.getModificationString()?.let {
databaseFile.databaseLastModified?.let {
holder.fileModification.text = it
holder.fileModification.visibility = View.VISIBLE
} ?: run {
@@ -99,7 +107,7 @@ class FileDatabaseHistoryAdapter(private val context: Context)
}
// Size
fileDatabaseInfo.getSizeString()?.let {
databaseFile.databaseSize?.let {
holder.fileSize.text = it
holder.fileSize.visibility = View.VISIBLE
} ?: run {
@@ -107,15 +115,24 @@ class FileDatabaseHistoryAdapter(private val context: Context)
}
// 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()
@@ -130,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
@@ -160,24 +179,58 @@ class FileDatabaseHistoryAdapter(private val context: Context)
listDatabaseFiles.clear()
}
fun addDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<FileDatabaseHistoryEntity>) {
listDatabaseFiles.clear()
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
fun addDatabaseFileHistory(fileDatabaseHistoryToAdd: DatabaseFile) {
listDatabaseFiles.add(0, fileDatabaseHistoryToAdd)
notifyItemInserted(0)
}
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: FileDatabaseHistoryEntity) {
listDatabaseFiles.remove(fileDatabaseHistoryToDelete)
fun updateDatabaseFileHistory(fileDatabaseHistoryToUpdate: DatabaseFile) {
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToUpdate)
if (listDatabaseFiles.remove(fileDatabaseHistoryToUpdate)) {
listDatabaseFiles.add(index, fileDatabaseHistoryToUpdate)
notifyItemChanged(index)
}
}
fun setOnFileDatabaseHistoryOpenListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
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
}
@@ -185,8 +238,9 @@ class FileDatabaseHistoryAdapter(private val context: Context)
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)

View File

@@ -46,7 +46,8 @@ class SearchEntryCursorAdapter(private val context: Context,
private val cursorInflater: LayoutInflater? = context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
private var displayUsername: Boolean = false
private var mDisplayUsername: Boolean = false
private var mOmitBackup: Boolean = true
private val iconColor: Int
init {
@@ -59,7 +60,8 @@ 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 {
@@ -93,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 {
""
@@ -129,7 +131,9 @@ class SearchEntryCursorAdapter(private val context: Context,
if (database.type == DatabaseKDBX.TYPE)
cursorKDBX = EntryCursorKDBX()
val searchGroup = database.createVirtualGroupFromSearch(query, SearchHelper.MAX_SEARCH_ENTRY)
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))) {

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,116 @@ 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 ->
val fileDatabaseHistory = FileDatabaseHistoryEntity(
databaseUri.toString(),
databaseFileToAddOrUpdate.databaseAlias ?: "",
databaseFileToAddOrUpdate.keyFileUri?.toString(),
System.currentTimeMillis()
)
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(fileDatabaseHistory.databaseUri)
// 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) {
ActionDatabaseAsyncTask(
IOActionTask(
{
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
}
@@ -121,7 +189,7 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
}
fun deleteAllKeyFiles() {
ActionDatabaseAsyncTask(
IOActionTask(
{
databaseFileHistoryDao.deleteAllKeyFiles()
}
@@ -129,7 +197,7 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
}
fun deleteAll() {
ActionDatabaseAsyncTask(
IOActionTask(
{
databaseFileHistoryDao.deleteAll()
}

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,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,12 +53,19 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
private var biometricMode: Mode = Mode.UNAVAILABLE
/**
* 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)
init {
@@ -77,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
@@ -210,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)
}
}
@@ -277,6 +287,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
fun destroy() {
// Close the biometric prompt
allowOpenBiometricPrompt = false
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
// Restore the checked listener
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
@@ -324,7 +335,9 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
}
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

@@ -20,12 +20,10 @@
package com.kunzisoft.keepass.crypto.finalkey
import java.io.IOException
import java.lang.Exception
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

View File

@@ -61,10 +61,10 @@ class Argon2Kdf internal constructor() : KdfEngine() {
UnsignedInt(it)
}
val memory = kdfParameters.getUInt64(PARAM_MEMORY)?.div(MEMORY_BLOCK_SIZE)?.let {
UnsignedInt.fromLong(it)
UnsignedInt.fromKotlinLong(it)
}
val iterations = kdfParameters.getUInt64(PARAM_ITERATIONS)?.let {
UnsignedInt.fromLong(it)
UnsignedInt.fromKotlinLong(it)
}
val version = kdfParameters.getUInt32(PARAM_VERSION)?.let {
UnsignedInt(it)
@@ -124,16 +124,16 @@ class Argon2Kdf internal constructor() : KdfEngine() {
override fun getParallelism(kdfParameters: KdfParameters): Long {
return kdfParameters.getUInt32(PARAM_PARALLELISM)?.let {
UnsignedInt(it).toLong()
UnsignedInt(it).toKotlinLong()
} ?: defaultParallelism
}
override fun setParallelism(kdfParameters: KdfParameters, parallelism: Long) {
kdfParameters.setUInt32(PARAM_PARALLELISM, UnsignedInt.fromLong(parallelism))
kdfParameters.setUInt32(PARAM_PARALLELISM, UnsignedInt.fromKotlinLong(parallelism))
}
override val defaultParallelism: Long
get() = DEFAULT_PARALLELISM.toLong()
get() = DEFAULT_PARALLELISM.toKotlinLong()
override val minParallelism: Long
get() = MIN_PARALLELISM
@@ -173,13 +173,13 @@ class Argon2Kdf internal constructor() : KdfEngine() {
private val MAX_VERSION = UnsignedInt(0x13)
private const val MIN_SALT = 8
private val MAX_SALT = UnsignedInt.MAX_VALUE.toLong()
private val MAX_SALT = UnsignedInt.MAX_VALUE.toKotlinLong()
private const val MIN_ITERATIONS: Long = 1L
private const val MAX_ITERATIONS = 4294967295L
private const val MIN_MEMORY = (1024 * 8).toLong()
private val MAX_MEMORY = UnsignedInt.MAX_VALUE.toLong()
private val MAX_MEMORY = UnsignedInt.MAX_VALUE.toKotlinLong()
private const val MEMORY_BLOCK_SIZE: Long = 1024L
private const val MIN_PARALLELISM: Long = 1L

View File

@@ -34,12 +34,12 @@ public class Argon2Native {
return nTransformMasterKey(
password,
salt,
parallelism.toInt(),
memory.toInt(),
iterations.toInt(),
parallelism.toKotlinInt(),
memory.toKotlinInt(),
iterations.toKotlinInt(),
secretKey,
associatedData,
version.toInt());
version.toKotlinInt());
}
private static native byte[] nTransformMasterKey(byte[] password, byte[] salt, int parallelism,

View File

@@ -52,7 +52,7 @@ abstract class KdfEngine : ObjectNameResource, Serializable {
get() = 1
open val maxKeyRounds: Long
get() = UnsignedInt.MAX_VALUE.toLong()
get() = UnsignedInt.MAX_VALUE.toKotlinLong()
/*
* MEMORY
@@ -73,7 +73,7 @@ abstract class KdfEngine : ObjectNameResource, Serializable {
get() = 1
open val maxMemoryUsage: Long
get() = UnsignedInt.MAX_VALUE.toLong()
get() = UnsignedInt.MAX_VALUE.toKotlinLong()
/*
* PARALLELISM
@@ -94,7 +94,7 @@ abstract class KdfEngine : ObjectNameResource, Serializable {
get() = 1L
open val maxParallelism: Long
get() = UnsignedInt.MAX_VALUE.toLong()
get() = UnsignedInt.MAX_VALUE.toKotlinLong()
companion object {
const val UNKNOWN_VALUE: Long = -1L

View File

@@ -37,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
@@ -45,7 +45,7 @@ open class AssignPasswordInDatabaseRunnable (
if (withMasterPassword)
this.mMasterPassword = masterPassword
if (withKeyFile)
this.mKeyFile = keyFile
this.mKeyFileUri = keyFile
}
override fun onStartRun() {
@@ -55,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)

View File

@@ -34,7 +34,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() {
@@ -42,29 +43,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(context.applicationContext.filesDir)
setError(e)
}
super.onStartRun()
}
override fun onFinishRun() {
super.onFinishRun()
override fun onActionRun() {
super.onActionRun()
if (result.isSuccess) {
// Add database to recent files
if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context.applicationContext)
.addOrUpdateDatabaseUri(mDatabaseUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mKeyFile else null)
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,7 +27,6 @@ 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
@@ -39,17 +38,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(context.applicationContext.filesDir)
}
override fun onActionRun() {
@@ -57,21 +53,17 @@ class LoadDatabaseRunnable(private val context: Context,
mDatabase.loadData(mUri, mPass, mKey,
mReadonly,
context.contentResolver,
cacheDirectory,
mOmitBackup,
context.applicationContext.filesDir,
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
if (PreferencesUtil.rememberDatabaseLocations(context)) {
@@ -88,11 +80,12 @@ class LoadDatabaseRunnable(private val context: Context,
// Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context)
// Start the opening notification
DatabaseOpenNotificationService.start(context)
} else {
mDatabase.closeAndClear(cacheDirectory)
mDatabase.closeAndClear(context.applicationContext.filesDir)
}
}
override fun onFinishRun() {
mLoadDatabaseResult?.invoke(result)
}
}

View File

@@ -27,7 +27,6 @@ import android.os.Build
import android.os.Bundle
import android.os.IBinder
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.element.Entry
@@ -37,7 +36,6 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
@@ -68,18 +66,17 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
import com.kunzisoft.keepass.timeout.TimeoutHelper
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
@@ -90,9 +87,6 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
TimeoutHelper.temporarilyDisableTimeout()
// Stop the opening notification
DatabaseOpenNotificationService.stop(activity)
startDialog(titleId, messageId, warningId)
}
@@ -102,21 +96,8 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
override fun onStopAction(actionTask: String, result: ActionRunnable.Result) {
onActionFinish?.invoke(actionTask, result)
// Remove the progress task
stopDialog()
TimeoutHelper.releaseTemporarilyDisableTimeout()
val inTime = if (activity is LockingActivity) {
TimeoutHelper.checkTimeAndLockIfTimeout(activity)
} else {
TimeoutHelper.checkTime(activity)
}
// Start the opening notification if in time
// (databaseOpenService is open manually in Action Open Task)
if (actionTask != ACTION_DATABASE_LOAD_TASK && inTime) {
DatabaseOpenNotificationService.start(activity)
}
}
}
@@ -237,12 +218,8 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
activity.stopService(intentDatabaseTask)
if (bundle != null)
intentDatabaseTask.putExtras(bundle)
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)
}
/*
@@ -261,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)
}
@@ -275,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)
@@ -294,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)
}

View File

@@ -46,7 +46,6 @@ 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.model.SearchInfo
import com.kunzisoft.keepass.stream.readBytes4ToUInt
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.SingletonHolder
@@ -71,6 +70,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() {
@@ -309,6 +315,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)
@@ -316,7 +324,6 @@ class Database {
readOnly: Boolean,
contentResolver: ContentResolver,
cacheDirectory: File,
omitBackup: Boolean,
fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
@@ -378,14 +385,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()
@@ -393,20 +398,24 @@ 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
}
fun createVirtualGroupFromSearch(searchQuery: String,
omitBackup: Boolean,
max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this, searchQuery, SearchParameters(), max)
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
searchQuery, SearchParameters(), omitBackup, max)
}
fun createVirtualGroupFromSearchInfo(searchInfoString: String,
omitBackup: Boolean,
max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this, searchInfoString, SearchParameters().apply {
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
searchInfoString, SearchParameters().apply {
searchInTitles = false
searchInUserNames = false
searchInPasswords = false
@@ -416,7 +425,7 @@ class Database {
searchInUUIDs = false
searchInTags = false
ignoreCase = true
}, max)
}, omitBackup, max)
}
@Throws(DatabaseOutputException::class)

View File

@@ -32,7 +32,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
@@ -329,22 +328,31 @@ class Entry : Node, EntryVersionedInterface<Group> {
fun getAttachments(): ArrayList<EntryAttachment> {
val attachments = ArrayList<EntryAttachment>()
val binaryDescriptionKDB = entryKDB?.binaryDescription ?: ""
val binaryKDB = entryKDB?.binaryData
if (binaryKDB != null) {
attachments.add(EntryAttachment(binaryDescriptionKDB, binaryKDB))
entryKDB?.binaryData?.let { binaryKDB ->
attachments.add(EntryAttachment(entryKDB?.binaryDescription ?: "", binaryKDB))
}
val actionEach = object : (Map.Entry<String, BinaryAttachment>)->Unit {
override fun invoke(mapEntry: Map.Entry<String, BinaryAttachment>) {
attachments.add(EntryAttachment(mapEntry.key, mapEntry.value))
entryKDBX?.binaries?.let { binariesKDBX ->
for ((key, value) in binariesKDBX) {
attachments.add(EntryAttachment(key, value))
}
}
entryKDBX?.binaries?.forEach(actionEach)
return attachments
}
fun removeAttachment(attachment: EntryAttachment) {
entryKDB?.apply {
if (binaryDescription == attachment.name
&& binaryData == attachment.binaryAttachment) {
binaryDescription = ""
binaryData = null
}
}
entryKDBX?.removeProtectedBinary(attachment.name)
}
fun getHistory(): ArrayList<Entry> {
val history = ArrayList<Entry>()
val entryKDBXHistory = entryKDBX?.history ?: ArrayList()

View File

@@ -25,14 +25,12 @@ import android.os.Parcelable
import com.kunzisoft.keepass.utils.ParcelableUtil
import com.kunzisoft.keepass.utils.UnsignedInt
import java.util.HashMap
class AutoType : Parcelable {
var enabled = true
var obfuscationOptions = OBF_OPT_NONE
var defaultSequence = ""
private var windowSeqPairs = HashMap<String, String>()
private var windowSeqPairs = LinkedHashMap<String, String>()
constructor()
@@ -58,7 +56,7 @@ class AutoType : Parcelable {
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeByte((if (enabled) 1 else 0).toByte())
dest.writeInt(obfuscationOptions.toInt())
dest.writeInt(obfuscationOptions.toKotlinInt())
dest.writeString(defaultSequence)
ParcelableUtil.writeStringParcelableMap(dest, windowSeqPairs)
}

View File

@@ -36,6 +36,7 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.utils.ParcelableUtil
import com.kunzisoft.keepass.utils.UnsignedLong
import java.util.*
import kotlin.collections.LinkedHashMap
class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
@@ -58,9 +59,9 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
super.icon = value
}
var iconCustom = IconImageCustom.UNKNOWN_ICON
private var customData = HashMap<String, String>()
var fields = HashMap<String, ProtectedString>()
var binaries = HashMap<String, BinaryAttachment>()
private var customData = LinkedHashMap<String, String>()
var fields = LinkedHashMap<String, ProtectedString>()
var binaries = LinkedHashMap<String, BinaryAttachment>()
var foregroundColor = ""
var backgroundColor = ""
var overrideURL = ""
@@ -123,7 +124,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeParcelable(iconCustom, flags)
dest.writeLong(usageCount.toLong())
dest.writeLong(usageCount.toKotlinLong())
dest.writeParcelable(locationChanged, flags)
ParcelableUtil.writeStringParcelableMap(dest, customData)
ParcelableUtil.writeStringParcelableMap(dest, flags, fields)
@@ -260,13 +261,11 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|| key == STR_NOTES)
}
var customFields = HashMap<String, ProtectedString>()
var customFields = LinkedHashMap<String, ProtectedString>()
get() {
field.clear()
for (entry in fields.entries) {
val key = entry.key
val value = entry.value
if (!isStandardField(entry.key)) {
for ((key, value) in fields) {
if (!isStandardField(key)) {
field[key] = ProtectedString(value.isProtected, decodeRefKey(mDecodeRef, key))
}
}
@@ -289,6 +288,10 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
binaries[key] = value
}
fun removeProtectedBinary(name: String) {
binaries.remove(name)
}
fun sizeOfHistory(): Int {
return history.size
}
@@ -334,7 +337,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
override fun touch(modified: Boolean, touchParents: Boolean) {
super.touch(modified, touchParents)
// TODO unsigned long
usageCount = UnsignedLong(usageCount.toLong() + 1)
usageCount = UnsignedLong(usageCount.toKotlinLong() + 1)
}
companion object {

View File

@@ -102,7 +102,7 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeParcelable(iconCustom, flags)
dest.writeLong(usageCount.toLong())
dest.writeLong(usageCount.toKotlinLong())
dest.writeParcelable(locationChanged, flags)
// TODO ParcelableUtil.writeStringParcelableMap(dest, customData);
dest.writeString(notes)

View File

@@ -26,7 +26,7 @@ class ProtectedString : Parcelable {
var isProtected: Boolean = false
private set
private var stringValue: String = ""
var stringValue: String = ""
constructor(toCopy: ProtectedString) {
this.isProtected = toCopy.isProtected

View File

@@ -97,11 +97,11 @@ class DatabaseHeaderKDB : DatabaseHeader() {
const val BUF_SIZE = 124
fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean {
return sig1.toInt() == PWM_DBSIG_1.toInt() && sig2.toInt() == DBSIG_2.toInt()
return sig1.toKotlinInt() == PWM_DBSIG_1.toKotlinInt() && sig2.toKotlinInt() == DBSIG_2.toKotlinInt()
}
fun compatibleHeaders(one: UnsignedInt, two: UnsignedInt): Boolean {
return one.toInt() and -0x100 == two.toInt() and -0x100
return one.toKotlinInt() and -0x100 == two.toKotlinInt() and -0x100
}
}

View File

@@ -176,10 +176,10 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
private fun readHeaderField(dis: LittleEndianDataInputStream): Boolean {
val fieldID = dis.read().toByte()
val fieldSize: Int = if (version.toLong() < FILE_VERSION_32_4.toLong()) {
val fieldSize: Int = if (version.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) {
dis.readUShort()
} else {
dis.readUInt().toInt()
dis.readUInt().toKotlinInt()
}
var fieldData: ByteArray? = null
@@ -202,20 +202,20 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
PwDbHeaderV4Fields.MasterSeed -> masterSeed = fieldData
PwDbHeaderV4Fields.TransformSeed -> if (version.toLong() < FILE_VERSION_32_4.toLong())
PwDbHeaderV4Fields.TransformSeed -> if (version.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong())
transformSeed = fieldData
PwDbHeaderV4Fields.TransformRounds -> if (version.toLong() < FILE_VERSION_32_4.toLong())
PwDbHeaderV4Fields.TransformRounds -> if (version.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong())
setTransformRound(fieldData)
PwDbHeaderV4Fields.EncryptionIV -> encryptionIV = fieldData
PwDbHeaderV4Fields.InnerRandomstreamKey -> if (version.toLong() < FILE_VERSION_32_4.toLong())
PwDbHeaderV4Fields.InnerRandomstreamKey -> if (version.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong())
innerRandomStreamKey = fieldData
PwDbHeaderV4Fields.StreamStartBytes -> streamStartBytes = fieldData
PwDbHeaderV4Fields.InnerRandomStreamID -> if (version.toLong() < FILE_VERSION_32_4.toLong())
PwDbHeaderV4Fields.InnerRandomStreamID -> if (version.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong())
setRandomStreamID(fieldData)
PwDbHeaderV4Fields.KdfParameters -> databaseV4.kdfParameters = KdfParameters.deserialize(fieldData)
@@ -261,7 +261,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
}
val flag = bytes4ToUInt(pbFlags)
if (flag.toLong() < 0 || flag.toLong() >= CompressionAlgorithm.values().size) {
if (flag.toKotlinLong() < 0 || flag.toKotlinLong() >= CompressionAlgorithm.values().size) {
throw IOException("Unrecognized compression flag.")
}
@@ -277,7 +277,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
}
val id = bytes4ToUInt(streamID)
if (id.toInt() < 0 || id.toInt() >= CrsAlgorithm.values().size) {
if (id.toKotlinInt() < 0 || id.toKotlinInt() >= CrsAlgorithm.values().size) {
throw IOException("Invalid stream id.")
}
@@ -292,8 +292,8 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
* @return true if it's a supported version
*/
private fun validVersion(version: UnsignedInt): Boolean {
return version.toInt() and FILE_VERSION_CRITICAL_MASK.toInt() <=
FILE_VERSION_32_4.toInt() and FILE_VERSION_CRITICAL_MASK.toInt()
return version.toKotlinInt() and FILE_VERSION_CRITICAL_MASK.toKotlinInt() <=
FILE_VERSION_32_4.toKotlinInt() and FILE_VERSION_CRITICAL_MASK.toKotlinInt()
}
companion object {
@@ -306,7 +306,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
val FILE_VERSION_32_4 = UnsignedInt(0x00040000)
fun getCompressionFromFlag(flag: UnsignedInt): CompressionAlgorithm? {
return when (flag.toInt()) {
return when (flag.toKotlinInt()) {
0 -> CompressionAlgorithm.None
1 -> CompressionAlgorithm.GZip
else -> null

View File

@@ -90,16 +90,16 @@ class DatabaseInputKDB(cacheDirectory: File,
// Select algorithm
when {
header.flags.toInt() and DatabaseHeaderKDB.FLAG_RIJNDAEL.toInt() != 0 -> {
header.flags.toKotlinInt() and DatabaseHeaderKDB.FLAG_RIJNDAEL.toKotlinInt() != 0 -> {
mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.AESRijndael
}
header.flags.toInt() and DatabaseHeaderKDB.FLAG_TWOFISH.toInt() != 0 -> {
header.flags.toKotlinInt() and DatabaseHeaderKDB.FLAG_TWOFISH.toKotlinInt() != 0 -> {
mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.Twofish
}
else -> throw InvalidAlgorithmDatabaseException()
}
mDatabaseToOpen.numberKeyEncryptionRounds = header.numKeyEncRounds.toLong()
mDatabaseToOpen.numberKeyEncryptionRounds = header.numKeyEncRounds.toKotlinLong()
// Generate transformedMasterKey from masterKey
mDatabaseToOpen.makeFinalKey(
@@ -160,11 +160,11 @@ class DatabaseInputKDB(cacheDirectory: File,
var newEntry: EntryKDB? = null
var currentGroupNumber = 0
var currentEntryNumber = 0
while (currentGroupNumber < header.numGroups.toLong()
|| currentEntryNumber < header.numEntries.toLong()) {
while (currentGroupNumber < header.numGroups.toKotlinLong()
|| currentEntryNumber < header.numEntries.toKotlinLong()) {
val fieldType = cipherInputStream.readBytes2ToUShort()
val fieldSize = cipherInputStream.readBytes4ToUInt().toInt()
val fieldSize = cipherInputStream.readBytes4ToUInt().toKotlinInt()
when (fieldType) {
0x0000 -> {
@@ -175,7 +175,7 @@ class DatabaseInputKDB(cacheDirectory: File,
when (fieldSize) {
4 -> {
newGroup = mDatabaseToOpen.createGroup().apply {
setGroupId(cipherInputStream.readBytes4ToUInt().toInt())
setGroupId(cipherInputStream.readBytes4ToUInt().toKotlinInt())
}
}
16 -> {
@@ -194,7 +194,7 @@ class DatabaseInputKDB(cacheDirectory: File,
} ?:
newEntry?.let { entry ->
val groupKDB = mDatabaseToOpen.createGroup()
groupKDB.nodeId = NodeIdInt(cipherInputStream.readBytes4ToUInt().toInt())
groupKDB.nodeId = NodeIdInt(cipherInputStream.readBytes4ToUInt().toKotlinInt())
entry.parent = groupKDB
}
}
@@ -203,7 +203,7 @@ class DatabaseInputKDB(cacheDirectory: File,
group.creationTime = cipherInputStream.readBytes5ToDate()
} ?:
newEntry?.let { entry ->
var iconId = cipherInputStream.readBytes4ToUInt().toInt()
var iconId = cipherInputStream.readBytes4ToUInt().toKotlinInt()
// Clean up after bug that set icon ids to -1
if (iconId == -1) {
iconId = 0
@@ -237,7 +237,7 @@ class DatabaseInputKDB(cacheDirectory: File,
}
0x0007 -> {
newGroup?.let { group ->
group.icon = mDatabaseToOpen.iconFactory.getIcon(cipherInputStream.readBytes4ToUInt().toInt())
group.icon = mDatabaseToOpen.iconFactory.getIcon(cipherInputStream.readBytes4ToUInt().toKotlinInt())
} ?:
newEntry?.let { entry ->
entry.password = cipherInputStream.readBytesToString(fieldSize,false)
@@ -253,7 +253,7 @@ class DatabaseInputKDB(cacheDirectory: File,
}
0x0009 -> {
newGroup?.let { group ->
group.groupFlags = cipherInputStream.readBytes4ToUInt().toInt()
group.groupFlags = cipherInputStream.readBytes4ToUInt().toKotlinInt()
} ?:
newEntry?.let { entry ->
entry.creationTime = cipherInputStream.readBytes5ToDate()

View File

@@ -132,7 +132,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
}
val isPlain: InputStream
if (mDatabase.kdbxVersion.toLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toLong()) {
if (mDatabase.kdbxVersion.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
val decrypted = attachCipherStream(databaseInputStream, cipher)
val dataDecrypted = LittleEndianDataInputStream(decrypted)
@@ -180,7 +180,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
else -> isPlain
}
if (mDatabase.kdbxVersion.toLong() >= DatabaseHeaderKDBX.FILE_VERSION_32_4.toLong()) {
if (mDatabase.kdbxVersion.toKotlinLong() >= DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
loadInnerHeader(inputStreamXml, header)
}
@@ -228,7 +228,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
header: DatabaseHeaderKDBX): Boolean {
val fieldId = dataInputStream.read().toByte()
val size = dataInputStream.readUInt().toInt()
val size = dataInputStream.readUInt().toKotlinInt()
if (size < 0) throw IOException("Corrupted file")
var data = ByteArray(0)
@@ -492,7 +492,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
} else if (name.equals(DatabaseKDBXXML.ElemNotes, ignoreCase = true)) {
ctxGroup?.notes = readString(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
ctxGroup?.icon = mDatabase.iconFactory.getIcon(readUInt(xpp, UnsignedInt(0)).toInt())
ctxGroup?.icon = mDatabase.iconFactory.getIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
ctxGroup?.iconCustom = mDatabase.iconFactory.getIcon(readUuid(xpp))
} else if (name.equals(DatabaseKDBXXML.ElemTimes, ignoreCase = true)) {
@@ -546,7 +546,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
KdbContext.Entry -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
ctxEntry?.nodeId = NodeIdUUID(readUuid(xpp))
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
ctxEntry?.icon = mDatabase.iconFactory.getIcon(readUInt(xpp, UnsignedInt(0)).toInt())
ctxEntry?.icon = mDatabase.iconFactory.getIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
ctxEntry?.iconCustom = mDatabase.iconFactory.getIcon(readUuid(xpp))
} else if (name.equals(DatabaseKDBXXML.ElemFgColor, ignoreCase = true)) {
@@ -819,7 +819,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
val sDate = readString(xpp)
var utcDate: Date? = null
if (mDatabase.kdbxVersion.toLong() >= DatabaseHeaderKDBX.FILE_VERSION_32_4.toLong()) {
if (mDatabase.kdbxVersion.toKotlinLong() >= DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
var buf = Base64.decode(sDate, BASE_64_FLAG)
if (buf.size != 8) {
val buf8 = ByteArray(8)

View File

@@ -90,7 +90,7 @@ constructor(private val databaseKDBX: DatabaseKDBX,
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CompressionFlags, uIntTo4Bytes(DatabaseHeaderKDBX.getFlagFromCompression(databaseKDBX.compressionAlgorithm)))
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.MasterSeed, header.masterSeed)
if (header.version.toLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toLong()) {
if (header.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.TransformSeed, header.transformSeed)
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.TransformRounds, longTo8Bytes(databaseKDBX.numberKeyEncryptionRounds))
} else {
@@ -101,7 +101,7 @@ constructor(private val databaseKDBX: DatabaseKDBX,
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.EncryptionIV, header.encryptionIV)
}
if (header.version.toLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toLong()) {
if (header.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomstreamKey, header.innerRandomStreamKey)
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.StreamStartBytes, header.streamStartBytes)
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomStreamID, uIntTo4Bytes(header.innerRandomStream!!.id))
@@ -136,7 +136,7 @@ constructor(private val databaseKDBX: DatabaseKDBX,
@Throws(IOException::class)
private fun writeHeaderFieldSize(size: Int) {
if (header.version.toLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toLong()) {
if (header.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
los.writeUShort(size)
} else {
los.writeInt(size)

View File

@@ -40,7 +40,7 @@ class DatabaseInnerHeaderOutputKDBX(private val database: DatabaseKDBX,
dataOutputStream.writeInt(4)
if (header.innerRandomStream == null)
throw IOException("Can't write innerRandomStream")
dataOutputStream.writeInt(header.innerRandomStream!!.id.toInt())
dataOutputStream.writeInt(header.innerRandomStream!!.id.toKotlinInt())
val streamKeySize = header.innerRandomStreamKey.size
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey.toInt())

View File

@@ -118,10 +118,10 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
when {
mDatabaseKDB.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael -> {
header.flags = UnsignedInt(header.flags.toInt() or DatabaseHeaderKDB.FLAG_RIJNDAEL.toInt())
header.flags = UnsignedInt(header.flags.toKotlinInt() or DatabaseHeaderKDB.FLAG_RIJNDAEL.toKotlinInt())
}
mDatabaseKDB.encryptionAlgorithm === EncryptionAlgorithm.Twofish -> {
header.flags = UnsignedInt(header.flags.toInt() or DatabaseHeaderKDB.FLAG_TWOFISH.toInt())
header.flags = UnsignedInt(header.flags.toKotlinInt() or DatabaseHeaderKDB.FLAG_TWOFISH.toKotlinInt())
}
else -> throw DatabaseOutputException("Unsupported algorithm.")
}
@@ -129,7 +129,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
header.version = DatabaseHeaderKDB.DBVER_DW
header.numGroups = UnsignedInt(mDatabaseKDB.numberOfGroups())
header.numEntries = UnsignedInt(mDatabaseKDB.numberOfEntries())
header.numKeyEncRounds = UnsignedInt.fromLong(mDatabaseKDB.numberKeyEncryptionRounds)
header.numKeyEncRounds = UnsignedInt.fromKotlinLong(mDatabaseKDB.numberKeyEncryptionRounds)
setIVs(header)

View File

@@ -85,7 +85,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
header = outputHeader(mOS)
val osPlain: OutputStream
osPlain = if (header!!.version.toLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toLong()) {
osPlain = if (header!!.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
val cos = attachStreamEncryptor(header!!, mOS)
cos.write(header!!.streamStartBytes)
@@ -105,7 +105,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
else -> osPlain
}
if (header!!.version.toLong() >= DatabaseHeaderKDBX.FILE_VERSION_32_4.toLong()) {
if (header!!.version.toKotlinLong() >= DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
val ihOut = DatabaseInnerHeaderOutputKDBX(mDatabaseKDBX, header!!, osXml)
ihOut.output()
}
@@ -209,7 +209,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
writeObject(DatabaseKDBXXML.ElemDbDescChanged, mDatabaseKDBX.descriptionChanged.date)
writeObject(DatabaseKDBXXML.ElemDbDefaultUser, mDatabaseKDBX.defaultUserName, true)
writeObject(DatabaseKDBXXML.ElemDbDefaultUserChanged, mDatabaseKDBX.defaultUserNameChanged.date)
writeObject(DatabaseKDBXXML.ElemDbMntncHistoryDays, mDatabaseKDBX.maintenanceHistoryDays.toLong())
writeObject(DatabaseKDBXXML.ElemDbMntncHistoryDays, mDatabaseKDBX.maintenanceHistoryDays.toKotlinLong())
writeObject(DatabaseKDBXXML.ElemDbColor, mDatabaseKDBX.color)
writeObject(DatabaseKDBXXML.ElemDbKeyChanged, mDatabaseKDBX.keyLastChanged.date)
writeObject(DatabaseKDBXXML.ElemDbKeyChangeRec, mDatabaseKDBX.keyChangeRecDays)
@@ -230,7 +230,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
writeUuid(DatabaseKDBXXML.ElemLastTopVisibleGroup, mDatabaseKDBX.lastTopVisibleGroupUUID)
// Seem to work properly if always in meta
if (header!!.version.toLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toLong())
if (header!!.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong())
writeMetaBinaries()
writeCustomData(mDatabaseKDBX.customData)
@@ -274,7 +274,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
Log.e(TAG, "Unable to retrieve header", unknownKDF)
}
if (header.version.toLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toLong()) {
if (header.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
header.innerRandomStream = CrsAlgorithm.Salsa20
header.innerRandomStreamKey = ByteArray(32)
} else {
@@ -288,7 +288,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
throw DatabaseOutputException("Invalid random cipher")
}
if (header.version.toLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toLong()) {
if (header.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
random.nextBytes(header.streamStartBytes)
}
@@ -385,7 +385,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeObject(name: String, value: Date) {
if (header!!.version.toLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toLong()) {
if (header!!.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
writeObject(name, DatabaseKDBXXML.DateFormatter.format(value))
} else {
val dt = DateTime(value)
@@ -489,7 +489,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.startTag(null, DatabaseKDBXXML.ElemAutoType)
writeObject(DatabaseKDBXXML.ElemAutoTypeEnabled, autoType.enabled)
writeObject(DatabaseKDBXXML.ElemAutoTypeObfuscation, autoType.obfuscationOptions.toLong())
writeObject(DatabaseKDBXXML.ElemAutoTypeObfuscation, autoType.obfuscationOptions.toKotlinLong())
if (autoType.defaultSequence.isNotEmpty()) {
writeObject(DatabaseKDBXXML.ElemAutoTypeDefaultSeq, autoType.defaultSequence, true)
@@ -629,7 +629,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
writeObject(DatabaseKDBXXML.ElemLastAccessTime, node.lastAccessTime.date)
writeObject(DatabaseKDBXXML.ElemExpiryTime, node.expiryTime.date)
writeObject(DatabaseKDBXXML.ElemExpires, node.expires)
writeObject(DatabaseKDBXXML.ElemUsageCount, node.usageCount.toLong())
writeObject(DatabaseKDBXXML.ElemUsageCount, node.usageCount.toKotlinLong())
writeObject(DatabaseKDBXXML.ElemLocationChanged, node.locationChanged.date)
xml.endTag(null, DatabaseKDBXXML.ElemTimes)

View File

@@ -104,7 +104,7 @@ class EntryOutputKDB
val binaryData = mEntry.binaryData
val binaryDataLength = binaryData?.length() ?: 0L
// Write data length
mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromLong(binaryDataLength)))
mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength)))
// Write data
if (binaryDataLength > 0) {
binaryData?.getInputDataStream().use { inputStream ->

View File

@@ -32,7 +32,7 @@ import com.kunzisoft.keepass.model.getSearchString
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
class SearchHelper(private val isOmitBackup: Boolean) {
class SearchHelper {
companion object {
const val MAX_SEARCH_ENTRY = 6
@@ -54,6 +54,7 @@ class SearchHelper(private val isOmitBackup: Boolean) {
// If search provide results
database.createVirtualGroupFromSearchInfo(
searchInfo.getSearchString(context),
PreferencesUtil.omitBackup(context),
MAX_SEARCH_ENTRY
)?.let { searchGroup ->
if (searchGroup.getNumberOfChildEntries() > 0) {
@@ -77,6 +78,7 @@ class SearchHelper(private val isOmitBackup: Boolean) {
fun createVirtualGroupWithSearchResult(database: Database,
searchQuery: String,
searchParameters: SearchParameters,
omitBackup: Boolean,
max: Int): Group? {
val searchGroup = database.createGroup()
@@ -101,7 +103,7 @@ class SearchHelper(private val isOmitBackup: Boolean) {
override fun operate(node: Group): Boolean {
return when {
incrementEntry >= max -> false
database.isGroupSearchable(node, isOmitBackup) -> true
database.isGroupSearchable(node, omitBackup) -> true
else -> false
}
}

View File

@@ -23,6 +23,7 @@ import android.content.res.Resources
import android.util.SparseIntArray
import com.kunzisoft.keepass.R
import java.text.DecimalFormat
import java.util.*
/**
* Class who construct dynamically database icons contains in a separate library
@@ -35,17 +36,13 @@ import java.text.DecimalFormat
*
* See *icon-pack-classic* module as sample
*
*
*/
class IconPack
/**
* Construct dynamically the icon pack provide by the string resource id
*
* @param packageName Context of the app to retrieve the resources
* @param packageName Context of the app to retrieve the resources
* @param resourceId String Id of the pack (ex : com.kunzisoft.keepass.icon.classic.R.string.resource_id)
*/
internal constructor(packageName: String, resources: Resources, resourceId: Int) {
class IconPack(packageName: String, resources: Resources, resourceId: Int) {
private val icons: SparseIntArray = SparseIntArray()
/**
@@ -84,7 +81,7 @@ internal constructor(packageName: String, resources: Resources, resourceId: Int)
while (num <= NB_ICONS) {
// To construct the id with name_ic_XX_32dp (ex : classic_ic_08_32dp )
val resId = resources.getIdentifier(
id + "_" + DecimalFormat("00").format(num.toLong()) + "_32dp",
id + "_" + String.format(Locale.ENGLISH, "%02d", num) + "_32dp",
"drawable",
packageName)
icons.put(num, resId)

View File

@@ -65,6 +65,9 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
removeEntryInfo()
assignKeyboardView()
}
lockReceiver?.backToPreviousKeyboardAction = {
switchToPreviousKeyboard()
}
registerLockReceiver(lockReceiver, true)
}
@@ -105,8 +108,17 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
val closeView = popupFieldsView.findViewById<View>(R.id.keyboard_popup_close)
closeView.setOnClickListener { popupCustomKeys?.dismiss() }
if (!Database.getInstance().loaded)
// Remove entry info if the database is not loaded
// or if entry info timestamp is before database loaded timestamp
val database = Database.getInstance()
val databaseTime = database.loadTimestamp
val entryTime = entryInfoTimestamp
if (!database.loaded
|| databaseTime == null
|| entryTime == null
|| entryTime < databaseTime) {
removeEntryInfo()
}
assignKeyboardView()
keyboardView?.setOnKeyboardActionListener(this)
keyboardView?.isPreviewEnabled = false
@@ -262,8 +274,12 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
}
private fun actionGoAutomatically() {
if (PreferencesUtil.isAutoGoActionEnable(this))
if (PreferencesUtil.isAutoGoActionEnable(this)) {
currentInputConnection.performEditorAction(EditorInfo.IME_ACTION_GO)
if (PreferencesUtil.isKeyboardPreviousFillInEnable(this)) {
switchToPreviousKeyboard()
}
}
}
override fun onPress(primaryCode: Int) {
@@ -314,10 +330,13 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
private const val KEY_URL = 520
private const val KEY_FIELDS = 530
// TODO Retrieve entry info from id and service when database is open
private var entryInfoKey: EntryInfo? = null
private var entryInfoTimestamp: Long? = null
private fun removeEntryInfo() {
entryInfoKey = null
entryInfoTimestamp = null
}
fun removeEntry(context: Context) {
@@ -327,6 +346,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
fun addEntryAndLaunchNotificationIfAllowed(context: Context, entry: EntryInfo, toast: Boolean = false) {
// Add a new entry
entryInfoKey = entry
entryInfoTimestamp = System.currentTimeMillis()
// Launch notification if allowed
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
}

View File

@@ -0,0 +1,26 @@
package com.kunzisoft.keepass.model
import android.net.Uri
data class DatabaseFile(var databaseUri: Uri? = null,
var keyFileUri: Uri? = null,
var databaseDecodedPath: String? = null,
var databaseAlias: String? = null,
var databaseFileExists: Boolean = false,
var databaseLastModified: String? = null,
var databaseSize: String? = null) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is DatabaseFile) return false
if (databaseUri == null || other.databaseUri == null) return false
if (databaseUri != other.databaseUri) return false
return true
}
override fun hashCode(): Int {
return databaseUri?.hashCode() ?: 0
}
}

View File

@@ -47,6 +47,22 @@ data class EntryAttachment(var name: String,
return 0
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is EntryAttachment) return false
if (name != other.name) return false
if (binaryAttachment != other.binaryAttachment) return false
return true
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + binaryAttachment.hashCode()
return result
}
companion object CREATOR : Parcelable.Creator<EntryAttachment> {
override fun createFromParcel(parcel: Parcel): EntryAttachment {
return EntryAttachment(parcel)

View File

@@ -49,9 +49,7 @@ class Field : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Field
if (other !is Field) return false
if (name != other.name) return false

View File

@@ -0,0 +1,56 @@
package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.Parcelable
class FocusedEditField : Parcelable {
var field: Field? = null
var cursorSelectionStart: Int = -1
var cursorSelectionEnd: Int = -1
constructor()
constructor(parcel: Parcel) {
this.field = parcel.readParcelable(Field::class.java.classLoader)
this.cursorSelectionStart = parcel.readInt()
this.cursorSelectionEnd = parcel.readInt()
}
fun destroy() {
this.field = null
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(field, flags)
parcel.writeInt(cursorSelectionStart)
parcel.writeInt(cursorSelectionEnd)
}
override fun describeContents(): Int {
return 0
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is FocusedEditField) return false
if (field != other.field) return false
return true
}
override fun hashCode(): Int {
return field?.hashCode() ?: 0
}
companion object CREATOR : Parcelable.Creator<FocusedEditField> {
override fun createFromParcel(parcel: Parcel): FocusedEditField {
return FocusedEditField(parcel)
}
override fun newArray(size: Int): Array<FocusedEditField?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.notifications
import android.app.PendingIntent
import android.content.ContentResolver
import android.content.Intent
import android.net.Uri
import android.os.Binder
@@ -29,7 +30,8 @@ import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.tasks.AttachmentFileAsyncTask
import com.kunzisoft.keepass.timeout.TimeoutHelper
import kotlinx.coroutines.*
import java.util.*
import kotlin.collections.HashMap
@@ -41,6 +43,8 @@ class AttachmentFileNotificationService: LockNotificationService() {
private var mActionTaskBinder = ActionTaskBinder()
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
private val mainScope = CoroutineScope(Dispatchers.Main)
inner class ActionTaskBinder: Binder() {
fun getService(): AttachmentFileNotificationService = this@AttachmentFileNotificationService
@@ -92,23 +96,26 @@ class AttachmentFileNotificationService: LockNotificationService() {
if (downloadFileUri != null
&& intent.hasExtra(ATTACHMENT_KEY)) {
val nextNotificationId = (downloadFileUris.values.maxBy { it.notificationId }
val nextNotificationId = (downloadFileUris.values.maxByOrNull { it.notificationId }
?.notificationId ?: notificationId) + 1
try {
intent.getParcelableExtra<EntryAttachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
val attachmentNotification = AttachmentNotification(nextNotificationId, entryAttachment)
downloadFileUris[downloadFileUri] = attachmentNotification
AttachmentFileAsyncTask(downloadFileUri,
attachmentNotification,
contentResolver).apply {
onUpdate = { uri, attachment, notificationIdAttach ->
newNotification(uri, attachment, notificationIdAttach)
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentProgress(downloadFileUri, attachment)
mainScope.launch {
AttachmentFileActionClass(downloadFileUri,
attachmentNotification,
contentResolver).apply {
onUpdate = { uri, attachment, notificationIdAttach ->
newNotification(uri, attachment, notificationIdAttach)
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentProgress(downloadFileUri, attachment)
}
}
}
}.execute()
}.executeAction()
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to download $downloadFileUri", e)
@@ -202,9 +209,9 @@ class AttachmentFileNotificationService: LockNotificationService() {
super.onDestroy()
}
data class AttachmentNotification(var notificationId: Int,
private data class AttachmentNotification(var notificationId: Int,
var entryAttachment: EntryAttachment,
var attachmentTask: AttachmentFileAsyncTask? = null) {
var attachmentTask: AttachmentFileActionClass? = null) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@@ -221,6 +228,78 @@ class AttachmentFileNotificationService: LockNotificationService() {
}
}
private class AttachmentFileActionClass(
private val fileUri: Uri,
private val attachmentNotification: AttachmentNotification,
private val contentResolver: ContentResolver) {
private val updateMinFrequency = 1000
private var previousSaveTime = System.currentTimeMillis()
var onUpdate: ((Uri, EntryAttachment, Int)->Unit)? = null
suspend fun executeAction() {
TimeoutHelper.temporarilyDisableTimeout()
// on pre execute
attachmentNotification.attachmentTask = this
attachmentNotification.entryAttachment.apply {
downloadState = AttachmentState.START
downloadProgression = 0
}
onUpdate?.invoke(fileUri,
attachmentNotification.entryAttachment,
attachmentNotification.notificationId)
withContext(Dispatchers.IO) {
// on Progress with thread
val asyncResult: Deferred<Boolean> = async {
var progressResult = true
try {
attachmentNotification.entryAttachment.apply {
downloadState = AttachmentState.IN_PROGRESS
binaryAttachment.download(fileUri, contentResolver, 1024) { percent ->
// Publish progress
val currentTime = System.currentTimeMillis()
if (previousSaveTime + updateMinFrequency < currentTime) {
attachmentNotification.entryAttachment.apply {
downloadState = AttachmentState.IN_PROGRESS
downloadProgression = percent
}
onUpdate?.invoke(fileUri,
attachmentNotification.entryAttachment,
attachmentNotification.notificationId)
Log.d(TAG, "Download file $fileUri : $percent%")
previousSaveTime = currentTime
}
}
}
} catch (e: Exception) {
progressResult = false
}
progressResult
}
// on post execute
withContext(Dispatchers.Main) {
val result = asyncResult.await()
attachmentNotification.attachmentTask = null
attachmentNotification.entryAttachment.apply {
downloadState = if (result) AttachmentState.COMPLETE else AttachmentState.ERROR
downloadProgression = 100
}
onUpdate?.invoke(fileUri,
attachmentNotification.entryAttachment,
attachmentNotification.notificationId)
}
}
}
companion object {
private val TAG = AttachmentFileActionClass::class.java.name
}
}
companion object {
private val TAG = AttachmentFileNotificationService::javaClass.name

View File

@@ -153,9 +153,6 @@ class ClipboardEntryNotificationService : LockNotificationService() {
val nextField = nextFields[0]
builder.setContentText(getString(R.string.select_to_copy, nextField.label))
builder.setContentIntent(getCopyPendingIntent(nextField, nextFields))
// Else tell to swipe for a clean
} else {
builder.setContentText(getString(R.string.clipboard_swipe_clean))
}
val cleanIntent = Intent(this, ClipboardEntryNotificationService::class.java)

View File

@@ -1,109 +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.notifications
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.closeDatabase
class DatabaseOpenNotificationService: LockNotificationService() {
override val notificationId: Int = 340
private fun stopNotificationAndSendLock() {
// Send lock action
sendBroadcast(Intent(LOCK_ACTION))
}
override fun actionOnLock() {
closeDatabase()
// Remove the lock timer (no more needed if it exists)
TimeoutHelper.cancelLockTimer(this)
// Service is stopped after receive the broadcast
super.actionOnLock()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
when(intent?.action) {
ACTION_CLOSE_DATABASE -> {
stopNotificationAndSendLock()
}
else -> {
val databaseIntent = Intent(this, GroupActivity::class.java)
var pendingDatabaseFlag = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
pendingDatabaseFlag = PendingIntent.FLAG_IMMUTABLE
}
val pendingDatabaseIntent = PendingIntent.getActivity(this, 0, databaseIntent, pendingDatabaseFlag)
val deleteIntent = Intent(this, DatabaseOpenNotificationService::class.java).apply {
action = ACTION_CLOSE_DATABASE
}
val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val database = Database.getInstance()
if (database.loaded) {
startForeground(notificationId, buildNewNotification().apply {
setSmallIcon(R.drawable.notification_ic_database_open)
setContentTitle(getString(R.string.database_opened))
setContentText(database.name + " (" + database.version + ")")
setAutoCancel(false)
setContentIntent(pendingDatabaseIntent)
// Unfortunately swipe is disabled in lollipop+
setDeleteIntent(pendingDeleteIntent)
addAction(R.drawable.ic_lock_white_24dp, getString(R.string.lock),
pendingDeleteIntent)
}.build())
} else {
stopSelf()
}
}
}
return START_STICKY
}
companion object {
const val ACTION_CLOSE_DATABASE = "ACTION_CLOSE_DATABASE"
fun start(context: Context) {
// Start the opening notification, keep it active to receive lock
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(Intent(context, DatabaseOpenNotificationService::class.java))
} else {
context.startService(Intent(context, DatabaseOpenNotificationService::class.java))
}
}
fun stop(context: Context) {
// Stop the opening notification
context.stopService(Intent(context, DatabaseOpenNotificationService::class.java))
}
}
}

View File

@@ -19,38 +19,54 @@
*/
package com.kunzisoft.keepass.notifications
import android.app.PendingIntent
import android.content.Intent
import android.net.Uri
import android.os.*
import android.os.Binder
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.database.action.*
import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseRunnable
import com.kunzisoft.keepass.database.action.history.RestoreEntryHistoryDatabaseRunnable
import com.kunzisoft.keepass.database.action.node.*
import com.kunzisoft.keepass.database.element.*
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.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.closeDatabase
import kotlinx.coroutines.*
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.collections.ArrayList
class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdater {
open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater {
override val notificationId: Int = 575
private var actionRunnableAsyncTask: ActionRunnableAsyncTask? = null
private lateinit var mDatabase: Database
private val mainScope = CoroutineScope(Dispatchers.Main)
private var mActionTaskBinder = ActionTaskBinder()
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
private var mAllowFinishAction = AtomicBoolean()
private var mActionRunning = false
private var mTitleId: Int? = null
private var mIconId: Int = R.drawable.notification_ic_database_load
private var mTitleId: Int = R.string.database_opened
private var mMessageId: Int? = null
private var mWarningId: Int? = null
@@ -59,11 +75,15 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
fun getService(): DatabaseTaskNotificationService = this@DatabaseTaskNotificationService
fun addActionTaskListener(actionTaskListener: ActionTaskListener) {
mAllowFinishAction.set(true)
mActionTaskListeners.add(actionTaskListener)
}
fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
mActionTaskListeners.remove(actionTaskListener)
if (mActionTaskListeners.size == 0) {
mAllowFinishAction.set(false)
}
}
}
@@ -73,50 +93,41 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
fun onStopAction(actionTask: String, result: ActionRunnable.Result)
}
/**
* Force to call [ActionTaskListener.onStartAction] if the action is still running
*/
fun checkAction() {
mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onStartAction(mTitleId, mMessageId, mWarningId)
if (mActionRunning) {
mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onStartAction(mTitleId, mMessageId, mWarningId)
}
}
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return mActionTaskBinder
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
if (intent == null) return START_REDELIVER_INTENT
mDatabase = Database.getInstance()
val intentAction = intent.action
// Create the notification
buildMessage(intent)
var saveAction = true
if (intent.hasExtra(SAVE_DATABASE_KEY)) {
saveAction = intent.getBooleanExtra(SAVE_DATABASE_KEY, saveAction)
val intentAction = intent?.action
if (intentAction == null && !mDatabase.loaded) {
stopSelf()
}
if (intentAction == ACTION_DATABASE_CLOSE) {
// Send lock action
sendBroadcast(Intent(LOCK_ACTION))
}
val titleId: Int = when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
ACTION_DATABASE_LOAD_TASK -> R.string.loading_database
else -> {
if (saveAction)
R.string.saving_database
else
R.string.command_execution
}
}
val messageId: Int? = when (intentAction) {
ACTION_DATABASE_LOAD_TASK -> null
else -> null
}
val warningId: Int? =
if (!saveAction
|| intentAction == ACTION_DATABASE_LOAD_TASK)
null
else
R.string.do_not_kill_app
val actionRunnable: ActionRunnable? = when (intentAction) {
val actionRunnable: ActionRunnable? = when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent)
ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent)
ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent)
@@ -145,52 +156,212 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
else -> null
}
actionRunnable?.let { actionRunnableNotNull ->
// Assign elements for updates
mTitleId = titleId
mMessageId = messageId
mWarningId = warningId
// Build and launch the action
if (actionRunnable != null) {
mainScope.launch {
executeAction(this@DatabaseTaskNotificationService,
{
mActionRunning = true
// Create the notification
newNotification(intent.getIntExtra(DATABASE_TASK_TITLE_KEY, titleId))
sendBroadcast(Intent(DATABASE_START_TASK_ACTION).apply {
putExtra(DATABASE_TASK_TITLE_KEY, mTitleId)
putExtra(DATABASE_TASK_MESSAGE_KEY, mMessageId)
putExtra(DATABASE_TASK_WARNING_KEY, mWarningId)
})
// Build and launch the action
actionRunnableAsyncTask = ActionRunnableAsyncTask(this,
{
sendBroadcast(Intent(DATABASE_START_TASK_ACTION).apply {
putExtra(DATABASE_TASK_TITLE_KEY, titleId)
putExtra(DATABASE_TASK_MESSAGE_KEY, messageId)
putExtra(DATABASE_TASK_WARNING_KEY, warningId)
})
mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onStartAction(mTitleId, mMessageId, mWarningId)
}
mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onStartAction(titleId, messageId, warningId)
}
},
{
actionRunnable
},
{ result ->
try {
mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onStopAction(intentAction!!, result)
}
} finally {
removeIntentData(intent)
TimeoutHelper.releaseTemporarilyDisableTimeout()
if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) {
if (!mDatabase.loaded) {
stopSelf()
} else {
// Restart the service to open lock notification
startService(Intent(applicationContext,
DatabaseTaskNotificationService::class.java))
}
}
}
}, { result ->
mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onStopAction(intentAction!!, result)
}
sendBroadcast(Intent(DATABASE_STOP_TASK_ACTION))
sendBroadcast(Intent(DATABASE_STOP_TASK_ACTION))
stopSelf()
}
)
actionRunnableAsyncTask?.execute({ actionRunnableNotNull })
mActionRunning = false
}
)
}
}
return START_REDELIVER_INTENT
return when (intentAction) {
ACTION_DATABASE_LOAD_TASK, null -> {
START_STICKY
}
else -> {
// Relaunch action if failed
START_REDELIVER_INTENT
}
}
}
private fun newNotification(title: Int) {
private fun buildMessage(intent: Intent?) {
// Assign elements for updates
val intentAction = intent?.action
val builder = buildNewNotification()
.setSmallIcon(R.drawable.notification_ic_database_load)
.setContentTitle(getString(title))
.setAutoCancel(false)
.setContentIntent(null)
startForeground(notificationId, builder.build())
var saveAction = false
if (intent != null && intent.hasExtra(SAVE_DATABASE_KEY)) {
saveAction = intent.getBooleanExtra(SAVE_DATABASE_KEY, saveAction)
}
mIconId = if (intentAction == null)
R.drawable.notification_ic_database_open
else
R.drawable.notification_ic_database_load
mTitleId = when {
saveAction -> {
R.string.saving_database
}
intentAction == null -> {
R.string.database_opened
}
else -> {
when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
ACTION_DATABASE_LOAD_TASK -> R.string.loading_database
ACTION_DATABASE_SAVE -> R.string.saving_database
else -> {
R.string.command_execution
}
}
}
}
mMessageId = when (intentAction) {
ACTION_DATABASE_LOAD_TASK -> null
else -> null
}
mWarningId =
if (!saveAction
|| intentAction == ACTION_DATABASE_LOAD_TASK)
null
else
R.string.do_not_kill_app
val notificationBuilder = buildNewNotification().apply {
setSmallIcon(mIconId)
intent?.let {
setContentTitle(getString(intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mTitleId)))
}
setAutoCancel(false)
setContentIntent(null)
}
if (intentAction == null) {
// Database is normally open
if (mDatabase.loaded) {
// Build Intents for notification action
var pendingDatabaseFlag = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
pendingDatabaseFlag = PendingIntent.FLAG_IMMUTABLE
}
val pendingDatabaseIntent = PendingIntent.getActivity(this,
0,
Intent(this, GroupActivity::class.java),
pendingDatabaseFlag)
val deleteIntent = Intent(this, DatabaseTaskNotificationService::class.java).apply {
action = ACTION_DATABASE_CLOSE
}
val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
// Add actions in notifications
notificationBuilder.apply {
setContentText(mDatabase.name + " (" + mDatabase.version + ")")
setContentIntent(pendingDatabaseIntent)
// Unfortunately swipe is disabled in lollipop+
setDeleteIntent(pendingDeleteIntent)
addAction(R.drawable.ic_lock_white_24dp, getString(R.string.lock),
pendingDeleteIntent)
}
}
}
// Create the notification
startForeground(notificationId, notificationBuilder.build())
}
private fun removeIntentData(intent: Intent?) {
intent?.action = null
intent?.removeExtra(DATABASE_TASK_TITLE_KEY)
intent?.removeExtra(DATABASE_TASK_MESSAGE_KEY)
intent?.removeExtra(DATABASE_TASK_WARNING_KEY)
intent?.removeExtra(DATABASE_URI_KEY)
intent?.removeExtra(MASTER_PASSWORD_CHECKED_KEY)
intent?.removeExtra(MASTER_PASSWORD_KEY)
intent?.removeExtra(KEY_FILE_CHECKED_KEY)
intent?.removeExtra(KEY_FILE_URI_KEY)
intent?.removeExtra(READ_ONLY_KEY)
intent?.removeExtra(CIPHER_ENTITY_KEY)
intent?.removeExtra(FIX_DUPLICATE_UUID_KEY)
intent?.removeExtra(GROUP_KEY)
intent?.removeExtra(ENTRY_KEY)
intent?.removeExtra(GROUP_ID_KEY)
intent?.removeExtra(ENTRY_ID_KEY)
intent?.removeExtra(GROUPS_ID_KEY)
intent?.removeExtra(ENTRIES_ID_KEY)
intent?.removeExtra(PARENT_ID_KEY)
intent?.removeExtra(ENTRY_HISTORY_POSITION_KEY)
intent?.removeExtra(SAVE_DATABASE_KEY)
intent?.removeExtra(OLD_NODES_KEY)
intent?.removeExtra(NEW_NODES_KEY)
intent?.removeExtra(OLD_ELEMENT_KEY)
intent?.removeExtra(NEW_ELEMENT_KEY)
}
/**
* Execute action with a coroutine
*/
private suspend fun executeAction(progressTaskUpdater: ProgressTaskUpdater,
onPreExecute: () -> Unit,
onExecute: (ProgressTaskUpdater?) -> ActionRunnable?,
onPostExecute: (result: ActionRunnable.Result) -> Unit) {
mAllowFinishAction.set(false)
TimeoutHelper.temporarilyDisableTimeout()
onPreExecute.invoke()
withContext(Dispatchers.IO) {
onExecute.invoke(progressTaskUpdater)?.apply {
val asyncResult: Deferred<ActionRunnable.Result> = async {
val startTime = System.currentTimeMillis()
var timeIsUp = false
// Run the actionRunnable
run()
// Wait onBind or 4 seconds max
while (!mAllowFinishAction.get() && !timeIsUp) {
delay(100)
if (startTime + 4000 < System.currentTimeMillis())
timeIsUp = true
}
result
}
withContext(Dispatchers.Main) {
onPostExecute.invoke(asyncResult.await())
}
}
}
}
override fun updateMessage(resId: Int) {
@@ -200,22 +371,32 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
}
}
override fun actionOnLock() {
if (!TimeoutHelper.temporarilyDisableTimeout) {
closeDatabase()
// Remove the lock timer (no more needed if it exists)
TimeoutHelper.cancelLockTimer(this)
// Service is stopped after receive the broadcast
super.actionOnLock()
}
}
private fun buildDatabaseCreateActionTask(intent: Intent): ActionRunnable? {
if (intent.hasExtra(DATABASE_URI_KEY)
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
&& intent.hasExtra(MASTER_PASSWORD_KEY)
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
&& intent.hasExtra(KEY_FILE_KEY)
&& intent.hasExtra(KEY_FILE_URI_KEY)
) {
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY)
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_URI_KEY)
if (databaseUri == null)
return null
return CreateDatabaseRunnable(this,
Database.getInstance(),
mDatabase,
databaseUri,
getString(R.string.database_default_name),
getString(R.string.database),
@@ -223,7 +404,12 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
intent.getStringExtra(MASTER_PASSWORD_KEY),
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
keyFileUri
)
) { result ->
result.data = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri)
putParcelable(KEY_FILE_URI_KEY, keyFileUri)
}
}
} else {
return null
}
@@ -233,15 +419,14 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
if (intent.hasExtra(DATABASE_URI_KEY)
&& intent.hasExtra(MASTER_PASSWORD_KEY)
&& intent.hasExtra(KEY_FILE_KEY)
&& intent.hasExtra(KEY_FILE_URI_KEY)
&& intent.hasExtra(READ_ONLY_KEY)
&& intent.hasExtra(CIPHER_ENTITY_KEY)
&& intent.hasExtra(FIX_DUPLICATE_UUID_KEY)
) {
val database = Database.getInstance()
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
val masterPassword: String? = intent.getStringExtra(MASTER_PASSWORD_KEY)
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY)
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_URI_KEY)
val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true)
val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY)
@@ -250,13 +435,12 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
return LoadDatabaseRunnable(
this,
database,
mDatabase,
databaseUri,
masterPassword,
keyFileUri,
readOnly,
cipherEntity,
PreferencesUtil.omitBackup(this),
intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false),
this
) { result ->
@@ -264,7 +448,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
result.data = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri)
putString(MASTER_PASSWORD_KEY, masterPassword)
putParcelable(KEY_FILE_KEY, keyFileUri)
putParcelable(KEY_FILE_URI_KEY, keyFileUri)
putBoolean(READ_ONLY_KEY, readOnly)
putParcelable(CIPHER_ENTITY_KEY, cipherEntity)
}
@@ -279,16 +463,16 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
&& intent.hasExtra(MASTER_PASSWORD_KEY)
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
&& intent.hasExtra(KEY_FILE_KEY)
&& intent.hasExtra(KEY_FILE_URI_KEY)
) {
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null
AssignPasswordInDatabaseRunnable(this,
Database.getInstance(),
mDatabase,
databaseUri,
intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false),
intent.getStringExtra(MASTER_PASSWORD_KEY),
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
intent.getParcelableExtra(KEY_FILE_KEY)
intent.getParcelableExtra(KEY_FILE_URI_KEY)
)
} else {
null
@@ -310,7 +494,6 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(PARENT_ID_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
@@ -318,9 +501,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|| newGroup == null)
return null
database.getGroupById(parentId)?.let { parent ->
mDatabase.getGroupById(parentId)?.let { parent ->
AddGroupRunnable(this,
database,
mDatabase,
newGroup,
parent,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
@@ -336,7 +519,6 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(GROUP_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
val groupId: NodeId<*>? = intent.getParcelableExtra(GROUP_ID_KEY)
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
@@ -344,9 +526,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|| newGroup == null)
return null
database.getGroupById(groupId)?.let { oldGroup ->
mDatabase.getGroupById(groupId)?.let { oldGroup ->
UpdateGroupRunnable(this,
database,
mDatabase,
oldGroup,
newGroup,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
@@ -362,7 +544,6 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(PARENT_ID_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
@@ -370,9 +551,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|| newEntry == null)
return null
database.getGroupById(parentId)?.let { parent ->
mDatabase.getGroupById(parentId)?.let { parent ->
AddEntryRunnable(this,
database,
mDatabase,
newEntry,
parent,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
@@ -388,7 +569,6 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(ENTRY_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
val entryId: NodeId<UUID>? = intent.getParcelableExtra(ENTRY_ID_KEY)
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
@@ -396,9 +576,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|| newEntry == null)
return null
database.getEntryById(entryId)?.let { oldEntry ->
mDatabase.getEntryById(entryId)?.let { oldEntry ->
UpdateEntryRunnable(this,
database,
mDatabase,
oldEntry,
newEntry,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
@@ -415,13 +595,12 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(PARENT_ID_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
database.getGroupById(parentId)?.let { newParent ->
mDatabase.getGroupById(parentId)?.let { newParent ->
CopyNodesRunnable(this,
database,
getListNodesFromBundle(database, intent.extras!!),
mDatabase,
getListNodesFromBundle(mDatabase, intent.extras!!),
newParent,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
@@ -437,13 +616,12 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(PARENT_ID_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
database.getGroupById(parentId)?.let { newParent ->
mDatabase.getGroupById(parentId)?.let { newParent ->
MoveNodesRunnable(this,
database,
getListNodesFromBundle(database, intent.extras!!),
mDatabase,
getListNodesFromBundle(mDatabase, intent.extras!!),
newParent,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
@@ -458,10 +636,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(ENTRIES_ID_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
DeleteNodesRunnable(this,
database,
getListNodesFromBundle(database, intent.extras!!),
mDatabase,
getListNodesFromBundle(mDatabase, intent.extras!!),
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
} else {
@@ -474,12 +651,11 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(ENTRY_HISTORY_POSITION_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
database.getEntryById(entryId)?.let { mainEntry ->
mDatabase.getEntryById(entryId)?.let { mainEntry ->
RestoreEntryHistoryDatabaseRunnable(this,
database,
mDatabase,
mainEntry,
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
@@ -494,12 +670,11 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(ENTRY_HISTORY_POSITION_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
database.getEntryById(entryId)?.let { mainEntry ->
mDatabase.getEntryById(entryId)?.let { mainEntry ->
DeleteEntryHistoryDatabaseRunnable(this,
database,
mDatabase,
mainEntry,
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
@@ -522,7 +697,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
return null
return UpdateCompressionBinariesDatabaseRunnable(this,
Database.getInstance(),
mDatabase,
oldElement,
newElement,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
@@ -539,7 +714,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
private fun buildDatabaseUpdateElementActionTask(intent: Intent): ActionRunnable? {
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
return SaveDatabaseRunnable(this,
Database.getInstance(),
mDatabase,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
).apply {
mAfterSaveDatabase = { result ->
@@ -557,48 +732,17 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
private fun buildDatabaseSave(intent: Intent): ActionRunnable? {
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
SaveDatabaseRunnable(this,
Database.getInstance(),
mDatabase,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
} else {
null
}
}
private class ActionRunnableAsyncTask(private val progressTaskUpdater: ProgressTaskUpdater,
private val onPreExecute: () -> Unit,
private val onPostExecute: (result: ActionRunnable.Result) -> Unit)
: AsyncTask<((ProgressTaskUpdater?) -> ActionRunnable), Void, ActionRunnable.Result>() {
override fun onPreExecute() {
super.onPreExecute()
onPreExecute.invoke()
}
override fun doInBackground(vararg actionRunnables: ((ProgressTaskUpdater?)-> ActionRunnable)?): ActionRunnable.Result {
var resultTask = ActionRunnable.Result(false)
actionRunnables.forEach {
it?.invoke(progressTaskUpdater)?.apply {
run()
resultTask = result
}
}
return resultTask
}
override fun onPostExecute(result: ActionRunnable.Result) {
super.onPostExecute(result)
onPostExecute.invoke(result)
}
}
companion object {
private val TAG = DatabaseTaskNotificationService::class.java.name
const val DATABASE_TASK_TITLE_KEY = "DATABASE_TASK_TITLE_KEY"
const val DATABASE_TASK_MESSAGE_KEY = "DATABASE_TASK_MESSAGE_KEY"
const val DATABASE_TASK_WARNING_KEY = "DATABASE_TASK_WARNING_KEY"
const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK"
const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK"
const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_TASK"
@@ -624,12 +768,17 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
const val ACTION_DATABASE_UPDATE_PARALLELISM_TASK = "ACTION_DATABASE_UPDATE_PARALLELISM_TASK"
const val ACTION_DATABASE_UPDATE_ITERATIONS_TASK = "ACTION_DATABASE_UPDATE_ITERATIONS_TASK"
const val ACTION_DATABASE_SAVE = "ACTION_DATABASE_SAVE"
const val ACTION_DATABASE_CLOSE = "ACTION_DATABASE_CLOSE"
const val DATABASE_TASK_TITLE_KEY = "DATABASE_TASK_TITLE_KEY"
const val DATABASE_TASK_MESSAGE_KEY = "DATABASE_TASK_MESSAGE_KEY"
const val DATABASE_TASK_WARNING_KEY = "DATABASE_TASK_WARNING_KEY"
const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
const val KEY_FILE_KEY = "KEY_FILE_KEY"
const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY"
const val READ_ONLY_KEY = "READ_ONLY_KEY"
const val CIPHER_ENTITY_KEY = "CIPHER_ENTITY_KEY"
const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY"

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.notifications
import android.content.Intent
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.LockReceiver
import com.kunzisoft.keepass.utils.registerLockReceiver
import com.kunzisoft.keepass.utils.unregisterLockReceiver

View File

@@ -88,30 +88,36 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
get() = otpModel.counter
@Throws(NumberFormatException::class)
set(value) {
otpModel.counter = if (value < MIN_HOTP_COUNTER || value > MAX_HOTP_COUNTER) {
otpModel.counter = if (isValidCounter(value)) {
value
} else {
TokenCalculator.HOTP_INITIAL_COUNTER
throw IllegalArgumentException()
} else value
}
}
var period
get() = otpModel.period
@Throws(NumberFormatException::class)
set(value) {
otpModel.period = if (value < MIN_TOTP_PERIOD || value > MAX_TOTP_PERIOD) {
otpModel.period = if (isValidPeriod(value)) {
value
} else {
TokenCalculator.TOTP_DEFAULT_PERIOD
throw NumberFormatException()
} else value
}
}
var digits
get() = otpModel.digits
@Throws(NumberFormatException::class)
set(value) {
otpModel.digits = if (value < MIN_OTP_DIGITS|| value > MAX_OTP_DIGITS) {
otpModel.digits = if (isValidDigits(value)) {
value
} else {
TokenCalculator.OTP_DEFAULT_DIGITS
throw NumberFormatException()
} else value
}
}
var algorithm
@@ -144,16 +150,15 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
@Throws(IllegalArgumentException::class)
fun setBase32Secret(secret: String) {
val secretChars = replaceBase32Chars(secret)
if (secretChars.isNotEmpty() && checkBase32Secret(secretChars))
otpModel.secret = Base32().decode(secretChars.toByteArray())
if (isValidBase32(secret))
otpModel.secret = Base32().decode(replaceBase32Chars(secret).toByteArray())
else
throw IllegalArgumentException()
}
@Throws(IllegalArgumentException::class)
fun setBase64Secret(secret: String) {
if (secret.isNotEmpty() && checkBase64Secret(secret))
if (isValidBase64(secret))
otpModel.secret = Base64().decode(secret.toByteArray())
else
throw IllegalArgumentException()
@@ -184,11 +189,33 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
const val MAX_HOTP_COUNTER = Long.MAX_VALUE
const val MIN_TOTP_PERIOD = 1
const val MAX_TOTP_PERIOD = 60
const val MAX_TOTP_PERIOD = 900
const val MIN_OTP_DIGITS = 4
const val MAX_OTP_DIGITS = 18
fun isValidCounter(counter: Long): Boolean {
return counter in MIN_HOTP_COUNTER..MAX_HOTP_COUNTER
}
fun isValidPeriod(period: Int): Boolean {
return period in MIN_TOTP_PERIOD..MAX_TOTP_PERIOD
}
fun isValidDigits(digits: Int): Boolean {
return digits in MIN_OTP_DIGITS..MAX_OTP_DIGITS
}
fun isValidBase32(secret: String): Boolean {
val secretChars = replaceBase32Chars(secret)
return secretChars.isNotEmpty() && checkBase32Secret(secretChars)
}
fun isValidBase64(secret: String): Boolean {
// TODO replace base 64 chars
return secret.isNotEmpty() && checkBase64Secret(secret)
}
fun replaceSpaceChars(parameter: String): String {
return parameter.replace("[\\r|\\n|\\t|\\s|\\u00A0]+".toRegex(), "")
}

View File

@@ -26,8 +26,6 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.otp.OtpElement.Companion.replaceSpaceChars
import com.kunzisoft.keepass.otp.TokenCalculator.*
import java.lang.Exception
import java.lang.StringBuilder
import java.net.URLEncoder
import java.util.*
import java.util.regex.Pattern

View File

@@ -166,7 +166,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
refreshRecycleBinGroup()
// Save the database if not in readonly mode
(context as SettingsActivity?)?.
mProgressDialogThread?.startDatabaseSave(mDatabaseAutoSaveEnabled)
mProgressDatabaseTaskProvider?.startDatabaseSave(mDatabaseAutoSaveEnabled)
true
}
true
@@ -546,7 +546,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
return when (item.itemId) {
R.id.menu_save_database -> {
settingActivity?.mProgressDialogThread?.startDatabaseSave(!mDatabaseReadOnly)
settingActivity?.mProgressDatabaseTaskProvider?.startDatabaseSave(!mDatabaseReadOnly)
true
}

View File

@@ -369,6 +369,18 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.keyboard_key_sound_default))
}
fun isKeyboardPreviousDatabaseCredentialsEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.keyboard_previous_database_credentials_key),
context.resources.getBoolean(R.bool.keyboard_previous_database_credentials_default))
}
fun isKeyboardPreviousFillInEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.keyboard_previous_fill_in_key),
context.resources.getBoolean(R.bool.keyboard_previous_fill_in_default))
}
fun isAutofillAutoSearchEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.autofill_auto_search_key),

View File

@@ -92,6 +92,9 @@ open class SettingsActivity
lockAndExit()
}
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, retrieveMainFragment())
@@ -102,7 +105,7 @@ open class SettingsActivity
backupManager = BackupManager(this)
mProgressDialogThread?.onActionFinish = { actionTask, result ->
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
// Call result in fragment
(supportFragmentManager
.findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?)
@@ -133,7 +136,7 @@ open class SettingsActivity
database.fileUri?.let { databaseUri ->
// Show the progress dialog now or after dialog confirmation
if (database.validatePasswordEncoding(masterPassword, keyFileChecked)) {
mProgressDialogThread?.startDatabaseAssignPassword(
mProgressDatabaseTaskProvider?.startDatabaseAssignPassword(
databaseUri,
masterPasswordChecked,
masterPassword,
@@ -143,7 +146,7 @@ open class SettingsActivity
} else {
PasswordEncodingDialogFragment().apply {
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
mProgressDialogThread?.startDatabaseAssignPassword(
mProgressDatabaseTaskProvider?.startDatabaseAssignPassword(
databaseUri,
masterPasswordChecked,
masterPassword,

View File

@@ -36,7 +36,6 @@ import com.kunzisoft.androidclearchroma.colormode.ColorMode
import com.kunzisoft.androidclearchroma.fragment.ChromaColorFragment
import com.kunzisoft.androidclearchroma.fragment.ChromaColorFragment.*
import com.kunzisoft.keepass.R
import java.lang.Exception
class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
@@ -87,7 +86,7 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
}
val oldColor = database.customColor
database.customColor = newColor
mProgressDialogThread?.startDatabaseSaveColor(oldColor, newColor, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveColor(oldColor, newColor, mDatabaseAutoSaveEnable)
}
onDialogClosed(true)

View File

@@ -64,7 +64,7 @@ class DatabaseDataCompressionPreferenceDialogFragmentCompat
database.compressionAlgorithm = newCompression
if (oldCompression != null && newCompression != null)
mProgressDialogThread?.startDatabaseSaveCompression(oldCompression, newCompression, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveCompression(oldCompression, newCompression, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -36,7 +36,7 @@ class DatabaseDefaultUsernamePreferenceDialogFragmentCompat : DatabaseSavePrefer
val newDefaultUsername = inputText
val oldDefaultUsername = database.defaultUsername
database.defaultUsername = newDefaultUsername
mProgressDialogThread?.startDatabaseSaveDefaultUsername(oldDefaultUsername, newDefaultUsername, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(oldDefaultUsername, newDefaultUsername, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -36,7 +36,7 @@ class DatabaseDescriptionPreferenceDialogFragmentCompat : DatabaseSavePreference
val newDescription = inputText
val oldDescription = database.description
database.description = newDescription
mProgressDialogThread?.startDatabaseSaveDescription(oldDescription, newDescription, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveDescription(oldDescription, newDescription, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -65,7 +65,7 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
database.encryptionAlgorithm = newAlgorithm
if (oldAlgorithm != null && newAlgorithm != null)
mProgressDialogThread?.startDatabaseSaveEncryption(oldAlgorithm, newAlgorithm, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveEncryption(oldAlgorithm, newAlgorithm, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -66,7 +66,7 @@ class DatabaseKeyDerivationPreferenceDialogFragmentCompat
val oldKdfEngine = database.kdfEngine
if (newKdfEngine != null && oldKdfEngine != null) {
database.kdfEngine = newKdfEngine
mProgressDialogThread?.startDatabaseSaveKeyDerivation(oldKdfEngine, newKdfEngine, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveKeyDerivation(oldKdfEngine, newKdfEngine, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -36,7 +36,7 @@ class DatabaseNamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogF
val newName = inputText
val oldName = database.name
database.name = newName
mProgressDialogThread?.startDatabaseSaveName(oldName, newName, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveName(oldName, newName, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.content.Context
import android.os.Bundle
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.settings.PreferencesUtil
import com.kunzisoft.keepass.settings.SettingsActivity
@@ -30,7 +30,7 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat : InputPreferenceDialo
protected var database: Database? = null
protected var mDatabaseAutoSaveEnable = true
protected var mProgressDialogThread: ProgressDialogThread? = null
protected var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -42,7 +42,7 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat : InputPreferenceDialo
super.onAttach(context)
// Attach dialog thread to start action
if (context is SettingsActivity) {
mProgressDialogThread = context.mProgressDialogThread
mProgressDatabaseTaskProvider = context.mProgressDatabaseTaskProvider
}
this.mDatabaseAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(context)

View File

@@ -60,7 +60,7 @@ class MaxHistoryItemsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDial
// Remove all history items
database.removeOldestHistoryForEachEntry()
mProgressDialogThread?.startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems, maxHistoryItems, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems, maxHistoryItems, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -56,7 +56,7 @@ class MaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialo
val oldMaxHistorySize = database.historyMaxSize
database.historyMaxSize = maxHistorySize
mProgressDialogThread?.startDatabaseSaveMaxHistorySize(oldMaxHistorySize, maxHistorySize, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(oldMaxHistorySize, maxHistorySize, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -48,7 +48,7 @@ class MemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFr
val oldMemoryUsage = database.memoryUsage
database.memoryUsage = memoryUsage
mProgressDialogThread?.startDatabaseSaveMemoryUsage(oldMemoryUsage, memoryUsage, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(oldMemoryUsage, memoryUsage, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -44,7 +44,7 @@ class ParallelismPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFr
val oldParallelism = database.parallelism
database.parallelism = parallelism
mProgressDialogThread?.startDatabaseSaveParallelism(
mProgressDatabaseTaskProvider?.startDatabaseSaveParallelism(
oldParallelism,
parallelism,
mDatabaseAutoSaveEnable)

View File

@@ -54,7 +54,7 @@ class RoundsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmen
database.numberKeyEncryptionRounds = Long.MAX_VALUE
}
mProgressDialogThread?.startDatabaseSaveIterations(oldRounds, rounds, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveIterations(oldRounds, rounds, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -81,7 +81,7 @@ class HashedBlockInputStream(inputStream: InputStream) : InputStream() {
bufferPos = 0
val index = baseStream.readUInt()
if (index.toLong() != bufferIndex) {
if (index.toKotlinLong() != bufferIndex) {
throw IOException("Invalid data format")
}
bufferIndex++
@@ -91,7 +91,7 @@ class HashedBlockInputStream(inputStream: InputStream) : InputStream() {
throw IOException("Invalid data format")
}
val bufferSize = baseStream.readBytes4ToUInt().toInt()
val bufferSize = baseStream.readBytes4ToUInt().toKotlinInt()
if (bufferSize == 0) {
for (hash in 0 until HASH_SIZE) {
if (storedHash[hash].toInt() != 0) {
@@ -141,7 +141,7 @@ class HashedBlockInputStream(inputStream: InputStream) : InputStream() {
if (!readHashedBlock()) return -1
}
val output = UnsignedInt.fromByte(buffer[bufferPos]).toInt()
val output = UnsignedInt.fromKotlinByte(buffer[bufferPos]).toKotlinInt()
bufferPos++
return output

View File

@@ -99,7 +99,7 @@ class HashedBlockOutputStream : OutputStream {
@Throws(IOException::class)
private fun writeHashedBlock() {
baseStream.writeUInt(UnsignedInt.fromLong(bufferIndex))
baseStream.writeUInt(UnsignedInt.fromKotlinLong(bufferIndex))
bufferIndex++
if (bufferPos > 0) {

View File

@@ -44,7 +44,7 @@ class HmacBlockInputStream(baseStream: InputStream, private val verify: Boolean,
if (!readSafeBlock()) return -1
}
val output = UnsignedInt.fromByte(buffer[bufferPos]).toInt()
val output = UnsignedInt.fromKotlinByte(buffer[bufferPos]).toKotlinInt()
bufferPos++
return output
@@ -101,7 +101,7 @@ class HmacBlockInputStream(baseStream: InputStream, private val verify: Boolean,
val blockSize = bytes4ToUInt(pbBlockSize)
bufferPos = 0
buffer = baseStream.readBytes(blockSize.toInt())
buffer = baseStream.readBytes(blockSize.toKotlinInt())
if (verify) {
val cmpHmac: ByteArray
@@ -135,7 +135,7 @@ class HmacBlockInputStream(baseStream: InputStream, private val verify: Boolean,
blockIndex++
if (blockSize.toLong() == 0L) {
if (blockSize.toKotlinLong() == 0L) {
endOfStream = true
return false
}

View File

@@ -171,11 +171,11 @@ fun bytes5ToDate(buf: ByteArray, calendar: Calendar = Calendar.getInstance()): D
System.arraycopy(buf, 0, cDate, 0, dateSize)
val readOffset = 0
val dw1 = UnsignedInt.fromByte(cDate[readOffset]).toInt()
val dw2 = UnsignedInt.fromByte(cDate[readOffset + 1]).toInt()
val dw3 = UnsignedInt.fromByte(cDate[readOffset + 2]).toInt()
val dw4 = UnsignedInt.fromByte(cDate[readOffset + 3]).toInt()
val dw5 = UnsignedInt.fromByte(cDate[readOffset + 4]).toInt()
val dw1 = UnsignedInt.fromKotlinByte(cDate[readOffset]).toKotlinInt()
val dw2 = UnsignedInt.fromKotlinByte(cDate[readOffset + 1]).toKotlinInt()
val dw3 = UnsignedInt.fromKotlinByte(cDate[readOffset + 2]).toKotlinInt()
val dw4 = UnsignedInt.fromKotlinByte(cDate[readOffset + 3]).toKotlinInt()
val dw5 = UnsignedInt.fromKotlinByte(cDate[readOffset + 4]).toKotlinInt()
// Unpack 5 byte structure to date and time
val year = dw1 shl 6 or (dw2 shr 2)
@@ -199,7 +199,7 @@ fun bytes5ToDate(buf: ByteArray, calendar: Calendar = Calendar.getInstance()): D
fun uIntTo4Bytes(value: UnsignedInt): ByteArray {
val buf = ByteArray(4)
for (i in 0 until 4) {
buf[i] = (value.toInt().ushr(8 * i) and 0xFF).toByte()
buf[i] = (value.toKotlinInt().ushr(8 * i) and 0xFF).toByte()
}
return buf
}
@@ -246,8 +246,8 @@ fun dateTo5Bytes(date: Date, calendar: Calendar = Calendar.getInstance()): ByteA
val minute = calendar.get(Calendar.MINUTE)
val second = calendar.get(Calendar.SECOND)
buf[0] = UnsignedInt(year shr 6 and 0x0000003F).toByte()
buf[1] = UnsignedInt(year and 0x0000003F shl 2 or (month shr 2 and 0x00000003)).toByte()
buf[0] = UnsignedInt(year shr 6 and 0x0000003F).toKotlinByte()
buf[1] = UnsignedInt(year and 0x0000003F shl 2 or (month shr 2 and 0x00000003)).toKotlinByte()
buf[2] = (month and 0x00000003 shl 6
or (day and 0x0000001F shl 1) or (hour shr 4 and 0x00000001)).toByte()
buf[3] = (hour and 0x0000000F shl 4 or (minute shr 2 and 0x0000000F)).toByte()

View File

@@ -20,8 +20,8 @@
package com.kunzisoft.keepass.tasks
import android.os.Bundle
import android.util.Log
import com.kunzisoft.keepass.database.exception.DatabaseException
import java.lang.Exception
/**
* Callback after a task is completed.
@@ -49,18 +49,26 @@ abstract class ActionRunnable: Runnable {
result.isSuccess = false
result.exception = null
result.message = message
showLog()
}
protected fun setError(exception: Exception) {
result.isSuccess = false
result.exception = null
result.message = exception.message
showLog()
}
protected fun setError(exception: DatabaseException) {
result.isSuccess = false
result.exception = exception
result.message = exception.message
showLog()
}
private fun showLog() {
val message = if (result.message != null) ", message=${result.message}" else ""
Log.e(TAG, "success=${result.isSuccess}$message", result.exception)
}
/**
@@ -70,4 +78,8 @@ abstract class ActionRunnable: Runnable {
var message: String? = null,
var exception: DatabaseException? = null,
var data: Bundle? = null)
companion object {
private const val TAG = "ActionRunnable"
}
}

View File

@@ -1,93 +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.tasks
import android.content.ContentResolver
import android.net.Uri
import android.os.AsyncTask
import android.util.Log
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
class AttachmentFileAsyncTask(
private val fileUri: Uri,
private val attachmentNotification: AttachmentFileNotificationService.AttachmentNotification,
private val contentResolver: ContentResolver)
: AsyncTask<Void, Int, Boolean>() {
private val updateMinFrequency = 1000
private var previousSaveTime = System.currentTimeMillis()
var onUpdate: ((Uri, EntryAttachment, Int)->Unit)? = null
override fun onPreExecute() {
super.onPreExecute()
attachmentNotification.attachmentTask = this
attachmentNotification.entryAttachment.apply {
downloadState = AttachmentState.START
downloadProgression = 0
}
onUpdate?.invoke(fileUri, attachmentNotification.entryAttachment, attachmentNotification.notificationId)
}
override fun doInBackground(vararg params: Void?): Boolean {
try {
attachmentNotification.entryAttachment.apply {
downloadState = AttachmentState.IN_PROGRESS
binaryAttachment.download(fileUri, contentResolver, 1024) { percent ->
publishProgress(percent)
}
}
} catch (e: Exception) {
return false
}
return true
}
override fun onProgressUpdate(vararg values: Int?) {
super.onProgressUpdate(*values)
val percent = values[0] ?: 0
val currentTime = System.currentTimeMillis()
if (previousSaveTime + updateMinFrequency < currentTime) {
attachmentNotification.entryAttachment.apply {
downloadState = AttachmentState.IN_PROGRESS
downloadProgression = percent
}
onUpdate?.invoke(fileUri, attachmentNotification.entryAttachment, attachmentNotification.notificationId)
Log.d(TAG, "Download file $fileUri : $percent%")
previousSaveTime = currentTime
}
}
override fun onPostExecute(result: Boolean) {
super.onPostExecute(result)
attachmentNotification.attachmentTask = null
attachmentNotification.entryAttachment.apply {
downloadState = if (result) AttachmentState.COMPLETE else AttachmentState.ERROR
downloadProgression = 100
}
onUpdate?.invoke(fileUri, attachmentNotification.entryAttachment, attachmentNotification.notificationId)
}
companion object {
private val TAG = AttachmentFileAsyncTask::class.java.name
}
}

View File

@@ -38,6 +38,7 @@ object TimeoutHelper {
private const val TAG = "TimeoutHelper"
private var lastAppTimeoutRecord: Long? = null
var temporarilyDisableTimeout = false
private set
@@ -84,9 +85,15 @@ object TimeoutHelper {
* Record the current time, to check it later with checkTime and start a new lock timer
*/
fun recordTime(context: Context) {
// Record timeout time in case timeout service is killed
PreferencesUtil.saveCurrentTime(context)
startLockTimer(context)
// To prevent spam registration, record after at least 2 seconds
if (lastAppTimeoutRecord == null
|| lastAppTimeoutRecord!! + 2000 <= System.currentTimeMillis()) {
Log.d(TAG, "Record app timeout")
// Record timeout time in case timeout service is killed
PreferencesUtil.saveCurrentTime(context)
startLockTimer(context)
lastAppTimeoutRecord = System.currentTimeMillis()
}
}
/**

View File

@@ -42,10 +42,12 @@ const val DATABASE_STOP_TASK_ACTION = "com.kunzisoft.keepass.DATABASE_STOP_TASK_
const val LOCK_ACTION = "com.kunzisoft.keepass.LOCK"
const val REMOVE_ENTRY_MAGIKEYBOARD_ACTION = "com.kunzisoft.keepass.REMOVE_ENTRY_MAGIKEYBOARD"
const val BACK_PREVIOUS_KEYBOARD_ACTION = "com.kunzisoft.keepass.BACK_PREVIOUS_KEYBOARD"
class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
var mLockPendingIntent: PendingIntent? = null
var backToPreviousKeyboardAction: (() -> Unit)? = null
override fun onReceive(context: Context, intent: Intent) {
// If allowed, lock and exit
@@ -76,7 +78,12 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
}
}
LOCK_ACTION,
REMOVE_ENTRY_MAGIKEYBOARD_ACTION -> lockAction.invoke()
REMOVE_ENTRY_MAGIKEYBOARD_ACTION -> {
lockAction.invoke()
}
BACK_PREVIOUS_KEYBOARD_ACTION -> {
backToPreviousKeyboardAction?.invoke()
}
else -> {}
}
}
@@ -93,14 +100,16 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
}
fun Context.registerLockReceiver(lockReceiver: LockReceiver?,
registerRemoveEntryMagikeyboard: Boolean = false) {
registerKeyboardAction: Boolean = false) {
lockReceiver?.let {
registerReceiver(it, IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_SCREEN_ON)
addAction(LOCK_ACTION)
if (registerRemoveEntryMagikeyboard)
if (registerKeyboardAction) {
addAction(REMOVE_ENTRY_MAGIKEYBOARD_ACTION)
addAction(BACK_PREVIOUS_KEYBOARD_ACTION)
}
})
}
}

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.utils
import android.os.Parcel
import android.os.Parcelable
import java.util.*
import kotlin.collections.LinkedHashMap
object ParcelableUtil {
@@ -51,7 +52,7 @@ object ParcelableUtil {
// For writing map with string key to a Parcel
fun <V : Parcelable> writeStringParcelableMap(
parcel: Parcel, flags: Int, map: Map<String, V>) {
parcel: Parcel, flags: Int, map: LinkedHashMap<String, V>) {
parcel.writeInt(map.size)
for ((key, value) in map) {
parcel.writeString(key)
@@ -61,9 +62,9 @@ object ParcelableUtil {
// For reading map with string key from a Parcel
fun <V : Parcelable> readStringParcelableMap(
parcel: Parcel, vClass: Class<V>): HashMap<String, V> {
parcel: Parcel, vClass: Class<V>): LinkedHashMap<String, V> {
val size = parcel.readInt()
val map = HashMap<String, V>(size)
val map = LinkedHashMap<String, V>(size)
for (i in 0 until size) {
val key: String? = parcel.readString()
val value: V? = vClass.cast(parcel.readParcelable(vClass.classLoader))
@@ -75,7 +76,7 @@ object ParcelableUtil {
// For writing map with string key and string value to a Parcel
fun writeStringParcelableMap(dest: Parcel, map: Map<String, String>) {
fun writeStringParcelableMap(dest: Parcel, map: LinkedHashMap<String, String>) {
dest.writeInt(map.size)
for ((key, value) in map) {
dest.writeString(key)
@@ -84,9 +85,9 @@ object ParcelableUtil {
}
// For reading map with string key and string value from a Parcel
fun readStringParcelableMap(parcel: Parcel): HashMap<String, String> {
fun readStringParcelableMap(parcel: Parcel): LinkedHashMap<String, String> {
val size = parcel.readInt()
val map = HashMap<String, String>(size)
val map = LinkedHashMap<String, String>(size)
for (i in 0 until size) {
val key: String? = parcel.readString()
val value: String? = parcel.readString()

View File

@@ -21,31 +21,31 @@ package com.kunzisoft.keepass.utils
class UnsignedInt(private var unsignedValue: Int) {
constructor(unsignedValue: UnsignedInt) : this(unsignedValue.toInt())
constructor(unsignedValue: UnsignedInt) : this(unsignedValue.toKotlinInt())
/**
* Get the int value
*/
fun toInt(): Int {
fun toKotlinInt(): Int {
return unsignedValue
}
/**
* Convert an unsigned Integer to Long
*/
fun toLong(): Long {
fun toKotlinLong(): Long {
return unsignedValue.toLong() and INT_TO_LONG_MASK
}
/**
* Convert an unsigned Integer to Byte
*/
fun toByte(): Byte {
fun toKotlinByte(): Byte {
return (unsignedValue and 0xFF).toByte()
}
override fun toString():String {
return toLong().toString()
return toKotlinLong().toString()
}
override fun equals(other: Any?): Boolean {
@@ -72,12 +72,12 @@ class UnsignedInt(private var unsignedValue: Int) {
/**
* Convert a byte to an unsigned byte
*/
fun fromByte(value: Byte): UnsignedInt {
fun fromKotlinByte(value: Byte): UnsignedInt {
return UnsignedInt(value.toInt() and 0xFF)
}
@Throws(NumberFormatException::class)
fun fromLong(value: Long): UnsignedInt {
fun fromKotlinLong(value: Long): UnsignedInt {
if (value > UINT_MAX_VALUE)
throw NumberFormatException("UInt value > $UINT_MAX_VALUE")
return UnsignedInt(value.toInt())

View File

@@ -24,7 +24,7 @@ class UnsignedLong(private var unsignedValue: Long) {
/**
* Convert an unsigned Integer to Long
*/
fun toLong(): Long {
fun toKotlinLong(): Long {
return unsignedValue
}

View File

@@ -127,13 +127,13 @@ open class VariantDictionary {
if (bType == VdType.None) {
break
}
val nameLen = inputStream.readUInt().toInt()
val nameLen = inputStream.readUInt().toKotlinInt()
val nameBuf = inputStream.readBytes(nameLen)
if (nameLen != nameBuf.size) {
throw IOException("Invalid format")
}
val name = String(nameBuf, UTF8Charset)
val valueLen = inputStream.readUInt().toInt()
val valueLen = inputStream.readUInt().toKotlinInt()
val valueBuf = inputStream.readBytes(valueLen)
if (valueLen != valueBuf.size) {
throw IOException("Invalid format")
@@ -149,7 +149,7 @@ open class VariantDictionary {
dictionary.setBool(name, valueBuf[0] != 0.toByte())
}
VdType.Int32 -> if (valueLen == 4) {
dictionary.setInt32(name, bytes4ToUInt(valueBuf).toInt())
dictionary.setInt32(name, bytes4ToUInt(valueBuf).toKotlinInt())
}
VdType.Int64 -> if (valueLen == 8) {
dictionary.setInt64(name, bytes64ToLong(valueBuf))

View File

@@ -121,7 +121,7 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
return super.onTouchEvent(event)
}
fun hideButtonOnScrollListener(dy: Int) {
fun hideOrShowButtonOnScrollListener(dy: Int) {
if (state == State.CLOSE) {
if (dy > 0 && addButtonView?.visibility == View.VISIBLE) {
hideButton()

View File

@@ -113,12 +113,4 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
message = context.getString(textId)
}
var hide: Boolean
get() {
return visibility != VISIBLE
}
set(value) {
visibility = if (value) View.GONE else View.VISIBLE
}
}

View File

@@ -0,0 +1,42 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.util.AttributeSet
import com.google.android.material.textfield.TextInputEditText
class EditTextSelectable @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: TextInputEditText(context, attrs) {
// TODO constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
// after material design upgrade
private val mOnSelectionChangedListeners: MutableList<OnSelectionChangedListener>?
init {
mOnSelectionChangedListeners = ArrayList()
}
fun addOnSelectionChangedListener(onSelectionChangedListener: OnSelectionChangedListener) {
mOnSelectionChangedListeners?.add(onSelectionChangedListener)
}
fun removeOnSelectionChangedListener(onSelectionChangedListener: OnSelectionChangedListener) {
mOnSelectionChangedListeners?.remove(onSelectionChangedListener)
}
fun removeAllOnSelectionChangedListeners() {
mOnSelectionChangedListeners?.clear()
}
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
mOnSelectionChangedListeners?.forEach {
it.onSelectionChanged(selStart, selEnd)
}
}
interface OnSelectionChangedListener {
fun onSelectionChanged(start: Int, end: Int)
}
}

View File

@@ -19,7 +19,6 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.graphics.Color
import android.text.util.Linkify
import android.util.AttributeSet
import android.view.LayoutInflater
@@ -34,7 +33,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.EntryAttachmentsAdapter
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Entry
@@ -52,7 +51,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
: LinearLayout(context, attrs, defStyle) {
private var fontInVisibility: Boolean = false
private val colorAccent: Int
private val userNameContainerView: View
private val userNameView: TextView
@@ -75,8 +73,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val notesContainerView: View
private val notesView: TextView
private val extrasContainerView: View
private val extrasView: ViewGroup
private val extraFieldsContainerView: View
private val extraFieldsListView: ViewGroup
private val creationDateView: TextView
private val modificationDateView: TextView
@@ -86,7 +84,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val attachmentsContainerView: View
private val attachmentsListView: RecyclerView
private val attachmentsAdapter = EntryAttachmentsAdapter(context)
private val attachmentsAdapter = EntryAttachmentsItemsAdapter(context, false)
private val historyContainerView: View
private val historyListView: RecyclerView
@@ -124,8 +122,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
notesContainerView = findViewById(R.id.entry_notes_container)
notesView = findViewById(R.id.entry_notes)
extrasContainerView = findViewById(R.id.extra_strings_container)
extrasView = findViewById(R.id.extra_strings)
extraFieldsContainerView = findViewById(R.id.extra_fields_container)
extraFieldsListView = findViewById(R.id.extra_fields_list)
attachmentsContainerView = findViewById(R.id.entry_attachments_container)
attachmentsListView = findViewById(R.id.entry_attachments_list)
@@ -150,11 +148,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
uuidView = findViewById(R.id.entry_UUID)
uuidReferenceView = findViewById(R.id.entry_UUID_reference)
val attrColorAccent = intArrayOf(R.attr.colorAccent)
val taColorAccent = context.theme.obtainStyledAttributes(attrColorAccent)
colorAccent = taColorAccent.getColor(0, Color.BLACK)
taColorAccent.recycle()
}
fun applyFontVisibilityToFields(fontInVisibility: Boolean) {
@@ -186,11 +179,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
if (fontInVisibility)
applyFontVisibility()
}
if (allowCopyPassword) {
passwordActionView.setColorFilter(colorAccent)
} else {
passwordActionView.setColorFilter(ContextCompat.getColor(context, R.color.grey_dark))
}
passwordActionView.isActivated = !allowCopyPassword
} else {
passwordContainerView.visibility = View.GONE
}
@@ -206,10 +195,10 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
}
fun atLeastOneFieldProtectedPresent(): Boolean {
extrasView.let {
extraFieldsListView.let {
for (i in 0 until it.childCount) {
val childCustomView = it.getChildAt(i)
if (childCustomView is EntryCustomField)
if (childCustomView is EntryExtraField)
if (childCustomView.isProtected)
return true
}
@@ -220,10 +209,10 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
fun setHiddenPasswordStyle(hiddenStyle: Boolean) {
passwordView.applyHiddenStyle(hiddenStyle)
// Hidden style for custom fields
extrasView.let {
extraFieldsListView.let {
for (i in 0 until it.childCount) {
val childCustomView = it.getChildAt(i)
if (childCustomView is EntryCustomField)
if (childCustomView is EntryExtraField)
childCustomView.setHiddenPasswordStyle(hiddenStyle)
}
}
@@ -304,30 +293,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
}
}
fun addExtraField(title: String,
value: ProtectedString,
enableActionButton: Boolean,
onActionClickListener: OnClickListener?) {
val entryCustomField: EntryCustomField? = EntryCustomField(context, attrs, defStyle)
entryCustomField?.apply {
setLabel(title)
setValue(value.toString(), value.isProtected)
enableActionButton(enableActionButton)
assignActionButtonClickListener(onActionClickListener)
applyFontVisibility(fontInVisibility)
}
entryCustomField?.let {
extrasView.addView(it)
}
extrasContainerView.visibility = View.VISIBLE
}
fun clearExtraFields() {
extrasView.removeAllViews()
extrasContainerView.visibility = View.GONE
}
fun assignCreationDate(date: DateInstant) {
creationDateView.text = date.getDateTimeString(resources)
}
@@ -357,56 +322,77 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
uuidReferenceView.text = UuidUtil.toHexString(uuid)
}
/* -------------
* Extra Fields
* -------------
*/
private fun showOrHideExtraFieldsContainer(hide: Boolean) {
extraFieldsContainerView.visibility = if (hide) View.GONE else View.VISIBLE
}
fun addExtraField(title: String,
value: ProtectedString,
allowCopy: Boolean,
onActionClickListener: OnClickListener?) {
val entryCustomField: EntryExtraField? = EntryExtraField(context, attrs, defStyle)
entryCustomField?.apply {
setLabel(title)
setValue(value.toString(), value.isProtected)
activateActionButton(allowCopy)
assignActionButtonClickListener(onActionClickListener)
applyFontVisibility(fontInVisibility)
}
entryCustomField?.let {
extraFieldsListView.addView(it)
}
showOrHideExtraFieldsContainer(false)
}
fun clearExtraFields() {
extraFieldsListView.removeAllViews()
showOrHideExtraFieldsContainer(true)
}
/* -------------
* Attachments
* -------------
*/
fun showAttachments(show: Boolean) {
private fun showAttachments(show: Boolean) {
attachmentsContainerView.visibility = if (show) View.VISIBLE else View.GONE
}
fun refreshAttachments() {
attachmentsAdapter.notifyDataSetChanged()
}
fun assignAttachments(attachments: ArrayList<EntryAttachment>) {
attachmentsAdapter.clear()
attachmentsAdapter.entryAttachmentsList.addAll(attachments)
fun assignAttachments(attachments: ArrayList<EntryAttachment>,
onAttachmentClicked: (attachment: EntryAttachment)->Unit) {
showAttachments(attachments.isNotEmpty())
attachmentsAdapter.assignItems(attachments)
attachmentsAdapter.onItemClickListener = { item ->
onAttachmentClicked.invoke(item)
}
}
fun updateAttachmentDownloadProgress(attachmentToDownload: EntryAttachment) {
attachmentsAdapter.updateProgress(attachmentToDownload)
}
fun onAttachmentClick(action: (attachment: EntryAttachment, position: Int)->Unit) {
attachmentsAdapter.onItemClickListener = { item, position ->
action.invoke(item, position)
}
}
/* -------------
* History
* -------------
*/
fun showHistory(show: Boolean) {
historyContainerView.visibility = if (show) View.VISIBLE else View.GONE
}
fun refreshHistory() {
historyAdapter.notifyDataSetChanged()
}
fun assignHistory(history: ArrayList<Entry>) {
fun assignHistory(history: ArrayList<Entry>, action: (historyItem: Entry, position: Int)->Unit) {
historyAdapter.clear()
historyAdapter.entryHistoryList.addAll(history)
}
fun onHistoryClick(action: (historyItem: Entry, position: Int)->Unit) {
historyAdapter.onItemClickListener = { item, position ->
action.invoke(item, position)
}
action.invoke(item, position)
}
historyContainerView.visibility = if (historyAdapter.entryHistoryList.isEmpty())
View.GONE
else
View.VISIBLE
historyAdapter.notifyDataSetChanged()
}
override fun generateDefaultLayoutParams(): LayoutParams {

View File

@@ -23,17 +23,24 @@ import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.adapters.EntryExtraFieldsItemsAdapter
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.icons.assignDefaultDatabaseIcon
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.model.FocusedEditField
import org.joda.time.Duration
import org.joda.time.Instant
@@ -51,11 +58,17 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
private val entryUrlView: EditText
private val entryPasswordLayoutView: TextInputLayout
private val entryPasswordView: EditText
private val entryConfirmationPasswordView: EditText
val entryPasswordGeneratorView: View
private val entryExpiresCheckBox: CompoundButton
private val entryExpiresTextView: TextView
private val entryNotesView: EditText
private val entryExtraFieldsContainer: ViewGroup
private val extraFieldsContainerView: ViewGroup
private val extraFieldsListView: RecyclerView
private val attachmentsContainerView: View
private val attachmentsListView: RecyclerView
private val extraFieldsAdapter = EntryExtraFieldsItemsAdapter(context)
private val attachmentsAdapter = EntryAttachmentsItemsAdapter(context, true)
private var iconColor: Int = 0
private var expiresInstant: DateInstant = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
@@ -80,11 +93,41 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
entryUrlView = findViewById(R.id.entry_edit_url)
entryPasswordLayoutView = findViewById(R.id.entry_edit_container_password)
entryPasswordView = findViewById(R.id.entry_edit_password)
entryConfirmationPasswordView = findViewById(R.id.entry_edit_confirmation_password)
entryPasswordGeneratorView = findViewById(R.id.entry_edit_password_generator_button)
entryExpiresCheckBox = findViewById(R.id.entry_edit_expires_checkbox)
entryExpiresTextView = findViewById(R.id.entry_edit_expires_text)
entryNotesView = findViewById(R.id.entry_edit_notes)
entryExtraFieldsContainer = findViewById(R.id.entry_edit_advanced_container)
extraFieldsContainerView = findViewById(R.id.extra_fields_container)
extraFieldsListView = findViewById(R.id.extra_fields_list)
// To hide or not the container
extraFieldsAdapter.onListSizeChangedListener = { previousSize, newSize ->
if (previousSize > 0 && newSize == 0) {
extraFieldsContainerView.collapse(true)
} else if (previousSize == 0 && newSize == 1) {
extraFieldsContainerView.expand(true)
}
}
extraFieldsListView?.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adapter = extraFieldsAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
attachmentsContainerView = findViewById(R.id.entry_attachments_container)
attachmentsListView = findViewById(R.id.entry_attachments_list)
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, true)
adapter = attachmentsAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
assignExpiresDateText()
@@ -98,6 +141,7 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
fun applyFontVisibilityToFields(fontInVisibility: Boolean) {
this.fontInVisibility = fontInVisibility
this.extraFieldsAdapter.applyFontVisibility = fontInVisibility
}
var title: String
@@ -148,10 +192,8 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
}
set(value) {
entryPasswordView.setText(value)
entryConfirmationPasswordView.setText(value)
if (fontInVisibility) {
entryPasswordView.applyFontVisibility()
entryConfirmationPasswordView.applyFontVisibility()
}
}
@@ -195,63 +237,53 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
entryNotesView.applyFontVisibility()
}
val customFields: MutableList<Field>
get() {
val customFieldsArray = ArrayList<Field>()
// Add extra fields from views
entryExtraFieldsContainer.let {
try {
for (i in 0 until it.childCount) {
val view = it.getChildAt(i) as EntryEditCustomField
val key = view.label
val value = view.value
val protect = view.isProtected
customFieldsArray.add(Field(key, ProtectedString(protect, value)))
}
} catch (exception: Exception) {
// Extra field container contains another type of view
}
}
return customFieldsArray
}
/**
* Add a new view to fill in the information of the customized field and focus it
/* -------------
* Extra Fields
* -------------
*/
fun addEmptyCustomField() {
// Fix current custom field before adding a new one
if (isValid()) {
val entryEditCustomField = EntryEditCustomField(context).apply {
setFontVisibility(fontInVisibility)
requestFocus()
}
entryExtraFieldsContainer.addView(entryEditCustomField)
}
fun getExtraField(): MutableList<Field> {
return extraFieldsAdapter.itemsList
}
fun getExtraFieldFocused(): FocusedEditField {
// To keep focused after an orientation change
return extraFieldsAdapter.getFocusedField()
}
/**
* Update a custom field or create a new one if doesn't exists
* Remove all children and add new views for each field
*/
fun putCustomField(name: String,
value: ProtectedString = ProtectedString()) {
var updateField = false
for (i in 0..entryExtraFieldsContainer.childCount) {
try {
val extraFieldView = entryExtraFieldsContainer.getChildAt(i) as EntryEditCustomField?
if (extraFieldView?.label == name) {
extraFieldView.setData(name, value, fontInVisibility)
updateField = true
break
}
} catch(e: Exception) {
// Simply ignore when child view is not a custom field
}
fun assignExtraFields(fields: List<Field>, focusedExtraField: FocusedEditField? = null) {
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
// Reinit focused field
extraFieldsAdapter.assignItems(fields, focusedExtraField)
}
/**
* Update an extra field or create a new one if doesn't exists
*/
fun putExtraField(extraField: Field) {
extraFieldsContainerView.visibility = View.VISIBLE
val oldField = extraFieldsAdapter.itemsList.firstOrNull { it.name == extraField.name }
oldField?.let {
if (extraField.protectedValue.stringValue.isEmpty())
extraField.protectedValue.stringValue = it.protectedValue.stringValue
}
if (!updateField) {
val entryEditCustomField = EntryEditCustomField(context).apply {
setData(name, value, fontInVisibility)
}
entryExtraFieldsContainer.addView(entryEditCustomField)
extraFieldsAdapter.putItem(extraField)
}
/* -------------
* Attachments
* -------------
*/
fun assignAttachments(attachments: ArrayList<EntryAttachment>,
onDeleteItem: (attachment: EntryAttachment)->Unit) {
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
attachmentsAdapter.assignItems(attachments)
attachmentsAdapter.onDeleteButtonClickListener = { item ->
onDeleteItem.invoke(item)
}
}
@@ -261,33 +293,7 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
* @return ErrorValidation An error with a message or a validation without message
*/
fun isValid(): Boolean {
// Validate password
if (entryPasswordView.text.toString() != entryConfirmationPasswordView.text.toString()) {
entryPasswordLayoutView.error = context.getString(R.string.error_pass_match)
return false
} else {
entryPasswordLayoutView.error = null
}
// Validate extra fields
entryExtraFieldsContainer.let {
try {
val customFieldLabelSet = HashSet<String>()
for (i in 0 until it.childCount) {
val entryEditCustomField = it.getChildAt(i) as EntryEditCustomField
if (customFieldLabelSet.contains(entryEditCustomField.label)) {
entryEditCustomField.setError(R.string.error_label_exists)
return false
}
customFieldLabelSet.add(entryEditCustomField.label)
if (!entryEditCustomField.isValid()) {
return false
}
}
} catch (exception: Exception) {
return false
}
}
// TODO
return true
}

View File

@@ -1,116 +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.view
import android.content.Context
import com.google.android.material.textfield.TextInputLayout
import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.annotation.StringRes
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.security.ProtectedString
class EntryEditCustomField @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: RelativeLayout(context, attrs, defStyle) {
private val labelLayoutView: TextInputLayout
private val labelView: TextView
private val valueView: EditText
private val protectionCheckView: CompoundButton
val label: String
get() = labelView.text.toString()
val value: String
get() = valueView.text.toString()
val isProtected: Boolean
get() = protectionCheckView.isChecked
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_entry_new_field, this)
val deleteView = findViewById<View>(R.id.entry_new_field_delete)
deleteView.setOnClickListener { deleteViewFromParent() }
labelLayoutView = findViewById(R.id.title_container)
labelView = findViewById(R.id.entry_new_field_label)
valueView = findViewById(R.id.entry_new_field_value)
protectionCheckView = findViewById(R.id.protection)
}
fun setData(label: String?, value: ProtectedString?, fontInVisibility: Boolean) {
if (label != null)
labelView.text = label
if (value != null) {
valueView.setText(value.toString())
protectionCheckView.isChecked = value.isProtected
}
setFontVisibility(fontInVisibility)
}
/**
* Validate or not the entry form
*
* @return ErrorValidation An error with a message or a validation without message
*/
fun isValid(): Boolean {
// Validate extra field
if (label.isEmpty()) {
setError(R.string.error_string_key)
return false
} else {
setError(null)
}
return true
}
fun setError(@StringRes errorId: Int?) {
labelLayoutView.error = if (errorId == null) null else {
context.getString(errorId)
}
}
fun setFontVisibility(applyFontVisibility: Boolean) {
if (applyFontVisibility)
valueView.applyFontVisibility()
}
private fun deleteViewFromParent() {
try {
val parent = parent as ViewGroup
parent.removeView(this)
parent.invalidate()
} catch (e: ClassCastException) {
Log.e(javaClass.name, "Unable to delete view", e)
}
}
}

View File

@@ -20,18 +20,16 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
class EntryCustomField @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
class EntryExtraField @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: LinearLayout(context, attrs, defStyle) {
private val labelView: TextView
@@ -39,21 +37,14 @@ class EntryCustomField @JvmOverloads constructor(context: Context,
private val actionImageView: ImageView
var isProtected = false
private val colorAccent: Int
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.item_entry_new_field, this)
inflater?.inflate(R.layout.item_entry_extra_field, this)
labelView = findViewById(R.id.title)
valueView = findViewById(R.id.value)
actionImageView = findViewById(R.id.action_image)
val attrColorAccent = intArrayOf(R.attr.colorAccent)
val taColorAccent = context.theme.obtainStyledAttributes(attrColorAccent)
colorAccent = taColorAccent.getColor(0, Color.BLACK)
taColorAccent.recycle()
}
fun applyFontVisibility(fontInVisibility: Boolean) {
@@ -74,12 +65,9 @@ class EntryCustomField @JvmOverloads constructor(context: Context,
valueView.applyHiddenStyle(isProtected && hiddenStyle)
}
fun enableActionButton(enable: Boolean) {
if (enable) {
actionImageView.setColorFilter(colorAccent)
} else {
actionImageView.setColorFilter(ContextCompat.getColor(context, R.color.grey_dark))
}
fun activateActionButton(enable: Boolean) {
// Reverse because isActivated show custom color and allow click
actionImageView.isActivated = !enable
}
fun assignActionButtonClickListener(onClickActionListener: OnClickListener?) {

View File

@@ -31,7 +31,7 @@ class SpecialModeView @JvmOverloads constructor(context: Context,
: Toolbar(context, attrs, defStyle) {
init {
setNavigationIcon(R.drawable.ic_close_white_24dp)
setNavigationIcon(R.drawable.ic_arrow_back_white_24dp)
title = resources.getString(R.string.selection_mode)
}

View File

@@ -19,6 +19,7 @@
*/
package com.kunzisoft.keepass.view
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.graphics.Color
@@ -28,10 +29,11 @@ import android.text.method.PasswordTransformationMethod
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.updatePadding
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
/**
@@ -72,45 +74,75 @@ fun Snackbar.asError(): Snackbar {
return this
}
fun Toolbar.collapse(animate: Boolean = true) {
val recordBarHeight = layoutParams.height
fun View.collapse(animate: Boolean = true,
onCollapseFinished: (() -> Unit)? = null) {
val recordViewHeight = layoutParams.height
val slideAnimator = ValueAnimator.ofInt(height, 0)
if (animate)
slideAnimator.duration = 300L
slideAnimator.addUpdateListener { animation ->
layoutParams.height = animation.animatedValue as Int
if (layoutParams.height <= 1) {
visibility = View.GONE
layoutParams.height = recordBarHeight
}
requestLayout()
}
AnimatorSet().apply {
play(slideAnimator)
interpolator = AccelerateDecelerateInterpolator()
addListener(object: Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator?) {
}
override fun onAnimationRepeat(animation: Animator?) {}
override fun onAnimationEnd(animation: Animator?) {
visibility = View.GONE
layoutParams.height = recordViewHeight
onCollapseFinished?.invoke()
}
override fun onAnimationCancel(animation: Animator?) {}
})
}.start()
}
fun Toolbar.expand(animate: Boolean = true) {
val actionBarHeight = layoutParams.height
fun View.expand(animate: Boolean = true,
defaultHeight: Int? = null,
onExpandFinished: (() -> Unit)? = null) {
val viewHeight = defaultHeight ?: layoutParams.height
layoutParams.height = 0
val slideAnimator = ValueAnimator
.ofInt(0, actionBarHeight)
.ofInt(0, viewHeight)
if (animate)
slideAnimator.duration = 300L
var alreadyVisible = false
slideAnimator.addUpdateListener { animation ->
layoutParams.height = animation.animatedValue as Int
if (layoutParams.height >= 1) {
if (!alreadyVisible && layoutParams.height > 0) {
visibility = View.VISIBLE
alreadyVisible = true
}
requestLayout()
}
AnimatorSet().apply {
play(slideAnimator)
interpolator = AccelerateDecelerateInterpolator()
addListener(object: Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator?) {}
override fun onAnimationRepeat(animation: Animator?) {}
override fun onAnimationEnd(animation: Animator?) {
onExpandFinished?.invoke()
}
override fun onAnimationCancel(animation: Animator?) {}
})
}.start()
}
fun View.updateLockPaddingLeft() {
updatePadding(resources.getDimensionPixelSize(
if (PreferencesUtil.showLockDatabaseButton(context)) {
R.dimen.lock_button_size
} else {
R.dimen.hidden_lock_button_size
}
))
}
fun CoordinatorLayout.showActionError(result: ActionRunnable.Result) {
if (!result.isSuccess) {
result.exception?.errorId?.let { errorId ->

View File

@@ -0,0 +1,27 @@
package com.kunzisoft.keepass.viewmodels
import android.app.Application
import android.net.Uri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.model.DatabaseFile
class DatabaseFileViewModel(application: Application) : AndroidViewModel(application) {
private var mFileDatabaseHistoryAction: FileDatabaseHistoryAction? = null
init {
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(application.applicationContext)
}
val databaseFileLoaded: MutableLiveData<DatabaseFile> by lazy {
MutableLiveData<DatabaseFile>()
}
fun loadDatabaseFile(databaseUri: Uri) {
mFileDatabaseHistoryAction?.getDatabaseFile(databaseUri) { databaseFileRetrieved ->
databaseFileLoaded.value = databaseFileRetrieved
}
}
}

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