mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
502 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
454561c2a1 | ||
|
|
deabcc9605 | ||
|
|
c3bdb9dd16 | ||
|
|
86d77c908f | ||
|
|
c5cbba5971 | ||
|
|
aea5493180 | ||
|
|
431832e50e | ||
|
|
daf239c93c | ||
|
|
2c669deae5 | ||
|
|
81969de76e | ||
|
|
5a29e7a83e | ||
|
|
9ed5aef40f | ||
|
|
9e025b4329 | ||
|
|
543da4fe24 | ||
|
|
e82e50a57a | ||
|
|
288d62d666 | ||
|
|
8ed224eeab | ||
|
|
22bafff943 | ||
|
|
02403cf566 | ||
|
|
eabbc6f037 | ||
|
|
27e60edaf4 | ||
|
|
f7d0b64dea | ||
|
|
b874484101 | ||
|
|
0ee72db2e9 | ||
|
|
4f07bac9ec | ||
|
|
80abaf70f2 | ||
|
|
d6acbc2fb1 | ||
|
|
caf93c019a | ||
|
|
7044efa7c3 | ||
|
|
5b4d8f971f | ||
|
|
75e8feb9b8 | ||
|
|
5d59419a06 | ||
|
|
ff2e47bf70 | ||
|
|
4721ed7c15 | ||
|
|
d9da328088 | ||
|
|
2ee506ac06 | ||
|
|
16c4e4dc5b | ||
|
|
28b8fb1b97 | ||
|
|
c8bae9fba6 | ||
|
|
920764ad33 | ||
|
|
a45d114527 | ||
|
|
62cca09045 | ||
|
|
ea126b90e2 | ||
|
|
777a20182e | ||
|
|
df62a9d32b | ||
|
|
47e211bf3c | ||
|
|
650ca3844e | ||
|
|
8de49fa027 | ||
|
|
35b7633f4d | ||
|
|
2c96a80280 | ||
|
|
c7000c8ac6 | ||
|
|
1dca2b4387 | ||
|
|
ccbe9ad21e | ||
|
|
1fe6f11ef4 | ||
|
|
9d4ab60fc4 | ||
|
|
ea8c5ad6e1 | ||
|
|
63a0f5f91c | ||
|
|
e8bacbdb6f | ||
|
|
bcc8226ccc | ||
|
|
ddec91a0c5 | ||
|
|
2c262bb29d | ||
|
|
ae0afb7b53 | ||
|
|
84e7fe5b60 | ||
|
|
ccb32b045a | ||
|
|
b2e29ac4bd | ||
|
|
7df6309a68 | ||
|
|
a203ad9f64 | ||
|
|
44d6e09337 | ||
|
|
7c59ec019a | ||
|
|
b457f64ec8 | ||
|
|
e152e59a61 | ||
|
|
2c620ad69a | ||
|
|
c08b405fc2 | ||
|
|
585e39c591 | ||
|
|
60e857dba9 | ||
|
|
9a4aa2b08f | ||
|
|
2900b08b70 | ||
|
|
b0936563a2 | ||
|
|
3d327b245a | ||
|
|
9a14de3448 | ||
|
|
59f83545cf | ||
|
|
d9da1ef085 | ||
|
|
279a27f740 | ||
|
|
6ba822ee48 | ||
|
|
e4445949c8 | ||
|
|
8178ff583d | ||
|
|
0e0e19a802 | ||
|
|
1e2e7d841f | ||
|
|
263c2f00eb | ||
|
|
39c0b57652 | ||
|
|
677baf549c | ||
|
|
848478c28b | ||
|
|
98ad33a589 | ||
|
|
3f33733f40 | ||
|
|
cdc4ae4fb3 | ||
|
|
3edfa8a6ce | ||
|
|
6e99b667af | ||
|
|
23c8735568 | ||
|
|
f39065044f | ||
|
|
7d2ae47b0f | ||
|
|
7247de6908 | ||
|
|
4c2aef8504 | ||
|
|
ac8717cf1f | ||
|
|
7761928064 | ||
|
|
198047406b | ||
|
|
1d57309db9 | ||
|
|
07af9f36b2 | ||
|
|
d157fea9be | ||
|
|
7692caf622 | ||
|
|
dc65a8823f | ||
|
|
2210932fdf | ||
|
|
0cb1bf4b7f | ||
|
|
44d175dd40 | ||
|
|
097feb6437 | ||
|
|
250c7e5f20 | ||
|
|
83740f77bb | ||
|
|
8267e13d22 | ||
|
|
a7e9396f35 | ||
|
|
0f6c8601d2 | ||
|
|
66e68092d5 | ||
|
|
09616a594c | ||
|
|
7ae58ea7ea | ||
|
|
9f1f85a3c4 | ||
|
|
9992cdfce0 | ||
|
|
f5fa08ce94 | ||
|
|
67f70f7f85 | ||
|
|
840bd56a3d | ||
|
|
c1e2d31cfd | ||
|
|
c35322d44e | ||
|
|
9867e46c3f | ||
|
|
946a120038 | ||
|
|
2cd1a17aa2 | ||
|
|
6e29fe2932 | ||
|
|
e6c6bf6613 | ||
|
|
72c53169df | ||
|
|
b78cce7b4f | ||
|
|
c40499ec31 | ||
|
|
ee158e517e | ||
|
|
70aebcf9aa | ||
|
|
b15870d441 | ||
|
|
fe7074736a | ||
|
|
69c523ffad | ||
|
|
6aeefdf43d | ||
|
|
5f4c8be3d3 | ||
|
|
3092e4c557 | ||
|
|
ea119068da | ||
|
|
14371ecf94 | ||
|
|
45b0fcfe15 | ||
|
|
00aa5f5586 | ||
|
|
79fd53fd4c | ||
|
|
357ee3daf0 | ||
|
|
c2f7897f10 | ||
|
|
75455c0c48 | ||
|
|
a394bb9f8e | ||
|
|
2f5a846493 | ||
|
|
90376b361d | ||
|
|
229cf6bf5f | ||
|
|
bc46737353 | ||
|
|
1db2243a2e | ||
|
|
7cf836b3cb | ||
|
|
f7bbd295d6 | ||
|
|
62dbd95b48 | ||
|
|
df07e9c719 | ||
|
|
9aaf72726e | ||
|
|
5289927619 | ||
|
|
fa8c686f75 | ||
|
|
df5f28b7c4 | ||
|
|
280d8368fa | ||
|
|
80dbff1f21 | ||
|
|
7ee68a8481 | ||
|
|
ac8dd42c45 | ||
|
|
eed2148b2a | ||
|
|
dc5345b6d3 | ||
|
|
221af0b5bb | ||
|
|
a10ccc1eb0 | ||
|
|
b72d858480 | ||
|
|
60412cc90b | ||
|
|
512ac87dc9 | ||
|
|
d97020d1c5 | ||
|
|
f82cb617ba | ||
|
|
f79281a1a0 | ||
|
|
7be1dbb78b | ||
|
|
f875787799 | ||
|
|
f82c208556 | ||
|
|
f2150e3d85 | ||
|
|
ecc198e8a0 | ||
|
|
949bc58a80 | ||
|
|
7d79fff16f | ||
|
|
3aeb678292 | ||
|
|
160ac41bed | ||
|
|
9dfbcbe89c | ||
|
|
30da529348 | ||
|
|
dd8d114711 | ||
|
|
2191a4a848 | ||
|
|
8b9ea8d988 | ||
|
|
46dda8567d | ||
|
|
6953da4d9a | ||
|
|
e987d6647e | ||
|
|
359d85727e | ||
|
|
a994bf9dd8 | ||
|
|
14c4e095f6 | ||
|
|
59dce0e56f | ||
|
|
1f54a893a7 | ||
|
|
9489f1ee3d | ||
|
|
dc3d720e8d | ||
|
|
efe30b598b | ||
|
|
42515bfb2d | ||
|
|
acb3657d95 | ||
|
|
e7159c9d36 | ||
|
|
f3fdca368b | ||
|
|
4ea3e08a45 | ||
|
|
1eebc72b21 | ||
|
|
9a91be7e36 | ||
|
|
48476f9b88 | ||
|
|
68c991eb9b | ||
|
|
b51c77b01b | ||
|
|
57105db554 | ||
|
|
4bd3bdaddf | ||
|
|
9cf59b8d73 | ||
|
|
a793b0bb42 | ||
|
|
4d9e2e1471 | ||
|
|
1719887e55 | ||
|
|
2be00aca9d | ||
|
|
6fd05c5ad7 | ||
|
|
65e404374f | ||
|
|
0b78731bb3 | ||
|
|
65d318ed88 | ||
|
|
1bfcea55a9 | ||
|
|
6780eb004d | ||
|
|
0e12b2d021 | ||
|
|
1b356f87ec | ||
|
|
bf24d0bae1 | ||
|
|
97c831d4bb | ||
|
|
e3e48ffa6d | ||
|
|
b678416122 | ||
|
|
df722925fa | ||
|
|
4cbc0d9806 | ||
|
|
b0f3711b4e | ||
|
|
14ec6579b2 | ||
|
|
30bf039473 | ||
|
|
33bea317b0 | ||
|
|
c6814dc05e | ||
|
|
16808069ec | ||
|
|
e813974e29 | ||
|
|
7e0010b536 | ||
|
|
93171adcb3 | ||
|
|
9501cc76a4 | ||
|
|
5d7db046ac | ||
|
|
46c259bc3e | ||
|
|
3bacff91d3 | ||
|
|
bd79d483d2 | ||
|
|
16e31f4881 | ||
|
|
afa23c393d | ||
|
|
54d23cb781 | ||
|
|
c757e410e9 | ||
|
|
39dd25567d | ||
|
|
32cd998c2a | ||
|
|
691fc6335e | ||
|
|
b0025d1416 | ||
|
|
2467d8b0e7 | ||
|
|
28993c53e7 | ||
|
|
efdea870f0 | ||
|
|
b2995ec862 | ||
|
|
2bcc84dbb2 | ||
|
|
70cc98ce33 | ||
|
|
6e055f398d | ||
|
|
9f6234f032 | ||
|
|
6135544b72 | ||
|
|
5f32ec218b | ||
|
|
3b9a884db2 | ||
|
|
ada6b85868 | ||
|
|
2f8a4f447c | ||
|
|
9bd6499271 | ||
|
|
76fcc919ef | ||
|
|
a382297edf | ||
|
|
4987dfe4f6 | ||
|
|
d1a2e50b8d | ||
|
|
b9e44b6166 | ||
|
|
af601edc94 | ||
|
|
6640dcf9cd | ||
|
|
9b2734ed38 | ||
|
|
afcfce056f | ||
|
|
8f3af2f27b | ||
|
|
706aae47b3 | ||
|
|
d494295b21 | ||
|
|
c1600a253b | ||
|
|
a3bc29ad8f | ||
|
|
83225ed157 | ||
|
|
f13f6dc01f | ||
|
|
2d2489443a | ||
|
|
0e5b7fbfa2 | ||
|
|
d16a8068f7 | ||
|
|
c5ef11febc | ||
|
|
027f31447a | ||
|
|
41d1a4e5fb | ||
|
|
924e3191cb | ||
|
|
ebdc6b8fd9 | ||
|
|
5c05128cd7 | ||
|
|
5bf5685a12 | ||
|
|
f7391cb4c4 | ||
|
|
bad7bd8884 | ||
|
|
e0524a1656 | ||
|
|
f59af9baa3 | ||
|
|
b36890ca82 | ||
|
|
22703af08b | ||
|
|
c4108269b3 | ||
|
|
94c9d090cf | ||
|
|
28b9235a43 | ||
|
|
3aebae5e15 | ||
|
|
2631cb75d6 | ||
|
|
07b8b4156f | ||
|
|
7978967c1a | ||
|
|
3f24ff4de3 | ||
|
|
7253dd82a6 | ||
|
|
2b8eb3ae7e | ||
|
|
429eae71cd | ||
|
|
e5c552defb | ||
|
|
5c950a2e2c | ||
|
|
577ff78189 | ||
|
|
3f3cde05f7 | ||
|
|
b48f2c3276 | ||
|
|
8e818846f0 | ||
|
|
1554e37f8c | ||
|
|
affaabd011 | ||
|
|
f34a8f991c | ||
|
|
c2b14d610b | ||
|
|
02693d0cbb | ||
|
|
23155279ab | ||
|
|
2d23f7403d | ||
|
|
ae411c6fd5 | ||
|
|
ab8d6075a9 | ||
|
|
bc5ae29a67 | ||
|
|
1a8aabc30c | ||
|
|
8c51d7f713 | ||
|
|
8a1485e7ce | ||
|
|
614145431a | ||
|
|
db25f1999f | ||
|
|
4ed231b9bb | ||
|
|
25a5342c11 | ||
|
|
c7202e3ca9 | ||
|
|
89c2e94cea | ||
|
|
3dc46771b5 | ||
|
|
0eac4d4d7f | ||
|
|
a0ceb788db | ||
|
|
98fb36d03a | ||
|
|
a670006517 | ||
|
|
9cdbe67cd4 | ||
|
|
bbe8af452c | ||
|
|
f5edf28ce1 | ||
|
|
8fc30f590b | ||
|
|
e578f23ebe | ||
|
|
99c488fc9e | ||
|
|
f6a710660d | ||
|
|
a61744bb65 | ||
|
|
17c3078c24 | ||
|
|
5fa7731b56 | ||
|
|
c8e2be4d8c | ||
|
|
e3db613a07 | ||
|
|
0f7839027f | ||
|
|
31b322a108 | ||
|
|
b3c0494618 | ||
|
|
78ddb0533d | ||
|
|
da2158e7f2 | ||
|
|
d2a1efb6e7 | ||
|
|
98a880db2d | ||
|
|
cb82ef8703 | ||
|
|
d56246767b | ||
|
|
bb477984aa | ||
|
|
5a3e650e02 | ||
|
|
3c96dd2fac | ||
|
|
da051e3ff3 | ||
|
|
d15b6323c2 | ||
|
|
ec5f8fe4a4 | ||
|
|
71d84d76f8 | ||
|
|
d33ed52ec2 | ||
|
|
3a970544bb | ||
|
|
792ac3a2e8 | ||
|
|
60bbc27401 | ||
|
|
cf7cbcb6e6 | ||
|
|
c126a8eba9 | ||
|
|
66a60d0357 | ||
|
|
1acdadd027 | ||
|
|
200be9dadd | ||
|
|
a73e2872a4 | ||
|
|
ce6f7729c5 | ||
|
|
c285411371 | ||
|
|
46394c600e | ||
|
|
bea9cb3248 | ||
|
|
1087dcd714 | ||
|
|
0b63029b7e | ||
|
|
9679d24414 | ||
|
|
7947fd53e5 | ||
|
|
8dedf75565 | ||
|
|
b5b7c12b49 | ||
|
|
51f4e3cc3a | ||
|
|
24b0315d2e | ||
|
|
be446220eb | ||
|
|
a3ef2d332e | ||
|
|
ba6fe576e3 | ||
|
|
abcef38102 | ||
|
|
d5780b2f30 | ||
|
|
f7e498a0a2 | ||
|
|
51ac7ca2de | ||
|
|
c94535f6b5 | ||
|
|
07457ae368 | ||
|
|
4767fff08c | ||
|
|
f0c3498ecc | ||
|
|
1eca52d0fe | ||
|
|
7fbac9ad2f | ||
|
|
6fb80ed50b | ||
|
|
47f63ac81b | ||
|
|
42cc0b28ba | ||
|
|
993806f781 | ||
|
|
8a5af33aaa | ||
|
|
a974e36e9e | ||
|
|
17bcb2b39e | ||
|
|
cddf02d0c1 | ||
|
|
75ff7ece37 | ||
|
|
ec2b407a20 | ||
|
|
1dc9d78e54 | ||
|
|
5742a75c9d | ||
|
|
b5e9ad6d7e | ||
|
|
6393025219 | ||
|
|
9ab3e289bc | ||
|
|
6454474886 | ||
|
|
c5720a7a03 | ||
|
|
41b15adc6d | ||
|
|
05b962e718 | ||
|
|
1f01ca7b85 | ||
|
|
5d3b4fa5ec | ||
|
|
754d2b2dd3 | ||
|
|
ffad62e3dc | ||
|
|
1c11e16565 | ||
|
|
edc12990b4 | ||
|
|
12ea234d18 | ||
|
|
0461206a61 | ||
|
|
663f9e3962 | ||
|
|
34ee948c8e | ||
|
|
1bb9c2e4fe | ||
|
|
8ab18ce5cc | ||
|
|
71be16826e | ||
|
|
926c09d9df | ||
|
|
66c065ae7f | ||
|
|
083ed7775c | ||
|
|
5185452495 | ||
|
|
fa15f226ab | ||
|
|
fea4da2a33 | ||
|
|
055c933f4b | ||
|
|
9bcb867748 | ||
|
|
26e3c03f5f | ||
|
|
c195c3b2d1 | ||
|
|
f9e0aacfeb | ||
|
|
37fef66647 | ||
|
|
9f99b67563 | ||
|
|
fe902648ad | ||
|
|
13b933cd0b | ||
|
|
1d3b1d1d80 | ||
|
|
67a612af3a | ||
|
|
a891683806 | ||
|
|
440a72fc42 | ||
|
|
696d2e5197 | ||
|
|
2b17d56fc7 | ||
|
|
a410ef5d9f | ||
|
|
fe94769541 | ||
|
|
c63ae9c00c | ||
|
|
d5ece8d007 | ||
|
|
692a971dc0 | ||
|
|
05b8370cc0 | ||
|
|
b6111b35a2 | ||
|
|
4d72687628 | ||
|
|
8f125983ce | ||
|
|
4279825caa | ||
|
|
77ae3a4623 | ||
|
|
4c222dbc54 | ||
|
|
4e0f93ee8a | ||
|
|
e99f3e6627 | ||
|
|
f73877c34a | ||
|
|
abd3f12cae | ||
|
|
00117f5b7b | ||
|
|
d7d728f93e | ||
|
|
dc9217c4ec | ||
|
|
95acb13b93 | ||
|
|
234cc00d9f | ||
|
|
d6ba164799 | ||
|
|
910aa03dc8 | ||
|
|
a3e4a4c873 | ||
|
|
2d7c843447 | ||
|
|
e342b45473 | ||
|
|
f354bccd58 | ||
|
|
98073134db | ||
|
|
360666b00b | ||
|
|
ce4c807870 | ||
|
|
f2783bdac8 | ||
|
|
875ed16c3b | ||
|
|
383ba56d1f | ||
|
|
45eb54e624 | ||
|
|
5aff4e2ed6 | ||
|
|
e73e47dd94 | ||
|
|
1c8ac5efbc | ||
|
|
90fa5e1ecd | ||
|
|
348994917b | ||
|
|
60dbea1027 |
50
CHANGELOG
50
CHANGELOG
@@ -1,3 +1,53 @@
|
||||
KeePassDX(2.8.7)
|
||||
* Downgrade to Android API 29 (crash on startup with API 30 on some devices)
|
||||
Sorry for the inconvenience
|
||||
|
||||
KeePassDX(2.8.6)
|
||||
* Fix Autofill recognition #712
|
||||
* Keep value after renaming custom field #709
|
||||
* Prevent random binary bug #713
|
||||
* Fix dialog background #717
|
||||
* Better domain recognition for autofill #702
|
||||
* Write custom data #651
|
||||
* Fix autolink #720
|
||||
|
||||
KeePassDX(2.8.5)
|
||||
* Fix Base 64 #708
|
||||
|
||||
KeePassDX(2.8.4)
|
||||
* Fix incomplete attachment deletion #684
|
||||
* Fix opening database v1 without backup folder #692
|
||||
* Fix ANR during first entry education #685
|
||||
* Entry edition as fragment and manual views to fix focus #686
|
||||
* Fix opening database with corrupted attachment #691
|
||||
* Manage empty keyfile #679
|
||||
|
||||
KeePassDX(2.8.3)
|
||||
* Upload attachments
|
||||
* Visibility button for each hidden field
|
||||
* Fix read header file
|
||||
* Fix deletion in KDB database
|
||||
* Fix minor issues
|
||||
|
||||
KeePassDX(2.8.2)
|
||||
* Fix themes / new UI
|
||||
* Fix multiples notifications
|
||||
* Fix entry in Magikeyboard memory
|
||||
* Fix biometric view visibility
|
||||
* Fix fields order
|
||||
* Upgrade code with ViewModel and LiveData
|
||||
|
||||
KeePassDX(2.8.1)
|
||||
* Capture exceptions in coroutines
|
||||
|
||||
KeePassDX(2.8)
|
||||
* Fix TOTP period (> 60s)
|
||||
* Fix searching in recycle bin
|
||||
* Settings to back to the previous keyboard during database credentials and after form filling
|
||||
* Improve action tasks
|
||||
* Improve recognition to reset app timeout
|
||||
* Fix minor issues
|
||||
|
||||
KeePassDX(2.7)
|
||||
* Add blocklists for autofill
|
||||
* Add autofill compatibility mode (usefull for Browser not compatible)
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 29
|
||||
versionCode = 35
|
||||
versionName = "2.7"
|
||||
versionCode = 43
|
||||
versionName = "2.8.7"
|
||||
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,14 +95,17 @@ 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'
|
||||
// WARNING: Bug with extra field
|
||||
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'
|
||||
// To upgrade with style
|
||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||
implementation "androidx.core:core-ktx:1.3.1"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||
// WARNING: To upgrade with style, bug in edit text
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
// Database
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
@@ -39,14 +40,15 @@ import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachment
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||
@@ -86,10 +88,10 @@ class EntryActivity : LockingActivity() {
|
||||
private var mShowPassword: Boolean = false
|
||||
|
||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||
private var mAttachmentsToDownload: HashMap<Int, EntryAttachment> = HashMap()
|
||||
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
|
||||
|
||||
private var clipboardHelper: ClipboardHelper? = null
|
||||
private var firstLaunchOfActivity: Boolean = false
|
||||
private var mFirstLaunchOfActivity: Boolean = false
|
||||
|
||||
private var iconColor: Int = 0
|
||||
|
||||
@@ -130,14 +132,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 +201,7 @@ class EntryActivity : LockingActivity() {
|
||||
val entryInfo = entry.getEntryInfo(Database.getInstance())
|
||||
|
||||
// Manage entry copy to start notification if allowed
|
||||
if (firstLaunchOfActivity) {
|
||||
if (mFirstLaunchOfActivity) {
|
||||
// Manage entry to launch copying notification if allowed
|
||||
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||
@@ -209,13 +214,13 @@ class EntryActivity : LockingActivity() {
|
||||
mAttachmentFileBinderManager?.apply {
|
||||
registerProgressTask()
|
||||
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||
override fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment) {
|
||||
entryContentsView?.updateAttachmentDownloadProgress(attachment)
|
||||
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||
entryContentsView?.putAttachment(entryAttachmentState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
firstLaunchOfActivity = false
|
||||
mFirstLaunchOfActivity = false
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -237,14 +242,13 @@ class EntryActivity : LockingActivity() {
|
||||
toolbar?.title = entryTitle
|
||||
|
||||
// Assign basic fields
|
||||
entryContentsView?.assignUserName(entry.username)
|
||||
entryContentsView?.assignUserNameCopyListener(View.OnClickListener {
|
||||
entryContentsView?.assignUserName(entry.username) {
|
||||
database.startManageEntry(entry)
|
||||
clipboardHelper?.timeoutCopyToClipboard(entry.username,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_user_name)))
|
||||
database.stopManageEntry(entry)
|
||||
})
|
||||
}
|
||||
|
||||
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
||||
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
||||
@@ -271,23 +275,25 @@ class EntryActivity : LockingActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
entryContentsView?.assignPassword(entry.password, allowCopyPasswordAndProtectedFields)
|
||||
if (allowCopyPasswordAndProtectedFields) {
|
||||
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
|
||||
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
|
||||
View.OnClickListener {
|
||||
database.startManageEntry(entry)
|
||||
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_password)))
|
||||
database.stopManageEntry(entry)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
entryContentsView?.assignPasswordCopyListener(showWarningClipboardDialogOnClickListener)
|
||||
showWarningClipboardDialogOnClickListener
|
||||
} else {
|
||||
entryContentsView?.assignPasswordCopyListener(null)
|
||||
null
|
||||
}
|
||||
}
|
||||
entryContentsView?.assignPassword(entry.password,
|
||||
allowCopyPasswordAndProtectedFields,
|
||||
onPasswordCopyClickListener)
|
||||
|
||||
//Assign OTP field
|
||||
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
|
||||
@@ -301,24 +307,22 @@ class EntryActivity : LockingActivity() {
|
||||
})
|
||||
|
||||
entryContentsView?.assignURL(entry.url)
|
||||
entryContentsView?.assignComment(entry.notes)
|
||||
entryContentsView?.assignNotes(entry.notes)
|
||||
|
||||
// Assign custom fields
|
||||
if (entry.allowCustomFields()) {
|
||||
if (mDatabase?.allowEntryCustomFields() == true) {
|
||||
entryContentsView?.clearExtraFields()
|
||||
|
||||
for (element in entry.customFields.entries) {
|
||||
val label = element.key
|
||||
val value = element.value
|
||||
|
||||
entry.getExtraFields().forEach { field ->
|
||||
val label = field.name
|
||||
val value = field.protectedValue
|
||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
||||
if (allowCopyProtectedField) {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, View.OnClickListener {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
|
||||
clipboardHelper?.timeoutCopyToClipboard(
|
||||
value.toString(),
|
||||
getString(R.string.copy_field, label)
|
||||
)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
@@ -329,28 +333,16 @@ class EntryActivity : LockingActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
||||
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
||||
|
||||
// Manage attachments
|
||||
val attachments = entry.getAttachments()
|
||||
val showAttachmentsView = attachments.isNotEmpty()
|
||||
entryContentsView?.showAttachments(showAttachmentsView)
|
||||
if (showAttachmentsView) {
|
||||
entryContentsView?.assignAttachments(attachments)
|
||||
entryContentsView?.onAttachmentClick { attachmentItem, _ ->
|
||||
when (attachmentItem.downloadState) {
|
||||
AttachmentState.NULL, AttachmentState.ERROR, AttachmentState.COMPLETE -> {
|
||||
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// TODO Stop download
|
||||
}
|
||||
mDatabase?.binaryPool?.let { binaryPool ->
|
||||
entryContentsView?.assignAttachments(entry.getAttachments(binaryPool).toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
||||
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||
}
|
||||
}
|
||||
}
|
||||
entryContentsView?.refreshAttachments()
|
||||
|
||||
// Assign dates
|
||||
entryContentsView?.assignCreationDate(entry.creationTime)
|
||||
@@ -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)
|
||||
@@ -408,16 +393,6 @@ class EntryActivity : LockingActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeShowPasswordIcon(togglePassword: MenuItem?) {
|
||||
if (mShowPassword) {
|
||||
togglePassword?.setTitle(R.string.menu_hide_password)
|
||||
togglePassword?.setIcon(R.drawable.ic_visibility_off_white_24dp)
|
||||
} else {
|
||||
togglePassword?.setTitle(R.string.menu_showpass)
|
||||
togglePassword?.setIcon(R.drawable.ic_visibility_white_24dp)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
@@ -433,15 +408,6 @@ class EntryActivity : LockingActivity() {
|
||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
||||
}
|
||||
|
||||
val togglePassword = menu.findItem(R.id.menu_toggle_pass)
|
||||
entryContentsView?.let {
|
||||
if (it.isPasswordPresent || it.atLeastOneFieldProtectedPresent()) {
|
||||
changeShowPasswordIcon(togglePassword)
|
||||
} else {
|
||||
togglePassword?.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
||||
gotoUrl?.apply {
|
||||
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
||||
@@ -457,35 +423,38 @@ class EntryActivity : LockingActivity() {
|
||||
}
|
||||
|
||||
// Show education views
|
||||
Handler().post { performedNextEducation(EntryActivityEducation(this), menu) }
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||
menu: Menu) {
|
||||
val entryCopyEducationPerformed = entryContentsView?.isUserNamePresent == true
|
||||
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
|
||||
val entryCopyEducationPerformed = entryFieldCopyView != null
|
||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||
findViewById(R.id.entry_user_name_action_image),
|
||||
entryFieldCopyView,
|
||||
{
|
||||
clipboardHelper?.timeoutCopyToClipboard(mEntry!!.username,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_user_name)))
|
||||
val appNameString = getString(R.string.app_name)
|
||||
clipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||
getString(R.string.copy_field, appNameString))
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryActivityEducation, menu)
|
||||
})
|
||||
|
||||
if (!entryCopyEducationPerformed) {
|
||||
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
|
||||
// entryEditEducationPerformed
|
||||
toolbar?.findViewById<View>(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||
toolbar!!.findViewById(R.id.menu_edit),
|
||||
{
|
||||
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryActivityEducation, menu)
|
||||
})
|
||||
menuEditView != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||
menuEditView,
|
||||
{
|
||||
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryActivityEducation, menu)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,12 +464,6 @@ class EntryActivity : LockingActivity() {
|
||||
MenuUtil.onContributionItemSelected(this)
|
||||
return true
|
||||
}
|
||||
R.id.menu_toggle_pass -> {
|
||||
mShowPassword = !mShowPassword
|
||||
changeShowPasswordIcon(item)
|
||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
||||
return true
|
||||
}
|
||||
R.id.menu_edit -> {
|
||||
mEntry?.let {
|
||||
EntryEditActivity.launch(this@EntryActivity, it)
|
||||
@@ -520,7 +483,7 @@ class EntryActivity : LockingActivity() {
|
||||
}
|
||||
R.id.menu_restore_entry_history -> {
|
||||
mEntryLastVersion?.let { mainEntry ->
|
||||
mProgressDialogThread?.startDatabaseRestoreEntryHistory(
|
||||
mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory(
|
||||
mainEntry,
|
||||
mEntryHistoryPosition,
|
||||
!mReadOnly && mAutoSaveEnable)
|
||||
@@ -528,20 +491,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 +523,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"
|
||||
|
||||
|
||||
@@ -22,8 +22,11 @@ import android.app.Activity
|
||||
import android.app.DatePickerDialog
|
||||
import android.app.TimePickerDialog
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
@@ -31,59 +34,75 @@ 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.view.isVisible
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.*
|
||||
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
|
||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||
import com.kunzisoft.keepass.model.*
|
||||
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.view.EntryEditContentsView
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.closeDatabase
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.view.showActionError
|
||||
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||
import org.joda.time.DateTime
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class EntryEditActivity : LockingActivity(),
|
||||
IconPickerDialogFragment.IconPickerListener,
|
||||
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
|
||||
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
||||
SetOTPDialogFragment.CreateOtpListener,
|
||||
DatePickerDialog.OnDateSetListener,
|
||||
TimePickerDialog.OnTimeSetListener {
|
||||
TimePickerDialog.OnTimeSetListener,
|
||||
FileTooBigDialogFragment.ActionChooseListener,
|
||||
ReplaceFileDialogFragment.ActionChooseListener {
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
// Refs of an entry and group in database, are not modifiable
|
||||
private var mEntry: Entry? = null
|
||||
private var mParent: Group? = null
|
||||
// New or copy of mEntry in the database to be modifiable
|
||||
private var mNewEntry: Entry? = null
|
||||
private var mIsNew: Boolean = false
|
||||
|
||||
// Views
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var scrollView: NestedScrollView? = null
|
||||
private var entryEditContentsView: EntryEditContentsView? = null
|
||||
private var entryEditAddToolBar: ActionMenuView? = null
|
||||
private var saveView: View? = null
|
||||
private var entryEditFragment: EntryEditFragment? = null
|
||||
private var entryEditAddToolBar: Toolbar? = null
|
||||
private var validateButton: View? = null
|
||||
private var lockView: View? = null
|
||||
|
||||
// To manage attachments
|
||||
private var mSelectFileHelper: SelectFileHelper? = null
|
||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||
private var mAllowMultipleAttachments: Boolean = false
|
||||
private var mTempAttachments = ArrayList<Attachment>()
|
||||
|
||||
// Education
|
||||
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
||||
|
||||
@@ -102,26 +121,13 @@ class EntryEditActivity : LockingActivity(),
|
||||
scrollView = findViewById(R.id.entry_edit_scroll)
|
||||
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||
|
||||
entryEditContentsView = findViewById(R.id.entry_edit_contents)
|
||||
entryEditContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||
entryEditContentsView?.onDateClickListener = View.OnClickListener {
|
||||
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
|
||||
val dateTime = DateTime(expiresDate)
|
||||
val defaultYear = dateTime.year
|
||||
val defaultMonth = dateTime.monthOfYear-1
|
||||
val defaultDay = dateTime.dayOfMonth
|
||||
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
|
||||
.show(supportFragmentManager, "DatePickerFragment")
|
||||
}
|
||||
}
|
||||
|
||||
lockView = findViewById(R.id.lock_button)
|
||||
lockView?.setOnClickListener {
|
||||
lockAndExit()
|
||||
}
|
||||
|
||||
// Focus view to reinitialize timeout
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(entryEditContentsView)
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
|
||||
|
||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
||||
@@ -129,6 +135,8 @@ class EntryEditActivity : LockingActivity(),
|
||||
// Likely the app has been killed exit the activity
|
||||
mDatabase = Database.getInstance()
|
||||
|
||||
var tempEntryInfo: EntryInfo? = null
|
||||
|
||||
// Entry is retrieve, it's an entry to update
|
||||
intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let {
|
||||
mIsNew = false
|
||||
@@ -144,89 +152,103 @@ class EntryEditActivity : LockingActivity(),
|
||||
entry.parent = mParent
|
||||
}
|
||||
}
|
||||
|
||||
// Create the new entry from the current one
|
||||
if (savedInstanceState == null
|
||||
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
||||
mEntry?.let { entry ->
|
||||
// Create a copy to modify
|
||||
mNewEntry = Entry(entry).also { newEntry ->
|
||||
// WARNING Remove the parent to keep memory with parcelable
|
||||
newEntry.removeParent()
|
||||
}
|
||||
}
|
||||
}
|
||||
tempEntryInfo = mEntry?.getEntryInfo(mDatabase, true)
|
||||
}
|
||||
|
||||
// Parent is retrieve, it's a new entry to create
|
||||
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let {
|
||||
mIsNew = true
|
||||
// Create an empty new entry
|
||||
if (savedInstanceState == null
|
||||
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
||||
mNewEntry = mDatabase?.createEntry()
|
||||
}
|
||||
mParent = mDatabase?.getGroupById(it)
|
||||
// Add the default icon from parent if not a folder
|
||||
val parentIcon = mParent?.icon
|
||||
tempEntryInfo = mDatabase?.createEntry()?.getEntryInfo(mDatabase, true)
|
||||
// Set default icon
|
||||
if (parentIcon != null
|
||||
&& parentIcon.iconId != IconImage.UNKNOWN_ID
|
||||
&& parentIcon.iconId != IconImageStandard.FOLDER) {
|
||||
temporarilySaveAndShowSelectedIcon(parentIcon)
|
||||
} else {
|
||||
mDatabase?.drawFactory?.let { iconFactory ->
|
||||
entryEditContentsView?.setDefaultIcon(iconFactory)
|
||||
tempEntryInfo?.icon = parentIcon
|
||||
}
|
||||
// Set default username
|
||||
tempEntryInfo?.username = mDatabase?.defaultUsername ?: ""
|
||||
}
|
||||
|
||||
// Build fragment to manage entry modification
|
||||
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
|
||||
if (entryEditFragment == null) {
|
||||
entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo)
|
||||
}
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG)
|
||||
.commit()
|
||||
entryEditFragment?.apply {
|
||||
drawFactory = mDatabase?.drawFactory
|
||||
setOnDateClickListener = View.OnClickListener {
|
||||
expiryTime.date.let { expiresDate ->
|
||||
val dateTime = DateTime(expiresDate)
|
||||
val defaultYear = dateTime.year
|
||||
val defaultMonth = dateTime.monthOfYear-1
|
||||
val defaultDay = dateTime.dayOfMonth
|
||||
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
|
||||
.show(supportFragmentManager, "DatePickerFragment")
|
||||
}
|
||||
}
|
||||
setOnPasswordGeneratorClickListener = View.OnClickListener {
|
||||
openPasswordGenerator()
|
||||
}
|
||||
// Add listener to the icon
|
||||
setOnIconViewClickListener = View.OnClickListener {
|
||||
IconPickerDialogFragment.launch(this@EntryEditActivity)
|
||||
}
|
||||
setOnRemoveAttachment = { attachment ->
|
||||
mAttachmentFileBinderManager?.removeBinaryAttachment(attachment)
|
||||
removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD))
|
||||
}
|
||||
setOnEditCustomField = { field ->
|
||||
editCustomField(field)
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the new entry after an orientation change
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
||||
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY)
|
||||
// Retrieve temp attachments in case of deletion
|
||||
if (savedInstanceState?.containsKey(TEMP_ATTACHMENTS) == true) {
|
||||
mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments
|
||||
}
|
||||
|
||||
// Close the activity if entry or parent can't be retrieve
|
||||
if (mNewEntry == null || mParent == null) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
populateViewsWithEntry(mNewEntry!!)
|
||||
|
||||
// Assign title
|
||||
title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry)
|
||||
|
||||
// Add listener to the icon
|
||||
entryEditContentsView?.setOnIconViewClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) }
|
||||
|
||||
// Bottom Bar
|
||||
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
|
||||
entryEditAddToolBar?.apply {
|
||||
menuInflater.inflate(R.menu.entry_edit, menu)
|
||||
|
||||
menu.findItem(R.id.menu_add_field).apply {
|
||||
val allowCustomField = mNewEntry?.allowCustomFields() == true
|
||||
val allowCustomField = mDatabase?.allowEntryCustomFields() == true
|
||||
isEnabled = allowCustomField
|
||||
isVisible = allowCustomField
|
||||
}
|
||||
|
||||
// Attachment not compatible below KitKat
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||
menu.findItem(R.id.menu_add_attachment).isVisible = false
|
||||
}
|
||||
|
||||
menu.findItem(R.id.menu_add_otp).apply {
|
||||
val allowOTP = mDatabase?.allowOTP == true
|
||||
isEnabled = allowOTP
|
||||
isVisible = allowOTP
|
||||
// OTP not compatible below KitKat
|
||||
isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
||||
}
|
||||
|
||||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.menu_generate_password -> {
|
||||
openPasswordGenerator()
|
||||
true
|
||||
}
|
||||
R.id.menu_add_field -> {
|
||||
addNewCustomField()
|
||||
true
|
||||
}
|
||||
R.id.menu_add_attachment -> {
|
||||
addNewAttachment(item)
|
||||
true
|
||||
}
|
||||
R.id.menu_add_otp -> {
|
||||
setupOTP()
|
||||
true
|
||||
@@ -236,20 +258,38 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
// To retrieve attachment
|
||||
mSelectFileHelper = SelectFileHelper(this)
|
||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||
|
||||
// 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 -> {
|
||||
if (result.isSuccess)
|
||||
finish()
|
||||
try {
|
||||
if (result.isSuccess) {
|
||||
var newNodes: List<Node> = ArrayList()
|
||||
result.data?.getBundle(DatabaseTaskNotificationService.NEW_NODES_KEY)?.let { newNodesBundle ->
|
||||
mDatabase?.let { database ->
|
||||
newNodes = DatabaseTaskNotificationService.getListNodesFromBundle(database, newNodesBundle)
|
||||
}
|
||||
}
|
||||
if (newNodes.size == 1) {
|
||||
mEntry = newNodes[0] as Entry?
|
||||
finish()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve entry after database action", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
coordinatorLayout?.showActionError(result)
|
||||
@@ -264,67 +304,58 @@ class EntryEditActivity : LockingActivity(),
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateViewsWithEntry(newEntry: Entry) {
|
||||
// Don't start the field reference manager, we want to see the raw ref
|
||||
mDatabase?.stopManageEntry(newEntry)
|
||||
// Padding if lock button visible
|
||||
entryEditAddToolBar?.updateLockPaddingLeft()
|
||||
|
||||
// Set info in temp parameters
|
||||
temporarilySaveAndShowSelectedIcon(newEntry.icon)
|
||||
|
||||
// Set info in view
|
||||
entryEditContentsView?.apply {
|
||||
title = newEntry.title
|
||||
username = if (mIsNew && newEntry.username.isEmpty())
|
||||
mDatabase?.defaultUsername ?: ""
|
||||
else
|
||||
newEntry.username
|
||||
url = newEntry.url
|
||||
password = newEntry.password
|
||||
expires = newEntry.expires
|
||||
if (expires)
|
||||
expiresDate = newEntry.expiryTime
|
||||
notes = newEntry.notes
|
||||
for (entry in newEntry.customFields.entries) {
|
||||
post {
|
||||
putCustomField(entry.key, entry.value)
|
||||
mAllowMultipleAttachments = mDatabase?.allowMultipleAttachments == true
|
||||
mAttachmentFileBinderManager?.apply {
|
||||
registerProgressTask()
|
||||
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||
when (entryAttachmentState.downloadState) {
|
||||
AttachmentState.START -> {
|
||||
entryEditFragment?.apply {
|
||||
// When only one attachment is allowed
|
||||
if (!mAllowMultipleAttachments) {
|
||||
clearAttachments()
|
||||
}
|
||||
putAttachment(entryAttachmentState)
|
||||
// Scroll to the attachment position
|
||||
getAttachmentViewPosition(entryAttachmentState) {
|
||||
scrollView?.smoothScrollTo(0, it.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
AttachmentState.IN_PROGRESS -> {
|
||||
entryEditFragment?.putAttachment(entryAttachmentState)
|
||||
}
|
||||
AttachmentState.COMPLETE -> {
|
||||
entryEditFragment?.apply {
|
||||
putAttachment(entryAttachmentState)
|
||||
// Scroll to the attachment position
|
||||
getAttachmentViewPosition(entryAttachmentState) {
|
||||
scrollView?.smoothScrollTo(0, it.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
AttachmentState.ERROR -> {
|
||||
entryEditFragment?.removeAttachment(entryAttachmentState)
|
||||
coordinatorLayout?.let {
|
||||
Snackbar.make(it, R.string.error_file_not_create, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateEntryWithViews(newEntry: Entry) {
|
||||
override fun onPause() {
|
||||
mAttachmentFileBinderManager?.unregisterProgressTask()
|
||||
|
||||
mDatabase?.startManageEntry(newEntry)
|
||||
|
||||
newEntry.apply {
|
||||
// Build info from view
|
||||
entryEditContentsView?.let { entryView ->
|
||||
removeAllFields()
|
||||
title = entryView.title
|
||||
username = entryView.username
|
||||
url = entryView.url
|
||||
password = entryView.password
|
||||
expires = entryView.expires
|
||||
if (entryView.expires) {
|
||||
expiryTime = entryView.expiresDate
|
||||
}
|
||||
notes = entryView. notes
|
||||
entryView.customFields.forEach { customField ->
|
||||
putExtraField(customField.name, customField.protectedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mDatabase?.stopManageEntry(newEntry)
|
||||
}
|
||||
|
||||
private fun temporarilySaveAndShowSelectedIcon(icon: IconImage) {
|
||||
mNewEntry?.icon = icon
|
||||
mDatabase?.drawFactory?.let { iconDrawFactory ->
|
||||
entryEditContentsView?.setIcon(iconDrawFactory, icon)
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -335,16 +366,98 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new customized field view and scroll to bottom
|
||||
* Add a new customized field
|
||||
*/
|
||||
private fun addNewCustomField() {
|
||||
entryEditContentsView?.addEmptyCustomField()
|
||||
EntryCustomFieldDialogFragment.getInstance().show(supportFragmentManager, "customFieldDialog")
|
||||
}
|
||||
|
||||
private fun editCustomField(field: Field) {
|
||||
EntryCustomFieldDialogFragment.getInstance(field).show(supportFragmentManager, "customFieldDialog")
|
||||
}
|
||||
|
||||
override fun onNewCustomFieldApproved(newField: Field) {
|
||||
entryEditFragment?.apply {
|
||||
putExtraField(newField)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEditCustomFieldApproved(oldField: Field, newField: Field) {
|
||||
entryEditFragment?.replaceExtraField(oldField, newField)
|
||||
}
|
||||
|
||||
override fun onDeleteCustomFieldApproved(oldField: Field) {
|
||||
entryEditFragment?.removeExtraField(oldField)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new attachment
|
||||
*/
|
||||
private fun addNewAttachment(item: MenuItem) {
|
||||
mSelectFileHelper?.selectFileOnClickViewListener?.onMenuItemClick(item)
|
||||
}
|
||||
|
||||
override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) {
|
||||
if (attachmentToUploadUri != null && fileName != null) {
|
||||
buildNewAttachment(attachmentToUploadUri, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?) {
|
||||
startUploadAttachment(attachmentToUploadUri, attachment)
|
||||
}
|
||||
|
||||
private fun startUploadAttachment(attachmentToUploadUri: Uri?, attachment: Attachment?) {
|
||||
if (attachmentToUploadUri != null && attachment != null) {
|
||||
// Start uploading in service
|
||||
mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, attachment)
|
||||
// Add in temp list
|
||||
mTempAttachments.add(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) {
|
||||
val compression = mDatabase?.compressionForNewEntry() ?: false
|
||||
mDatabase?.buildNewBinary(UriUtil.getBinaryDir(this), compression)?.let { binaryAttachment ->
|
||||
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||
// Ask to replace the current attachment
|
||||
if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) ||
|
||||
entryEditFragment?.containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD)) == true) {
|
||||
ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment)
|
||||
.show(supportFragmentManager, "replacementFileFragment")
|
||||
} else {
|
||||
startUploadAttachment(attachmentToUploadUri, entryAttachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||
uri?.let { attachmentToUploadUri ->
|
||||
// TODO Async to get the name
|
||||
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
||||
documentFile.name?.let { fileName ->
|
||||
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
|
||||
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
|
||||
.show(supportFragmentManager, "fileTooBigFragment")
|
||||
} else {
|
||||
buildNewAttachment(attachmentToUploadUri, fileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up OTP (HOTP or TOTP) and add it as extra field
|
||||
*/
|
||||
private fun setupOTP() {
|
||||
// Retrieve the current otpElement if exists
|
||||
// and open the dialog to set up the OTP
|
||||
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
|
||||
SetOTPDialogFragment.build(entryEditFragment?.getEntryInfo()?.otpModel)
|
||||
.show(supportFragmentManager, "addOTPDialog")
|
||||
}
|
||||
|
||||
@@ -352,24 +465,35 @@ class EntryEditActivity : LockingActivity(),
|
||||
* Saves the new entry or update an existing entry in the database
|
||||
*/
|
||||
private fun saveEntry() {
|
||||
// Get the temp entry
|
||||
entryEditFragment?.getEntryInfo()?.let { newEntryInfo ->
|
||||
|
||||
// Launch a validation and show the error if present
|
||||
if (entryEditContentsView?.isValid() == true) {
|
||||
// Clone the entry
|
||||
mNewEntry?.let { newEntry ->
|
||||
if (mIsNew) {
|
||||
// Create new one
|
||||
mDatabase?.createEntry()
|
||||
} else {
|
||||
// Create a clone
|
||||
Entry(mEntry!!)
|
||||
}?.let { newEntry ->
|
||||
|
||||
// WARNING Add the parent previously deleted
|
||||
newEntry.parent = mEntry?.parent
|
||||
newEntry.setEntryInfo(mDatabase, newEntryInfo)
|
||||
// Build info
|
||||
newEntry.lastAccessTime = DateInstant()
|
||||
newEntry.lastModificationTime = DateInstant()
|
||||
|
||||
populateEntryWithViews(newEntry)
|
||||
// Delete temp attachment if not used
|
||||
mTempAttachments.forEach {
|
||||
mDatabase?.binaryPool?.let { binaryPool ->
|
||||
if (!newEntry.getAttachments(binaryPool).contains(it)) {
|
||||
mDatabase?.removeAttachmentIfNotUsed(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open a progress dialog and save entry
|
||||
if (mIsNew) {
|
||||
mParent?.let { parent ->
|
||||
mProgressDialogThread?.startDatabaseCreateEntry(
|
||||
mProgressDatabaseTaskProvider?.startDatabaseCreateEntry(
|
||||
newEntry,
|
||||
parent,
|
||||
!mReadOnly && mAutoSaveEnable
|
||||
@@ -377,7 +501,7 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
} else {
|
||||
mEntry?.let { oldEntry ->
|
||||
mProgressDialogThread?.startDatabaseUpdateEntry(
|
||||
mProgressDatabaseTaskProvider?.startDatabaseUpdateEntry(
|
||||
oldEntry,
|
||||
newEntry,
|
||||
!mReadOnly && mAutoSaveEnable
|
||||
@@ -397,30 +521,23 @@ class EntryEditActivity : LockingActivity(),
|
||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
|
||||
entryEditActivityEducation?.let {
|
||||
Handler().post { performedNextEducation(it) }
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||
val passwordGeneratorView: View? = entryEditAddToolBar?.findViewById(R.id.menu_generate_password)
|
||||
val generatePasswordEducationPerformed = passwordGeneratorView != null
|
||||
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||
passwordGeneratorView,
|
||||
{
|
||||
openPasswordGenerator()
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryEditActivityEducation)
|
||||
}
|
||||
)
|
||||
if (!generatePasswordEducationPerformed) {
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
entryEditActivityEducation?.let {
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
|
||||
}
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||
if (entryEditFragment?.generatePasswordEducationPerformed(entryEditActivityEducation) != true) {
|
||||
val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field)
|
||||
val addNewFieldEducationPerformed = mNewEntry != null
|
||||
&& mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
|
||||
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
|
||||
val addNewFieldEducationPerformed = mDatabase?.allowEntryCustomFields() == true
|
||||
&& addNewFieldView != null
|
||||
&& addNewFieldView.isVisible
|
||||
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
||||
addNewFieldView,
|
||||
{
|
||||
@@ -431,13 +548,29 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
)
|
||||
if (!addNewFieldEducationPerformed) {
|
||||
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
|
||||
setupOtpView != null && setupOtpView.visibility == View.VISIBLE
|
||||
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
||||
setupOtpView,
|
||||
val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment)
|
||||
val addAttachmentEducationPerformed = attachmentView != null
|
||||
&& attachmentView.isVisible
|
||||
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
||||
attachmentView,
|
||||
{
|
||||
setupOTP()
|
||||
})
|
||||
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(attachmentView)
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryEditActivityEducation)
|
||||
}
|
||||
)
|
||||
if (!addAttachmentEducationPerformed) {
|
||||
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
|
||||
setupOtpView != null
|
||||
&& setupOtpView.isVisible
|
||||
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
||||
setupOtpView,
|
||||
{
|
||||
setupOTP()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -445,7 +578,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)
|
||||
@@ -460,16 +593,28 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
override fun onOtpCreated(otpElement: OtpElement) {
|
||||
var titleOTP: String? = null
|
||||
var usernameOTP: String? = null
|
||||
// Build a temp entry to get title and username (by ref)
|
||||
entryEditFragment?.getEntryInfo()?.let { entryInfo ->
|
||||
val entryTemp = mDatabase?.createEntry()
|
||||
entryTemp?.setEntryInfo(mDatabase, entryInfo)
|
||||
mDatabase?.startManageEntry(entryTemp)
|
||||
titleOTP = entryTemp?.title
|
||||
usernameOTP = entryTemp?.username
|
||||
mDatabase?.stopManageEntry(mEntry)
|
||||
}
|
||||
// Update the otp field with otpauth:// url
|
||||
val otpField = OtpEntryFields.buildOtpField(otpElement,
|
||||
mEntry?.title, mEntry?.username)
|
||||
entryEditContentsView?.putCustomField(otpField.name, otpField.protectedValue)
|
||||
mEntry?.putExtraField(otpField.name, otpField.protectedValue)
|
||||
val otpField = OtpEntryFields.buildOtpField(otpElement, titleOTP, usernameOTP)
|
||||
mEntry?.putExtraField(Field(otpField.name, otpField.protectedValue))
|
||||
entryEditFragment?.apply {
|
||||
putExtraField(otpField)
|
||||
}
|
||||
}
|
||||
|
||||
override fun iconPicked(bundle: Bundle) {
|
||||
IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon ->
|
||||
temporarilySaveAndShowSelectedIcon(icon)
|
||||
entryEditFragment?.icon = icon
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,9 +622,9 @@ class EntryEditActivity : LockingActivity(),
|
||||
// To fix android 4.4 issue
|
||||
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
|
||||
if (datePicker?.isShown == true) {
|
||||
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
|
||||
entryEditFragment?.expiryTime?.date?.let { expiresDate ->
|
||||
// Save the date
|
||||
entryEditContentsView?.expiresDate =
|
||||
entryEditFragment?.expiryTime =
|
||||
DateInstant(DateTime(expiresDate)
|
||||
.withYear(year)
|
||||
.withMonthOfYear(month + 1)
|
||||
@@ -496,9 +641,9 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) {
|
||||
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
|
||||
entryEditFragment?.expiryTime?.date?.let { expiresDate ->
|
||||
// Save the date
|
||||
entryEditContentsView?.expiresDate =
|
||||
entryEditFragment?.expiryTime =
|
||||
DateInstant(DateTime(expiresDate)
|
||||
.withHourOfDay(hours)
|
||||
.withMinuteOfHour(minutes)
|
||||
@@ -507,21 +652,19 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
mNewEntry?.let {
|
||||
populateEntryWithViews(it)
|
||||
outState.putParcelable(KEY_NEW_ENTRY, it)
|
||||
}
|
||||
|
||||
outState.putParcelableArrayList(TEMP_ATTACHMENTS, mTempAttachments)
|
||||
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun acceptPassword(bundle: Bundle) {
|
||||
bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID)?.let {
|
||||
entryEditContentsView?.password = it
|
||||
entryEditFragment?.password = it
|
||||
}
|
||||
|
||||
entryEditActivityEducation?.let {
|
||||
Handler().post { performedNextEducation(it) }
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,10 +684,10 @@ class EntryEditActivity : LockingActivity(),
|
||||
override fun finish() {
|
||||
// Assign entry callback as a result in all case
|
||||
try {
|
||||
mNewEntry?.let {
|
||||
mEntry?.let { entry ->
|
||||
val bundle = Bundle()
|
||||
val intentEntry = Intent()
|
||||
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, mNewEntry)
|
||||
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry)
|
||||
intentEntry.putExtras(bundle)
|
||||
if (mIsNew) {
|
||||
setResult(ADD_ENTRY_RESULT_CODE, intentEntry)
|
||||
@@ -568,7 +711,7 @@ class EntryEditActivity : LockingActivity(),
|
||||
const val KEY_PARENT = "parent"
|
||||
|
||||
// SaveInstanceState
|
||||
const val KEY_NEW_ENTRY = "new_entry"
|
||||
const val TEMP_ATTACHMENTS = "TEMP_ATTACHMENTS"
|
||||
|
||||
// Keys for callback
|
||||
const val ADD_ENTRY_RESULT_CODE = 31
|
||||
@@ -576,6 +719,8 @@ class EntryEditActivity : LockingActivity(),
|
||||
const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129
|
||||
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
|
||||
|
||||
const val ENTRY_EDIT_FRAGMENT_TAG = "ENTRY_EDIT_FRAGMENT_TAG"
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to update an existing entry
|
||||
*
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.model.*
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||
import com.kunzisoft.keepass.view.collapse
|
||||
import com.kunzisoft.keepass.view.expand
|
||||
|
||||
class EntryEditFragment: StylishFragment() {
|
||||
|
||||
private lateinit var entryTitleLayoutView: TextInputLayout
|
||||
private lateinit var entryTitleView: EditText
|
||||
private lateinit var entryIconView: ImageView
|
||||
private lateinit var entryUserNameView: EditText
|
||||
private lateinit var entryUrlView: EditText
|
||||
private lateinit var entryPasswordLayoutView: TextInputLayout
|
||||
private lateinit var entryPasswordView: EditText
|
||||
private lateinit var entryPasswordGeneratorView: View
|
||||
private lateinit var entryExpiresCheckBox: CompoundButton
|
||||
private lateinit var entryExpiresTextView: TextView
|
||||
private lateinit var entryNotesView: EditText
|
||||
private lateinit var extraFieldsContainerView: View
|
||||
private lateinit var extraFieldsListView: ViewGroup
|
||||
private lateinit var attachmentsContainerView: View
|
||||
private lateinit var attachmentsListView: RecyclerView
|
||||
|
||||
private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter
|
||||
|
||||
private var fontInVisibility: Boolean = false
|
||||
private var iconColor: Int = 0
|
||||
private var expiresInstant: DateInstant = DateInstant.IN_ONE_MONTH
|
||||
|
||||
var drawFactory: IconDrawableFactory? = null
|
||||
var setOnDateClickListener: View.OnClickListener? = null
|
||||
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
|
||||
var setOnIconViewClickListener: View.OnClickListener? = null
|
||||
var setOnEditCustomField: ((Field) -> Unit)? = null
|
||||
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
|
||||
|
||||
// Elements to modify the current entry
|
||||
private var mEntryInfo = EntryInfo()
|
||||
private var mLastFocusedEditField: FocusedEditField? = null
|
||||
private var mExtraViewToRequestFocus: EditText? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
val rootView = inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_entry_edit_contents, container, false)
|
||||
|
||||
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
|
||||
|
||||
entryTitleLayoutView = rootView.findViewById(R.id.entry_edit_container_title)
|
||||
entryTitleView = rootView.findViewById(R.id.entry_edit_title)
|
||||
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
|
||||
entryIconView.setOnClickListener {
|
||||
setOnIconViewClickListener?.onClick(it)
|
||||
}
|
||||
|
||||
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
|
||||
entryUrlView = rootView.findViewById(R.id.entry_edit_url)
|
||||
entryPasswordLayoutView = rootView.findViewById(R.id.entry_edit_container_password)
|
||||
entryPasswordView = rootView.findViewById(R.id.entry_edit_password)
|
||||
entryPasswordGeneratorView = rootView.findViewById(R.id.entry_edit_password_generator_button)
|
||||
entryPasswordGeneratorView.setOnClickListener {
|
||||
setOnPasswordGeneratorClickListener?.onClick(it)
|
||||
}
|
||||
entryExpiresCheckBox = rootView.findViewById(R.id.entry_edit_expires_checkbox)
|
||||
entryExpiresTextView = rootView.findViewById(R.id.entry_edit_expires_text)
|
||||
entryExpiresTextView.setOnClickListener {
|
||||
if (entryExpiresCheckBox.isChecked)
|
||||
setOnDateClickListener?.onClick(it)
|
||||
}
|
||||
|
||||
entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
|
||||
|
||||
extraFieldsContainerView = rootView.findViewById(R.id.extra_fields_container)
|
||||
extraFieldsListView = rootView.findViewById(R.id.extra_fields_list)
|
||||
|
||||
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
|
||||
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
|
||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
|
||||
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
|
||||
if (previousSize > 0 && newSize == 0) {
|
||||
attachmentsContainerView.collapse(true)
|
||||
} else if (previousSize == 0 && newSize == 1) {
|
||||
attachmentsContainerView.expand(true)
|
||||
}
|
||||
}
|
||||
attachmentsListView.apply {
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
adapter = attachmentsAdapter
|
||||
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
}
|
||||
|
||||
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
|
||||
assignExpiresDateText()
|
||||
}
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
|
||||
taIconColor?.recycle()
|
||||
|
||||
// Retrieve the new entry after an orientation change
|
||||
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
|
||||
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
||||
else if (savedInstanceState?.containsKey(KEY_TEMP_ENTRY_INFO) == true) {
|
||||
mEntryInfo = savedInstanceState.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
||||
}
|
||||
|
||||
if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) {
|
||||
mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD) ?: mLastFocusedEditField
|
||||
}
|
||||
|
||||
populateViewsWithEntry()
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
super.onDetach()
|
||||
|
||||
drawFactory = null
|
||||
setOnDateClickListener = null
|
||||
setOnPasswordGeneratorClickListener = null
|
||||
setOnIconViewClickListener = null
|
||||
setOnRemoveAttachment = null
|
||||
setOnEditCustomField = null
|
||||
}
|
||||
|
||||
fun getEntryInfo(): EntryInfo? {
|
||||
populateEntryWithViews()
|
||||
return mEntryInfo
|
||||
}
|
||||
|
||||
fun generatePasswordEducationPerformed(entryEditActivityEducation: EntryEditActivityEducation): Boolean {
|
||||
return entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||
entryPasswordGeneratorView,
|
||||
{
|
||||
GeneratePasswordDialogFragment().show(parentFragmentManager, "PasswordGeneratorFragment")
|
||||
},
|
||||
{
|
||||
try {
|
||||
(activity as? EntryEditActivity?)?.performedNextEducation(entryEditActivityEducation)
|
||||
} catch (ignore: Exception) {}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun populateViewsWithEntry() {
|
||||
// Set info in view
|
||||
icon = mEntryInfo.icon
|
||||
title = mEntryInfo.title
|
||||
username = mEntryInfo.username
|
||||
url = mEntryInfo.url
|
||||
password = mEntryInfo.password
|
||||
expires = mEntryInfo.expires
|
||||
expiryTime = mEntryInfo.expiryTime
|
||||
notes = mEntryInfo.notes
|
||||
assignExtraFields(mEntryInfo.customFields) { fields ->
|
||||
setOnEditCustomField?.invoke(fields)
|
||||
}
|
||||
assignAttachments(mEntryInfo.attachments, StreamDirection.UPLOAD) { attachment ->
|
||||
setOnRemoveAttachment?.invoke(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateEntryWithViews() {
|
||||
// Icon already populate
|
||||
mEntryInfo.title = title
|
||||
mEntryInfo.username = username
|
||||
mEntryInfo.url = url
|
||||
mEntryInfo.password = password
|
||||
mEntryInfo.expires = expires
|
||||
mEntryInfo.expiryTime = expiryTime
|
||||
mEntryInfo.notes = notes
|
||||
mEntryInfo.customFields = getExtraFields()
|
||||
mEntryInfo.otpModel = OtpEntryFields.parseFields { key ->
|
||||
getExtraFields().firstOrNull { it.name == key }?.protectedValue?.toString()
|
||||
}?.otpModel
|
||||
mEntryInfo.attachments = getAttachments()
|
||||
}
|
||||
|
||||
var title: String
|
||||
get() {
|
||||
return entryTitleView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryTitleView.setText(value)
|
||||
if (fontInVisibility)
|
||||
entryTitleView.applyFontVisibility()
|
||||
}
|
||||
|
||||
var icon: IconImage
|
||||
get() {
|
||||
return mEntryInfo.icon
|
||||
}
|
||||
set(value) {
|
||||
mEntryInfo.icon = value
|
||||
drawFactory?.let { drawFactory ->
|
||||
entryIconView.assignDatabaseIcon(drawFactory, value, iconColor)
|
||||
}
|
||||
}
|
||||
|
||||
var username: String
|
||||
get() {
|
||||
return entryUserNameView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryUserNameView.setText(value)
|
||||
if (fontInVisibility)
|
||||
entryUserNameView.applyFontVisibility()
|
||||
}
|
||||
|
||||
var url: String
|
||||
get() {
|
||||
return entryUrlView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryUrlView.setText(value)
|
||||
if (fontInVisibility)
|
||||
entryUrlView.applyFontVisibility()
|
||||
}
|
||||
|
||||
var password: String
|
||||
get() {
|
||||
return entryPasswordView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryPasswordView.setText(value)
|
||||
if (fontInVisibility) {
|
||||
entryPasswordView.applyFontVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignExpiresDateText() {
|
||||
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
|
||||
entryExpiresTextView.setOnClickListener(setOnDateClickListener)
|
||||
expiresInstant.getDateTimeString(resources)
|
||||
} else {
|
||||
entryExpiresTextView.setOnClickListener(null)
|
||||
resources.getString(R.string.never)
|
||||
}
|
||||
if (fontInVisibility)
|
||||
entryExpiresTextView.applyFontVisibility()
|
||||
}
|
||||
|
||||
var expires: Boolean
|
||||
get() {
|
||||
return entryExpiresCheckBox.isChecked
|
||||
}
|
||||
set(value) {
|
||||
if (!value) {
|
||||
expiresInstant = DateInstant.IN_ONE_MONTH
|
||||
}
|
||||
entryExpiresCheckBox.isChecked = value
|
||||
assignExpiresDateText()
|
||||
}
|
||||
|
||||
var expiryTime: DateInstant
|
||||
get() {
|
||||
return if (expires)
|
||||
expiresInstant
|
||||
else
|
||||
DateInstant.NEVER_EXPIRE
|
||||
}
|
||||
set(value) {
|
||||
if (expires)
|
||||
expiresInstant = value
|
||||
assignExpiresDateText()
|
||||
}
|
||||
|
||||
var notes: String
|
||||
get() {
|
||||
return entryNotesView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryNotesView.setText(value)
|
||||
if (fontInVisibility)
|
||||
entryNotesView.applyFontVisibility()
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Extra Fields
|
||||
* -------------
|
||||
*/
|
||||
|
||||
private var mExtraFieldsList: MutableList<Field> = ArrayList()
|
||||
private var mOnEditButtonClickListener: ((item: Field)->Unit)? = null
|
||||
|
||||
private fun buildViewFromField(extraField: Field): View? {
|
||||
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
val itemView: View? = inflater?.inflate(R.layout.item_entry_edit_extra_field, extraFieldsListView, false)
|
||||
itemView?.id = View.NO_ID
|
||||
|
||||
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
|
||||
extraFieldValueContainer?.isPasswordVisibilityToggleEnabled = extraField.protectedValue.isProtected
|
||||
extraFieldValueContainer?.hint = extraField.name
|
||||
extraFieldValueContainer?.id = View.NO_ID
|
||||
|
||||
val extraFieldValue: TextInputEditText? = itemView?.findViewById(R.id.entry_extra_field_value)
|
||||
extraFieldValue?.apply {
|
||||
if (extraField.protectedValue.isProtected) {
|
||||
inputType = extraFieldValue.inputType or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
|
||||
}
|
||||
setText(extraField.protectedValue.toString())
|
||||
if (fontInVisibility)
|
||||
applyFontVisibility()
|
||||
}
|
||||
extraFieldValue?.id = View.NO_ID
|
||||
extraFieldValue?.tag = "FIELD_VALUE_TAG"
|
||||
if (mLastFocusedEditField?.field == extraField) {
|
||||
mExtraViewToRequestFocus = extraFieldValue
|
||||
}
|
||||
|
||||
val extraFieldEditButton: View? = itemView?.findViewById(R.id.entry_extra_field_edit)
|
||||
extraFieldEditButton?.setOnClickListener {
|
||||
mOnEditButtonClickListener?.invoke(extraField)
|
||||
}
|
||||
extraFieldEditButton?.id = View.NO_ID
|
||||
|
||||
return itemView
|
||||
}
|
||||
|
||||
fun getExtraFields(): List<Field> {
|
||||
mLastFocusedEditField = null
|
||||
for (index in 0 until extraFieldsListView.childCount) {
|
||||
val extraFieldValue: EditText = extraFieldsListView.getChildAt(index)
|
||||
.findViewWithTag("FIELD_VALUE_TAG")
|
||||
val extraField = mExtraFieldsList[index]
|
||||
extraField.protectedValue.stringValue = extraFieldValue.text?.toString() ?: ""
|
||||
if (extraFieldValue.isFocused) {
|
||||
mLastFocusedEditField = FocusedEditField().apply {
|
||||
field = extraField
|
||||
cursorSelectionStart = extraFieldValue.selectionStart
|
||||
cursorSelectionEnd = extraFieldValue.selectionEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
return mExtraFieldsList
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all children and add new views for each field
|
||||
*/
|
||||
fun assignExtraFields(fields: List<Field>,
|
||||
onEditButtonClickListener: ((item: Field)->Unit)?) {
|
||||
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
|
||||
// Reinit focused field
|
||||
mExtraFieldsList.clear()
|
||||
mExtraFieldsList.addAll(fields)
|
||||
extraFieldsListView.removeAllViews()
|
||||
fields.forEach {
|
||||
extraFieldsListView.addView(buildViewFromField(it))
|
||||
}
|
||||
// Request last focus
|
||||
mLastFocusedEditField?.let { focusField ->
|
||||
mExtraViewToRequestFocus?.apply {
|
||||
requestFocus()
|
||||
setSelection(focusField.cursorSelectionStart,
|
||||
focusField.cursorSelectionEnd)
|
||||
}
|
||||
}
|
||||
mLastFocusedEditField = null
|
||||
mOnEditButtonClickListener = onEditButtonClickListener
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an extra field or create a new one if doesn't exists, the old value is lost
|
||||
*/
|
||||
fun putExtraField(extraField: Field) {
|
||||
extraFieldsContainerView.visibility = View.VISIBLE
|
||||
val oldField = mExtraFieldsList.firstOrNull { it.name == extraField.name }
|
||||
oldField?.let {
|
||||
val index = mExtraFieldsList.indexOf(oldField)
|
||||
mExtraFieldsList.removeAt(index)
|
||||
mExtraFieldsList.add(index, extraField)
|
||||
extraFieldsListView.removeViewAt(index)
|
||||
val newView = buildViewFromField(extraField)
|
||||
extraFieldsListView.addView(newView, index)
|
||||
newView?.requestFocus()
|
||||
} ?: kotlin.run {
|
||||
mExtraFieldsList.add(extraField)
|
||||
val newView = buildViewFromField(extraField)
|
||||
extraFieldsListView.addView(newView)
|
||||
newView?.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an extra field and keep the old value
|
||||
*/
|
||||
fun replaceExtraField(oldExtraField: Field, newExtraField: Field) {
|
||||
extraFieldsContainerView.visibility = View.VISIBLE
|
||||
val index = mExtraFieldsList.indexOf(oldExtraField)
|
||||
val oldValueEditText: EditText = extraFieldsListView.getChildAt(index)
|
||||
.findViewWithTag("FIELD_VALUE_TAG")
|
||||
val oldValue = oldValueEditText.text.toString()
|
||||
val newExtraFieldWithOldValue = Field(newExtraField).apply {
|
||||
this.protectedValue.stringValue = oldValue
|
||||
}
|
||||
mExtraFieldsList.removeAt(index)
|
||||
mExtraFieldsList.add(index, newExtraFieldWithOldValue)
|
||||
extraFieldsListView.removeViewAt(index)
|
||||
val newView = buildViewFromField(newExtraFieldWithOldValue)
|
||||
extraFieldsListView.addView(newView, index)
|
||||
newView?.requestFocus()
|
||||
}
|
||||
|
||||
fun removeExtraField(oldExtraField: Field) {
|
||||
val previousSize = mExtraFieldsList.size
|
||||
val index = mExtraFieldsList.indexOf(oldExtraField)
|
||||
extraFieldsListView.getChildAt(index)?.let {
|
||||
it.collapse(true) {
|
||||
mExtraFieldsList.removeAt(index)
|
||||
extraFieldsListView.removeViewAt(index)
|
||||
val newSize = mExtraFieldsList.size
|
||||
|
||||
if (previousSize > 0 && newSize == 0) {
|
||||
extraFieldsContainerView.collapse(true)
|
||||
} else if (previousSize == 0 && newSize == 1) {
|
||||
extraFieldsContainerView.expand(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Attachments
|
||||
* -------------
|
||||
*/
|
||||
|
||||
fun getAttachments(): List<Attachment> {
|
||||
return attachmentsAdapter.itemsList.map { it.attachment }
|
||||
}
|
||||
|
||||
fun assignAttachments(attachments: List<Attachment>,
|
||||
streamDirection: StreamDirection,
|
||||
onDeleteItem: (attachment: Attachment)->Unit) {
|
||||
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
|
||||
attachmentsAdapter.onDeleteButtonClickListener = { item ->
|
||||
onDeleteItem.invoke(item.attachment)
|
||||
}
|
||||
}
|
||||
|
||||
fun containsAttachment(): Boolean {
|
||||
return !attachmentsAdapter.isEmpty()
|
||||
}
|
||||
|
||||
fun containsAttachment(attachment: EntryAttachmentState): Boolean {
|
||||
return attachmentsAdapter.contains(attachment)
|
||||
}
|
||||
|
||||
fun putAttachment(attachment: EntryAttachmentState) {
|
||||
attachmentsContainerView.visibility = View.VISIBLE
|
||||
attachmentsAdapter.putItem(attachment)
|
||||
}
|
||||
|
||||
fun removeAttachment(attachment: EntryAttachmentState) {
|
||||
attachmentsAdapter.removeItem(attachment)
|
||||
}
|
||||
|
||||
fun clearAttachments() {
|
||||
attachmentsAdapter.clear()
|
||||
}
|
||||
|
||||
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
|
||||
attachmentsListView.postDelayed({
|
||||
position.invoke(attachmentsContainerView.y
|
||||
+ attachmentsListView.y
|
||||
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
|
||||
?: 0F)
|
||||
)
|
||||
}, 250)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
populateEntryWithViews()
|
||||
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
|
||||
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
|
||||
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
|
||||
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
|
||||
|
||||
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
|
||||
return EntryEditFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,13 +28,16 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
@@ -43,19 +46,22 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||
import kotlinx.android.synthetic.main.activity_file_selection.*
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
@@ -64,11 +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
|
||||
|
||||
@@ -76,9 +82,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
|
||||
private var mDatabaseFileUri: Uri? = null
|
||||
|
||||
private var mOpenFileHelper: OpenFileHelper? = null
|
||||
private var mSelectFileHelper: SelectFileHelper? = null
|
||||
|
||||
private var mProgressDialogThread: ProgressDialogThread? = null
|
||||
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -92,22 +98,15 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
toolbar.title = ""
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
fileManagerExplanationButton = findViewById(R.id.file_manager_explanation_button)
|
||||
fileManagerExplanationButton?.setOnClickListener {
|
||||
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() }
|
||||
|
||||
// Open database button
|
||||
mOpenFileHelper = OpenFileHelper(this)
|
||||
mSelectFileHelper = SelectFileHelper(this)
|
||||
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
||||
openDatabaseButtonView?.apply {
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.let {
|
||||
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
||||
setOnClickListener(it)
|
||||
setOnLongClickListener(it)
|
||||
}
|
||||
@@ -120,26 +119,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 +160,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,7 +210,6 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
/**
|
||||
* Create a new file by calling the content provider
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun createNewFile() {
|
||||
createDocument(this, getString(R.string.database_file_name_default) +
|
||||
getString(R.string.database_file_extension_default), "application/x-keepass")
|
||||
@@ -271,8 +303,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 +313,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 +321,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 +355,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
mDatabaseFileUri?.let { databaseUri ->
|
||||
|
||||
// Create the new database
|
||||
mProgressDialogThread?.startDatabaseCreate(
|
||||
mProgressDatabaseTaskProvider?.startDatabaseCreate(
|
||||
databaseUri,
|
||||
masterPasswordChecked,
|
||||
masterPassword,
|
||||
@@ -366,8 +383,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||
if (uri != null) {
|
||||
launchPasswordActivityWithPath(uri)
|
||||
}
|
||||
@@ -396,18 +412,19 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
||||
}
|
||||
|
||||
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
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()
|
||||
},
|
||||
@@ -422,7 +439,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
openDatabaseButtonView!!,
|
||||
{tapTargetView ->
|
||||
tapTargetView?.let {
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
|
||||
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
||||
}
|
||||
},
|
||||
{}
|
||||
@@ -431,6 +448,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
|
||||
}
|
||||
|
||||
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,10 +28,12 @@ import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.RequiresApi
|
||||
@@ -87,6 +89,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 +106,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 +123,13 @@ class GroupActivity : LockingActivity(),
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (isFinishing) {
|
||||
return
|
||||
}
|
||||
mDatabase = Database.getInstance()
|
||||
|
||||
// Construct main view
|
||||
setContentView(layoutInflater.inflate(R.layout.activity_group, null))
|
||||
|
||||
// Initialize views
|
||||
rootContainerView = findViewById(R.id.activity_group_container_view)
|
||||
coordinatorLayout = findViewById(R.id.group_coordinator)
|
||||
iconView = findViewById(R.id.group_icon)
|
||||
numberChildrenView = findViewById(R.id.group_numbers)
|
||||
@@ -150,7 +153,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 +219,7 @@ class GroupActivity : LockingActivity(),
|
||||
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, database)
|
||||
|
||||
// Init dialog thread
|
||||
mProgressDialogThread?.onActionFinish = { actionTask, result ->
|
||||
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
||||
|
||||
var oldNodes: List<Node> = ArrayList()
|
||||
result.data?.getBundle(OLD_NODES_KEY)?.let { oldNodesBundle ->
|
||||
@@ -389,7 +392,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 +478,8 @@ class GroupActivity : LockingActivity(),
|
||||
enableAddGroup(addGroupEnabled)
|
||||
enableAddEntry(addEntryEnabled)
|
||||
|
||||
showButton()
|
||||
if (actionNodeMode == null)
|
||||
showButton()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,7 +509,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 +553,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 +631,7 @@ class GroupActivity : LockingActivity(),
|
||||
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
|
||||
// Copy
|
||||
mCurrentGroup?.let { newParent ->
|
||||
mProgressDialogThread?.startDatabaseCopyNodes(
|
||||
mProgressDatabaseTaskProvider?.startDatabaseCopyNodes(
|
||||
nodes,
|
||||
newParent,
|
||||
!mReadOnly && mAutoSaveEnable
|
||||
@@ -625,7 +641,7 @@ class GroupActivity : LockingActivity(),
|
||||
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
|
||||
// Move
|
||||
mCurrentGroup?.let { newParent ->
|
||||
mProgressDialogThread?.startDatabaseMoveNodes(
|
||||
mProgressDatabaseTaskProvider?.startDatabaseMoveNodes(
|
||||
nodes,
|
||||
newParent,
|
||||
!mReadOnly && mAutoSaveEnable
|
||||
@@ -659,7 +675,7 @@ class GroupActivity : LockingActivity(),
|
||||
&& database.isRecycleBinEnabled
|
||||
&& database.recycleBin != mCurrentGroup) {
|
||||
|
||||
mProgressDialogThread?.startDatabaseDeleteNodes(
|
||||
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
|
||||
nodes,
|
||||
!mReadOnly && mAutoSaveEnable
|
||||
)
|
||||
@@ -674,7 +690,7 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
override fun permanentlyDeleteNodes(nodes: List<Node>) {
|
||||
mProgressDialogThread?.startDatabaseDeleteNodes(
|
||||
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
|
||||
nodes,
|
||||
!mReadOnly && mAutoSaveEnable
|
||||
)
|
||||
@@ -693,6 +709,8 @@ class GroupActivity : LockingActivity(),
|
||||
assignGroupViewElements()
|
||||
// Refresh suggestions to change preferences
|
||||
mSearchSuggestionAdapter?.reInit(this)
|
||||
// Padding if lock button visible
|
||||
toolbarAction?.updateLockPaddingLeft()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -759,7 +777,7 @@ class GroupActivity : LockingActivity(),
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
// Launch education screen
|
||||
Handler().post { performedNextEducation(GroupActivityEducation(this), menu) }
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(GroupActivityEducation(this), menu) }
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -833,7 +851,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 +885,7 @@ class GroupActivity : LockingActivity(),
|
||||
// Not really needed here because added in runnable but safe
|
||||
newGroup.parent = currentGroup
|
||||
|
||||
mProgressDialogThread?.startDatabaseCreateGroup(
|
||||
mProgressDatabaseTaskProvider?.startDatabaseCreateGroup(
|
||||
newGroup,
|
||||
currentGroup,
|
||||
!mReadOnly && mAutoSaveEnable
|
||||
@@ -889,7 +907,7 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
}
|
||||
// If group updated save it in the database
|
||||
mProgressDialogThread?.startDatabaseUpdateGroup(
|
||||
mProgressDatabaseTaskProvider?.startDatabaseUpdateGroup(
|
||||
oldGroupToUpdate,
|
||||
updateGroup,
|
||||
!mReadOnly && mAutoSaveEnable
|
||||
|
||||
@@ -97,6 +97,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
nodeClickListener = null
|
||||
onScrollListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -266,14 +272,15 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
}
|
||||
|
||||
fun actionNodesCallback(nodes: List<Node>,
|
||||
menuListener: NodesActionMenuListener?) : ActionMode.Callback {
|
||||
menuListener: NodesActionMenuListener?,
|
||||
actionModeCallback: ActionMode.Callback) : ActionMode.Callback {
|
||||
|
||||
return object : ActionMode.Callback {
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
nodeActionSelectionMode = false
|
||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||
return true
|
||||
return actionModeCallback.onCreateActionMode(mode, menu)
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
@@ -318,7 +325,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
// Add the number of items selected in title
|
||||
mode?.title = nodes.size.toString()
|
||||
|
||||
return true
|
||||
return actionModeCallback.onPrepareActionMode(mode, menu)
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||
@@ -348,7 +355,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
nodeActionSelectionMode = false
|
||||
returnValue
|
||||
}
|
||||
else -> false
|
||||
else -> actionModeCallback.onActionItemClicked(mode, item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,6 +365,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
mAdapter?.unselectActionNodes()
|
||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||
nodeActionSelectionMode = false
|
||||
actionModeCallback.onDestroyActionMode(mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,37 +21,38 @@ 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
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
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
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.activities.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 +61,18 @@ import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.FileDatabaseInfo
|
||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.closeDatabase
|
||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||
import kotlinx.android.synthetic.main.activity_password.*
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
@@ -83,16 +86,17 @@ 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
|
||||
|
||||
private var mRememberKeyFile: Boolean = false
|
||||
private var mOpenFileHelper: OpenFileHelper? = null
|
||||
private var mSelectFileHelper: SelectFileHelper? = null
|
||||
|
||||
private var mPermissionAsked = false
|
||||
private var readOnly: Boolean = false
|
||||
@@ -107,7 +111,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,16 +133,16 @@ 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)
|
||||
mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
|
||||
keyFileSelectionView?.apply {
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.let {
|
||||
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
||||
setOnClickListener(it)
|
||||
setOnLongClickListener(it)
|
||||
}
|
||||
@@ -165,12 +169,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 +233,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 +255,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 +379,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 +392,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 +425,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 +456,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
biometricInitialize = true
|
||||
} else {
|
||||
advancedUnlockedManager?.destroy()
|
||||
advancedUnlockInfoView?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
if (!biometricInitialize) {
|
||||
@@ -530,7 +512,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 +587,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
readOnly: Boolean,
|
||||
cipherDatabaseEntity: CipherDatabaseEntity?,
|
||||
fixDuplicateUUID: Boolean) {
|
||||
mProgressDialogThread?.startDatabaseLoad(
|
||||
mProgressDatabaseTaskProvider?.startDatabaseLoad(
|
||||
databaseUri,
|
||||
password,
|
||||
keyFile,
|
||||
@@ -682,7 +664,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
if (!performedEductionInProgress) {
|
||||
performedEductionInProgress = true
|
||||
// Show education views
|
||||
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -769,7 +751,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
}
|
||||
|
||||
var keyFileResult = false
|
||||
mOpenFileHelper?.let {
|
||||
mSelectFileHelper?.let {
|
||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
@@ -783,7 +765,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
when (resultCode) {
|
||||
LockingActivity.RESULT_EXIT_LOCK -> {
|
||||
clearCredentialsViews()
|
||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
||||
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
|
||||
}
|
||||
Activity.RESULT_CANCELED -> {
|
||||
clearCredentialsViews()
|
||||
|
||||
@@ -25,16 +25,19 @@ import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.text.Editable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||
|
||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
@@ -56,7 +59,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
|
||||
private var mListener: AssignPasswordDialogListener? = null
|
||||
|
||||
private var mOpenFileHelper: OpenFileHelper? = null
|
||||
private var mSelectFileHelper: SelectFileHelper? = null
|
||||
|
||||
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
||||
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
|
||||
|
||||
private val passwordTextWatcher = object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
@@ -85,6 +92,17 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
mEmptyPasswordConfirmationDialog?.dismiss()
|
||||
mEmptyPasswordConfirmationDialog = null
|
||||
mNoKeyConfirmationDialog?.dismiss()
|
||||
mNoKeyConfirmationDialog = null
|
||||
mEmptyKeyFileConfirmationDialog?.dismiss()
|
||||
mEmptyKeyFileConfirmationDialog = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
|
||||
@@ -99,11 +117,15 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
|
||||
rootView = inflater.inflate(R.layout.fragment_set_password, null)
|
||||
builder.setView(rootView)
|
||||
.setTitle(R.string.assign_master_key)
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
|
||||
val credentialsInfo: ImageView? = rootView?.findViewById(R.id.credentials_information)
|
||||
credentialsInfo?.setOnClickListener {
|
||||
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
|
||||
}
|
||||
|
||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
||||
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
||||
passwordView = rootView?.findViewById(R.id.pass_password)
|
||||
@@ -113,10 +135,10 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||
|
||||
mOpenFileHelper = OpenFileHelper(this)
|
||||
mSelectFileHelper = SelectFileHelper(this)
|
||||
keyFileSelectionView?.apply {
|
||||
setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
||||
setOnLongClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
||||
setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
||||
setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
||||
}
|
||||
|
||||
val dialog = builder.create()
|
||||
@@ -129,7 +151,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
mMasterPassword = ""
|
||||
mKeyFile = null
|
||||
|
||||
var error = verifyPassword() || verifyFile()
|
||||
var error = verifyPassword() || verifyKeyFile()
|
||||
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
||||
error = true
|
||||
if (allowNoMasterKey)
|
||||
@@ -199,7 +221,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
return error
|
||||
}
|
||||
|
||||
private fun verifyFile(): Boolean {
|
||||
private fun verifyKeyFile(): Boolean {
|
||||
var error = false
|
||||
if (keyFileCheckBox != null
|
||||
&& keyFileCheckBox!!.isChecked) {
|
||||
@@ -219,7 +241,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setMessage(R.string.warning_empty_password)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (!verifyFile()) {
|
||||
if (!verifyKeyFile()) {
|
||||
mListener?.onAssignKeyDialogPositiveClick(
|
||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
||||
@@ -227,7 +249,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
builder.create().show()
|
||||
mEmptyPasswordConfirmationDialog = builder.create()
|
||||
mEmptyPasswordConfirmationDialog?.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,18 +265,44 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
this@AssignMasterKeyDialogFragment.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
builder.create().show()
|
||||
mNoKeyConfirmationDialog = builder.create()
|
||||
mNoKeyConfirmationDialog?.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showEmptyKeyFileConfirmationDialog() {
|
||||
activity?.let {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setMessage(SpannableStringBuilder().apply {
|
||||
append(getString(R.string.warning_empty_keyfile))
|
||||
append("\n\n")
|
||||
append(getString(R.string.warning_empty_keyfile_explanation))
|
||||
append("\n\n")
|
||||
append(getString(R.string.warning_sure_add_file))
|
||||
})
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
keyFileCheckBox?.isChecked = false
|
||||
keyFileSelectionView?.uri = null
|
||||
}
|
||||
mEmptyKeyFileConfirmationDialog = builder.create()
|
||||
mEmptyKeyFileConfirmationDialog?.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||
uri?.let { pathUri ->
|
||||
keyFileCheckBox?.isChecked = true
|
||||
keyFileSelectionView?.uri = pathUri
|
||||
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||
keyFileSelectionView?.error = null
|
||||
keyFileCheckBox?.isChecked = true
|
||||
keyFileSelectionView?.uri = pathUri
|
||||
if (lengthFile <= 0L) {
|
||||
showEmptyKeyFileConfirmationDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,11 @@ class DatePickerFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
// Create a new instance of DatePickerDialog and return it
|
||||
return context?.let {
|
||||
|
||||
@@ -46,6 +46,11 @@ class DeleteNodesDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
arguments?.apply {
|
||||
@@ -64,10 +69,10 @@ class DeleteNodesDialogFragment : DialogFragment() {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
builder.setMessage(getString(R.string.warning_permanently_delete_nodes))
|
||||
builder.setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mListener?.permanentlyDeleteNodes(mNodesToDelete)
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.no) { _, _ -> dismiss() }
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.Button
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
|
||||
|
||||
class EntryCustomFieldDialogFragment: DialogFragment() {
|
||||
|
||||
private var oldField: Field? = null
|
||||
|
||||
private var entryCustomFieldListener: EntryCustomFieldListener? = null
|
||||
|
||||
private var customFieldLabelContainer: TextInputLayout? = null
|
||||
private var customFieldLabel: TextView? = null
|
||||
private var customFieldDeleteButton: ImageView? = null
|
||||
private var customFieldProtectionButton: CompoundButton? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
entryCustomFieldListener = context as EntryCustomFieldListener
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + EntryCustomFieldListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
entryCustomFieldListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_entry_new_field, null)
|
||||
customFieldLabelContainer = root?.findViewById(R.id.entry_custom_field_label_container)
|
||||
customFieldLabel = root?.findViewById(R.id.entry_custom_field_label)
|
||||
customFieldDeleteButton = root?.findViewById(R.id.entry_custom_field_delete)
|
||||
customFieldProtectionButton = root?.findViewById(R.id.entry_custom_field_protection)
|
||||
|
||||
oldField = arguments?.getParcelable(KEY_FIELD)
|
||||
oldField?.let { oldCustomField ->
|
||||
customFieldLabel?.text = oldCustomField.name
|
||||
customFieldProtectionButton?.isChecked = oldCustomField.protectedValue.isProtected
|
||||
|
||||
customFieldDeleteButton?.visibility = View.VISIBLE
|
||||
customFieldDeleteButton?.setOnClickListener {
|
||||
entryCustomFieldListener?.onDeleteCustomFieldApproved(oldCustomField)
|
||||
(dialog as AlertDialog?)?.dismiss()
|
||||
}
|
||||
} ?: run {
|
||||
customFieldDeleteButton?.visibility = View.GONE
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
val dialogCreated = builder.create()
|
||||
|
||||
customFieldLabel?.requestFocus()
|
||||
customFieldLabel?.imeOptions = EditorInfo.IME_ACTION_DONE
|
||||
customFieldLabel?.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
approveIfValid()
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
dialogCreated.window?.setSoftInputMode(SOFT_INPUT_STATE_VISIBLE)
|
||||
return dialogCreated
|
||||
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// To prevent auto dismiss
|
||||
val d = dialog as AlertDialog?
|
||||
if (d != null) {
|
||||
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||
positiveButton.setOnClickListener {
|
||||
approveIfValid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun approveIfValid() {
|
||||
if (isValid()) {
|
||||
oldField?.let {
|
||||
// New property with old value
|
||||
entryCustomFieldListener?.onEditCustomFieldApproved(it,
|
||||
Field(customFieldLabel?.text?.toString() ?: "",
|
||||
ProtectedString(customFieldProtectionButton?.isChecked == true,
|
||||
it.protectedValue.stringValue))
|
||||
)
|
||||
} ?: run {
|
||||
entryCustomFieldListener?.onNewCustomFieldApproved(
|
||||
Field(customFieldLabel?.text?.toString() ?: "",
|
||||
ProtectedString(customFieldProtectionButton?.isChecked == true))
|
||||
)
|
||||
}
|
||||
(dialog as AlertDialog?)?.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValid(): Boolean {
|
||||
return if (customFieldLabel?.text?.toString()?.isNotEmpty() != true) {
|
||||
setError(R.string.error_string_key)
|
||||
false
|
||||
} else {
|
||||
setError(null)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun setError(@StringRes errorId: Int?) {
|
||||
customFieldLabelContainer?.error = if (errorId == null) null else {
|
||||
requireContext().getString(errorId)
|
||||
}
|
||||
}
|
||||
|
||||
interface EntryCustomFieldListener {
|
||||
fun onNewCustomFieldApproved(newField: Field)
|
||||
fun onEditCustomFieldApproved(oldField: Field, newField: Field)
|
||||
fun onDeleteCustomFieldApproved(oldField: Field)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_FIELD = "KEY_FIELD"
|
||||
|
||||
fun getInstance(): EntryCustomFieldDialogFragment {
|
||||
return EntryCustomFieldDialogFragment()
|
||||
}
|
||||
|
||||
fun getInstance(field: Field): EntryCustomFieldDialogFragment {
|
||||
return EntryCustomFieldDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(KEY_FIELD, field)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
/**
|
||||
* Custom Dialog to confirm big file to upload
|
||||
*/
|
||||
class FileTooBigDialogFragment : DialogFragment() {
|
||||
|
||||
private var mActionChooseListener: ActionChooseListener? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
// Verify that the host activity implements the callback interface
|
||||
try {
|
||||
mActionChooseListener = context as ActionChooseListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + ActionChooseListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mActionChooseListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setMessage(SpannableStringBuilder().apply {
|
||||
append(getString(R.string.warning_file_too_big))
|
||||
append("\n\n")
|
||||
append(getString(R.string.warning_sure_add_file))
|
||||
})
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mActionChooseListener?.onValidateUploadFileTooBig(
|
||||
arguments?.getParcelable(KEY_FILE_URI),
|
||||
arguments?.getString(KEY_FILE_NAME))
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
dismiss()
|
||||
}
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
interface ActionChooseListener {
|
||||
fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAX_WARNING_BINARY_FILE = 5242880
|
||||
|
||||
private const val KEY_FILE_URI = "KEY_FILE_URI"
|
||||
private const val KEY_FILE_NAME = "KEY_FILE_NAME"
|
||||
|
||||
fun build(attachmentToUploadUri: Uri,
|
||||
fileName: String): FileTooBigDialogFragment {
|
||||
val fragment = FileTooBigDialogFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
|
||||
putString(KEY_FILE_NAME, fileName)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,11 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
@@ -73,7 +73,11 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
editGroupListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
@@ -34,6 +34,7 @@ import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.widget.ImageViewCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.icons.IconPack
|
||||
@@ -56,6 +57,11 @@ class IconPickerDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
iconPickerListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
@@ -132,7 +138,7 @@ class IconPickerDialogFragment : DialogFragment() {
|
||||
return bundle.getParcelable(KEY_ICON_STANDARD)
|
||||
}
|
||||
|
||||
fun launch(activity: AppCompatActivity) {
|
||||
fun launch(activity: FragmentActivity) {
|
||||
// Create an instance of the dialog fragment and show it
|
||||
val dialog = IconPickerDialogFragment()
|
||||
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
|
||||
|
||||
@@ -21,20 +21,51 @@ package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||
|
||||
var positiveButtonClickListener: DialogInterface.OnClickListener? = null
|
||||
private var mListener: Listener? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
mListener = context as Listener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + Listener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
|
||||
val masterPasswordChecked: Boolean = savedInstanceState?.getBoolean(MASTER_PASSWORD_CHECKED_KEY) ?: false
|
||||
val masterPassword: String? = savedInstanceState?.getString(MASTER_PASSWORD_KEY)
|
||||
val keyFileChecked: Boolean = savedInstanceState?.getBoolean(KEY_FILE_CHECKED_KEY) ?: false
|
||||
val keyFile: Uri? = savedInstanceState?.getParcelable(KEY_FILE_URI_KEY)
|
||||
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
|
||||
builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener)
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mListener?.onPasswordEncodingValidateListener(
|
||||
databaseUri,
|
||||
masterPasswordChecked,
|
||||
masterPassword,
|
||||
keyFileChecked,
|
||||
keyFile
|
||||
)
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
||||
|
||||
return builder.create()
|
||||
@@ -42,5 +73,36 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
||||
masterPasswordChecked: Boolean,
|
||||
masterPassword: String?,
|
||||
keyFileChecked: Boolean,
|
||||
keyFile: Uri?)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
||||
private const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
|
||||
private const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
|
||||
private const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
|
||||
private const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY"
|
||||
|
||||
fun getInstance(databaseUri: Uri,
|
||||
masterPasswordChecked: Boolean,
|
||||
masterPassword: String?,
|
||||
keyFileChecked: Boolean,
|
||||
keyFile: Uri?): SortDialogFragment {
|
||||
val fragment = SortDialogFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||
putBoolean(MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
||||
putString(MASTER_PASSWORD_KEY, masterPassword)
|
||||
putBoolean(KEY_FILE_CHECKED_KEY, keyFileChecked)
|
||||
putParcelable(KEY_FILE_URI_KEY, keyFile)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
|
||||
/**
|
||||
* Custom Dialog to confirm big file to upload
|
||||
*/
|
||||
class ReplaceFileDialogFragment : DialogFragment() {
|
||||
|
||||
private var mActionChooseListener: ActionChooseListener? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
// Verify that the host activity implements the callback interface
|
||||
try {
|
||||
mActionChooseListener = context as ActionChooseListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + ActionChooseListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mActionChooseListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setMessage(SpannableStringBuilder().apply {
|
||||
append(getString(R.string.warning_replace_file))
|
||||
append("\n\n")
|
||||
append(getString(R.string.warning_sure_add_file))
|
||||
})
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mActionChooseListener?.onValidateReplaceFile(
|
||||
arguments?.getParcelable(KEY_FILE_URI),
|
||||
arguments?.getParcelable(KEY_ENTRY_ATTACHMENT))
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
dismiss()
|
||||
}
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
interface ActionChooseListener {
|
||||
fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_FILE_URI = "KEY_FILE_URI"
|
||||
private const val KEY_ENTRY_ATTACHMENT = "KEY_ENTRY_ATTACHMENT"
|
||||
|
||||
fun build(attachmentToUploadUri: Uri,
|
||||
attachment: Attachment): ReplaceFileDialogFragment {
|
||||
val fragment = ReplaceFileDialogFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
|
||||
putParcelable(KEY_ENTRY_ATTACHMENT, attachment)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import android.text.TextWatcher
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
@@ -89,9 +90,9 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
private var mSecretWellFormed = false
|
||||
private var mCounterWellFormed = true
|
||||
private var mPeriodWellFormed = true
|
||||
private var mDigitsWellFormed = true
|
||||
private var mCounterWellFormed = false
|
||||
private var mPeriodWellFormed = false
|
||||
private var mDigitsWellFormed = false
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
@@ -106,6 +107,11 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mCreateOTPElementListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
@@ -152,6 +158,28 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
otpCounterTextView?.setOnTouchListener(mOnTouchListener)
|
||||
otpDigitsTextView?.setOnTouchListener(mOnTouchListener)
|
||||
|
||||
// To manage focus
|
||||
otpPeriodTextView?.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||
otpDigitsTextView?.requestFocus()
|
||||
true
|
||||
} else
|
||||
false
|
||||
}
|
||||
otpCounterTextView?.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||
otpDigitsTextView?.requestFocus()
|
||||
true
|
||||
} else
|
||||
false
|
||||
}
|
||||
otpCounterTextView?.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||
root?.requestFocus(View.FOCUS_DOWN)
|
||||
true
|
||||
} else
|
||||
false
|
||||
}
|
||||
|
||||
// HOTP / TOTP Type selection
|
||||
val otpTypeArray = OtpType.values()
|
||||
@@ -365,14 +393,26 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
private fun upgradeParameters() {
|
||||
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
|
||||
.indexOf(mOtpElement.algorithm))
|
||||
|
||||
val secret = mOtpElement.getBase32Secret()
|
||||
otpSecretTextView?.apply {
|
||||
setText(mOtpElement.getBase32Secret())
|
||||
setText(secret)
|
||||
// Cursor at end
|
||||
setSelection(this.text.length)
|
||||
}
|
||||
otpCounterTextView?.setText(mOtpElement.counter.toString())
|
||||
otpPeriodTextView?.setText(mOtpElement.period.toString())
|
||||
otpDigitsTextView?.setText(mOtpElement.digits.toString())
|
||||
mSecretWellFormed = OtpElement.isValidBase32(secret)
|
||||
|
||||
val counter = mOtpElement.counter
|
||||
otpCounterTextView?.setText(counter.toString())
|
||||
mCounterWellFormed = OtpElement.isValidCounter(counter)
|
||||
|
||||
val period = mOtpElement.period
|
||||
otpPeriodTextView?.setText(period.toString())
|
||||
mPeriodWellFormed = OtpElement.isValidPeriod(period)
|
||||
|
||||
val digits = mOtpElement.digits
|
||||
otpDigitsTextView?.setText(digits.toString())
|
||||
mDigitsWellFormed = OtpElement.isValidDigits(digits)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
|
||||
@@ -54,6 +54,11 @@ class SortDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
@@ -25,6 +25,11 @@ class TimePickerFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
// Create a new instance of DatePickerDialog and return it
|
||||
return context?.let {
|
||||
|
||||
@@ -28,19 +28,20 @@ import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class OpenFileHelper {
|
||||
class SelectFileHelper {
|
||||
|
||||
private var activity: Activity? = null
|
||||
private var fragment: Fragment? = null
|
||||
|
||||
val openFileOnClickViewListener: OpenFileOnClickViewListener
|
||||
get() = OpenFileOnClickViewListener()
|
||||
val selectFileOnClickViewListener: SelectFileOnClickViewListener
|
||||
get() = SelectFileOnClickViewListener()
|
||||
|
||||
constructor(context: Activity) {
|
||||
this.activity = context
|
||||
@@ -52,7 +53,10 @@ class OpenFileHelper {
|
||||
this.fragment = context
|
||||
}
|
||||
|
||||
inner class OpenFileOnClickViewListener : View.OnClickListener, View.OnLongClickListener {
|
||||
inner class SelectFileOnClickViewListener :
|
||||
View.OnClickListener,
|
||||
View.OnLongClickListener,
|
||||
MenuItem.OnMenuItemClickListener {
|
||||
|
||||
private fun onAbstractClick(longClick: Boolean = false) {
|
||||
try {
|
||||
@@ -85,17 +89,22 @@ class OpenFileHelper {
|
||||
onAbstractClick(true)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||
onAbstractClick()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun openActivityWithActionOpenDocument() {
|
||||
val intentOpenDocument = Intent(APP_ACTION_OPEN_DOCUMENT).apply {
|
||||
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||
@@ -108,10 +117,10 @@ class OpenFileHelper {
|
||||
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||
@@ -226,12 +235,6 @@ class OpenFileHelper {
|
||||
|
||||
private const val TAG = "OpenFileHelper"
|
||||
|
||||
private var APP_ACTION_OPEN_DOCUMENT: String = try {
|
||||
Intent::class.java.getField("ACTION_OPEN_DOCUMENT").get(null) as String
|
||||
} catch (e: Exception) {
|
||||
"android.intent.action.OPEN_DOCUMENT"
|
||||
}
|
||||
|
||||
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
|
||||
|
||||
private const val GET_CONTENT = 25745
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.view.collapse
|
||||
|
||||
abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val context: Context)
|
||||
: RecyclerView.Adapter<T>() {
|
||||
|
||||
protected val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
var itemsList: MutableList<Item> = ArrayList()
|
||||
private set
|
||||
|
||||
var onDeleteButtonClickListener: ((item: Item)->Unit)? = null
|
||||
private var mItemToRemove: Item? = null
|
||||
|
||||
var onListSizeChangedListener: ((previousSize: Int, newSize: Int)->Unit)? = null
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return itemsList.size
|
||||
}
|
||||
|
||||
open fun assignItems(items: List<Item>) {
|
||||
val previousSize = itemsList.size
|
||||
itemsList.apply {
|
||||
clear()
|
||||
addAll(items)
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
|
||||
}
|
||||
|
||||
open fun isEmpty(): Boolean {
|
||||
return itemsList.isEmpty()
|
||||
}
|
||||
|
||||
open fun contains(item: Item): Boolean {
|
||||
return itemsList.contains(item)
|
||||
}
|
||||
|
||||
open fun indexOf(item: Item): Int {
|
||||
return itemsList.indexOf(item)
|
||||
}
|
||||
|
||||
open fun putItem(item: Item) {
|
||||
val previousSize = itemsList.size
|
||||
if (itemsList.contains(item)) {
|
||||
val index = itemsList.indexOf(item)
|
||||
itemsList.removeAt(index)
|
||||
itemsList.add(index, item)
|
||||
notifyItemChanged(index)
|
||||
} else {
|
||||
itemsList.add(item)
|
||||
notifyItemInserted(itemsList.indexOf(item))
|
||||
}
|
||||
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Only replace [oldItem] by [newItem] if [oldItem] exists
|
||||
*/
|
||||
open fun replaceItem(oldItem: Item, newItem: Item) {
|
||||
if (itemsList.contains(oldItem)) {
|
||||
val index = itemsList.indexOf(oldItem)
|
||||
itemsList.removeAt(index)
|
||||
itemsList.add(index, newItem)
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only remove [item] if doesn't exists
|
||||
*/
|
||||
open fun removeItem(item: Item) {
|
||||
if (itemsList.contains(item)) {
|
||||
mItemToRemove = item
|
||||
notifyItemChanged(itemsList.indexOf(item))
|
||||
}
|
||||
}
|
||||
|
||||
protected fun performDeletion(holder: T, item: Item): Boolean {
|
||||
val effectivelyDeletionPerformed = mItemToRemove == item
|
||||
if (effectivelyDeletionPerformed) {
|
||||
holder.itemView.collapse(true) {
|
||||
deleteItem(item)
|
||||
}
|
||||
}
|
||||
return effectivelyDeletionPerformed
|
||||
}
|
||||
|
||||
protected fun onBindDeleteButton(holder: T, deleteButton: View, item: Item, position: Int) {
|
||||
deleteButton.apply {
|
||||
visibility = View.VISIBLE
|
||||
if (performDeletion(holder, item)) {
|
||||
setOnClickListener(null)
|
||||
} else {
|
||||
setOnClickListener {
|
||||
onDeleteButtonClickListener?.invoke(item)
|
||||
mItemToRemove = item
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteItem(item: Item) {
|
||||
val previousSize = itemsList.size
|
||||
val position = itemsList.indexOf(item)
|
||||
if (position >= 0) {
|
||||
itemsList.removeAt(position)
|
||||
notifyItemRemoved(position)
|
||||
mItemToRemove = null
|
||||
for (i in 0 until itemsList.size) {
|
||||
notifyItemChanged(i)
|
||||
}
|
||||
}
|
||||
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
itemsList.clear()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.text.format.Formatter
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachment
|
||||
|
||||
class EntryAttachmentsAdapter(val context: Context) : RecyclerView.Adapter<EntryAttachmentsAdapter.EntryBinariesViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
var entryAttachmentsList: MutableList<EntryAttachment> = ArrayList()
|
||||
var onItemClickListener: ((item: EntryAttachment, position: Int)->Unit)? = null
|
||||
|
||||
private val mDatabase = Database.getInstance()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder {
|
||||
return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) {
|
||||
val entryAttachment = entryAttachmentsList[position]
|
||||
|
||||
holder.binaryFileTitle.text = entryAttachment.name
|
||||
holder.binaryFileSize.text = Formatter.formatFileSize(context,
|
||||
entryAttachment.binaryAttachment.length())
|
||||
holder.binaryFileCompression.apply {
|
||||
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip
|
||||
|| entryAttachment.binaryAttachment.isCompressed == true) {
|
||||
text = CompressionAlgorithm.GZip.getName(context.resources)
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
text = ""
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
holder.binaryFileProgress.apply {
|
||||
visibility = when (entryAttachment.downloadState) {
|
||||
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
|
||||
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
|
||||
}
|
||||
progress = entryAttachment.downloadProgression
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
onItemClickListener?.invoke(entryAttachment, position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return entryAttachmentsList.size
|
||||
}
|
||||
|
||||
fun updateProgress(entryAttachment: EntryAttachment) {
|
||||
val indexEntryAttachment = entryAttachmentsList.indexOfLast { current -> current.name == entryAttachment.name }
|
||||
if (indexEntryAttachment != -1) {
|
||||
entryAttachmentsList[indexEntryAttachment] = entryAttachment
|
||||
notifyItemChanged(indexEntryAttachment)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
entryAttachmentsList.clear()
|
||||
}
|
||||
|
||||
inner class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
|
||||
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
|
||||
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
|
||||
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.TypedArray
|
||||
import android.graphics.Color
|
||||
import android.text.format.Formatter
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
|
||||
|
||||
class EntryAttachmentsItemsAdapter(context: Context)
|
||||
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
|
||||
|
||||
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
|
||||
|
||||
private var mTitleColor: Int
|
||||
|
||||
init {
|
||||
// Get the primary text color of the theme
|
||||
val typedValue = TypedValue()
|
||||
context.theme.resolveAttribute(android.R.attr.textColorPrimary, typedValue, true)
|
||||
val typedArray: TypedArray = context.obtainStyledAttributes(typedValue.data, intArrayOf(
|
||||
android.R.attr.textColor))
|
||||
mTitleColor = typedArray.getColor(0, -1)
|
||||
typedArray.recycle()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder {
|
||||
return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) {
|
||||
val entryAttachmentState = itemsList[position]
|
||||
|
||||
holder.itemView.visibility = View.VISIBLE
|
||||
holder.binaryFileBroken.apply {
|
||||
setColorFilter(Color.RED)
|
||||
visibility = if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
holder.binaryFileTitle.text = entryAttachmentState.attachment.name
|
||||
if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
|
||||
holder.binaryFileTitle.setTextColor(Color.RED)
|
||||
} else {
|
||||
holder.binaryFileTitle.setTextColor(mTitleColor)
|
||||
}
|
||||
holder.binaryFileSize.text = Formatter.formatFileSize(context,
|
||||
entryAttachmentState.attachment.binaryAttachment.length())
|
||||
holder.binaryFileCompression.apply {
|
||||
if (entryAttachmentState.attachment.binaryAttachment.isCompressed) {
|
||||
text = CompressionAlgorithm.GZip.getName(context.resources)
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
text = ""
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
when (entryAttachmentState.streamDirection) {
|
||||
StreamDirection.UPLOAD -> {
|
||||
holder.binaryFileProgressIcon.isActivated = true
|
||||
when (entryAttachmentState.downloadState) {
|
||||
AttachmentState.START,
|
||||
AttachmentState.IN_PROGRESS -> {
|
||||
holder.binaryFileProgressContainer.visibility = View.VISIBLE
|
||||
holder.binaryFileProgress.apply {
|
||||
visibility = View.VISIBLE
|
||||
progress = entryAttachmentState.downloadProgression
|
||||
}
|
||||
holder.binaryFileDeleteButton.apply {
|
||||
visibility = View.GONE
|
||||
setOnClickListener(null)
|
||||
}
|
||||
}
|
||||
AttachmentState.NULL,
|
||||
AttachmentState.ERROR,
|
||||
AttachmentState.COMPLETE -> {
|
||||
holder.binaryFileProgressContainer.visibility = View.GONE
|
||||
holder.binaryFileProgress.visibility = View.GONE
|
||||
holder.binaryFileDeleteButton.apply {
|
||||
visibility = View.VISIBLE
|
||||
onBindDeleteButton(holder, this, entryAttachmentState, position)
|
||||
}
|
||||
}
|
||||
}
|
||||
holder.itemView.setOnClickListener(null)
|
||||
}
|
||||
StreamDirection.DOWNLOAD -> {
|
||||
holder.binaryFileProgressIcon.isActivated = false
|
||||
holder.binaryFileProgressContainer.visibility = View.VISIBLE
|
||||
holder.binaryFileDeleteButton.visibility = View.GONE
|
||||
holder.binaryFileProgress.apply {
|
||||
visibility = when (entryAttachmentState.downloadState) {
|
||||
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
|
||||
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
|
||||
}
|
||||
progress = entryAttachmentState.downloadProgression
|
||||
}
|
||||
holder.itemView.setOnClickListener {
|
||||
onItemClickListener?.invoke(entryAttachmentState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
var binaryFileBroken: ImageView = itemView.findViewById(R.id.item_attachment_broken)
|
||||
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
|
||||
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
|
||||
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
|
||||
var binaryFileProgressContainer: View = itemView.findViewById(R.id.item_attachment_progress_container)
|
||||
var binaryFileProgressIcon: ImageView = itemView.findViewById(R.id.item_attachment_icon)
|
||||
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
|
||||
var binaryFileDeleteButton: View = itemView.findViewById(R.id.item_attachment_delete_button)
|
||||
}
|
||||
}
|
||||
@@ -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,43 +65,49 @@ 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
|
||||
holder.fileModificationContainer.visibility = View.VISIBLE
|
||||
} ?: run {
|
||||
holder.fileModification.visibility = View.GONE
|
||||
holder.fileModificationContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
// 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,33 +179,68 @@ 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
|
||||
}
|
||||
|
||||
inner class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)
|
||||
|
||||
var defaultFileButton: CompoundButton = itemView.findViewById(R.id.default_file_button)
|
||||
var fileAlias: TextView = itemView.findViewById(R.id.file_alias)
|
||||
var fileInformation: ImageView = itemView.findViewById(R.id.file_information)
|
||||
var fileInformationButton: ImageView = itemView.findViewById(R.id.file_information_button)
|
||||
|
||||
var fileMainSwitcher: ViewSwitcher = itemView.findViewById(R.id.file_main_switcher)
|
||||
var fileAliasEdit: EditText = itemView.findViewById(R.id.file_alias_edit)
|
||||
@@ -196,6 +250,7 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
||||
var fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
|
||||
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
|
||||
var filePath: TextView = itemView.findViewById(R.id.file_path)
|
||||
var fileModificationContainer: ViewGroup = itemView.findViewById(R.id.file_modification_container)
|
||||
var fileModification: TextView = itemView.findViewById(R.id.file_modification)
|
||||
var fileSize: TextView = itemView.findViewById(R.id.file_size)
|
||||
}
|
||||
|
||||
@@ -338,6 +338,9 @@ class NodeAdapter (private val context: Context)
|
||||
}
|
||||
}
|
||||
|
||||
holder.attachmentIcon?.visibility =
|
||||
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||
|
||||
mDatabase.stopManageEntry(entry)
|
||||
}
|
||||
|
||||
@@ -391,6 +394,7 @@ class NodeAdapter (private val context: Context)
|
||||
var text: TextView = itemView.findViewById(R.id.node_text)
|
||||
var subText: TextView = itemView.findViewById(R.id.node_subtext)
|
||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -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))) {
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.app
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class App : MultiDexApplication() {
|
||||
|
||||
@@ -33,7 +34,7 @@ class App : MultiDexApplication() {
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
||||
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
|
||||
super.onTerminate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,120 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun getAllFileDatabaseHistories(fileHistoryResultListener: (fileDatabaseHistoryResult: List<FileDatabaseHistoryEntity>?) -> Unit) {
|
||||
ActionDatabaseAsyncTask(
|
||||
fun getDatabaseFileList(databaseFileListResult: (List<DatabaseFile>) -> Unit) {
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileHistoryDao.getAll()
|
||||
val hideBrokenLocations = PreferencesUtil.hideBrokenLocations(applicationContext)
|
||||
// Show only uri accessible
|
||||
val databaseFileListLoaded = ArrayList<DatabaseFile>()
|
||||
databaseFileHistoryDao.getAll().forEach { fileDatabaseHistoryEntity ->
|
||||
val fileDatabaseInfo = FileDatabaseInfo(applicationContext, fileDatabaseHistoryEntity.databaseUri)
|
||||
if (hideBrokenLocations && fileDatabaseInfo.exists
|
||||
|| !hideBrokenLocations) {
|
||||
databaseFileListLoaded.add(
|
||||
DatabaseFile(
|
||||
UriUtil.parse(fileDatabaseHistoryEntity.databaseUri),
|
||||
UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri),
|
||||
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
||||
fileDatabaseInfo.exists,
|
||||
fileDatabaseInfo.getModificationString(),
|
||||
fileDatabaseInfo.getSizeString()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
databaseFileListLoaded
|
||||
},
|
||||
{
|
||||
fileHistoryResultListener.invoke(it)
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null) {
|
||||
addOrUpdateFileDatabaseHistory(FileDatabaseHistoryEntity(
|
||||
databaseUri.toString(),
|
||||
"",
|
||||
keyFileUri?.toString(),
|
||||
System.currentTimeMillis()
|
||||
), true)
|
||||
}
|
||||
|
||||
fun addOrUpdateFileDatabaseHistory(fileDatabaseHistory: FileDatabaseHistoryEntity, unmodifiedAlias: Boolean = false) {
|
||||
ActionDatabaseAsyncTask(
|
||||
{
|
||||
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(fileDatabaseHistory.databaseUri)
|
||||
|
||||
if (unmodifiedAlias) {
|
||||
fileDatabaseHistory.databaseAlias = fileDatabaseHistoryRetrieve?.databaseAlias ?: ""
|
||||
}
|
||||
// Update values if history element not yet in the database
|
||||
if (fileDatabaseHistoryRetrieve == null) {
|
||||
databaseFileHistoryDao.add(fileDatabaseHistory)
|
||||
} else {
|
||||
databaseFileHistoryDao.update(fileDatabaseHistory)
|
||||
databaseFileList ->
|
||||
databaseFileList?.let {
|
||||
databaseFileListResult.invoke(it)
|
||||
}
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteFileDatabaseHistory(fileDatabaseHistory: FileDatabaseHistoryEntity,
|
||||
fileHistoryDeletedResult: (FileDatabaseHistoryEntity?) -> Unit) {
|
||||
ActionDatabaseAsyncTask(
|
||||
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null,
|
||||
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
|
||||
addOrUpdateDatabaseFile(DatabaseFile(
|
||||
databaseUri,
|
||||
keyFileUri
|
||||
), databaseFileAddedOrUpdatedResult)
|
||||
}
|
||||
|
||||
fun addOrUpdateDatabaseFile(databaseFileToAddOrUpdate: DatabaseFile,
|
||||
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileHistoryDao.delete(fileDatabaseHistory)
|
||||
databaseFileToAddOrUpdate.databaseUri?.let { databaseUri ->
|
||||
// Try to get info in database first
|
||||
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||
|
||||
// Complete alias if not exists
|
||||
val fileDatabaseHistory = FileDatabaseHistoryEntity(
|
||||
databaseUri.toString(),
|
||||
databaseFileToAddOrUpdate.databaseAlias
|
||||
?: fileDatabaseHistoryRetrieve?.databaseAlias
|
||||
?: "",
|
||||
databaseFileToAddOrUpdate.keyFileUri?.toString(),
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
// Update values if history element not yet in the database
|
||||
if (fileDatabaseHistoryRetrieve == null) {
|
||||
databaseFileHistoryDao.add(fileDatabaseHistory)
|
||||
} else {
|
||||
databaseFileHistoryDao.update(fileDatabaseHistory)
|
||||
}
|
||||
|
||||
val fileDatabaseInfo = FileDatabaseInfo(applicationContext,
|
||||
fileDatabaseHistory.databaseUri)
|
||||
DatabaseFile(
|
||||
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
||||
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
||||
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
||||
fileDatabaseInfo.exists,
|
||||
fileDatabaseInfo.getModificationString(),
|
||||
fileDatabaseInfo.getSizeString()
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
if (it != null && it > 0)
|
||||
fileHistoryDeletedResult.invoke(fileDatabaseHistory)
|
||||
else
|
||||
fileHistoryDeletedResult.invoke(null)
|
||||
databaseFileAddedOrUpdatedResult?.invoke(it)
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteDatabaseFile(databaseFileToDelete: DatabaseFile,
|
||||
databaseFileDeletedResult: (DatabaseFile?) -> Unit) {
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileToDelete.databaseUri?.let { databaseUri ->
|
||||
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())?.let { fileDatabaseHistory ->
|
||||
val returnValue = databaseFileHistoryDao.delete(fileDatabaseHistory)
|
||||
if (returnValue > 0) {
|
||||
DatabaseFile(
|
||||
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
||||
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
||||
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||
databaseFileToDelete.databaseAlias
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
databaseFileDeletedResult.invoke(it)
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
|
||||
ActionDatabaseAsyncTask(
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
|
||||
}
|
||||
@@ -121,7 +193,7 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
||||
}
|
||||
|
||||
fun deleteAllKeyFiles() {
|
||||
ActionDatabaseAsyncTask(
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileHistoryDao.deleteAllKeyFiles()
|
||||
}
|
||||
@@ -129,7 +201,7 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
||||
}
|
||||
|
||||
fun deleteAll() {
|
||||
ActionDatabaseAsyncTask(
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileHistoryDao.deleteAll()
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,9 +68,11 @@ internal class StructureParser(private val structure: AssistStructure) {
|
||||
|
||||
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
|
||||
// Get the domain of a web app
|
||||
node.webDomain?.let {
|
||||
result?.domain = it
|
||||
Log.d(TAG, "Autofill domain: $it")
|
||||
node.webDomain?.let { webDomain ->
|
||||
if (webDomain.isNotEmpty()) {
|
||||
result?.domain = webDomain
|
||||
Log.d(TAG, "Autofill domain: $webDomain")
|
||||
}
|
||||
}
|
||||
|
||||
// Only parse visible nodes
|
||||
@@ -100,19 +102,14 @@ internal class StructureParser(private val structure: AssistStructure) {
|
||||
val autofillId = node.autofillId
|
||||
node.autofillHints?.forEach {
|
||||
when {
|
||||
it.equals(View.AUTOFILL_HINT_USERNAME, true)
|
||||
|| it.equals(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
||||
|| it.equals("email", true)
|
||||
|| it.equals(View.AUTOFILL_HINT_PHONE, true)
|
||||
|| it.contains("OrUsername", true)
|
||||
|| it.contains("OrEmailAddress", true)
|
||||
|| it.contains("OrEmail", true)
|
||||
|| it.contains("OrPhone", true)-> {
|
||||
it.contains(View.AUTOFILL_HINT_USERNAME, true)
|
||||
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
||||
|| it.contains("email", true)
|
||||
|| it.contains(View.AUTOFILL_HINT_PHONE, true)-> {
|
||||
result?.usernameId = autofillId
|
||||
Log.d(TAG, "Autofill username hint")
|
||||
}
|
||||
it.equals(View.AUTOFILL_HINT_PASSWORD, true)
|
||||
|| it.contains("password", true) -> {
|
||||
it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
|
||||
result?.passwordId = autofillId
|
||||
Log.d(TAG, "Autofill password hint")
|
||||
// Username not needed in this case
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,17 +19,23 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
||||
|
||||
import com.kunzisoft.keepass.stream.LittleEndianDataInputStream
|
||||
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
|
||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
||||
import com.kunzisoft.keepass.stream.uuidTo16Bytes
|
||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
class KdfParameters(val uuid: UUID) : VariantDictionary() {
|
||||
class KdfParameters: VariantDictionary {
|
||||
|
||||
val uuid: UUID
|
||||
|
||||
constructor(uuid: UUID): super() {
|
||||
this.uuid = uuid
|
||||
}
|
||||
|
||||
constructor(uuid: UUID, d: VariantDictionary): super(d) {
|
||||
this.uuid = uuid
|
||||
}
|
||||
|
||||
fun setParamUUID() {
|
||||
setByteArray(PARAM_UUID, uuidTo16Bytes(uuid))
|
||||
@@ -41,25 +47,17 @@ class KdfParameters(val uuid: UUID) : VariantDictionary() {
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun deserialize(data: ByteArray): KdfParameters? {
|
||||
val inputStream = LittleEndianDataInputStream(ByteArrayInputStream(data))
|
||||
val dictionary = deserialize(inputStream)
|
||||
val dictionary = VariantDictionary.deserialize(data)
|
||||
|
||||
val uuidBytes = dictionary.getByteArray(PARAM_UUID) ?: return null
|
||||
val uuid = bytes16ToUuid(uuidBytes)
|
||||
|
||||
val kdfParameters = KdfParameters(uuid)
|
||||
kdfParameters.copyTo(dictionary)
|
||||
return kdfParameters
|
||||
return KdfParameters(uuid, dictionary)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun serialize(kdfParameters: KdfParameters): ByteArray {
|
||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||
val outputStream = LittleEndianDataOutputStream(byteArrayOutputStream)
|
||||
|
||||
serialize(kdfParameters, outputStream)
|
||||
|
||||
return byteArrayOutputStream.toByteArray()
|
||||
return VariantDictionary.serialize(kdfParameters)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -25,6 +25,8 @@ import android.util.Log
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.closeDatabase
|
||||
|
||||
class CreateDatabaseRunnable(context: Context,
|
||||
private val mDatabase: Database,
|
||||
@@ -34,7 +36,8 @@ class CreateDatabaseRunnable(context: Context,
|
||||
withMasterPassword: Boolean,
|
||||
masterPassword: String?,
|
||||
withKeyFile: Boolean,
|
||||
keyFile: Uri?)
|
||||
keyFile: Uri?,
|
||||
private val createDatabaseResult: ((Result) -> Unit)?)
|
||||
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile) {
|
||||
|
||||
override fun onStartRun() {
|
||||
@@ -42,29 +45,36 @@ class CreateDatabaseRunnable(context: Context,
|
||||
// Create new database record
|
||||
mDatabase.apply {
|
||||
createData(mDatabaseUri, databaseName, rootName)
|
||||
// Set Database state
|
||||
loaded = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
mDatabase.closeAndClear()
|
||||
mDatabase.closeAndClear(UriUtil.getBinaryDir(context))
|
||||
setError(e)
|
||||
}
|
||||
|
||||
super.onStartRun()
|
||||
}
|
||||
|
||||
override fun onFinishRun() {
|
||||
super.onFinishRun()
|
||||
override fun onActionRun() {
|
||||
super.onActionRun()
|
||||
|
||||
if (result.isSuccess) {
|
||||
// Add database to recent files
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,11 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.closeDatabase
|
||||
|
||||
class LoadDatabaseRunnable(private val context: Context,
|
||||
private val mDatabase: Database,
|
||||
@@ -39,17 +40,14 @@ class LoadDatabaseRunnable(private val context: Context,
|
||||
private val mKey: Uri?,
|
||||
private val mReadonly: Boolean,
|
||||
private val mCipherEntity: CipherDatabaseEntity?,
|
||||
private val mOmitBackup: Boolean,
|
||||
private val mFixDuplicateUUID: Boolean,
|
||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||
private val mDuplicateUuidAction: ((Result) -> Unit)?)
|
||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||
: ActionRunnable() {
|
||||
|
||||
private val cacheDirectory = context.applicationContext.filesDir
|
||||
|
||||
override fun onStartRun() {
|
||||
// Clear before we load
|
||||
mDatabase.closeAndClear(cacheDirectory)
|
||||
mDatabase.closeAndClear(UriUtil.getBinaryDir(context))
|
||||
}
|
||||
|
||||
override fun onActionRun() {
|
||||
@@ -57,21 +55,17 @@ class LoadDatabaseRunnable(private val context: Context,
|
||||
mDatabase.loadData(mUri, mPass, mKey,
|
||||
mReadonly,
|
||||
context.contentResolver,
|
||||
cacheDirectory,
|
||||
mOmitBackup,
|
||||
UriUtil.getBinaryDir(context),
|
||||
mFixDuplicateUUID,
|
||||
progressTaskUpdater)
|
||||
}
|
||||
catch (e: DuplicateUuidDatabaseException) {
|
||||
mDuplicateUuidAction?.invoke(result)
|
||||
setError(e)
|
||||
}
|
||||
catch (e: LoadDatabaseException) {
|
||||
setError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishRun() {
|
||||
if (result.isSuccess) {
|
||||
// Save keyFile in app database
|
||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||
@@ -88,11 +82,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(UriUtil.getBinaryDir(context))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishRun() {
|
||||
mLoadDatabaseResult?.invoke(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,9 @@ import android.content.*
|
||||
import android.content.Context.BIND_ABOVE_CLIENT
|
||||
import android.content.Context.BIND_NOT_FOREGROUND
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.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 +35,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
|
||||
@@ -48,6 +45,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
|
||||
@@ -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)
|
||||
}
|
||||
@@ -486,6 +463,13 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
||||
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
|
||||
newMaxHistoryItems: Int,
|
||||
save: Boolean) {
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
|
||||
class RemoveUnlinkedDataDatabaseRunnable (
|
||||
context: Context,
|
||||
database: Database,
|
||||
saveDatabase: Boolean)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase) {
|
||||
|
||||
override fun onActionRun() {
|
||||
try {
|
||||
database.removeUnlinkedAttachments()
|
||||
} catch (e: Exception) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
super.onActionRun()
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ class DeleteEntryHistoryDatabaseRunnable (
|
||||
|
||||
override fun onStartRun() {
|
||||
try {
|
||||
mainEntry.removeEntryFromHistory(entryHistoryPosition)
|
||||
database.removeEntryHistory(mainEntry, entryHistoryPosition)
|
||||
} catch (e: Exception) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
@@ -64,6 +64,10 @@ class DeleteNodesRunnable(context: Context,
|
||||
} else {
|
||||
database.deleteEntry(currentNode)
|
||||
}
|
||||
// Remove the oldest attachments
|
||||
currentNode.getAttachments(database.binaryPool).forEach {
|
||||
database.removeAttachmentIfNotUsed(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
@@ -40,16 +41,34 @@ class UpdateEntryRunnable constructor(
|
||||
// WARNING : Re attribute parent removed in entry edit activity to save memory
|
||||
mNewEntry.addParentFrom(mOldEntry)
|
||||
|
||||
// Build oldest attachments
|
||||
val oldEntryAttachments = mOldEntry.getAttachments(database.binaryPool, true)
|
||||
val newEntryAttachments = mNewEntry.getAttachments(database.binaryPool, true)
|
||||
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
|
||||
// Not use equals because only check name
|
||||
newEntryAttachments.forEach { newAttachment ->
|
||||
oldEntryAttachments.forEach { oldAttachment ->
|
||||
if (oldAttachment.name == newAttachment.name
|
||||
&& oldAttachment.binaryAttachment == newAttachment.binaryAttachment)
|
||||
attachmentsToRemove.remove(oldAttachment)
|
||||
}
|
||||
}
|
||||
|
||||
// Update entry with new values
|
||||
mOldEntry.updateWith(mNewEntry)
|
||||
mNewEntry.touch(modified = true, touchParents = true)
|
||||
|
||||
// Create an entry history (an entry history don't have history)
|
||||
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
|
||||
database.removeOldestEntryHistory(mOldEntry)
|
||||
database.removeOldestEntryHistory(mOldEntry, database.binaryPool)
|
||||
|
||||
// Only change data in index
|
||||
database.updateEntry(mOldEntry)
|
||||
|
||||
// Remove oldest attachments
|
||||
attachmentsToRemove.forEach {
|
||||
database.removeAttachmentIfNotUsed(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nodeFinish(): ActionNodesValues {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||
|
||||
data class Attachment(var name: String,
|
||||
var binaryAttachment: BinaryAttachment) : Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readString() ?: "",
|
||||
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment()
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(name)
|
||||
parcel.writeParcelable(binaryAttachment, flags)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "$name at $binaryAttachment"
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Attachment) return false
|
||||
|
||||
if (name != other.name) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return name.hashCode()
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<Attachment> {
|
||||
override fun createFromParcel(parcel: Parcel): Attachment {
|
||||
return Attachment(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<Attachment?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,7 @@ import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.database.*
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
@@ -46,13 +44,13 @@ 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
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
|
||||
class Database {
|
||||
@@ -71,6 +69,13 @@ class Database {
|
||||
val drawFactory = IconDrawableFactory()
|
||||
|
||||
var loaded = false
|
||||
set(value) {
|
||||
field = value
|
||||
loadTimestamp = if (field) System.currentTimeMillis() else null
|
||||
}
|
||||
|
||||
var loadTimestamp: Long? = null
|
||||
private set
|
||||
|
||||
val iconFactory: IconImageFactory
|
||||
get() {
|
||||
@@ -151,6 +156,17 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
fun compressionForNewEntry(): Boolean {
|
||||
if (mDatabaseKDB != null)
|
||||
return false
|
||||
// Default compression not necessary if stored in header
|
||||
mDatabaseKDBX?.let {
|
||||
return it.compressionAlgorithm == CompressionAlgorithm.GZip
|
||||
&& it.kdbxVersion.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
|
||||
newCompression: CompressionAlgorithm) {
|
||||
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
|
||||
@@ -262,14 +278,14 @@ class Database {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if RecycleBin is available or not for this version of database
|
||||
* @return true if RecycleBin available
|
||||
* Determine if a configurable RecycleBin is available or not for this version of database
|
||||
* @return true if a configurable RecycleBin available
|
||||
*/
|
||||
val allowRecycleBin: Boolean
|
||||
val allowConfigurableRecycleBin: Boolean
|
||||
get() = mDatabaseKDBX != null
|
||||
|
||||
var isRecycleBinEnabled: Boolean
|
||||
// TODO #394 isRecycleBinEnabled mDatabaseKDB
|
||||
// Backup is always enabled in KDB database
|
||||
get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false
|
||||
set(value) {
|
||||
mDatabaseKDBX?.isRecycleBinEnabled = value
|
||||
@@ -287,12 +303,12 @@ class Database {
|
||||
}
|
||||
|
||||
fun ensureRecycleBinExists(resources: Resources) {
|
||||
mDatabaseKDB?.ensureRecycleBinExists()
|
||||
mDatabaseKDB?.ensureBackupExists()
|
||||
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
||||
}
|
||||
|
||||
fun removeRecycleBin() {
|
||||
// TODO #394 delete backup mDatabaseKDB?.removeRecycleBin()
|
||||
// Don't allow remove backup in KDB
|
||||
mDatabaseKDBX?.removeRecycleBin()
|
||||
}
|
||||
|
||||
@@ -309,6 +325,8 @@ class Database {
|
||||
fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
|
||||
setDatabaseKDBX(DatabaseKDBX(databaseName, rootName))
|
||||
this.fileUri = databaseUri
|
||||
// Set Database state
|
||||
this.loaded = true
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
@@ -316,7 +334,6 @@ class Database {
|
||||
readOnly: Boolean,
|
||||
contentResolver: ContentResolver,
|
||||
cacheDirectory: File,
|
||||
omitBackup: Boolean,
|
||||
fixDuplicateUUID: Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
|
||||
@@ -378,14 +395,12 @@ class Database {
|
||||
else -> throw SignatureDatabaseException()
|
||||
}
|
||||
|
||||
this.mSearchHelper = SearchHelper(omitBackup)
|
||||
this.mSearchHelper = SearchHelper()
|
||||
loaded = true
|
||||
|
||||
} catch (e: LoadDatabaseException) {
|
||||
Log.e("KPD", "Database::loadData", e)
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Log.e("KPD", "Database::loadData", e)
|
||||
throw FileNotFoundDatabaseException()
|
||||
} finally {
|
||||
keyFileInputStream?.close()
|
||||
@@ -393,20 +408,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 +435,39 @@ class Database {
|
||||
searchInUUIDs = false
|
||||
searchInTags = false
|
||||
ignoreCase = true
|
||||
}, max)
|
||||
}, omitBackup, max)
|
||||
}
|
||||
|
||||
val binaryPool: BinaryPool
|
||||
get() {
|
||||
return mDatabaseKDBX?.binaryPool ?: BinaryPool()
|
||||
}
|
||||
|
||||
val allowMultipleAttachments: Boolean
|
||||
get() {
|
||||
if (mDatabaseKDB != null)
|
||||
return false
|
||||
if (mDatabaseKDBX != null)
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
fun buildNewBinary(cacheDirectory: File,
|
||||
compressed: Boolean = false,
|
||||
protected: Boolean = false): BinaryAttachment? {
|
||||
return mDatabaseKDB?.buildNewBinary(cacheDirectory)
|
||||
?: mDatabaseKDBX?.buildNewBinary(cacheDirectory, compressed, protected)
|
||||
}
|
||||
|
||||
fun removeAttachmentIfNotUsed(attachment: Attachment) {
|
||||
// No need in KDB database because unique attachment by entry
|
||||
// Don't clear to fix upload multiple times
|
||||
mDatabaseKDBX?.removeUnlinkedAttachment(attachment.binaryAttachment, false)
|
||||
}
|
||||
|
||||
fun removeUnlinkedAttachments() {
|
||||
// No check in database KDB because unique attachment by entry
|
||||
mDatabaseKDBX?.removeUnlinkedAttachments(true)
|
||||
}
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
@@ -464,7 +515,7 @@ class Database {
|
||||
} else {
|
||||
var outputStream: OutputStream? = null
|
||||
try {
|
||||
outputStream = contentResolver.openOutputStream(uri)
|
||||
outputStream = contentResolver.openOutputStream(uri, "rwt")
|
||||
outputStream?.let { definedOutputStream ->
|
||||
val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
|
||||
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
|
||||
@@ -709,7 +760,7 @@ class Database {
|
||||
fun canRecycle(entry: Entry): Boolean {
|
||||
var canRecycle: Boolean? = null
|
||||
entry.entryKDB?.let {
|
||||
canRecycle = mDatabaseKDB?.canRecycle()
|
||||
canRecycle = mDatabaseKDB?.canRecycle(it)
|
||||
}
|
||||
entry.entryKDBX?.let {
|
||||
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
||||
@@ -720,7 +771,7 @@ class Database {
|
||||
fun canRecycle(group: Group): Boolean {
|
||||
var canRecycle: Boolean? = null
|
||||
group.groupKDB?.let {
|
||||
canRecycle = mDatabaseKDB?.canRecycle()
|
||||
canRecycle = mDatabaseKDB?.canRecycle(it)
|
||||
}
|
||||
group.groupKDBX?.let {
|
||||
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
||||
@@ -772,18 +823,25 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
fun startManageEntry(entry: Entry) {
|
||||
fun startManageEntry(entry: Entry?) {
|
||||
mDatabaseKDBX?.let {
|
||||
entry.startToManageFieldReferences(it)
|
||||
entry?.startToManageFieldReferences(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopManageEntry(entry: Entry) {
|
||||
fun stopManageEntry(entry: Entry?) {
|
||||
mDatabaseKDBX?.let {
|
||||
entry.stopToManageFieldReferences()
|
||||
entry?.stopToManageFieldReferences()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if database allows custom field
|
||||
*/
|
||||
fun allowEntryCustomFields(): Boolean {
|
||||
return mDatabaseKDBX != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove oldest history for each entry if more than max items or max memory
|
||||
*/
|
||||
@@ -791,7 +849,7 @@ class Database {
|
||||
rootGroup?.doForEachChildAndForIt(
|
||||
object : NodeHandler<Entry>() {
|
||||
override fun operate(node: Entry): Boolean {
|
||||
removeOldestEntryHistory(node)
|
||||
removeOldestEntryHistory(node, binaryPool)
|
||||
return true
|
||||
}
|
||||
},
|
||||
@@ -799,34 +857,19 @@ class Database {
|
||||
override fun operate(node: Group): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun removeEachEntryHistory() {
|
||||
rootGroup?.doForEachChildAndForIt(
|
||||
object : NodeHandler<Entry>() {
|
||||
override fun operate(node: Entry): Boolean {
|
||||
node.removeAllHistory()
|
||||
return true
|
||||
}
|
||||
},
|
||||
object : NodeHandler<Group>() {
|
||||
override fun operate(node: Group): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove oldest history if more than max items or max memory
|
||||
*/
|
||||
fun removeOldestEntryHistory(entry: Entry) {
|
||||
fun removeOldestEntryHistory(entry: Entry, binaryPool: BinaryPool) {
|
||||
mDatabaseKDBX?.let {
|
||||
|
||||
val maxItems = historyMaxItems
|
||||
if (maxItems >= 0) {
|
||||
while (entry.getHistory().size > maxItems) {
|
||||
entry.removeOldestEntryFromHistory()
|
||||
removeOldestEntryHistory(entry)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -835,11 +878,10 @@ class Database {
|
||||
while (true) {
|
||||
var historySize: Long = 0
|
||||
for (entryHistory in entry.getHistory()) {
|
||||
historySize += entryHistory.getSize()
|
||||
historySize += entryHistory.getSize(binaryPool)
|
||||
}
|
||||
|
||||
if (historySize > maxSize) {
|
||||
entry.removeOldestEntryFromHistory()
|
||||
removeOldestEntryHistory(entry)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@@ -848,6 +890,22 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeOldestEntryHistory(entry: Entry) {
|
||||
entry.removeOldestEntryFromHistory()?.let {
|
||||
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
|
||||
removeAttachmentIfNotUsed(attachmentToRemove)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEntryHistory(entry: Entry, entryHistoryPosition: Int) {
|
||||
entry.removeEntryFromHistory(entryHistoryPosition)?.let {
|
||||
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
|
||||
removeAttachmentIfNotUsed(attachmentToRemove)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object : SingletonHolder<Database>(::Database) {
|
||||
|
||||
private val TAG = Database::class.java.name
|
||||
|
||||
@@ -23,6 +23,8 @@ import android.content.res.Resources
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import org.joda.time.Duration
|
||||
import org.joda.time.Instant
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@@ -95,6 +97,7 @@ class DateInstant : Parcelable {
|
||||
companion object {
|
||||
|
||||
val NEVER_EXPIRE = neverExpire
|
||||
val IN_ONE_MONTH = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
|
||||
private val dateFormat = SimpleDateFormat.getDateTimeInstance()
|
||||
|
||||
private val neverExpire: DateInstant
|
||||
|
||||
@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.database.BinaryPool
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
@@ -32,9 +33,6 @@ import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.model.EntryAttachment
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
@@ -285,66 +283,90 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve custom fields to show, key is the label, value is the value of field (protected or not)
|
||||
* Retrieve extra fields to show, key is the label, value is the value of field (protected or not)
|
||||
* @return Map of label/value
|
||||
*/
|
||||
val customFields: HashMap<String, ProtectedString>
|
||||
get() = entryKDBX?.customFields ?: HashMap()
|
||||
|
||||
/**
|
||||
* To redefine if version of entry allow custom field,
|
||||
* @return true if entry allows custom field
|
||||
*/
|
||||
fun allowCustomFields(): Boolean {
|
||||
return entryKDBX?.allowCustomFields() ?: false
|
||||
}
|
||||
|
||||
fun removeAllFields() {
|
||||
entryKDBX?.removeAllFields()
|
||||
fun getExtraFields(): List<Field> {
|
||||
val extraFields = ArrayList<Field>()
|
||||
entryKDBX?.let {
|
||||
for (field in it.customFields) {
|
||||
extraFields.add(Field(field.key, field.value))
|
||||
}
|
||||
}
|
||||
return extraFields
|
||||
}
|
||||
|
||||
/**
|
||||
* Update or add an extra field to the list (standard or custom)
|
||||
* @param label Label of field, must be unique
|
||||
* @param value Value of field
|
||||
*/
|
||||
fun putExtraField(label: String, value: ProtectedString) {
|
||||
entryKDBX?.putExtraField(label, value)
|
||||
fun putExtraField(field: Field) {
|
||||
entryKDBX?.putExtraField(field.name, field.protectedValue)
|
||||
}
|
||||
|
||||
fun getOtpElement(): OtpElement? {
|
||||
return OtpEntryFields.parseFields { key ->
|
||||
customFields[key]?.toString()
|
||||
private fun addExtraFields(fields: List<Field>) {
|
||||
fields.forEach {
|
||||
putExtraField(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun startToManageFieldReferences(db: DatabaseKDBX) {
|
||||
entryKDBX?.startToManageFieldReferences(db)
|
||||
private fun removeAllFields() {
|
||||
entryKDBX?.removeAllFields()
|
||||
}
|
||||
|
||||
fun getOtpElement(): OtpElement? {
|
||||
entryKDBX?.let {
|
||||
return OtpEntryFields.parseFields { key ->
|
||||
it.customFields[key]?.toString()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun startToManageFieldReferences(database: DatabaseKDBX) {
|
||||
entryKDBX?.startToManageFieldReferences(database)
|
||||
}
|
||||
|
||||
fun stopToManageFieldReferences() {
|
||||
entryKDBX?.stopToManageFieldReferences()
|
||||
}
|
||||
|
||||
fun getAttachments(): ArrayList<EntryAttachment> {
|
||||
val attachments = ArrayList<EntryAttachment>()
|
||||
|
||||
val binaryDescriptionKDB = entryKDB?.binaryDescription ?: ""
|
||||
val binaryKDB = entryKDB?.binaryData
|
||||
if (binaryKDB != null) {
|
||||
attachments.add(EntryAttachment(binaryDescriptionKDB, binaryKDB))
|
||||
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> {
|
||||
val attachments = ArrayList<Attachment>()
|
||||
entryKDB?.getAttachment()?.let {
|
||||
attachments.add(it)
|
||||
}
|
||||
|
||||
val actionEach = object : (Map.Entry<String, BinaryAttachment>)->Unit {
|
||||
override fun invoke(mapEntry: Map.Entry<String, BinaryAttachment>) {
|
||||
attachments.add(EntryAttachment(mapEntry.key, mapEntry.value))
|
||||
}
|
||||
entryKDBX?.getAttachments(binaryPool, inHistory)?.let {
|
||||
attachments.addAll(it)
|
||||
}
|
||||
entryKDBX?.binaries?.forEach(actionEach)
|
||||
|
||||
return attachments
|
||||
}
|
||||
|
||||
fun containsAttachment(): Boolean {
|
||||
return entryKDB?.containsAttachment() == true
|
||||
|| entryKDBX?.containsAttachment() == true
|
||||
}
|
||||
|
||||
private fun addAttachments(binaryPool: BinaryPool, attachments: List<Attachment>) {
|
||||
attachments.forEach {
|
||||
putAttachment(it, binaryPool)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeAttachment(attachment: Attachment) {
|
||||
entryKDB?.removeAttachment(attachment)
|
||||
entryKDBX?.removeAttachment(attachment)
|
||||
}
|
||||
|
||||
private fun removeAllAttachments() {
|
||||
entryKDB?.removeAttachment()
|
||||
entryKDBX?.removeAttachments()
|
||||
}
|
||||
|
||||
private fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
|
||||
entryKDB?.putAttachment(attachment)
|
||||
entryKDBX?.putAttachment(attachment, binaryPool)
|
||||
}
|
||||
|
||||
fun getHistory(): ArrayList<Entry> {
|
||||
val history = ArrayList<Entry>()
|
||||
val entryKDBXHistory = entryKDBX?.history ?: ArrayList()
|
||||
@@ -360,20 +382,22 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEntryFromHistory(position: Int) {
|
||||
entryKDBX?.removeEntryFromHistory(position)
|
||||
fun removeEntryFromHistory(position: Int): Entry? {
|
||||
entryKDBX?.removeEntryFromHistory(position)?.let {
|
||||
return Entry(it)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun removeAllHistory() {
|
||||
entryKDBX?.removeAllHistory()
|
||||
fun removeOldestEntryFromHistory(): Entry? {
|
||||
entryKDBX?.removeOldestEntryFromHistory()?.let {
|
||||
return Entry(it)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun removeOldestEntryFromHistory() {
|
||||
entryKDBX?.removeOldestEntryFromHistory()
|
||||
}
|
||||
|
||||
fun getSize(): Long {
|
||||
return entryKDBX?.size ?: 0L
|
||||
fun getSize(binaryPool: BinaryPool): Long {
|
||||
return entryKDBX?.getSize(binaryPool) ?: 0L
|
||||
}
|
||||
|
||||
fun containsCustomData(): Boolean {
|
||||
@@ -396,26 +420,54 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
database?.stopManageEntry(this)
|
||||
else
|
||||
database?.startManageEntry(this)
|
||||
|
||||
entryInfo.id = nodeId.toString()
|
||||
entryInfo.title = title
|
||||
entryInfo.icon = icon
|
||||
entryInfo.username = username
|
||||
entryInfo.password = password
|
||||
entryInfo.expires = expires
|
||||
entryInfo.expiryTime = expiryTime
|
||||
entryInfo.url = url
|
||||
entryInfo.notes = notes
|
||||
for (entry in customFields.entries) {
|
||||
entryInfo.customFields.add(
|
||||
Field(entry.key, entry.value))
|
||||
}
|
||||
entryInfo.customFields = getExtraFields()
|
||||
// Add otpElement to generate token
|
||||
entryInfo.otpModel = getOtpElement()?.otpModel
|
||||
// Replace parameter fields by generated OTP fields
|
||||
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
||||
if (!raw) {
|
||||
// Replace parameter fields by generated OTP fields
|
||||
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
||||
}
|
||||
database?.binaryPool?.let { binaryPool ->
|
||||
entryInfo.attachments = getAttachments(binaryPool)
|
||||
}
|
||||
|
||||
if (!raw)
|
||||
database?.stopManageEntry(this)
|
||||
return entryInfo
|
||||
}
|
||||
|
||||
fun setEntryInfo(database: Database?, newEntryInfo: EntryInfo) {
|
||||
database?.startManageEntry(this)
|
||||
|
||||
removeAllFields()
|
||||
removeAllAttachments()
|
||||
// NodeId stay as is
|
||||
title = newEntryInfo.title
|
||||
icon = newEntryInfo.icon
|
||||
username = newEntryInfo.username
|
||||
password = newEntryInfo.password
|
||||
expires = newEntryInfo.expires
|
||||
expiryTime = newEntryInfo.expiryTime
|
||||
url = newEntryInfo.url
|
||||
notes = newEntryInfo.notes
|
||||
addExtraFields(newEntryInfo.customFields)
|
||||
database?.binaryPool?.let { binaryPool ->
|
||||
addAttachments(binaryPool, newEntryInfo.attachments)
|
||||
}
|
||||
|
||||
database?.stopManageEntry(this)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
@@ -17,10 +17,8 @@
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element.security
|
||||
package com.kunzisoft.keepass.database.element.database
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.stream.readBytes
|
||||
@@ -30,76 +28,90 @@ import java.util.zip.GZIPOutputStream
|
||||
|
||||
class BinaryAttachment : Parcelable {
|
||||
|
||||
var isCompressed: Boolean? = null
|
||||
private var dataFile: File? = null
|
||||
var isCompressed: Boolean = false
|
||||
private set
|
||||
var isProtected: Boolean = false
|
||||
private set
|
||||
private var dataFile: File? = null
|
||||
var isCorrupted: Boolean = false
|
||||
|
||||
fun length(): Long {
|
||||
if (dataFile != null)
|
||||
return dataFile!!.length()
|
||||
return 0
|
||||
return dataFile?.length() ?: 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty protected binary
|
||||
*/
|
||||
constructor() {
|
||||
this.isCompressed = null
|
||||
this.isProtected = false
|
||||
this.dataFile = null
|
||||
}
|
||||
constructor()
|
||||
|
||||
constructor(dataFile: File, enableProtection: Boolean = false, compressed: Boolean? = null) {
|
||||
this.isCompressed = compressed
|
||||
this.isProtected = enableProtection
|
||||
constructor(dataFile: File, compressed: Boolean = false, protected: Boolean = false) {
|
||||
this.dataFile = dataFile
|
||||
this.isCompressed = compressed
|
||||
this.isProtected = protected
|
||||
}
|
||||
|
||||
private constructor(parcel: Parcel) {
|
||||
val compressedByte = parcel.readByte().toInt()
|
||||
isCompressed = if (compressedByte == 2) null else compressedByte != 0
|
||||
isProtected = parcel.readByte().toInt() != 0
|
||||
parcel.readString()?.let {
|
||||
dataFile = File(it)
|
||||
}
|
||||
isCompressed = parcel.readByte().toInt() != 0
|
||||
isProtected = parcel.readByte().toInt() != 0
|
||||
isCorrupted = parcel.readByte().toInt() != 0
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getInputDataStream(): InputStream {
|
||||
return when {
|
||||
dataFile != null -> FileInputStream(dataFile!!)
|
||||
length() > 0 -> FileInputStream(dataFile!!)
|
||||
else -> ByteArrayInputStream(ByteArray(0))
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getUnGzipInputDataStream(): InputStream {
|
||||
return if (isCompressed)
|
||||
GZIPInputStream(getInputDataStream())
|
||||
else
|
||||
getInputDataStream()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getOutputDataStream(): OutputStream {
|
||||
return when {
|
||||
dataFile != null -> FileOutputStream(dataFile!!)
|
||||
else -> throw IOException("Unable to write in an unknown file")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getGzipOutputDataStream(): OutputStream {
|
||||
return if (isCompressed) {
|
||||
GZIPOutputStream(getOutputDataStream())
|
||||
} else {
|
||||
getOutputDataStream()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
||||
dataFile?.let { concreteDataFile ->
|
||||
// To compress, create a new binary with file
|
||||
if (isCompressed != true) {
|
||||
if (!isCompressed) {
|
||||
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
||||
var outputStream: GZIPOutputStream? = null
|
||||
var inputStream: InputStream? = null
|
||||
try {
|
||||
outputStream = GZIPOutputStream(FileOutputStream(fileBinaryCompress))
|
||||
inputStream = getInputDataStream()
|
||||
inputStream.readBytes(bufferSize) { buffer ->
|
||||
outputStream.write(buffer)
|
||||
}
|
||||
} finally {
|
||||
inputStream?.close()
|
||||
outputStream?.close()
|
||||
|
||||
// Remove unGzip file
|
||||
if (concreteDataFile.delete()) {
|
||||
if (fileBinaryCompress.renameTo(concreteDataFile)) {
|
||||
// Harmonize with database compression
|
||||
isCompressed = true
|
||||
GZIPOutputStream(FileOutputStream(fileBinaryCompress)).use { outputStream ->
|
||||
getInputDataStream().use { inputStream ->
|
||||
inputStream.readBytes(bufferSize) { buffer ->
|
||||
outputStream.write(buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove unGzip file
|
||||
if (concreteDataFile.delete()) {
|
||||
if (fileBinaryCompress.renameTo(concreteDataFile)) {
|
||||
// Harmonize with database compression
|
||||
isCompressed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,52 +119,20 @@ class BinaryAttachment : Parcelable {
|
||||
@Throws(IOException::class)
|
||||
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
||||
dataFile?.let { concreteDataFile ->
|
||||
if (isCompressed != false) {
|
||||
if (isCompressed) {
|
||||
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
||||
var outputStream: FileOutputStream? = null
|
||||
var inputStream: GZIPInputStream? = null
|
||||
try {
|
||||
outputStream = FileOutputStream(fileBinaryDecompress)
|
||||
inputStream = GZIPInputStream(getInputDataStream())
|
||||
inputStream.readBytes(bufferSize) { buffer ->
|
||||
outputStream.write(buffer)
|
||||
}
|
||||
} finally {
|
||||
inputStream?.close()
|
||||
outputStream?.close()
|
||||
|
||||
// Remove gzip file
|
||||
if (concreteDataFile.delete()) {
|
||||
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
|
||||
// Harmonize with database compression
|
||||
isCompressed = false
|
||||
FileOutputStream(fileBinaryDecompress).use { outputStream ->
|
||||
getUnGzipInputDataStream().use { inputStream ->
|
||||
inputStream.readBytes(bufferSize) { buffer ->
|
||||
outputStream.write(buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun download(createdFileUri: Uri,
|
||||
contentResolver: ContentResolver,
|
||||
bufferSize: Int = DEFAULT_BUFFER_SIZE,
|
||||
update: ((percent: Int)->Unit)? = null) {
|
||||
|
||||
var dataDownloaded = 0
|
||||
contentResolver.openOutputStream(createdFileUri).use { outputStream ->
|
||||
outputStream?.let { fileOutputStream ->
|
||||
if (isCompressed == true) {
|
||||
GZIPInputStream(getInputDataStream())
|
||||
} else {
|
||||
getInputDataStream()
|
||||
}.use { inputStream ->
|
||||
inputStream.readBytes(bufferSize) { buffer ->
|
||||
fileOutputStream.write(buffer)
|
||||
dataDownloaded += buffer.size
|
||||
try {
|
||||
val percentDownload = (100 * dataDownloaded / length()).toInt()
|
||||
update?.invoke(percentDownload)
|
||||
} catch (e: Exception) {}
|
||||
// Remove gzip file
|
||||
if (concreteDataFile.delete()) {
|
||||
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
|
||||
// Harmonize with database compression
|
||||
isCompressed = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,26 +159,33 @@ class BinaryAttachment : Parcelable {
|
||||
|
||||
return isCompressed == other.isCompressed
|
||||
&& isProtected == other.isProtected
|
||||
&& isCorrupted == other.isCorrupted
|
||||
&& sameData
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
|
||||
var result = 0
|
||||
result = 31 * result + if (isCompressed == null) 2 else if (isCompressed!!) 1 else 0
|
||||
result = 31 * result + if (isCompressed) 1 else 0
|
||||
result = 31 * result + if (isProtected) 1 else 0
|
||||
result = 31 * result + if (isCorrupted) 1 else 0
|
||||
result = 31 * result + dataFile!!.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return dataFile.toString()
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
dest.writeByte((if (isCompressed == null) 2 else if (isCompressed!!) 1 else 0).toByte())
|
||||
dest.writeByte((if (isProtected) 1 else 0).toByte())
|
||||
dest.writeString(dataFile?.absolutePath)
|
||||
dest.writeByte((if (isCompressed) 1 else 0).toByte())
|
||||
dest.writeByte((if (isProtected) 1 else 0).toByte())
|
||||
dest.writeByte((if (isCorrupted) 1 else 0).toByte())
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -19,52 +19,137 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element.database
|
||||
|
||||
import android.util.SparseArray
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import java.io.IOException
|
||||
|
||||
class BinaryPool {
|
||||
private val pool = SparseArray<BinaryAttachment>()
|
||||
private val pool = LinkedHashMap<Int, BinaryAttachment>()
|
||||
|
||||
/**
|
||||
* To get a binary by the pool key (ref attribute in entry)
|
||||
*/
|
||||
operator fun get(key: Int): BinaryAttachment? {
|
||||
return pool[key]
|
||||
}
|
||||
|
||||
fun put(key: Int, value: BinaryAttachment) {
|
||||
pool.put(key, value)
|
||||
/**
|
||||
* To linked a binary with a pool key, if the pool key doesn't exists, create an unused one
|
||||
*/
|
||||
fun put(key: Int?, value: BinaryAttachment) {
|
||||
if (key == null)
|
||||
put(value)
|
||||
else
|
||||
pool[key] = value
|
||||
}
|
||||
|
||||
fun doForEachBinary(action: (key: Int, binary: BinaryAttachment) -> Unit) {
|
||||
for (i in 0 until pool.size()) {
|
||||
action.invoke(i, pool.get(pool.keyAt(i)))
|
||||
/**
|
||||
* To put a [binaryAttachment] in the pool,
|
||||
* if already exists, replace the current one,
|
||||
* else add it with a new key
|
||||
*/
|
||||
fun put(binaryAttachment: BinaryAttachment): Int {
|
||||
var key = findKey(binaryAttachment)
|
||||
if (key == null) {
|
||||
key = findUnusedKey()
|
||||
}
|
||||
pool[key] = binaryAttachment
|
||||
return key
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a binary from the pool, the file is not deleted
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun clear() {
|
||||
doForEachBinary { _, binary ->
|
||||
binary.clear()
|
||||
fun remove(binaryAttachment: BinaryAttachment) {
|
||||
findKey(binaryAttachment)?.let {
|
||||
pool.remove(it)
|
||||
}
|
||||
pool.clear()
|
||||
// Don't clear attachment here because a file can be used in many BinaryAttachment
|
||||
}
|
||||
|
||||
fun add(fileBinary: BinaryAttachment) {
|
||||
if (findKey(fileBinary) == null) {
|
||||
pool.put(findUnusedKey(), fileBinary)
|
||||
}
|
||||
}
|
||||
|
||||
fun findUnusedKey(): Int {
|
||||
var unusedKey = pool.size()
|
||||
while (get(unusedKey) != null)
|
||||
/**
|
||||
* Utility method to find an unused key in the pool
|
||||
*/
|
||||
private fun findUnusedKey(): Int {
|
||||
var unusedKey = 0
|
||||
while (pool[unusedKey] != null)
|
||||
unusedKey++
|
||||
return unusedKey
|
||||
}
|
||||
|
||||
fun findKey(pb: BinaryAttachment): Int? {
|
||||
for (i in 0 until pool.size()) {
|
||||
if (pool.get(pool.keyAt(i)) == pb) return i
|
||||
/**
|
||||
* Return key of [binaryAttachmentToRetrieve] or null if not found
|
||||
*/
|
||||
private fun findKey(binaryAttachmentToRetrieve: BinaryAttachment): Int? {
|
||||
val contains = pool.containsValue(binaryAttachmentToRetrieve)
|
||||
return if (!contains)
|
||||
null
|
||||
else {
|
||||
for ((key, binary) in pool) {
|
||||
if (binary == binaryAttachmentToRetrieve) {
|
||||
return key
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to order binaries and solve index problem in database v4
|
||||
*/
|
||||
private fun orderedBinaries(): List<KeyBinary> {
|
||||
val keyBinaryList = ArrayList<KeyBinary>()
|
||||
for ((key, binary) in pool) {
|
||||
keyBinaryList.add(KeyBinary(key, binary))
|
||||
}
|
||||
return keyBinaryList
|
||||
}
|
||||
|
||||
/**
|
||||
* To register a binary with a ref corresponding to an ordered index
|
||||
*/
|
||||
fun getBinaryIndexFromKey(key: Int): Int? {
|
||||
val index = orderedBinaries().indexOfFirst { it.key == key }
|
||||
return if (index < 0)
|
||||
null
|
||||
else
|
||||
index
|
||||
}
|
||||
|
||||
/**
|
||||
* Different from doForEach, provide an ordered index to each binary
|
||||
*/
|
||||
fun doForEachOrderedBinary(action: (index: Int, keyBinary: KeyBinary) -> Unit) {
|
||||
orderedBinaries().forEachIndexed(action)
|
||||
}
|
||||
|
||||
/**
|
||||
* To do an action on each binary in the pool
|
||||
*/
|
||||
fun doForEachBinary(action: (binary: BinaryAttachment) -> Unit) {
|
||||
pool.values.forEach { action.invoke(it) }
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun clear() {
|
||||
doForEachBinary {
|
||||
it.clear()
|
||||
}
|
||||
pool.clear()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val stringBuffer = StringBuffer()
|
||||
for ((key, value) in pool) {
|
||||
if (stringBuffer.isNotEmpty())
|
||||
stringBuffer.append(", {$key:$value}")
|
||||
else
|
||||
stringBuffer.append("{$key:$value}")
|
||||
}
|
||||
return stringBuffer.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility data class to order binaries
|
||||
*/
|
||||
data class KeyBinary(val key: Int, val binary: BinaryAttachment)
|
||||
}
|
||||
|
||||
@@ -26,8 +26,10 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.security.DigestOutputStream
|
||||
@@ -38,10 +40,12 @@ import kotlin.collections.ArrayList
|
||||
|
||||
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
|
||||
var backupGroupId: Int = BACKUP_FOLDER_UNDEFINED_ID
|
||||
private var backupGroupId: Int = BACKUP_FOLDER_UNDEFINED_ID
|
||||
|
||||
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
||||
|
||||
private var binaryIncrement = 0
|
||||
|
||||
override val version: String
|
||||
get() = "KeePass 1"
|
||||
|
||||
@@ -57,7 +61,12 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
|
||||
// Retrieve backup group in index
|
||||
val backupGroup: GroupKDB?
|
||||
get() = if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID) null else getGroupById(backupGroupId)
|
||||
get() {
|
||||
return if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
|
||||
null
|
||||
else
|
||||
getGroupById(backupGroupId)
|
||||
}
|
||||
|
||||
override val kdfEngine: KdfEngine?
|
||||
get() = kdfListV3[0]
|
||||
@@ -177,6 +186,9 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
override fun isInRecycleBin(group: GroupKDB): Boolean {
|
||||
var currentGroup: GroupKDB? = group
|
||||
|
||||
if (backupGroup == null)
|
||||
return false
|
||||
|
||||
if (currentGroup == backupGroup)
|
||||
return true
|
||||
|
||||
@@ -192,10 +204,10 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the recycle bin tree exists, if enabled and create it
|
||||
* Ensure that the backup tree exists if enabled, and create it
|
||||
* if it doesn't exist
|
||||
*/
|
||||
fun ensureRecycleBinExists() {
|
||||
fun ensureBackupExists() {
|
||||
rootGroups.forEach { currentGroup ->
|
||||
if (currentGroup.level == 0
|
||||
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
|
||||
@@ -219,21 +231,25 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
* @param node Node to remove
|
||||
* @return true if node can be recycle, false elsewhere
|
||||
*/
|
||||
// TODO #394 Backup KDB
|
||||
// fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
|
||||
fun canRecycle(): Boolean {
|
||||
fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
|
||||
if (backupGroup == null)
|
||||
ensureBackupExists()
|
||||
if (node == backupGroup)
|
||||
return false
|
||||
backupGroup?.let {
|
||||
if (node.isContainedIn(it))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun recycle(group: GroupKDB) {
|
||||
ensureRecycleBinExists()
|
||||
removeGroupFrom(group, group.parent)
|
||||
addGroupTo(group, backupGroup)
|
||||
group.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun recycle(entry: EntryKDB) {
|
||||
ensureRecycleBinExists()
|
||||
removeEntryFrom(entry, entry.parent)
|
||||
addEntryTo(entry, backupGroup)
|
||||
entry.afterAssignNewParent()
|
||||
@@ -249,6 +265,13 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
addEntryTo(entry, origParent)
|
||||
}
|
||||
|
||||
fun buildNewBinary(cacheDirectory: File): BinaryAttachment {
|
||||
// Generate an unique new file
|
||||
val fileInCache = File(cacheDirectory, binaryIncrement.toString())
|
||||
binaryIncrement++
|
||||
return BinaryAttachment(fileInCache)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TYPE = DatabaseKDB::class.java
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
||||
@@ -46,6 +47,7 @@ import com.kunzisoft.keepass.utils.UnsignedInt
|
||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
||||
import org.w3c.dom.Node
|
||||
import org.w3c.dom.Text
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
@@ -105,6 +107,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
val customData = HashMap<String, String>()
|
||||
|
||||
var binaryPool = BinaryPool()
|
||||
private var binaryIncrement = 0 // Unique id (don't use current time because CPU too fast)
|
||||
|
||||
var localizedAppName = "KeePassDX"
|
||||
|
||||
@@ -173,33 +176,51 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
|
||||
fun changeBinaryCompression(oldCompression: CompressionAlgorithm,
|
||||
newCompression: CompressionAlgorithm) {
|
||||
binaryPool.doForEachBinary { key, binary ->
|
||||
|
||||
try {
|
||||
when (oldCompression) {
|
||||
CompressionAlgorithm.None -> {
|
||||
when (newCompression) {
|
||||
CompressionAlgorithm.None -> {
|
||||
}
|
||||
CompressionAlgorithm.GZip -> {
|
||||
// To compress, create a new binary with file
|
||||
binary.compress(BUFFER_SIZE_BYTES)
|
||||
}
|
||||
}
|
||||
}
|
||||
when (oldCompression) {
|
||||
CompressionAlgorithm.None -> {
|
||||
when (newCompression) {
|
||||
CompressionAlgorithm.None -> {}
|
||||
CompressionAlgorithm.GZip -> {
|
||||
when (newCompression) {
|
||||
CompressionAlgorithm.None -> {
|
||||
// To decompress, create a new binary with file
|
||||
binary.decompress(BUFFER_SIZE_BYTES)
|
||||
}
|
||||
CompressionAlgorithm.GZip -> {
|
||||
}
|
||||
// Only in databaseV3.1, in databaseV4 the header is zipped during the save
|
||||
if (kdbxVersion.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) {
|
||||
compressAllBinaries()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CompressionAlgorithm.GZip -> {
|
||||
// In databaseV4 the header is zipped during the save, so not necessary here
|
||||
if (kdbxVersion.toKotlinLong() >= FILE_VERSION_32_4.toKotlinLong()) {
|
||||
decompressAllBinaries()
|
||||
} else {
|
||||
when (newCompression) {
|
||||
CompressionAlgorithm.None -> {
|
||||
decompressAllBinaries()
|
||||
}
|
||||
CompressionAlgorithm.GZip -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun compressAllBinaries() {
|
||||
binaryPool.doForEachBinary { binary ->
|
||||
try {
|
||||
// To compress, create a new binary with file
|
||||
binary.compress(BUFFER_SIZE_BYTES)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to change compression for $key")
|
||||
Log.e(TAG, "Unable to compress $binary", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decompressAllBinaries() {
|
||||
binaryPool.doForEachBinary { binary ->
|
||||
try {
|
||||
binary.decompress(BUFFER_SIZE_BYTES)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to decompress $binary", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -536,6 +557,60 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
return publicCustomData.size() > 0
|
||||
}
|
||||
|
||||
fun buildNewBinary(cacheDirectory: File,
|
||||
compression: Boolean,
|
||||
protection: Boolean,
|
||||
binaryPoolId: Int? = null): BinaryAttachment {
|
||||
// New file with current time
|
||||
val fileInCache = File(cacheDirectory, binaryIncrement.toString())
|
||||
binaryIncrement++
|
||||
val binaryAttachment = BinaryAttachment(fileInCache, compression, protection)
|
||||
// add attachment to pool
|
||||
binaryPool.put(binaryPoolId, binaryAttachment)
|
||||
return binaryAttachment
|
||||
}
|
||||
|
||||
fun removeUnlinkedAttachment(binary: BinaryAttachment, clear: Boolean) {
|
||||
val listBinaries = ArrayList<BinaryAttachment>()
|
||||
listBinaries.add(binary)
|
||||
removeUnlinkedAttachments(listBinaries, clear)
|
||||
}
|
||||
|
||||
fun removeUnlinkedAttachments(clear: Boolean) {
|
||||
removeUnlinkedAttachments(emptyList(), clear)
|
||||
}
|
||||
|
||||
private fun removeUnlinkedAttachments(binaries: List<BinaryAttachment>, clear: Boolean) {
|
||||
// Build binaries to remove with all binaries known
|
||||
val binariesToRemove = ArrayList<BinaryAttachment>()
|
||||
if (binaries.isEmpty()) {
|
||||
binaryPool.doForEachBinary { binary ->
|
||||
binariesToRemove.add(binary)
|
||||
}
|
||||
} else {
|
||||
binariesToRemove.addAll(binaries)
|
||||
}
|
||||
// Remove binaries from the list
|
||||
rootGroup?.doForEachChild(object : NodeHandler<EntryKDBX>() {
|
||||
override fun operate(node: EntryKDBX): Boolean {
|
||||
node.getAttachments(binaryPool, true).forEach {
|
||||
binariesToRemove.remove(it.binaryAttachment)
|
||||
}
|
||||
return binariesToRemove.isNotEmpty()
|
||||
}
|
||||
}, null)
|
||||
// Effective removing
|
||||
binariesToRemove.forEach {
|
||||
try {
|
||||
binaryPool.remove(it)
|
||||
if (clear)
|
||||
it.clear()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to clean binaries", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
||||
if (password == null)
|
||||
return true
|
||||
|
||||
@@ -27,7 +27,6 @@ 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.database.exception.DuplicateUuidDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.KeyFileEmptyDatabaseException
|
||||
import java.io.*
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
@@ -136,7 +135,6 @@ abstract class DatabaseVersioned<
|
||||
}
|
||||
|
||||
when (keyData.size.toLong()) {
|
||||
0L -> throw KeyFileEmptyDatabaseException()
|
||||
32L -> return keyData
|
||||
64L -> try {
|
||||
return hexStringToByteArray(String(keyData))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -26,8 +26,10 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeKDBInterface
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
/**
|
||||
* Structure containing information about one entry.
|
||||
@@ -135,6 +137,29 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
||||
override val type: Type
|
||||
get() = Type.ENTRY
|
||||
|
||||
fun getAttachment(): Attachment? {
|
||||
val binary = binaryData
|
||||
return if (binary != null)
|
||||
Attachment(binaryDescription, binary)
|
||||
else null
|
||||
}
|
||||
|
||||
fun containsAttachment(): Boolean {
|
||||
return binaryData != null
|
||||
}
|
||||
|
||||
fun putAttachment(attachment: Attachment) {
|
||||
this.binaryDescription = attachment.name
|
||||
this.binaryData = attachment.binaryAttachment
|
||||
}
|
||||
|
||||
fun removeAttachment(attachment: Attachment? = null) {
|
||||
if (attachment == null || this.binaryDescription == attachment.name) {
|
||||
this.binaryDescription = ""
|
||||
this.binaryData = null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/** Size of byte buffer needed to hold this struct. */
|
||||
|
||||
@@ -21,7 +21,9 @@ package com.kunzisoft.keepass.database.element.entry
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.database.BinaryPool
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
@@ -31,11 +33,12 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||
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.utils.ParcelableUtil
|
||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.LinkedHashMap
|
||||
|
||||
class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
|
||||
|
||||
@@ -58,9 +61,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>()
|
||||
var customData = LinkedHashMap<String, String>()
|
||||
var fields = LinkedHashMap<String, ProtectedString>()
|
||||
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
|
||||
var foregroundColor = ""
|
||||
var backgroundColor = ""
|
||||
var overrideURL = ""
|
||||
@@ -69,36 +72,32 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
var additional = ""
|
||||
var tags = ""
|
||||
|
||||
val size: Long
|
||||
get() {
|
||||
var size = FIXED_LENGTH_SIZE
|
||||
fun getSize(binaryPool: BinaryPool): Long {
|
||||
var size = FIXED_LENGTH_SIZE
|
||||
|
||||
for (entry in fields.entries) {
|
||||
size += entry.key.length.toLong()
|
||||
size += entry.value.length().toLong()
|
||||
}
|
||||
|
||||
for ((key, value) in binaries) {
|
||||
size += key.length.toLong()
|
||||
size += value.length()
|
||||
}
|
||||
|
||||
size += autoType.defaultSequence.length.toLong()
|
||||
for ((key, value) in autoType.entrySet()) {
|
||||
size += key.length.toLong()
|
||||
size += value.length.toLong()
|
||||
}
|
||||
|
||||
for (entry in history) {
|
||||
size += entry.size
|
||||
}
|
||||
|
||||
size += overrideURL.length.toLong()
|
||||
size += tags.length.toLong()
|
||||
|
||||
return size
|
||||
for (entry in fields.entries) {
|
||||
size += entry.key.length.toLong()
|
||||
size += entry.value.length().toLong()
|
||||
}
|
||||
|
||||
size += getAttachmentsSize(binaryPool)
|
||||
|
||||
size += autoType.defaultSequence.length.toLong()
|
||||
for ((key, value) in autoType.entrySet()) {
|
||||
size += key.length.toLong()
|
||||
size += value.length.toLong()
|
||||
}
|
||||
|
||||
for (entry in history) {
|
||||
size += entry.getSize(binaryPool)
|
||||
}
|
||||
|
||||
size += overrideURL.length.toLong()
|
||||
size += tags.length.toLong()
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
override var expires: Boolean = false
|
||||
|
||||
constructor() : super()
|
||||
@@ -109,7 +108,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
|
||||
customData = ParcelableUtil.readStringParcelableMap(parcel)
|
||||
fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java)
|
||||
binaries = ParcelableUtil.readStringParcelableMap(parcel, BinaryAttachment::class.java)
|
||||
binaries = ParcelableUtil.readStringIntMap(parcel)
|
||||
foregroundColor = parcel.readString() ?: foregroundColor
|
||||
backgroundColor = parcel.readString() ?: backgroundColor
|
||||
overrideURL = parcel.readString() ?: overrideURL
|
||||
@@ -123,11 +122,11 @@ 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)
|
||||
ParcelableUtil.writeStringParcelableMap(dest, flags, binaries)
|
||||
ParcelableUtil.writeStringIntMap(dest, binaries)
|
||||
dest.writeString(foregroundColor)
|
||||
dest.writeString(backgroundColor)
|
||||
dest.writeString(overrideURL)
|
||||
@@ -166,8 +165,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
tags = source.tags
|
||||
}
|
||||
|
||||
fun startToManageFieldReferences(db: DatabaseKDBX) {
|
||||
this.mDatabase = db
|
||||
fun startToManageFieldReferences(database: DatabaseKDBX) {
|
||||
this.mDatabase = database
|
||||
this.mDecodeRef = true
|
||||
}
|
||||
|
||||
@@ -260,23 +259,17 @@ 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))
|
||||
}
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
fun allowCustomFields(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
fun removeAllFields() {
|
||||
fields.clear()
|
||||
}
|
||||
@@ -285,12 +278,47 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
fields[label] = value
|
||||
}
|
||||
|
||||
fun putProtectedBinary(key: String, value: BinaryAttachment) {
|
||||
binaries[key] = value
|
||||
/**
|
||||
* It's a list because history labels can be defined multiple times
|
||||
*/
|
||||
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> {
|
||||
val entryAttachmentList = ArrayList<Attachment>()
|
||||
for ((label, poolId) in binaries) {
|
||||
binaryPool[poolId]?.let { binary ->
|
||||
entryAttachmentList.add(Attachment(label, binary))
|
||||
}
|
||||
}
|
||||
if (inHistory) {
|
||||
history.forEach {
|
||||
entryAttachmentList.addAll(it.getAttachments(binaryPool, false))
|
||||
}
|
||||
}
|
||||
return entryAttachmentList
|
||||
}
|
||||
|
||||
fun sizeOfHistory(): Int {
|
||||
return history.size
|
||||
fun containsAttachment(): Boolean {
|
||||
return binaries.isNotEmpty()
|
||||
}
|
||||
|
||||
fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
|
||||
binaries[attachment.name] = binaryPool.put(attachment.binaryAttachment)
|
||||
}
|
||||
|
||||
fun removeAttachment(attachment: Attachment) {
|
||||
binaries.remove(attachment.name)
|
||||
}
|
||||
|
||||
fun removeAttachments() {
|
||||
binaries.clear()
|
||||
}
|
||||
|
||||
private fun getAttachmentsSize(binaryPool: BinaryPool): Long {
|
||||
var size = 0L
|
||||
for ((label, poolId) in binaries) {
|
||||
size += label.length.toLong()
|
||||
size += binaryPool[poolId]?.length() ?: 0
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
override fun putCustomData(key: String, value: String) {
|
||||
@@ -305,15 +333,11 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
history.add(entry)
|
||||
}
|
||||
|
||||
fun removeEntryFromHistory(position: Int) {
|
||||
history.removeAt(position)
|
||||
fun removeEntryFromHistory(position: Int): EntryKDBX? {
|
||||
return history.removeAt(position)
|
||||
}
|
||||
|
||||
fun removeAllHistory() {
|
||||
history.clear()
|
||||
}
|
||||
|
||||
fun removeOldestEntryFromHistory() {
|
||||
fun removeOldestEntryFromHistory(): EntryKDBX? {
|
||||
var min: Date? = null
|
||||
var index = -1
|
||||
|
||||
@@ -326,15 +350,15 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
}
|
||||
}
|
||||
|
||||
if (index != -1) {
|
||||
return if (index != -1) {
|
||||
history.removeAt(index)
|
||||
}
|
||||
} else null
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -116,13 +116,6 @@ class InvalidCredentialsDatabaseException : LoadDatabaseException {
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class KeyFileEmptyDatabaseException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.keyfile_is_empty
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class NoMemoryDatabaseException: LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_out_of_memory
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.kunzisoft.keepass.database.exception.VersionDatabaseException
|
||||
import com.kunzisoft.keepass.stream.*
|
||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
@@ -176,10 +177,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
|
||||
@@ -192,38 +193,37 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldID == PwDbHeaderV4Fields.EndOfHeader)
|
||||
return true
|
||||
|
||||
if (fieldData != null)
|
||||
when (fieldID) {
|
||||
PwDbHeaderV4Fields.EndOfHeader -> return true
|
||||
|
||||
PwDbHeaderV4Fields.CipherID -> setCipher(fieldData)
|
||||
|
||||
PwDbHeaderV4Fields.CompressionFlags -> setCompressionFlags(fieldData)
|
||||
|
||||
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)
|
||||
|
||||
PwDbHeaderV4Fields.PublicCustomData -> {
|
||||
databaseV4.publicCustomData = KdfParameters.deserialize(fieldData)!! // TODO verify
|
||||
throw IOException("Invalid header type: $fieldID")
|
||||
}
|
||||
PwDbHeaderV4Fields.PublicCustomData -> databaseV4.publicCustomData = VariantDictionary.deserialize(fieldData)
|
||||
|
||||
else -> throw IOException("Invalid header type: $fieldID")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -27,14 +27,12 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.exception.*
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||
import com.kunzisoft.keepass.stream.*
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import org.joda.time.Instant
|
||||
import java.io.*
|
||||
import java.security.*
|
||||
import java.util.*
|
||||
@@ -90,16 +88,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 +158,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 +173,7 @@ class DatabaseInputKDB(cacheDirectory: File,
|
||||
when (fieldSize) {
|
||||
4 -> {
|
||||
newGroup = mDatabaseToOpen.createGroup().apply {
|
||||
setGroupId(cipherInputStream.readBytes4ToUInt().toInt())
|
||||
setGroupId(cipherInputStream.readBytes4ToUInt().toKotlinInt())
|
||||
}
|
||||
}
|
||||
16 -> {
|
||||
@@ -194,7 +192,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 +201,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 +235,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 +251,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()
|
||||
@@ -282,11 +280,9 @@ class DatabaseInputKDB(cacheDirectory: File,
|
||||
0x000E -> {
|
||||
newEntry?.let { entry ->
|
||||
if (fieldSize > 0) {
|
||||
// Generate an unique new file with timestamp
|
||||
val binaryFile = File(cacheDirectory,
|
||||
Instant.now().millis.toString())
|
||||
entry.binaryData = BinaryAttachment(binaryFile)
|
||||
BufferedOutputStream(FileOutputStream(binaryFile)).use { outputStream ->
|
||||
val binaryAttachment = mDatabaseToOpen.buildNewBinary(cacheDirectory)
|
||||
entry.binaryData = binaryAttachment
|
||||
BufferedOutputStream(binaryAttachment.getOutputDataStream()).use { outputStream ->
|
||||
cipherInputStream.readBytes(fieldSize,
|
||||
DatabaseKDB.BUFFER_SIZE_BYTES) { buffer ->
|
||||
outputStream.write(buffer)
|
||||
|
||||
@@ -20,12 +20,14 @@
|
||||
package com.kunzisoft.keepass.database.file.input
|
||||
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||
import com.kunzisoft.keepass.crypto.StreamCipherFactory
|
||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
||||
@@ -35,7 +37,7 @@ import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.database.exception.*
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||
@@ -49,12 +51,14 @@ import org.bouncycastle.crypto.StreamCipher
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
import java.io.*
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.nio.charset.Charset
|
||||
import java.text.ParseException
|
||||
import java.util.*
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.CipherInputStream
|
||||
import kotlin.math.min
|
||||
@@ -68,9 +72,6 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
|
||||
private var hashOfHeader: ByteArray? = null
|
||||
|
||||
private val unusedCacheFileName: String
|
||||
get() = mDatabase.binaryPool.findUnusedKey().toString()
|
||||
|
||||
private var readNextNode = true
|
||||
private val ctxGroups = Stack<GroupKDBX>()
|
||||
private var ctxGroup: GroupKDBX? = null
|
||||
@@ -132,7 +133,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 +181,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,13 +229,15 @@ 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)
|
||||
if (size > 0) {
|
||||
if (fieldId != DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
|
||||
if (fieldId != DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) {
|
||||
// TODO OOM here
|
||||
data = dataInputStream.readBytes(size)
|
||||
}
|
||||
}
|
||||
|
||||
var result = true
|
||||
@@ -249,18 +252,16 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
header.innerRandomStreamKey = data
|
||||
}
|
||||
DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary -> {
|
||||
val flag = dataInputStream.readBytes(1)[0].toInt() != 0
|
||||
val protectedFlag = flag && DatabaseHeaderKDBX.KdbxBinaryFlags.Protected.toInt() != DatabaseHeaderKDBX.KdbxBinaryFlags.None.toInt()
|
||||
val byteLength = size - 1
|
||||
// Read in a file
|
||||
val file = File(cacheDirectory, unusedCacheFileName)
|
||||
FileOutputStream(file).use { outputStream ->
|
||||
val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
||||
val byteLength = size - 1
|
||||
// No compression at this level
|
||||
val protectedBinary = mDatabase.buildNewBinary(cacheDirectory, false, protectedFlag)
|
||||
protectedBinary.getOutputDataStream().use { outputStream ->
|
||||
dataInputStream.readBytes(byteLength, DatabaseKDBX.BUFFER_SIZE_BYTES) { buffer ->
|
||||
outputStream.write(buffer)
|
||||
}
|
||||
}
|
||||
val protectedBinary = BinaryAttachment(file, protectedFlag)
|
||||
mDatabase.binaryPool.add(protectedBinary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,14 +444,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
}
|
||||
|
||||
KdbContext.Binaries -> if (name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
|
||||
val key = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrId)
|
||||
if (key != null) {
|
||||
val pbData = readBinary(xpp)
|
||||
val id = Integer.parseInt(key)
|
||||
mDatabase.binaryPool.put(id, pbData!!)
|
||||
} else {
|
||||
readUnknown(xpp)
|
||||
}
|
||||
readBinary(xpp)
|
||||
} else {
|
||||
readUnknown(xpp)
|
||||
}
|
||||
@@ -492,7 +486,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 +540,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)) {
|
||||
@@ -749,8 +743,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
if (entryInHistory) {
|
||||
ctxEntry = ctxHistoryBase
|
||||
return KdbContext.EntryHistory
|
||||
}
|
||||
else if (ctxEntry != null) {
|
||||
} else if (ctxEntry != null) {
|
||||
// Add entry to the index only when close the XML element
|
||||
mDatabase.addEntryIndex(ctxEntry!!)
|
||||
}
|
||||
@@ -766,8 +759,9 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
|
||||
return KdbContext.Entry
|
||||
} else if (ctx == KdbContext.EntryBinary && name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
|
||||
if (ctxBinaryName != null && ctxBinaryValue != null)
|
||||
ctxEntry?.putProtectedBinary(ctxBinaryName!!, ctxBinaryValue!!)
|
||||
if (ctxBinaryName != null && ctxBinaryValue != null) {
|
||||
ctxEntry?.putAttachment(Attachment(ctxBinaryName!!, ctxBinaryValue!!), mDatabase.binaryPool)
|
||||
}
|
||||
ctxBinaryName = null
|
||||
ctxBinaryValue = null
|
||||
|
||||
@@ -819,7 +813,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)
|
||||
@@ -885,9 +879,14 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
if (encoded.isEmpty()) {
|
||||
return DatabaseVersioned.UUID_ZERO
|
||||
}
|
||||
val buf = Base64.decode(encoded, BASE_64_FLAG)
|
||||
|
||||
return bytes16ToUuid(buf)
|
||||
return try {
|
||||
val buf = Base64.decode(encoded, BASE_64_FLAG)
|
||||
bytes16ToUuid(buf)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to read base 64 UUID, create a random one", e)
|
||||
UUID.randomUUID()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
@@ -947,50 +946,69 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
|
||||
// Reference Id to a binary already present in binary pool
|
||||
val ref = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrRef)
|
||||
if (ref != null) {
|
||||
xpp.next() // Consume end tag
|
||||
// New id to a binary
|
||||
val key = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrId)
|
||||
|
||||
val id = Integer.parseInt(ref)
|
||||
return mDatabase.binaryPool[id]
|
||||
}
|
||||
|
||||
// New binary to retrieve
|
||||
else {
|
||||
var compressed = false
|
||||
var protected = false
|
||||
|
||||
if (xpp.attributeCount > 0) {
|
||||
val compress = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrCompressed)
|
||||
if (compress != null) {
|
||||
compressed = compress.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
|
||||
}
|
||||
|
||||
val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
|
||||
if (protect != null) {
|
||||
protected = protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
|
||||
return when {
|
||||
ref != null -> {
|
||||
xpp.next() // Consume end tag
|
||||
val id = Integer.parseInt(ref)
|
||||
// A ref is not necessarily an index in Database V3.1
|
||||
var binaryRetrieve = mDatabase.binaryPool[id]
|
||||
// Create empty binary if not retrieved in pool
|
||||
if (binaryRetrieve == null) {
|
||||
binaryRetrieve = mDatabase.buildNewBinary(cacheDirectory,
|
||||
compression = false, protection = true, binaryPoolId = id)
|
||||
}
|
||||
return binaryRetrieve
|
||||
}
|
||||
|
||||
val base64 = readString(xpp)
|
||||
if (base64.isEmpty())
|
||||
return BinaryAttachment()
|
||||
val data = Base64.decode(base64, BASE_64_FLAG)
|
||||
|
||||
val file = File(cacheDirectory, unusedCacheFileName)
|
||||
return FileOutputStream(file).use { outputStream ->
|
||||
// Force compression in this specific case
|
||||
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip
|
||||
&& !compressed) {
|
||||
GZIPOutputStream(outputStream).write(data)
|
||||
BinaryAttachment(file, protected, true)
|
||||
} else {
|
||||
outputStream.write(data)
|
||||
BinaryAttachment(file, protected, compressed)
|
||||
}
|
||||
key != null -> {
|
||||
createBinary(key.toIntOrNull(), xpp)
|
||||
}
|
||||
else -> {
|
||||
// New binary to retrieve
|
||||
createBinary(null, xpp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun createBinary(binaryId: Int?, xpp: XmlPullParser): BinaryAttachment? {
|
||||
var compressed = false
|
||||
var protected = true
|
||||
|
||||
if (xpp.attributeCount > 0) {
|
||||
val compress = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrCompressed)
|
||||
if (compress != null) {
|
||||
compressed = compress.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
|
||||
}
|
||||
|
||||
val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
|
||||
if (protect != null) {
|
||||
protected = protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
val base64 = readString(xpp)
|
||||
if (base64.isEmpty())
|
||||
return null
|
||||
|
||||
// Build the new binary and compress
|
||||
val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory, compressed, protected, binaryId)
|
||||
try {
|
||||
binaryAttachment.getOutputDataStream().use { outputStream ->
|
||||
outputStream.write(Base64.decode(base64, BASE_64_FLAG))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to read base 64 attachment", e)
|
||||
binaryAttachment.isCorrupted = true
|
||||
binaryAttachment.getOutputDataStream().use { outputStream ->
|
||||
outputStream.write(base64.toByteArray())
|
||||
}
|
||||
}
|
||||
return binaryAttachment
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun readString(xpp: XmlPullParser): String {
|
||||
val buf = readProtectedBase64String(xpp)
|
||||
@@ -1045,6 +1063,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = DatabaseInputKDBX::class.java.name
|
||||
|
||||
private val DEFAULT_HISTORY_DAYS = UnsignedInt(365)
|
||||
|
||||
@Throws(XmlPullParserException::class)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BU
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
|
||||
import com.kunzisoft.keepass.stream.readBytes
|
||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import kotlin.experimental.or
|
||||
@@ -36,33 +37,40 @@ class DatabaseInnerHeaderOutputKDBX(private val database: DatabaseKDBX,
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun output() {
|
||||
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID.toInt())
|
||||
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID)
|
||||
dataOutputStream.writeInt(4)
|
||||
if (header.innerRandomStream == null)
|
||||
throw IOException("Can't write innerRandomStream")
|
||||
dataOutputStream.writeInt(header.innerRandomStream!!.id.toInt())
|
||||
dataOutputStream.writeUInt(header.innerRandomStream!!.id)
|
||||
|
||||
val streamKeySize = header.innerRandomStreamKey.size
|
||||
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey.toInt())
|
||||
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey)
|
||||
dataOutputStream.writeInt(streamKeySize)
|
||||
dataOutputStream.write(header.innerRandomStreamKey)
|
||||
|
||||
database.binaryPool.doForEachBinary { _, protectedBinary ->
|
||||
database.binaryPool.doForEachOrderedBinary { _, keyBinary ->
|
||||
val protectedBinary = keyBinary.binary
|
||||
// Force decompression to add binary in header
|
||||
protectedBinary.decompress()
|
||||
// Write type binary
|
||||
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
|
||||
// Write size
|
||||
dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length() + 1))
|
||||
// Write protected flag
|
||||
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
|
||||
if (protectedBinary.isProtected) {
|
||||
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
||||
}
|
||||
dataOutputStream.writeByte(flag)
|
||||
|
||||
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary.toInt())
|
||||
dataOutputStream.writeInt(protectedBinary.length().toInt() + 1) // TODO verify
|
||||
dataOutputStream.write(flag.toInt())
|
||||
|
||||
protectedBinary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||
dataOutputStream.write(buffer)
|
||||
protectedBinary.getInputDataStream().use { inputStream ->
|
||||
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||
dataOutputStream.write(buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader.toInt())
|
||||
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader)
|
||||
dataOutputStream.writeInt(0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||
@@ -55,7 +55,6 @@ import java.io.OutputStream
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.CipherOutputStream
|
||||
@@ -85,7 +84,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 +104,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 +208,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 +229,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 +273,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 +287,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)
|
||||
}
|
||||
|
||||
@@ -360,6 +359,9 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
|
||||
writeFields(entry.fields)
|
||||
writeEntryBinaries(entry.binaries)
|
||||
if (entry.containsCustomData()) {
|
||||
writeCustomData(entry.customData)
|
||||
}
|
||||
writeAutoType(entry.autoType)
|
||||
|
||||
if (!isHistory) {
|
||||
@@ -385,7 +387,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)
|
||||
@@ -422,7 +424,6 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
private fun writeBinary(binary : BinaryAttachment) {
|
||||
val binaryLength = binary.length()
|
||||
if (binaryLength > 0) {
|
||||
|
||||
if (binary.isProtected) {
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
|
||||
|
||||
@@ -433,21 +434,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
xml.text(charArray, 0, charArray.size)
|
||||
}
|
||||
} else {
|
||||
// Force binary compression from database (compression was harmonized during import)
|
||||
if (mDatabaseKDBX.compressionAlgorithm === CompressionAlgorithm.GZip) {
|
||||
if (binary.isCompressed) {
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
|
||||
}
|
||||
|
||||
// Force decompression in this specific case
|
||||
val binaryInputStream = if (mDatabaseKDBX.compressionAlgorithm == CompressionAlgorithm.None
|
||||
&& binary.isCompressed == true) {
|
||||
GZIPInputStream(binary.getInputDataStream())
|
||||
} else {
|
||||
binary.getInputDataStream()
|
||||
}
|
||||
|
||||
// Write the XML
|
||||
binaryInputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||
binary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||
val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray()
|
||||
xml.text(charArray, 0, charArray.size)
|
||||
}
|
||||
@@ -459,10 +450,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
private fun writeMetaBinaries() {
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
|
||||
|
||||
mDatabaseKDBX.binaryPool.doForEachBinary { key, binary ->
|
||||
// Use indexes because necessarily in DatabaseV4 (binary header ref is the order)
|
||||
mDatabaseKDBX.binaryPool.doForEachOrderedBinary { index, keyBinary ->
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrId, key.toString())
|
||||
writeBinary(binary)
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
|
||||
writeBinary(keyBinary.binary)
|
||||
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
||||
}
|
||||
|
||||
@@ -489,7 +481,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)
|
||||
@@ -559,23 +551,22 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
private fun writeEntryBinaries(binaries: Map<String, BinaryAttachment>) {
|
||||
for ((key, binary) in binaries) {
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemKey)
|
||||
xml.text(safeXmlString(key))
|
||||
xml.endTag(null, DatabaseKDBXXML.ElemKey)
|
||||
private fun writeEntryBinaries(binaries: LinkedHashMap<String, Int>) {
|
||||
for ((label, poolId) in binaries) {
|
||||
// Retrieve the right index with the poolId, don't use ref because of header in DatabaseV4
|
||||
mDatabaseKDBX.binaryPool.getBinaryIndexFromKey(poolId)?.toString()?.let { indexString ->
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemKey)
|
||||
xml.text(safeXmlString(label))
|
||||
xml.endTag(null, DatabaseKDBXXML.ElemKey)
|
||||
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemValue)
|
||||
val ref = mDatabaseKDBX.binaryPool.findKey(binary)
|
||||
if (ref != null) {
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrRef, ref.toString())
|
||||
} else {
|
||||
writeBinary(binary)
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemValue)
|
||||
// Use only pool data in Meta to save binaries
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrRef, indexString)
|
||||
xml.endTag(null, DatabaseKDBXXML.ElemValue)
|
||||
|
||||
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
||||
}
|
||||
xml.endTag(null, DatabaseKDBXXML.ElemValue)
|
||||
|
||||
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -629,7 +620,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)
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,9 +95,9 @@ open class Education(val activity: Activity) {
|
||||
R.string.education_entry_edit_key,
|
||||
R.string.education_password_generator_key,
|
||||
R.string.education_entry_new_field_key,
|
||||
R.string.education_add_attachment_key,
|
||||
R.string.education_setup_OTP_key)
|
||||
|
||||
|
||||
/**
|
||||
* Get preferences bundle for education
|
||||
*/
|
||||
@@ -272,6 +272,18 @@ open class Education(val activity: Activity) {
|
||||
context.resources.getBoolean(R.bool.education_entry_new_field_default))
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the explanatory view of the new attachment button in an entry has already been displayed.
|
||||
*
|
||||
* @param context The context to open the SharedPreferences
|
||||
* @return boolean value of education_add_attachment_key key
|
||||
*/
|
||||
fun isEducationAddAttachmentPerformed(context: Context): Boolean {
|
||||
val prefs = getEducationSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.education_add_attachment_key),
|
||||
context.resources.getBoolean(R.bool.education_add_attachment_default))
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the explanatory view to setup OTP has already been displayed.
|
||||
*
|
||||
|
||||
@@ -29,6 +29,10 @@ import com.kunzisoft.keepass.R
|
||||
class EntryEditActivityEducation(activity: Activity)
|
||||
: Education(activity) {
|
||||
|
||||
/**
|
||||
* Check and display learning views
|
||||
* Displays the explanation for the password generator
|
||||
*/
|
||||
fun checkAndPerformedGeneratePasswordEducation(educationView: View,
|
||||
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
||||
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
|
||||
@@ -56,7 +60,7 @@ class EntryEditActivityEducation(activity: Activity)
|
||||
|
||||
/**
|
||||
* Check and display learning views
|
||||
* Displays the explanation for the icon selection, the password generator and for a new field
|
||||
* Displays the explanation to create a new field
|
||||
*/
|
||||
fun checkAndPerformedEntryNewFieldEducation(educationView: View,
|
||||
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
||||
@@ -83,6 +87,35 @@ class EntryEditActivityEducation(activity: Activity)
|
||||
R.string.education_entry_new_field_key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and display learning views
|
||||
* Displays the explanation for to upload attachment
|
||||
*/
|
||||
fun checkAndPerformedAttachmentEducation(educationView: View,
|
||||
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
||||
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
|
||||
return checkAndPerformedEducation(!isEducationAddAttachmentPerformed(activity),
|
||||
TapTarget.forView(educationView,
|
||||
activity.getString(R.string.education_add_attachment_title),
|
||||
activity.getString(R.string.education_add_attachment_summary))
|
||||
.textColorInt(Color.WHITE)
|
||||
.tintTarget(true)
|
||||
.cancelable(true),
|
||||
object : TapTargetView.Listener() {
|
||||
override fun onTargetClick(view: TapTargetView) {
|
||||
super.onTargetClick(view)
|
||||
onEducationViewClick?.invoke(view)
|
||||
}
|
||||
|
||||
override fun onOuterCircleClick(view: TapTargetView?) {
|
||||
super.onOuterCircleClick(view)
|
||||
view?.dismiss(false)
|
||||
onOuterViewClick?.invoke(view)
|
||||
}
|
||||
},
|
||||
R.string.education_add_attachment_key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and display learning views
|
||||
* Displays the explanation to setup OTP
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -21,24 +21,25 @@ package com.kunzisoft.keepass.model
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||
import com.kunzisoft.keepass.utils.readEnum
|
||||
import com.kunzisoft.keepass.utils.writeEnum
|
||||
|
||||
data class EntryAttachment(var name: String,
|
||||
var binaryAttachment: BinaryAttachment,
|
||||
var downloadState: AttachmentState = AttachmentState.NULL,
|
||||
var downloadProgression: Int = 0) : Parcelable {
|
||||
data class EntryAttachmentState(var attachment: Attachment,
|
||||
var streamDirection: StreamDirection,
|
||||
var downloadState: AttachmentState = AttachmentState.NULL,
|
||||
var downloadProgression: Int = 0) : Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readString() ?: "",
|
||||
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment(),
|
||||
parcel.readParcelable(Attachment::class.java.classLoader) ?: Attachment("", BinaryAttachment()),
|
||||
parcel.readEnum<StreamDirection>() ?: StreamDirection.DOWNLOAD,
|
||||
parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL,
|
||||
parcel.readInt())
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(name)
|
||||
parcel.writeParcelable(binaryAttachment, flags)
|
||||
parcel.writeParcelable(attachment, flags)
|
||||
parcel.writeEnum(streamDirection)
|
||||
parcel.writeEnum(downloadState)
|
||||
parcel.writeInt(downloadProgression)
|
||||
}
|
||||
@@ -47,12 +48,25 @@ data class EntryAttachment(var name: String,
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<EntryAttachment> {
|
||||
override fun createFromParcel(parcel: Parcel): EntryAttachment {
|
||||
return EntryAttachment(parcel)
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is EntryAttachmentState) return false
|
||||
|
||||
if (attachment != other.attachment) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return attachment.hashCode()
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<EntryAttachmentState> {
|
||||
override fun createFromParcel(parcel: Parcel): EntryAttachmentState {
|
||||
return EntryAttachmentState(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<EntryAttachment?> {
|
||||
override fun newArray(size: Int): Array<EntryAttachmentState?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,10 @@ package com.kunzisoft.keepass.model
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||
import java.util.*
|
||||
@@ -30,12 +33,15 @@ class EntryInfo : Parcelable {
|
||||
|
||||
var id: String = ""
|
||||
var title: String = ""
|
||||
var icon: IconImage? = null
|
||||
var icon: IconImage = IconImageStandard()
|
||||
var username: String = ""
|
||||
var password: String = ""
|
||||
var expires: Boolean = false
|
||||
var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH
|
||||
var url: String = ""
|
||||
var notes: String = ""
|
||||
var customFields: MutableList<Field> = ArrayList()
|
||||
var customFields: List<Field> = ArrayList()
|
||||
var attachments: List<Attachment> = ArrayList()
|
||||
var otpModel: OtpModel? = null
|
||||
|
||||
constructor()
|
||||
@@ -43,12 +49,15 @@ class EntryInfo : Parcelable {
|
||||
private constructor(parcel: Parcel) {
|
||||
id = parcel.readString() ?: id
|
||||
title = parcel.readString() ?: title
|
||||
icon = parcel.readParcelable(IconImage::class.java.classLoader)
|
||||
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
|
||||
username = parcel.readString() ?: username
|
||||
password = parcel.readString() ?: password
|
||||
expires = parcel.readInt() != 0
|
||||
expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime
|
||||
url = parcel.readString() ?: url
|
||||
notes = parcel.readString() ?: notes
|
||||
parcel.readList(customFields as List<Field>, Field::class.java.classLoader)
|
||||
parcel.readList(customFields, Field::class.java.classLoader)
|
||||
parcel.readList(attachments, Attachment::class.java.classLoader)
|
||||
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
|
||||
}
|
||||
|
||||
@@ -62,9 +71,12 @@ class EntryInfo : Parcelable {
|
||||
parcel.writeParcelable(icon, flags)
|
||||
parcel.writeString(username)
|
||||
parcel.writeString(password)
|
||||
parcel.writeInt(if (expires) 1 else 0)
|
||||
parcel.writeParcelable(expiryTime, flags)
|
||||
parcel.writeString(url)
|
||||
parcel.writeString(notes)
|
||||
parcel.writeArray(customFields.toTypedArray())
|
||||
parcel.writeArray(attachments.toTypedArray())
|
||||
parcel.writeParcelable(otpModel, flags)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,11 @@ class Field : Parcelable {
|
||||
this.protectedValue = value
|
||||
}
|
||||
|
||||
constructor(fieldToCopy: Field) {
|
||||
this.name = fieldToCopy.name
|
||||
this.protectedValue = fieldToCopy.protectedValue
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) {
|
||||
this.name = parcel.readString() ?: name
|
||||
this.protectedValue = parcel.readParcelable(ProtectedString::class.java.classLoader) ?: protectedValue
|
||||
@@ -49,9 +54,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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.kunzisoft.keepass.model
|
||||
|
||||
enum class StreamDirection {
|
||||
UPLOAD, DOWNLOAD
|
||||
}
|
||||
@@ -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
|
||||
@@ -27,53 +28,55 @@ import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachment
|
||||
import com.kunzisoft.keepass.tasks.AttachmentFileAsyncTask
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.stream.readBytes
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.BufferedInputStream
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
|
||||
class AttachmentFileNotificationService: LockNotificationService() {
|
||||
|
||||
override val notificationId: Int = 10000
|
||||
private val attachmentNotificationList = CopyOnWriteArrayList<AttachmentNotification>()
|
||||
|
||||
private var mActionTaskBinder = ActionTaskBinder()
|
||||
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
|
||||
|
||||
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
inner class ActionTaskBinder: Binder() {
|
||||
|
||||
fun getService(): AttachmentFileNotificationService = this@AttachmentFileNotificationService
|
||||
|
||||
fun addActionTaskListener(actionTaskListener: ActionTaskListener) {
|
||||
mActionTaskListeners.add(actionTaskListener)
|
||||
|
||||
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
|
||||
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
|
||||
entry.value.attachmentTask?.onUpdate = { uri, attachment, notificationIdAttach ->
|
||||
newNotification(uri, attachment, notificationIdAttach)
|
||||
mActionTaskListeners.forEach { actionListener ->
|
||||
actionListener.onAttachmentProgress(entry.key, attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
|
||||
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
|
||||
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
|
||||
entry.value.attachmentTask?.onUpdate = null
|
||||
}
|
||||
})
|
||||
|
||||
mActionTaskListeners.remove(actionTaskListener)
|
||||
}
|
||||
}
|
||||
|
||||
private val attachmentFileActionListener = object: AttachmentFileAction.AttachmentFileActionListener {
|
||||
override fun onUpdate(attachmentNotification: AttachmentNotification) {
|
||||
newNotification(attachmentNotification)
|
||||
mActionTaskListeners.forEach { actionListener ->
|
||||
actionListener.onAttachmentAction(attachmentNotification.uri,
|
||||
attachmentNotification.entryAttachmentState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ActionTaskListener {
|
||||
fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment)
|
||||
fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
@@ -83,46 +86,36 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
|
||||
val downloadFileUri: Uri? = if (intent?.hasExtra(DOWNLOAD_FILE_URI_KEY) == true) {
|
||||
intent.getParcelableExtra(DOWNLOAD_FILE_URI_KEY)
|
||||
val downloadFileUri: Uri? = if (intent?.hasExtra(FILE_URI_KEY) == true) {
|
||||
intent.getParcelableExtra(FILE_URI_KEY)
|
||||
} else null
|
||||
|
||||
when(intent?.action) {
|
||||
ACTION_ATTACHMENT_FILE_START_UPLOAD -> {
|
||||
actionUploadOrDownload(downloadFileUri,
|
||||
intent,
|
||||
StreamDirection.UPLOAD)
|
||||
}
|
||||
ACTION_ATTACHMENT_FILE_START_DOWNLOAD -> {
|
||||
if (downloadFileUri != null
|
||||
&& intent.hasExtra(ATTACHMENT_KEY)) {
|
||||
|
||||
val nextNotificationId = (downloadFileUris.values.maxBy { 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)
|
||||
}
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to download $downloadFileUri", e)
|
||||
actionUploadOrDownload(downloadFileUri,
|
||||
intent,
|
||||
StreamDirection.DOWNLOAD)
|
||||
}
|
||||
ACTION_ATTACHMENT_REMOVE -> {
|
||||
intent.getParcelableExtra<Attachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
|
||||
attachmentNotificationList.firstOrNull { it.entryAttachmentState.attachment == entryAttachment }?.let { elementToRemove ->
|
||||
attachmentNotificationList.remove(elementToRemove)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (downloadFileUri != null) {
|
||||
downloadFileUris[downloadFileUri]?.notificationId?.let {
|
||||
notificationManager?.cancel(it)
|
||||
downloadFileUris.remove(downloadFileUri)
|
||||
attachmentNotificationList.firstOrNull { it.uri == downloadFileUri }?.let { elementToRemove ->
|
||||
notificationManager?.cancel(elementToRemove.notificationId)
|
||||
attachmentNotificationList.remove(elementToRemove)
|
||||
}
|
||||
}
|
||||
if (downloadFileUris.isEmpty()) {
|
||||
if (attachmentNotificationList.isEmpty()) {
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
@@ -131,25 +124,35 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun checkCurrentAttachmentProgress() {
|
||||
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
|
||||
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
|
||||
mActionTaskListeners.forEach { actionListener ->
|
||||
actionListener.onAttachmentProgress(entry.key, entry.value.entryAttachment)
|
||||
}
|
||||
attachmentNotificationList.forEach { attachmentNotification ->
|
||||
mActionTaskListeners.forEach { actionListener ->
|
||||
actionListener.onAttachmentAction(
|
||||
attachmentNotification.uri,
|
||||
attachmentNotification.entryAttachmentState
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun newNotification(downloadFileUri: Uri,
|
||||
entryAttachment: EntryAttachment,
|
||||
notificationIdAttachment: Int) {
|
||||
@Synchronized
|
||||
fun removeAttachmentAction(entryAttachment: EntryAttachmentState) {
|
||||
attachmentNotificationList.firstOrNull {
|
||||
it.entryAttachmentState == entryAttachment
|
||||
}?.let {
|
||||
attachmentNotificationList.remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun newNotification(attachmentNotification: AttachmentNotification) {
|
||||
|
||||
val pendingContentIntent = PendingIntent.getActivity(this,
|
||||
0,
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
setDataAndType(downloadFileUri, contentResolver.getType(downloadFileUri))
|
||||
setDataAndType(attachmentNotification.uri,
|
||||
contentResolver.getType(attachmentNotification.uri))
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
|
||||
@@ -157,54 +160,84 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
||||
0,
|
||||
Intent(this, AttachmentFileNotificationService::class.java).apply {
|
||||
// No action to delete the service
|
||||
putExtra(DOWNLOAD_FILE_URI_KEY, downloadFileUri)
|
||||
putExtra(FILE_URI_KEY, attachmentNotification.uri)
|
||||
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
|
||||
val fileName = DocumentFile.fromSingleUri(this, downloadFileUri)?.name ?: ""
|
||||
val fileName = DocumentFile.fromSingleUri(this, attachmentNotification.uri)?.name ?: ""
|
||||
|
||||
val builder = buildNewNotification().apply {
|
||||
setSmallIcon(R.drawable.ic_file_download_white_24dp)
|
||||
setContentTitle(getString(R.string.download_attachment, fileName))
|
||||
when (attachmentNotification.entryAttachmentState.streamDirection) {
|
||||
StreamDirection.UPLOAD -> {
|
||||
setSmallIcon(R.drawable.ic_file_upload_white_24dp)
|
||||
setContentTitle(getString(R.string.upload_attachment, fileName))
|
||||
}
|
||||
StreamDirection.DOWNLOAD -> {
|
||||
setSmallIcon(R.drawable.ic_file_download_white_24dp)
|
||||
setContentTitle(getString(R.string.download_attachment, fileName))
|
||||
}
|
||||
}
|
||||
setAutoCancel(false)
|
||||
when (entryAttachment.downloadState) {
|
||||
when (attachmentNotification.entryAttachmentState.downloadState) {
|
||||
AttachmentState.NULL, AttachmentState.START -> {
|
||||
setContentText(getString(R.string.download_initialization))
|
||||
setOngoing(true)
|
||||
}
|
||||
AttachmentState.IN_PROGRESS -> {
|
||||
if (entryAttachment.downloadProgression > 100) {
|
||||
if (attachmentNotification.entryAttachmentState.downloadProgression > 100) {
|
||||
setContentText(getString(R.string.download_finalization))
|
||||
} else {
|
||||
setProgress(100, entryAttachment.downloadProgression, false)
|
||||
setContentText(getString(R.string.download_progression, entryAttachment.downloadProgression))
|
||||
setProgress(100,
|
||||
attachmentNotification.entryAttachmentState.downloadProgression,
|
||||
false)
|
||||
setContentText(getString(R.string.download_progression,
|
||||
attachmentNotification.entryAttachmentState.downloadProgression))
|
||||
}
|
||||
setOngoing(true)
|
||||
}
|
||||
AttachmentState.COMPLETE, AttachmentState.ERROR -> {
|
||||
AttachmentState.COMPLETE -> {
|
||||
setContentText(getString(R.string.download_complete))
|
||||
setContentIntent(pendingContentIntent)
|
||||
when (attachmentNotification.entryAttachmentState.streamDirection) {
|
||||
StreamDirection.UPLOAD -> {
|
||||
|
||||
}
|
||||
StreamDirection.DOWNLOAD -> {
|
||||
setContentIntent(pendingContentIntent)
|
||||
}
|
||||
}
|
||||
setDeleteIntent(pendingDeleteIntent)
|
||||
setOngoing(false)
|
||||
}
|
||||
AttachmentState.ERROR -> {
|
||||
setContentText(getString(R.string.error_file_not_create))
|
||||
setOngoing(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
when (attachmentNotification.entryAttachmentState.downloadState) {
|
||||
AttachmentState.ERROR,
|
||||
AttachmentState.COMPLETE -> {
|
||||
stopForeground(false)
|
||||
notificationManager?.notify(attachmentNotification.notificationId, builder.build())
|
||||
} else -> {
|
||||
startForeground(attachmentNotification.notificationId, builder.build())
|
||||
}
|
||||
}
|
||||
notificationManager?.notify(notificationIdAttachment, builder.build())
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
|
||||
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
|
||||
entry.value.attachmentTask?.onUpdate = null
|
||||
notificationManager?.cancel(entry.value.notificationId)
|
||||
}
|
||||
})
|
||||
attachmentNotificationList.forEach { attachmentNotification ->
|
||||
attachmentNotification.attachmentFileAction?.listener = null
|
||||
notificationManager?.cancel(attachmentNotification.notificationId)
|
||||
}
|
||||
attachmentNotificationList.clear()
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
data class AttachmentNotification(var notificationId: Int,
|
||||
var entryAttachment: EntryAttachment,
|
||||
var attachmentTask: AttachmentFileAsyncTask? = null) {
|
||||
private data class AttachmentNotification(var uri: Uri,
|
||||
var notificationId: Int,
|
||||
var entryAttachmentState: EntryAttachmentState,
|
||||
var attachmentFileAction: AttachmentFileAction? = null) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
@@ -221,15 +254,188 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun actionUploadOrDownload(downloadFileUri: Uri?,
|
||||
intent: Intent,
|
||||
streamDirection: StreamDirection) {
|
||||
if (downloadFileUri != null
|
||||
&& intent.hasExtra(ATTACHMENT_KEY)) {
|
||||
try {
|
||||
intent.getParcelableExtra<Attachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
|
||||
|
||||
val nextNotificationId = (attachmentNotificationList.maxByOrNull { it.notificationId }
|
||||
?.notificationId ?: notificationId) + 1
|
||||
val entryAttachmentState = EntryAttachmentState(entryAttachment, streamDirection)
|
||||
val attachmentNotification = AttachmentNotification(downloadFileUri, nextNotificationId, entryAttachmentState)
|
||||
|
||||
// Add action to the list on start
|
||||
attachmentNotificationList.add(attachmentNotification)
|
||||
|
||||
mainScope.launch {
|
||||
AttachmentFileAction(attachmentNotification,
|
||||
contentResolver).apply {
|
||||
listener = attachmentFileActionListener
|
||||
}.executeAction()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to upload/download $downloadFileUri", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AttachmentFileAction(
|
||||
private val attachmentNotification: AttachmentNotification,
|
||||
private val contentResolver: ContentResolver) {
|
||||
|
||||
private val updateMinFrequency = 1000
|
||||
private var previousSaveTime = System.currentTimeMillis()
|
||||
var listener: AttachmentFileActionListener? = null
|
||||
|
||||
interface AttachmentFileActionListener {
|
||||
fun onUpdate(attachmentNotification: AttachmentNotification)
|
||||
}
|
||||
|
||||
suspend fun executeAction() {
|
||||
|
||||
// on pre execute
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
TimeoutHelper.temporarilyDisableTimeout()
|
||||
|
||||
attachmentNotification.attachmentFileAction = this@AttachmentFileAction
|
||||
attachmentNotification.entryAttachmentState.apply {
|
||||
downloadState = AttachmentState.START
|
||||
downloadProgression = 0
|
||||
}
|
||||
listener?.onUpdate(attachmentNotification)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
// on Progress with thread
|
||||
val asyncResult: Deferred<Boolean> = async {
|
||||
var progressResult = true
|
||||
try {
|
||||
attachmentNotification.entryAttachmentState.apply {
|
||||
downloadState = AttachmentState.IN_PROGRESS
|
||||
|
||||
when (streamDirection) {
|
||||
StreamDirection.UPLOAD -> {
|
||||
uploadToDatabase(
|
||||
attachmentNotification.uri,
|
||||
attachment.binaryAttachment,
|
||||
contentResolver, 1024) { percent ->
|
||||
publishProgress(percent)
|
||||
}
|
||||
}
|
||||
StreamDirection.DOWNLOAD -> {
|
||||
downloadFromDatabase(
|
||||
attachmentNotification.uri,
|
||||
attachment.binaryAttachment,
|
||||
contentResolver, 1024) { percent ->
|
||||
publishProgress(percent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to upload or download file", e)
|
||||
progressResult = false
|
||||
}
|
||||
progressResult
|
||||
}
|
||||
|
||||
// on post execute
|
||||
withContext(Dispatchers.Main) {
|
||||
val result = asyncResult.await()
|
||||
attachmentNotification.attachmentFileAction = null
|
||||
attachmentNotification.entryAttachmentState.apply {
|
||||
downloadState = if (result) AttachmentState.COMPLETE else AttachmentState.ERROR
|
||||
downloadProgression = 100
|
||||
}
|
||||
listener?.onUpdate(attachmentNotification)
|
||||
TimeoutHelper.releaseTemporarilyDisableTimeout()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadFromDatabase(attachmentToUploadUri: Uri,
|
||||
binaryAttachment: BinaryAttachment,
|
||||
contentResolver: ContentResolver,
|
||||
bufferSize: Int = DEFAULT_BUFFER_SIZE,
|
||||
update: ((percent: Int)->Unit)? = null) {
|
||||
var dataDownloaded = 0L
|
||||
val fileSize = binaryAttachment.length()
|
||||
UriUtil.getUriOutputStream(contentResolver, attachmentToUploadUri)?.use { outputStream ->
|
||||
binaryAttachment.getUnGzipInputDataStream().use { inputStream ->
|
||||
inputStream.readBytes(bufferSize) { buffer ->
|
||||
outputStream.write(buffer)
|
||||
dataDownloaded += buffer.size
|
||||
try {
|
||||
val percentDownload = (100 * dataDownloaded / fileSize).toInt()
|
||||
update?.invoke(percentDownload)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun uploadToDatabase(attachmentFromDownloadUri: Uri,
|
||||
binaryAttachment: BinaryAttachment,
|
||||
contentResolver: ContentResolver,
|
||||
bufferSize: Int = DEFAULT_BUFFER_SIZE,
|
||||
update: ((percent: Int)->Unit)? = null) {
|
||||
var dataUploaded = 0L
|
||||
val fileSize = contentResolver.openFileDescriptor(attachmentFromDownloadUri, "r")?.statSize ?: 0
|
||||
UriUtil.getUriInputStream(contentResolver, attachmentFromDownloadUri)?.let { inputStream ->
|
||||
binaryAttachment.getGzipOutputDataStream().use { outputStream ->
|
||||
BufferedInputStream(inputStream).use { attachmentBufferedInputStream ->
|
||||
attachmentBufferedInputStream.readBytes(bufferSize) { buffer ->
|
||||
outputStream.write(buffer)
|
||||
dataUploaded += buffer.size
|
||||
try {
|
||||
val percentDownload = (100 * dataUploaded / fileSize).toInt()
|
||||
update?.invoke(percentDownload)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun publishProgress(percent: Int) {
|
||||
// Publish progress
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (previousSaveTime + updateMinFrequency < currentTime) {
|
||||
attachmentNotification.entryAttachmentState.apply {
|
||||
downloadState = AttachmentState.IN_PROGRESS
|
||||
downloadProgression = percent
|
||||
}
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
listener?.onUpdate(attachmentNotification)
|
||||
Log.d(TAG, "Download file ${attachmentNotification.uri} : $percent%")
|
||||
}
|
||||
previousSaveTime = currentTime
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = AttachmentFileAction::class.java.name
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = AttachmentFileNotificationService::javaClass.name
|
||||
|
||||
const val ACTION_ATTACHMENT_FILE_START_UPLOAD = "ACTION_ATTACHMENT_FILE_START_UPLOAD"
|
||||
const val ACTION_ATTACHMENT_FILE_START_DOWNLOAD = "ACTION_ATTACHMENT_FILE_START_DOWNLOAD"
|
||||
const val ACTION_ATTACHMENT_REMOVE = "ACTION_ATTACHMENT_REMOVE"
|
||||
|
||||
const val DOWNLOAD_FILE_URI_KEY = "DOWNLOAD_FILE_URI_KEY"
|
||||
const val FILE_URI_KEY = "FILE_URI_KEY"
|
||||
const val ATTACHMENT_KEY = "ATTACHMENT_KEY"
|
||||
|
||||
private val downloadFileUris = HashMap<Uri, AttachmentNotification>()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
@@ -130,6 +141,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
||||
ACTION_DATABASE_RESTORE_ENTRY_HISTORY -> buildDatabaseRestoreEntryHistoryActionTask(intent)
|
||||
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> buildDatabaseDeleteEntryHistoryActionTask(intent)
|
||||
ACTION_DATABASE_UPDATE_COMPRESSION_TASK -> buildDatabaseUpdateCompressionActionTask(intent)
|
||||
ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK -> buildDatabaseRemoveUnlinkedDataActionTask(intent)
|
||||
ACTION_DATABASE_UPDATE_NAME_TASK,
|
||||
ACTION_DATABASE_UPDATE_DESCRIPTION_TASK,
|
||||
ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK,
|
||||
@@ -145,52 +157,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 +372,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 +405,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 +420,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 +436,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 +449,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 +464,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 +495,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 +502,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 +520,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 +527,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 +545,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 +552,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 +570,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 +577,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 +596,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 +617,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 +637,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 +652,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 +671,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 +698,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
||||
return null
|
||||
|
||||
return UpdateCompressionBinariesDatabaseRunnable(this,
|
||||
Database.getInstance(),
|
||||
mDatabase,
|
||||
oldElement,
|
||||
newElement,
|
||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||
@@ -536,10 +712,26 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildDatabaseRemoveUnlinkedDataActionTask(intent: Intent): ActionRunnable? {
|
||||
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||
|
||||
return RemoveUnlinkedDataDatabaseRunnable(this,
|
||||
mDatabase,
|
||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||
).apply {
|
||||
mAfterSaveDatabase = { result ->
|
||||
result.data = intent.extras
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
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 +749,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"
|
||||
@@ -616,6 +777,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
||||
const val ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK = "ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK"
|
||||
const val ACTION_DATABASE_UPDATE_COLOR_TASK = "ACTION_DATABASE_UPDATE_COLOR_TASK"
|
||||
const val ACTION_DATABASE_UPDATE_COMPRESSION_TASK = "ACTION_DATABASE_UPDATE_COMPRESSION_TASK"
|
||||
const val ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK = "ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK"
|
||||
const val ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK = "ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK"
|
||||
const val ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK = "ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK"
|
||||
const val ACTION_DATABASE_UPDATE_ENCRYPTION_TASK = "ACTION_DATABASE_UPDATE_ENCRYPTION_TASK"
|
||||
@@ -624,12 +786,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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(), "")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -349,7 +347,7 @@ object OtpEntryFields {
|
||||
* Build new generated fields in a new list from [fieldsToParse] in parameter,
|
||||
* Remove parameters fields use to generate auto fields
|
||||
*/
|
||||
fun generateAutoFields(fieldsToParse: MutableList<Field>): MutableList<Field> {
|
||||
fun generateAutoFields(fieldsToParse: List<Field>): MutableList<Field> {
|
||||
val newCustomFields: MutableList<Field> = ArrayList(fieldsToParse)
|
||||
// Remove parameter fields
|
||||
val otpField = Field(OTP_FIELD)
|
||||
|
||||
@@ -40,6 +40,11 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mCallback = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.preferences, rootKey)
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
AlertDialog.Builder(context)
|
||||
.setMessage(resources.getString(R.string.biometric_delete_all_key_warning))
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.setPositiveButton(resources.getString(android.R.string.yes)
|
||||
.setPositiveButton(resources.getString(android.R.string.ok)
|
||||
) { _, _ ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
BiometricUnlockDatabaseHelper.deleteEntryKeyInKeystoreForBiometric(
|
||||
@@ -260,7 +260,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
}
|
||||
CipherDatabaseAction.getInstance(context.applicationContext).deleteAll()
|
||||
}
|
||||
.setNegativeButton(resources.getString(android.R.string.no))
|
||||
.setNegativeButton(resources.getString(android.R.string.cancel))
|
||||
{ _, _ -> }.show()
|
||||
}
|
||||
false
|
||||
|
||||
@@ -135,7 +135,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
||||
findPreference<Preference>(getString(R.string.database_version_key))
|
||||
?.summary = mDatabase.version
|
||||
|
||||
val dbCompressionPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_compression_key))
|
||||
val dbCompressionPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_data_key))
|
||||
|
||||
// Database compression
|
||||
dbDataCompressionPref = findPreference(getString(R.string.database_data_compression_key))
|
||||
@@ -150,7 +150,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
||||
recycleBinGroupPref = findPreference(getString(R.string.recycle_bin_group_key))
|
||||
|
||||
// Recycle bin
|
||||
if (mDatabase.allowRecycleBin) {
|
||||
if (mDatabase.allowConfigurableRecycleBin) {
|
||||
val recycleBinEnablePref: SwitchPreference? = findPreference(getString(R.string.recycle_bin_enable_key))
|
||||
recycleBinEnablePref?.apply {
|
||||
isChecked = mDatabase.isRecycleBinEnabled
|
||||
@@ -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
|
||||
@@ -482,6 +482,9 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
||||
getString(R.string.database_data_compression_key) -> {
|
||||
dialogFragment = DatabaseDataCompressionPreferenceDialogFragmentCompat.newInstance(preference.key)
|
||||
}
|
||||
getString(R.string.database_data_remove_unlinked_attachments_key) -> {
|
||||
dialogFragment = DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat.newInstance(preference.key)
|
||||
}
|
||||
getString(R.string.max_history_items_key) -> {
|
||||
dialogFragment = MaxHistoryItemsPreferenceDialogFragmentCompat.newInstance(preference.key)
|
||||
}
|
||||
@@ -546,7 +549,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
||||
|
||||
return when (item.itemId) {
|
||||
R.id.menu_save_database -> {
|
||||
settingActivity?.mProgressDialogThread?.startDatabaseSave(!mDatabaseReadOnly)
|
||||
settingActivity?.mProgressDatabaseTaskProvider?.startDatabaseSave(!mDatabaseReadOnly)
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -231,12 +231,6 @@ object PreferencesUtil {
|
||||
context.resources.getBoolean(R.bool.biometric_auto_open_prompt_default))
|
||||
}
|
||||
|
||||
fun isFullFilePathEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.full_file_path_enable_key),
|
||||
context.resources.getBoolean(R.bool.full_file_path_enable_default))
|
||||
}
|
||||
|
||||
fun getListSort(context: Context): SortNodeEnum {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
prefs.getString(context.getString(R.string.sort_node_key),
|
||||
@@ -369,6 +363,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),
|
||||
|
||||
@@ -42,7 +42,8 @@ import com.kunzisoft.keepass.view.showActionError
|
||||
open class SettingsActivity
|
||||
: LockingActivity(),
|
||||
MainPreferenceFragment.Callback,
|
||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener,
|
||||
PasswordEncodingDialogFragment.Listener {
|
||||
|
||||
private var backupManager: BackupManager? = null
|
||||
|
||||
@@ -50,23 +51,6 @@ open class SettingsActivity
|
||||
private var toolbar: Toolbar? = null
|
||||
private var lockView: View? = null
|
||||
|
||||
companion object {
|
||||
|
||||
private const val SHOW_LOCK = "SHOW_LOCK"
|
||||
private const val TAG_NESTED = "TAG_NESTED"
|
||||
|
||||
fun launch(activity: Activity, readOnly: Boolean, timeoutEnable: Boolean) {
|
||||
val intent = Intent(activity, SettingsActivity::class.java)
|
||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
||||
intent.putExtra(TIMEOUT_ENABLE_KEY, timeoutEnable)
|
||||
if (!timeoutEnable) {
|
||||
activity.startActivity(intent)
|
||||
} else if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the main fragment to show in first
|
||||
* @return The main fragment
|
||||
@@ -83,7 +67,11 @@ open class SettingsActivity
|
||||
|
||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
toolbar?.setTitle(R.string.settings)
|
||||
|
||||
if (savedInstanceState?.getString(TITLE_KEY).isNullOrEmpty())
|
||||
toolbar?.setTitle(R.string.settings)
|
||||
else
|
||||
toolbar?.title = savedInstanceState?.getString(TITLE_KEY)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
@@ -92,6 +80,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 +93,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?)
|
||||
@@ -125,6 +116,22 @@ open class SettingsActivity
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
override fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
||||
masterPasswordChecked: Boolean,
|
||||
masterPassword: String?,
|
||||
keyFileChecked: Boolean,
|
||||
keyFile: Uri?) {
|
||||
databaseUri?.let {
|
||||
mProgressDatabaseTaskProvider?.startDatabaseAssignPassword(
|
||||
databaseUri,
|
||||
masterPasswordChecked,
|
||||
masterPassword,
|
||||
keyFileChecked,
|
||||
keyFile
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean,
|
||||
masterPassword: String?,
|
||||
keyFileChecked: Boolean,
|
||||
@@ -133,7 +140,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,
|
||||
@@ -141,18 +148,12 @@ open class SettingsActivity
|
||||
keyFile
|
||||
)
|
||||
} else {
|
||||
PasswordEncodingDialogFragment().apply {
|
||||
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
|
||||
mProgressDialogThread?.startDatabaseAssignPassword(
|
||||
databaseUri,
|
||||
masterPasswordChecked,
|
||||
masterPassword,
|
||||
keyFileChecked,
|
||||
keyFile
|
||||
)
|
||||
}
|
||||
show(supportFragmentManager, "passwordEncodingTag")
|
||||
}
|
||||
PasswordEncodingDialogFragment.getInstance(databaseUri,
|
||||
masterPasswordChecked,
|
||||
masterPassword,
|
||||
keyFileChecked,
|
||||
keyFile
|
||||
).show(supportFragmentManager, "passwordEncodingTag")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,8 +162,7 @@ open class SettingsActivity
|
||||
override fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean,
|
||||
masterPassword: String?,
|
||||
keyFileChecked: Boolean,
|
||||
keyFile: Uri?) {
|
||||
}
|
||||
keyFile: Uri?) {}
|
||||
|
||||
private fun hideOrShowLockButton(key: NestedSettingsFragment.Screen) {
|
||||
if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||
@@ -217,5 +217,24 @@ open class SettingsActivity
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putBoolean(SHOW_LOCK, lockView?.visibility == View.VISIBLE)
|
||||
outState.putString(TITLE_KEY, toolbar?.title?.toString())
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val SHOW_LOCK = "SHOW_LOCK"
|
||||
private const val TITLE_KEY = "TITLE_KEY"
|
||||
private const val TAG_NESTED = "TAG_NESTED"
|
||||
|
||||
fun launch(activity: Activity, readOnly: Boolean, timeoutEnable: Boolean) {
|
||||
val intent = Intent(activity, SettingsActivity::class.java)
|
||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
||||
intent.putExtra(TIMEOUT_ENABLE_KEY, timeoutEnable)
|
||||
if (!timeoutEnable) {
|
||||
activity.startActivity(intent)
|
||||
} else if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user