mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
575 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
dae19bbccf | ||
|
|
c81f83887e | ||
|
|
04e555dde9 | ||
|
|
be94518e31 | ||
|
|
66bec1e08c | ||
|
|
f61ce10716 | ||
|
|
b1b92b2995 | ||
|
|
bd9f2c4757 | ||
|
|
a3ead2153e | ||
|
|
e12f008b92 | ||
|
|
d064ece0ff | ||
|
|
379fbf68b1 | ||
|
|
83783c1a88 | ||
|
|
c7e46205b3 | ||
|
|
0c61f0ded2 | ||
|
|
49e2ec0498 | ||
|
|
fb0a74c101 | ||
|
|
245a7ddfe9 | ||
|
|
ca8874c2e1 | ||
|
|
dbcd7c8e03 | ||
|
|
9cce63659e | ||
|
|
be0bbab0c8 | ||
|
|
7b6d3698c4 | ||
|
|
56daca8b4f | ||
|
|
ed382d102e | ||
|
|
745de2502e | ||
|
|
503a4b1374 | ||
|
|
0b71b2d659 | ||
|
|
2ad244df94 | ||
|
|
d0da4f03a6 | ||
|
|
ab0cd4152a | ||
|
|
93d04bfe60 | ||
|
|
b1ccb40bd3 | ||
|
|
d177001ea8 | ||
|
|
2f921897c7 | ||
|
|
1a0f7146ce | ||
|
|
dff2386594 | ||
|
|
55cc782cc6 | ||
|
|
6903099873 | ||
|
|
6e2d84be33 | ||
|
|
9e542d0bbe | ||
|
|
ade9af9ecd | ||
|
|
59f11a1b26 | ||
|
|
cc1d6e2b47 | ||
|
|
2655b1b3d1 | ||
|
|
053dd28f8c | ||
|
|
65e4cf83d8 | ||
|
|
419099318c | ||
|
|
972edd3a30 | ||
|
|
cc4125e766 | ||
|
|
da40cc9830 | ||
|
|
29d7e2dcfe | ||
|
|
3d906fd582 | ||
|
|
0cad43c18b | ||
|
|
c66b686a63 | ||
|
|
e2a1e3f327 | ||
|
|
2b5cf75a53 | ||
|
|
1c350ac87b | ||
|
|
ef6539909c | ||
|
|
47d7c6fe65 | ||
|
|
c94c5fbc95 | ||
|
|
63a833d114 | ||
|
|
72162128e2 | ||
|
|
a8f712c335 | ||
|
|
ea8888a685 | ||
|
|
2aecf69b67 | ||
|
|
1a4c24dd86 | ||
|
|
6cf2b45051 | ||
|
|
38dd2bdf6e | ||
|
|
8784f1da70 | ||
|
|
b9208ea94e | ||
|
|
dfb648b480 | ||
|
|
f7af4f06ea | ||
|
|
d98f9eb62c | ||
|
|
fef1335f51 | ||
|
|
59f14cbdd4 | ||
|
|
e8645c543f | ||
|
|
b92f6177e3 | ||
|
|
2888829a4f | ||
|
|
3d392ead19 | ||
|
|
a79b2bfa79 | ||
|
|
15e1e2f02e | ||
|
|
9e652803d0 | ||
|
|
7795fceb72 | ||
|
|
9f01f26ea6 | ||
|
|
abbc9a1d7a | ||
|
|
2c15befb8e | ||
|
|
ba8ea84c4e | ||
|
|
97912046da | ||
|
|
43e99c23e3 | ||
|
|
ffe14d75fc | ||
|
|
872ef66641 | ||
|
|
6363862ec2 | ||
|
|
775a112e83 | ||
|
|
7168904290 | ||
|
|
4213209b08 | ||
|
|
5dce91b7f6 | ||
|
|
6d7fb9f87c | ||
|
|
cc6c9dd8d1 | ||
|
|
21ebec52f3 | ||
|
|
11023ab225 | ||
|
|
fe97b15905 | ||
|
|
de8738aa03 | ||
|
|
568bbf9126 | ||
|
|
a2481652da | ||
|
|
33e3d3272d | ||
|
|
b909651c29 | ||
|
|
16794c5252 | ||
|
|
49ad270d88 | ||
|
|
df9b6cb7e8 | ||
|
|
c6c4551928 | ||
|
|
de84353eb0 | ||
|
|
6f05b80f34 | ||
|
|
797dc706e2 | ||
|
|
bdb615bcf9 | ||
|
|
1b98717b0e | ||
|
|
7bd1aedada | ||
|
|
34bf8f9d1f | ||
|
|
b07f70e9fe | ||
|
|
e635788955 | ||
|
|
bdb1cef3e5 | ||
|
|
e5a32e65c0 | ||
|
|
5fc77922b4 | ||
|
|
ce8add2895 | ||
|
|
88e1e5b770 | ||
|
|
76ecbd3497 | ||
|
|
e33c9b932f | ||
|
|
038f6caa04 | ||
|
|
36f5249d71 | ||
|
|
570702a5bd | ||
|
|
5d03c9c644 | ||
|
|
83906def4a | ||
|
|
33e0f25fb1 | ||
|
|
2080de4139 | ||
|
|
decaca889c | ||
|
|
794a39032f | ||
|
|
cc3cf17060 | ||
|
|
b07da4bfa8 | ||
|
|
4f52a4ee79 | ||
|
|
cf77b999a3 | ||
|
|
c9f062bdd9 | ||
|
|
12669f7ea0 | ||
|
|
03451d2a6e | ||
|
|
b5d7595f87 | ||
|
|
0d3d760a43 | ||
|
|
1cfc86e5ce | ||
|
|
938343323d | ||
|
|
37112715c0 | ||
|
|
bc80c10193 | ||
|
|
d989f48226 | ||
|
|
45bcbb3a3d | ||
|
|
f4a74e0254 | ||
|
|
ace83cf68d | ||
|
|
209b9b9a6f | ||
|
|
0d9dd6a33e | ||
|
|
e4137a031e | ||
|
|
41e9ea7e4f | ||
|
|
1a8b88973d | ||
|
|
3109e251b9 | ||
|
|
452ab280f1 | ||
|
|
34ecfbb846 | ||
|
|
11fb97d275 | ||
|
|
4bb30d075b | ||
|
|
f38af55c05 | ||
|
|
8a007376cc | ||
|
|
940a1dfc78 | ||
|
|
cbd64d1602 | ||
|
|
955c9992d0 | ||
|
|
87606818cd | ||
|
|
ffcd264bac | ||
|
|
04c70cf99d | ||
|
|
0e0b6fca07 | ||
|
|
b24a789352 | ||
|
|
3d4f447037 | ||
|
|
7aa52ca00a | ||
|
|
a94c9050c5 | ||
|
|
9b18e71e12 | ||
|
|
d5ff048bd0 | ||
|
|
76f44f2573 | ||
|
|
9988f1a76f | ||
|
|
1559376bc2 | ||
|
|
4d0f252acd | ||
|
|
d5a84e66ad | ||
|
|
1dfe3453f4 | ||
|
|
a7dc9f31b9 | ||
|
|
d275f18b3c | ||
|
|
d770429401 | ||
|
|
501f0be99f | ||
|
|
a70122f873 | ||
|
|
1e1bd15a06 | ||
|
|
164fb1f4f5 | ||
|
|
0206514bdb | ||
|
|
ecf2b42fa4 | ||
|
|
df683aeea9 | ||
|
|
cdd05958f7 | ||
|
|
4ec5fc05fb | ||
|
|
4019cafac1 | ||
|
|
4333e179d1 | ||
|
|
87d67be428 | ||
|
|
350863adff | ||
|
|
d125fc46a0 | ||
|
|
5f4c079071 | ||
|
|
5ec576a76e | ||
|
|
9edb00ebbb | ||
|
|
59843e5464 | ||
|
|
4fe0ccf807 | ||
|
|
2ee012d3fe | ||
|
|
161b4cb5f5 | ||
|
|
f9bee68d71 | ||
|
|
e0d98aca1b | ||
|
|
d47518c00c | ||
|
|
58f97dc3ab | ||
|
|
c7706cb80c |
47
CHANGELOG
47
CHANGELOG
@@ -1,3 +1,50 @@
|
|||||||
|
KeePassDX(2.8.3)
|
||||||
|
* Upload attachments
|
||||||
|
* Visibility button for each hidden field
|
||||||
|
* Fix read header file
|
||||||
|
* Fix deletion in KDB database
|
||||||
|
* Fix minor issues
|
||||||
|
|
||||||
|
KeePassDX(2.8.2)
|
||||||
|
* Fix themes / new UI
|
||||||
|
* Fix multiples notifications
|
||||||
|
* Fix entry in Magikeyboard memory
|
||||||
|
* Fix biometric view visibility
|
||||||
|
* Fix fields order
|
||||||
|
* Upgrade code with ViewModel and LiveData
|
||||||
|
|
||||||
|
KeePassDX(2.8.1)
|
||||||
|
* Capture exceptions in coroutines
|
||||||
|
|
||||||
|
KeePassDX(2.8)
|
||||||
|
* Fix TOTP period (> 60s)
|
||||||
|
* Fix searching in recycle bin
|
||||||
|
* Settings to back to the previous keyboard during database credentials and after form filling
|
||||||
|
* Improve action tasks
|
||||||
|
* Improve recognition to reset app timeout
|
||||||
|
* Fix minor issues
|
||||||
|
|
||||||
|
KeePassDX(2.7)
|
||||||
|
* Add blocklists for autofill
|
||||||
|
* Add autofill compatibility mode (usefull for Browser not compatible)
|
||||||
|
* Upgrade autofill recognition algorithm
|
||||||
|
* Setting to search through web subdomains
|
||||||
|
* Refactoring selection mode
|
||||||
|
|
||||||
|
KeePassDX(2.6)
|
||||||
|
* Share a web domain to automatically search for an entry
|
||||||
|
* Default group icon for a new entry
|
||||||
|
* Better autofill recognition
|
||||||
|
* Fix entry not visually deleted in search
|
||||||
|
* Fix hanged loading dialog
|
||||||
|
* Fix auto open biometric prompt if comes from background
|
||||||
|
* Minor fixes
|
||||||
|
|
||||||
|
KeePassDX(2.5)
|
||||||
|
* First stable version of KeePassDX
|
||||||
|
* Fork completely rewritten from the KeePassDroid project
|
||||||
|
* Fix small issues from the last Release Candidate
|
||||||
|
|
||||||
KeePassDX(2.5RC2)
|
KeePassDX(2.5RC2)
|
||||||
* Replacement of Spongy Castle by Bouncy Castle
|
* Replacement of Spongy Castle by Bouncy Castle
|
||||||
* Update Autofill compatibility
|
* Update Autofill compatibility
|
||||||
|
|||||||
91
README.md
Normal file
91
README.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Android KeepassDX
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> KeepassDX is a **multi-format KeePass manager for Android devices**. The app allows creating keys and passwords in a secure way by integrating with the Android design standards.
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Create database files / entries and groups.
|
||||||
|
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
||||||
|
- **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePassXC, …).
|
||||||
|
- Allows opening and **copying URI / URL fields quickly**.
|
||||||
|
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
|
||||||
|
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
|
||||||
|
- Material design with **themes**.
|
||||||
|
- **Auto-Fill** and Integration.
|
||||||
|
- Field filling **keyboard**.
|
||||||
|
- **History** of each entry.
|
||||||
|
- Precise management of **settings**.
|
||||||
|
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
||||||
|
|
||||||
|
KeepassDX is **open source** and **ad-free**.
|
||||||
|
|
||||||
|
## What is KeePassDX?
|
||||||
|
|
||||||
|
An alternative to remembering an endless list of passwords manually. This is made more difficult by **using different passwords for each account**. If you use one password everywhere and security fails only one of those places, it grants access to your e-mail account, website, etc, and you may not know about it or notice, before bad things happen.
|
||||||
|
|
||||||
|
KeePassDX is a **password manager for Android**, which helps you **manage your passwords in a secure way**. You can put all your passwords in one database, locked with a **master key** and/or a **keyfile**. You **only have to remember one single master password and/or select the keyfile** to unlock the whole database. The databases are encrypted using the best and **most secure encryption algorithms** currently known.
|
||||||
|
|
||||||
|
## Small print?
|
||||||
|
|
||||||
|
KeePassDX is under **open source GPL3 license**, meaning you can use, study, change and share it at will. Copyleft ensures it stays that way.
|
||||||
|
From the full source, anyone can build, fork, and check whether for example the encryption algorithms are implemented correctly.
|
||||||
|
There is **no advertising**.
|
||||||
|
|
||||||
|
Do not worry, **the main features remain completely free**.
|
||||||
|
|
||||||
|
Optional visual styles are accessible after a contribution (and a congratulatory message (Ո‿Ո) ) or the purchase of an extended version to encourage contribution to the work of open source projects!
|
||||||
|
*If you contribute to the project and do not have access to the styles, do not hesitate to contact the author at [contact@kunzisoft.com](contact@kunzisoft.com).*
|
||||||
|
|
||||||
|
## Contributions
|
||||||
|
|
||||||
|
* Add features by making a **[pull request](https://help.github.com/articles/about-pull-requests/)**.
|
||||||
|
* Help to **[translate](https://hosted.weblate.org/projects/keepass-dx/strings/)** KeePassDX to your language (on [Weblate](https://hosted.weblate.org/projects/keepass-dx/) or by sending a [pull request](https://help.github.com/articles/about-pull-requests/)).
|
||||||
|
* **[Donate](https://www.kunzisoft.com/donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
|
||||||
|
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePassDX.
|
||||||
|
|
||||||
|
## Download
|
||||||
|
|
||||||
|
*[F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) is the recommended way of installing, a libre software project that verifies that all the libraries and app code is libre software.*
|
||||||
|
|
||||||
|
[<img src="https://f-droid.org/badge/get-it-on.png"
|
||||||
|
alt="Get it on F-Droid"
|
||||||
|
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
|
||||||
|
|
||||||
|
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
||||||
|
alt="Get it on Google Play"
|
||||||
|
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
||||||
|
|
||||||
|
## Frequently Asked Questions
|
||||||
|
|
||||||
|
Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ)
|
||||||
|
|
||||||
|
## Other devices
|
||||||
|
|
||||||
|
- [KeePass](https://keepass.info/) (https://keepass.info/) is the original and official project for the desktop, with technical documentation for standardized database files. It is updated regularly with active maintenance (written in C#).
|
||||||
|
|
||||||
|
- [KeePassXC](https://keepassxc.org/) (https://keepassxc.org/) is an alternative integration of KeePass written in C++.
|
||||||
|
|
||||||
|
- [KeeWeb](https://keeweb.info/) (https://keeweb.info/) is a web version that is also compatible with KeePass files.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright © 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||||
|
|
||||||
|
This file is part of KeePassDX.
|
||||||
|
|
||||||
|
[KeePassDX](https://www.keepassdx.com) is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
*This project is a fork of [KeepassDroid](https://github.com/bpellin/keepassdroid) by bpellin.*
|
||||||
88
ReadMe.md
88
ReadMe.md
@@ -1,88 +0,0 @@
|
|||||||
# Android KeepassDX
|
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> KeepassDX is a **multi-format KeePass manager for Android devices**. The application allows to create keys and passwords in a secure way by integrating with the Android design standards.
|
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Create database files / entries and groups
|
|
||||||
* Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm
|
|
||||||
* **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePassXC...)
|
|
||||||
* Allows **fast copy** of fields and opening of URI / URL
|
|
||||||
* **Biometric recognition** for fast unlocking *(Fingerprint / Face unlock / ...)*
|
|
||||||
* **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA)
|
|
||||||
* Material design with **themes**
|
|
||||||
* **AutoFill** and Integration
|
|
||||||
* Field filling **keyboard**
|
|
||||||
* **History** of each entry
|
|
||||||
* Precise management of **settings**
|
|
||||||
* Code written in **native language** *(Kotlin / Java / JNI / C)*
|
|
||||||
|
|
||||||
KeepassDX is **open source** and **ad-free**.
|
|
||||||
|
|
||||||
## What is KeePassDX?
|
|
||||||
|
|
||||||
Today you need to remember many passwords. You need a password for your e-mail account, your website's FTP password, online passwords (like website member account), etc. etc. etc. The list is endless. Also, you **should use different passwords for each account**. Because if you use only one password everywhere and someone gets this password you have a problem... A serious problem. The thief would have access to your e-mail account, website, etc. Unimaginable.
|
|
||||||
|
|
||||||
KeePassDX is a **free open source password manager for Android**, which helps you to **manage your passwords in a secure way**. You can put all your passwords in one database, which is locked with one **master key** or a **key file**. So you **only have to remember one single master password or select the key file** to unlock the whole database. The databases are encrypted using the best and **most secure encryption algorithms** currently known.
|
|
||||||
|
|
||||||
## Is it really free?
|
|
||||||
|
|
||||||
Yes, KeePassDX is under **free license (OSI certified)** and **without advertising**. You can have a look at its full source and check whether the encryption algorithms are implemented correctly.
|
|
||||||
|
|
||||||
*Note : If you access the application from a store, visual styles may not be available to encourage contribution to the work of open source projects. These optional styles are accessible after a contribution (and a congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free. If you contribute to the project and you do not have access to the themes, do not hesitate to contact me at [contact@kunzisoft.com](contact@kunzisoft.com), I will give you the procedure.*
|
|
||||||
|
|
||||||
## Contributions
|
|
||||||
|
|
||||||
You can contribute in different ways to help us on our work.
|
|
||||||
|
|
||||||
* Add features by a **[pull request](https://help.github.com/articles/about-pull-requests/)**.
|
|
||||||
* Help to **[translate](https://hosted.weblate.org/projects/keepass-dx/strings/)** into your language (By using [Weblate](https://hosted.weblate.org/projects/keepass-dx/) or with a manual [pull request](https://help.github.com/articles/about-pull-requests/))
|
|
||||||
* **[Donate](https://www.kunzisoft.com/donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
|
|
||||||
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePassDX
|
|
||||||
|
|
||||||
## Download
|
|
||||||
|
|
||||||
*We recommend the installation from [F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) which verifies that all libraries and application code are open source.*
|
|
||||||
|
|
||||||
[<img src="https://f-droid.org/badge/get-it-on.png"
|
|
||||||
alt="Get it on F-Droid"
|
|
||||||
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
|
|
||||||
|
|
||||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
|
||||||
alt="Get it on Google Play"
|
|
||||||
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
|
||||||
|
|
||||||
## Frequently Asked Questions
|
|
||||||
|
|
||||||
Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ)
|
|
||||||
|
|
||||||
## Other devices
|
|
||||||
|
|
||||||
- [KeePass](https://keepass.info/) (https://keepass.info/) is the original and official project for desktop, with technical documentation for standardized database files. It is updated regularly with active maintenance (written in C#).
|
|
||||||
|
|
||||||
- [KeePassXC](https://keepassxc.org/) (https://keepassxc.org/) is an alternative integration to KeePass written in C++.
|
|
||||||
|
|
||||||
- [KeeWeb](https://keeweb.info/) (https://keeweb.info/) is a web version also compatible with KeePass files.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Copyright (c) 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
|
||||||
|
|
||||||
This file is part of KeePassDX.
|
|
||||||
|
|
||||||
[KeePassDX](https://www.keepassdx.com) is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
*This project is a fork of [KeepassDroid](https://github.com/bpellin/keepassdroid) by bpellin.*
|
|
||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 14
|
minSdkVersion 14
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode = 32
|
versionCode = 39
|
||||||
versionName = "2.5RC2"
|
versionName = "2.8.3"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
@@ -42,9 +42,6 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dexOptions {
|
|
||||||
}
|
|
||||||
|
|
||||||
flavorDimensions "version"
|
flavorDimensions "version"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
libre {
|
libre {
|
||||||
@@ -53,7 +50,7 @@ android {
|
|||||||
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
buildConfigField "boolean", "FULL_VERSION", "true"
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "false"
|
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", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
}
|
}
|
||||||
pro {
|
pro {
|
||||||
@@ -72,7 +69,7 @@ android {
|
|||||||
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "false"
|
buildConfigField "boolean", "FULL_VERSION", "false"
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
||||||
}
|
}
|
||||||
@@ -88,42 +85,46 @@ android {
|
|||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def room_version = "2.2.5"
|
def room_version = "2.2.5"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
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.preference:preference:1.1.1'
|
||||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.biometric:biometric:1.0.1'
|
implementation 'androidx.biometric:biometric:1.0.1'
|
||||||
implementation "androidx.core:core-ktx:1.2.0"
|
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||||
|
implementation "androidx.core:core-ktx:1.3.1"
|
||||||
|
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||||
// To upgrade with style
|
// To upgrade with style
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
implementation 'com.google.android.material:material:1.0.0'
|
||||||
// Database
|
// Database
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
kapt "androidx.room:room-compiler:$room_version"
|
kapt "androidx.room:room-compiler:$room_version"
|
||||||
// Crypto
|
// Crypto
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.65'
|
implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01'
|
||||||
// Time
|
// Time
|
||||||
implementation 'joda-time:joda-time:2.9.9'
|
implementation 'joda-time:joda-time:2.10.6'
|
||||||
// Color
|
// Color
|
||||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3'
|
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3'
|
||||||
// Education
|
// Education
|
||||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
||||||
// Apache Commons Collections
|
// Apache Commons Collections
|
||||||
implementation 'commons-collections:commons-collections:3.2.1'
|
implementation 'commons-collections:commons-collections:3.2.2'
|
||||||
implementation 'org.apache.commons:commons-io:1.3.2'
|
|
||||||
// Apache Commons Codec
|
// Apache Commons Codec
|
||||||
implementation 'commons-codec:commons-codec:1.11'
|
implementation 'commons-codec:commons-codec:1.14'
|
||||||
// Icon pack
|
// Icon pack
|
||||||
implementation project(path: ':icon-pack-classic')
|
implementation project(path: ':icon-pack-classic')
|
||||||
implementation project(path: ':icon-pack-material')
|
implementation project(path: ':icon-pack-material')
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
androidTestImplementation 'junit:junit:4.12'
|
androidTestImplementation 'junit:junit:4.13'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,23 @@ import junit.framework.TestCase
|
|||||||
class UnsignedIntTest: TestCase() {
|
class UnsignedIntTest: TestCase() {
|
||||||
|
|
||||||
fun testUInt() {
|
fun testUInt() {
|
||||||
val standardInt = UnsignedInt(15).toInt()
|
val standardInt = UnsignedInt(15).toKotlinInt()
|
||||||
assertEquals(15, standardInt)
|
assertEquals(15, standardInt)
|
||||||
val unsignedInt = UnsignedInt(-1).toLong()
|
val unsignedInt = UnsignedInt(-1).toKotlinLong()
|
||||||
assertEquals(4294967295L, unsignedInt)
|
assertEquals(4294967295L, unsignedInt)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testMaxValue() {
|
fun testMaxValue() {
|
||||||
val maxValue = UnsignedInt.MAX_VALUE.toLong()
|
val maxValue = UnsignedInt.MAX_VALUE.toKotlinLong()
|
||||||
assertEquals(4294967295L, maxValue)
|
assertEquals(4294967295L, maxValue)
|
||||||
val longValue = UnsignedInt.fromLong(4294967295L).toLong()
|
val longValue = UnsignedInt.fromKotlinLong(4294967295L).toKotlinLong()
|
||||||
assertEquals(longValue, maxValue)
|
assertEquals(longValue, maxValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testLong() {
|
fun testLong() {
|
||||||
val longValue = UnsignedInt.fromLong(50L).toInt()
|
val longValue = UnsignedInt.fromKotlinLong(50L).toKotlinInt()
|
||||||
assertEquals(50, longValue)
|
assertEquals(50, longValue)
|
||||||
val uIntLongValue = UnsignedInt.fromLong(4294967290).toLong()
|
val uIntLongValue = UnsignedInt.fromKotlinLong(4294967290).toKotlinLong()
|
||||||
assertEquals(4294967290, uIntLongValue)
|
assertEquals(4294967290, uIntLongValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,11 +35,11 @@ class ValuesTest : TestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteLongMax() {
|
fun testReadWriteLongMax() {
|
||||||
testReadWriteLong(java.lang.Byte.MAX_VALUE)
|
testReadWriteLong(Byte.MAX_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteLongMin() {
|
fun testReadWriteLongMin() {
|
||||||
testReadWriteLong(java.lang.Byte.MIN_VALUE)
|
testReadWriteLong(Byte.MIN_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteLongRnd() {
|
fun testReadWriteLongRnd() {
|
||||||
@@ -62,11 +62,11 @@ class ValuesTest : TestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteIntMin() {
|
fun testReadWriteIntMin() {
|
||||||
testReadWriteInt(java.lang.Byte.MIN_VALUE)
|
testReadWriteInt(Byte.MIN_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteIntMax() {
|
fun testReadWriteIntMax() {
|
||||||
testReadWriteInt(java.lang.Byte.MAX_VALUE)
|
testReadWriteInt(Byte.MAX_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testReadWriteInt(value: Byte) {
|
private fun testReadWriteInt(value: Byte) {
|
||||||
@@ -103,11 +103,11 @@ class ValuesTest : TestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteShortMin() {
|
fun testReadWriteShortMin() {
|
||||||
testReadWriteShort(java.lang.Byte.MIN_VALUE)
|
testReadWriteShort(Byte.MIN_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteShortMax() {
|
fun testReadWriteShortMax() {
|
||||||
testReadWriteShort(java.lang.Byte.MAX_VALUE)
|
testReadWriteShort(Byte.MAX_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testReadWriteShort(value: Byte) {
|
private fun testReadWriteShort(value: Byte) {
|
||||||
@@ -125,15 +125,15 @@ class ValuesTest : TestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteByteMin() {
|
fun testReadWriteByteMin() {
|
||||||
testReadWriteByte(java.lang.Byte.MIN_VALUE)
|
testReadWriteByte(Byte.MIN_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteByteMax() {
|
fun testReadWriteByteMax() {
|
||||||
testReadWriteShort(java.lang.Byte.MAX_VALUE)
|
testReadWriteShort(Byte.MAX_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testReadWriteByte(value: Byte) {
|
private fun testReadWriteByte(value: Byte) {
|
||||||
val dest: Byte = UnsignedInt(UnsignedInt.fromByte(value)).toByte()
|
val dest: Byte = UnsignedInt(UnsignedInt.fromKotlinByte(value)).toKotlinByte()
|
||||||
assert(value == dest)
|
assert(value == dest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,33 +124,42 @@
|
|||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
||||||
android:configChanges="keyboardHidden"
|
android:windowSoftInputMode="adjustPan|stateAlwaysHidden" />
|
||||||
android:windowSoftInputMode="adjustResize" />
|
|
||||||
<!-- About and Settings -->
|
<!-- About and Settings -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:label="@string/about" />
|
android:label="@string/about" />
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
<activity
|
||||||
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
|
android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
||||||
|
android:theme="@style/Theme.Transparent"
|
||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
<activity
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
||||||
<activity android:name="com.kunzisoft.keepass.magikeyboard.KeyboardLauncherActivity"
|
<activity
|
||||||
android:label="@string/keyboard_name"
|
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
||||||
android:exported="true">
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
||||||
|
android:theme="@style/Theme.Transparent">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity"
|
||||||
|
android:theme="@style/Theme.Transparent" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
||||||
android:label="@string/keyboard_setting_label">
|
android:label="@string/keyboard_setting_label">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false" />
|
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
|
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
|
|||||||
Binary file not shown.
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
@@ -26,27 +26,45 @@ import android.content.Intent
|
|||||||
import android.content.IntentSender
|
import android.content.IntentSender
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.GroupActivity
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class AutofillLauncherActivity : AppCompatActivity() {
|
class AutofillLauncherActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
|
// Build search param
|
||||||
|
val searchInfo = SearchInfo().apply {
|
||||||
|
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
||||||
|
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
||||||
|
}
|
||||||
|
|
||||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||||
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
|
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
|
||||||
if (assistStructure != null) {
|
|
||||||
// Build search param
|
if (assistStructure == null) {
|
||||||
val searchInfo = SearchInfo().apply {
|
setResult(Activity.RESULT_CANCELED)
|
||||||
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
finish()
|
||||||
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
} else if (!KeeAutofillService.searchAllowedFor(searchInfo.applicationId,
|
||||||
}
|
PreferencesUtil.applicationIdBlocklist(this))
|
||||||
|
|| !KeeAutofillService.searchAllowedFor(searchInfo.webDomain,
|
||||||
|
PreferencesUtil.webDomainBlocklist(this))) {
|
||||||
|
// If item not allowed, show a toast
|
||||||
|
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
// If database is open
|
// If database is open
|
||||||
AutofillHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
Database.getInstance(),
|
Database.getInstance(),
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ items ->
|
{ items ->
|
||||||
@@ -57,17 +75,17 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
{
|
{
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
GroupActivity.launchForAutofillResult(this,
|
GroupActivity.launchForAutofillResult(this,
|
||||||
assistStructure)
|
assistStructure,
|
||||||
|
false,
|
||||||
|
searchInfo)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// If database not open
|
// If database not open
|
||||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||||
assistStructure, searchInfo)
|
assistStructure,
|
||||||
|
searchInfo)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
setResult(Activity.RESULT_CANCELED)
|
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -45,8 +45,9 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
|||||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||||
import com.kunzisoft.keepass.model.AttachmentState
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
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.AttachmentFileNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||||
@@ -86,10 +87,10 @@ class EntryActivity : LockingActivity() {
|
|||||||
private var mShowPassword: Boolean = false
|
private var mShowPassword: Boolean = false
|
||||||
|
|
||||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
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 clipboardHelper: ClipboardHelper? = null
|
||||||
private var firstLaunchOfActivity: Boolean = false
|
private var mFirstLaunchOfActivity: Boolean = false
|
||||||
|
|
||||||
private var iconColor: Int = 0
|
private var iconColor: Int = 0
|
||||||
|
|
||||||
@@ -130,14 +131,17 @@ class EntryActivity : LockingActivity() {
|
|||||||
lockAndExit()
|
lockAndExit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Focus view to reinitialize timeout
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
|
||||||
|
|
||||||
// Init the clipboard helper
|
// Init the clipboard helper
|
||||||
clipboardHelper = ClipboardHelper(this)
|
clipboardHelper = ClipboardHelper(this)
|
||||||
firstLaunchOfActivity = true
|
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
|
||||||
|
|
||||||
// Init attachment service binder manager
|
// Init attachment service binder manager
|
||||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
|
|
||||||
mProgressDialogThread?.onActionFinish = { actionTask, result ->
|
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
||||||
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
||||||
@@ -196,7 +200,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
val entryInfo = entry.getEntryInfo(Database.getInstance())
|
val entryInfo = entry.getEntryInfo(Database.getInstance())
|
||||||
|
|
||||||
// Manage entry copy to start notification if allowed
|
// Manage entry copy to start notification if allowed
|
||||||
if (firstLaunchOfActivity) {
|
if (mFirstLaunchOfActivity) {
|
||||||
// Manage entry to launch copying notification if allowed
|
// Manage entry to launch copying notification if allowed
|
||||||
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
||||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||||
@@ -209,13 +213,13 @@ class EntryActivity : LockingActivity() {
|
|||||||
mAttachmentFileBinderManager?.apply {
|
mAttachmentFileBinderManager?.apply {
|
||||||
registerProgressTask()
|
registerProgressTask()
|
||||||
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||||
override fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment) {
|
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||||
entryContentsView?.updateAttachmentDownloadProgress(attachment)
|
entryContentsView?.putAttachment(entryAttachmentState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
firstLaunchOfActivity = false
|
mFirstLaunchOfActivity = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -237,14 +241,13 @@ class EntryActivity : LockingActivity() {
|
|||||||
toolbar?.title = entryTitle
|
toolbar?.title = entryTitle
|
||||||
|
|
||||||
// Assign basic fields
|
// Assign basic fields
|
||||||
entryContentsView?.assignUserName(entry.username)
|
entryContentsView?.assignUserName(entry.username) {
|
||||||
entryContentsView?.assignUserNameCopyListener(View.OnClickListener {
|
|
||||||
database.startManageEntry(entry)
|
database.startManageEntry(entry)
|
||||||
clipboardHelper?.timeoutCopyToClipboard(entry.username,
|
clipboardHelper?.timeoutCopyToClipboard(entry.username,
|
||||||
getString(R.string.copy_field,
|
getString(R.string.copy_field,
|
||||||
getString(R.string.entry_user_name)))
|
getString(R.string.entry_user_name)))
|
||||||
database.stopManageEntry(entry)
|
database.stopManageEntry(entry)
|
||||||
})
|
}
|
||||||
|
|
||||||
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
||||||
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
||||||
@@ -271,23 +274,25 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entryContentsView?.assignPassword(entry.password, allowCopyPasswordAndProtectedFields)
|
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
|
||||||
if (allowCopyPasswordAndProtectedFields) {
|
View.OnClickListener {
|
||||||
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
|
|
||||||
database.startManageEntry(entry)
|
database.startManageEntry(entry)
|
||||||
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
||||||
getString(R.string.copy_field,
|
getString(R.string.copy_field,
|
||||||
getString(R.string.entry_password)))
|
getString(R.string.entry_password)))
|
||||||
database.stopManageEntry(entry)
|
database.stopManageEntry(entry)
|
||||||
})
|
}
|
||||||
} else {
|
} else {
|
||||||
// If dialog not already shown
|
// If dialog not already shown
|
||||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||||
entryContentsView?.assignPasswordCopyListener(showWarningClipboardDialogOnClickListener)
|
showWarningClipboardDialogOnClickListener
|
||||||
} else {
|
} else {
|
||||||
entryContentsView?.assignPasswordCopyListener(null)
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
entryContentsView?.assignPassword(entry.password,
|
||||||
|
allowCopyPasswordAndProtectedFields,
|
||||||
|
onPasswordCopyClickListener)
|
||||||
|
|
||||||
//Assign OTP field
|
//Assign OTP field
|
||||||
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
|
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
|
||||||
@@ -301,24 +306,20 @@ class EntryActivity : LockingActivity() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
entryContentsView?.assignURL(entry.url)
|
entryContentsView?.assignURL(entry.url)
|
||||||
entryContentsView?.assignComment(entry.notes)
|
entryContentsView?.assignNotes(entry.notes)
|
||||||
|
|
||||||
// Assign custom fields
|
// Assign custom fields
|
||||||
if (entry.allowCustomFields()) {
|
if (entry.allowCustomFields()) {
|
||||||
entryContentsView?.clearExtraFields()
|
entryContentsView?.clearExtraFields()
|
||||||
|
for ((label, value) in entry.customFields) {
|
||||||
for (element in entry.customFields.entries) {
|
|
||||||
val label = element.key
|
|
||||||
val value = element.value
|
|
||||||
|
|
||||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
||||||
if (allowCopyProtectedField) {
|
if (allowCopyProtectedField) {
|
||||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, View.OnClickListener {
|
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
|
||||||
clipboardHelper?.timeoutCopyToClipboard(
|
clipboardHelper?.timeoutCopyToClipboard(
|
||||||
value.toString(),
|
value.toString(),
|
||||||
getString(R.string.copy_field, label)
|
getString(R.string.copy_field, label)
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
} else {
|
} else {
|
||||||
// If dialog not already shown
|
// If dialog not already shown
|
||||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||||
@@ -329,28 +330,16 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
||||||
|
|
||||||
// Manage attachments
|
// Manage attachments
|
||||||
val attachments = entry.getAttachments()
|
mDatabase?.binaryPool?.let { binaryPool ->
|
||||||
val showAttachmentsView = attachments.isNotEmpty()
|
entryContentsView?.assignAttachments(entry.getAttachments(binaryPool).toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
||||||
entryContentsView?.showAttachments(showAttachmentsView)
|
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
||||||
if (showAttachmentsView) {
|
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||||
entryContentsView?.assignAttachments(attachments)
|
|
||||||
entryContentsView?.onAttachmentClick { attachmentItem, _ ->
|
|
||||||
when (attachmentItem.downloadState) {
|
|
||||||
AttachmentState.NULL, AttachmentState.ERROR, AttachmentState.COMPLETE -> {
|
|
||||||
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
|
||||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// TODO Stop download
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entryContentsView?.refreshAttachments()
|
|
||||||
|
|
||||||
// Assign dates
|
// Assign dates
|
||||||
entryContentsView?.assignCreationDate(entry.creationTime)
|
entryContentsView?.assignCreationDate(entry.creationTime)
|
||||||
@@ -370,16 +359,9 @@ class EntryActivity : LockingActivity() {
|
|||||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||||
taColorAccent.recycle()
|
taColorAccent.recycle()
|
||||||
}
|
}
|
||||||
val entryHistory = entry.getHistory()
|
entryContentsView?.assignHistory(entry.getHistory()) { historyItem, position ->
|
||||||
val showHistoryView = entryHistory.isNotEmpty()
|
launch(this, historyItem, mReadOnly, position)
|
||||||
entryContentsView?.showHistory(showHistoryView)
|
|
||||||
if (showHistoryView) {
|
|
||||||
entryContentsView?.assignHistory(entryHistory)
|
|
||||||
entryContentsView?.onHistoryClick { historyItem, position ->
|
|
||||||
launch(this, historyItem, mReadOnly, position)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
entryContentsView?.refreshHistory()
|
|
||||||
|
|
||||||
// Assign special data
|
// Assign special data
|
||||||
entryContentsView?.assignUUID(entry.nodeId.id)
|
entryContentsView?.assignUUID(entry.nodeId.id)
|
||||||
@@ -408,16 +390,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 {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
@@ -433,15 +405,6 @@ class EntryActivity : LockingActivity() {
|
|||||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
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)
|
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
||||||
gotoUrl?.apply {
|
gotoUrl?.apply {
|
||||||
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
||||||
@@ -464,28 +427,31 @@ class EntryActivity : LockingActivity() {
|
|||||||
|
|
||||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||||
menu: Menu) {
|
menu: Menu) {
|
||||||
val entryCopyEducationPerformed = entryContentsView?.isUserNamePresent == true
|
val entryFieldCopyView = findViewById<View>(R.id.entry_field_copy)
|
||||||
|
val entryCopyEducationPerformed = entryFieldCopyView != null
|
||||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||||
findViewById(R.id.entry_user_name_action_image),
|
entryFieldCopyView,
|
||||||
{
|
{
|
||||||
clipboardHelper?.timeoutCopyToClipboard(mEntry!!.username,
|
val appNameString = getString(R.string.app_name)
|
||||||
getString(R.string.copy_field,
|
clipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||||
getString(R.string.entry_user_name)))
|
getString(R.string.copy_field, appNameString))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(entryActivityEducation, menu)
|
performedNextEducation(entryActivityEducation, menu)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!entryCopyEducationPerformed) {
|
if (!entryCopyEducationPerformed) {
|
||||||
|
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
|
||||||
// entryEditEducationPerformed
|
// entryEditEducationPerformed
|
||||||
toolbar?.findViewById<View>(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
menuEditView != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||||
toolbar!!.findViewById(R.id.menu_edit),
|
menuEditView,
|
||||||
{
|
{
|
||||||
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(entryActivityEducation, menu)
|
performedNextEducation(entryActivityEducation, menu)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,12 +461,6 @@ class EntryActivity : LockingActivity() {
|
|||||||
MenuUtil.onContributionItemSelected(this)
|
MenuUtil.onContributionItemSelected(this)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_toggle_pass -> {
|
|
||||||
mShowPassword = !mShowPassword
|
|
||||||
changeShowPasswordIcon(item)
|
|
||||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.menu_edit -> {
|
R.id.menu_edit -> {
|
||||||
mEntry?.let {
|
mEntry?.let {
|
||||||
EntryEditActivity.launch(this@EntryActivity, it)
|
EntryEditActivity.launch(this@EntryActivity, it)
|
||||||
@@ -520,7 +480,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
R.id.menu_restore_entry_history -> {
|
R.id.menu_restore_entry_history -> {
|
||||||
mEntryLastVersion?.let { mainEntry ->
|
mEntryLastVersion?.let { mainEntry ->
|
||||||
mProgressDialogThread?.startDatabaseRestoreEntryHistory(
|
mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory(
|
||||||
mainEntry,
|
mainEntry,
|
||||||
mEntryHistoryPosition,
|
mEntryHistoryPosition,
|
||||||
!mReadOnly && mAutoSaveEnable)
|
!mReadOnly && mAutoSaveEnable)
|
||||||
@@ -528,20 +488,25 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
R.id.menu_delete_entry_history -> {
|
R.id.menu_delete_entry_history -> {
|
||||||
mEntryLastVersion?.let { mainEntry ->
|
mEntryLastVersion?.let { mainEntry ->
|
||||||
mProgressDialogThread?.startDatabaseDeleteEntryHistory(
|
mProgressDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(
|
||||||
mainEntry,
|
mainEntry,
|
||||||
mEntryHistoryPosition,
|
mEntryHistoryPosition,
|
||||||
!mReadOnly && mAutoSaveEnable)
|
!mReadOnly && mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.menu_save_database -> {
|
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)
|
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
|
||||||
|
outState.putBoolean(KEY_FIRST_LAUNCH_ACTIVITY, mFirstLaunchOfActivity)
|
||||||
|
}
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
// Transit data in previous Activity after an update
|
// Transit data in previous Activity after an update
|
||||||
@@ -555,6 +520,8 @@ class EntryActivity : LockingActivity() {
|
|||||||
companion object {
|
companion object {
|
||||||
private val TAG = EntryActivity::class.java.name
|
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 = "KEY_ENTRY"
|
||||||
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
|
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import android.app.Activity
|
|||||||
import android.app.DatePickerDialog
|
import android.app.DatePickerDialog
|
||||||
import android.app.TimePickerDialog
|
import android.app.TimePickerDialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -31,20 +33,22 @@ import android.view.View
|
|||||||
import android.widget.DatePicker
|
import android.widget.DatePicker
|
||||||
import android.widget.TimePicker
|
import android.widget.TimePicker
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.widget.ActionMenuView
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.widget.NestedScrollView
|
import androidx.core.widget.NestedScrollView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.*
|
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.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.*
|
||||||
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.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
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.ClipboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
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.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
@@ -52,19 +56,25 @@ import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
|||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.EntryEditContentsView
|
import com.kunzisoft.keepass.view.EntryEditContentsView
|
||||||
import com.kunzisoft.keepass.view.showActionError
|
import com.kunzisoft.keepass.view.showActionError
|
||||||
|
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class EntryEditActivity : LockingActivity(),
|
class EntryEditActivity : LockingActivity(),
|
||||||
IconPickerDialogFragment.IconPickerListener,
|
IconPickerDialogFragment.IconPickerListener,
|
||||||
|
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
|
||||||
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
||||||
SetOTPDialogFragment.CreateOtpListener,
|
SetOTPDialogFragment.CreateOtpListener,
|
||||||
DatePickerDialog.OnDateSetListener,
|
DatePickerDialog.OnDateSetListener,
|
||||||
TimePickerDialog.OnTimeSetListener {
|
TimePickerDialog.OnTimeSetListener,
|
||||||
|
FileTooBigDialogFragment.ActionChooseListener,
|
||||||
|
ReplaceFileDialogFragment.ActionChooseListener {
|
||||||
|
|
||||||
private var mDatabase: Database? = null
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
@@ -79,10 +89,17 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var scrollView: NestedScrollView? = null
|
private var scrollView: NestedScrollView? = null
|
||||||
private var entryEditContentsView: EntryEditContentsView? = null
|
private var entryEditContentsView: EntryEditContentsView? = null
|
||||||
private var entryEditAddToolBar: ActionMenuView? = null
|
private var entryEditAddToolBar: Toolbar? = null
|
||||||
private var saveView: View? = null
|
private var validateButton: View? = null
|
||||||
private var lockView: View? = null
|
private var lockView: View? = null
|
||||||
|
|
||||||
|
private var mFocusedEditExtraField: FocusedEditField? = null
|
||||||
|
|
||||||
|
// To manage attachments
|
||||||
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||||
|
private var mAllowMultipleAttachments: Boolean = false
|
||||||
|
|
||||||
// Education
|
// Education
|
||||||
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
||||||
|
|
||||||
@@ -113,6 +130,9 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
.show(supportFragmentManager, "DatePickerFragment")
|
.show(supportFragmentManager, "DatePickerFragment")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
entryEditContentsView?.entryPasswordGeneratorView?.setOnClickListener {
|
||||||
|
openPasswordGenerator()
|
||||||
|
}
|
||||||
|
|
||||||
lockView = findViewById(R.id.lock_button)
|
lockView = findViewById(R.id.lock_button)
|
||||||
lockView?.setOnClickListener {
|
lockView?.setOnClickListener {
|
||||||
@@ -120,7 +140,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Focus view to reinitialize timeout
|
// Focus view to reinitialize timeout
|
||||||
resetAppTimeoutWhenViewFocusedOrChanged(entryEditContentsView)
|
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
|
||||||
|
|
||||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
||||||
@@ -145,8 +165,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create the new entry from the current one
|
// Create the new entry from the current one
|
||||||
if (savedInstanceState == null
|
if (savedInstanceState?.containsKey(KEY_NEW_ENTRY) != true) {
|
||||||
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
|
||||||
mEntry?.let { entry ->
|
mEntry?.let { entry ->
|
||||||
// Create a copy to modify
|
// Create a copy to modify
|
||||||
mNewEntry = Entry(entry).also { newEntry ->
|
mNewEntry = Entry(entry).also { newEntry ->
|
||||||
@@ -161,23 +180,32 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let {
|
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let {
|
||||||
mIsNew = true
|
mIsNew = true
|
||||||
// Create an empty new entry
|
// Create an empty new entry
|
||||||
if (savedInstanceState == null
|
if (savedInstanceState?.containsKey(KEY_NEW_ENTRY) != true) {
|
||||||
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
|
||||||
mNewEntry = mDatabase?.createEntry()
|
mNewEntry = mDatabase?.createEntry()
|
||||||
}
|
}
|
||||||
mParent = mDatabase?.getGroupById(it)
|
mParent = mDatabase?.getGroupById(it)
|
||||||
// Add the default icon
|
// Add the default icon from parent if not a folder
|
||||||
mDatabase?.drawFactory?.let { iconFactory ->
|
val parentIcon = mParent?.icon
|
||||||
entryEditContentsView?.setDefaultIcon(iconFactory)
|
if (parentIcon != null
|
||||||
|
&& parentIcon.iconId != IconImage.UNKNOWN_ID
|
||||||
|
&& parentIcon.iconId != IconImageStandard.FOLDER) {
|
||||||
|
temporarilySaveAndShowSelectedIcon(parentIcon)
|
||||||
|
} else {
|
||||||
|
mDatabase?.drawFactory?.let { iconFactory ->
|
||||||
|
entryEditContentsView?.setDefaultIcon(iconFactory)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the new entry after an orientation change
|
// Retrieve the new entry after an orientation change
|
||||||
if (savedInstanceState != null
|
if (savedInstanceState?.containsKey(KEY_NEW_ENTRY) == true) {
|
||||||
&& savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
|
||||||
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY)
|
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (savedInstanceState?.containsKey(EXTRA_FIELD_FOCUSED_ENTRY) == true) {
|
||||||
|
mFocusedEditExtraField = savedInstanceState.getParcelable(EXTRA_FIELD_FOCUSED_ENTRY)
|
||||||
|
}
|
||||||
|
|
||||||
// Close the activity if entry or parent can't be retrieve
|
// Close the activity if entry or parent can't be retrieve
|
||||||
if (mNewEntry == null || mParent == null) {
|
if (mNewEntry == null || mParent == null) {
|
||||||
finish()
|
finish()
|
||||||
@@ -203,22 +231,28 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
isVisible = 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 {
|
menu.findItem(R.id.menu_add_otp).apply {
|
||||||
val allowOTP = mDatabase?.allowOTP == true
|
val allowOTP = mDatabase?.allowOTP == true
|
||||||
isEnabled = allowOTP
|
isEnabled = allowOTP
|
||||||
isVisible = allowOTP
|
// OTP not compatible below KitKat
|
||||||
|
isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnMenuItemClickListener { item ->
|
setOnMenuItemClickListener { item ->
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.menu_generate_password -> {
|
|
||||||
openPasswordGenerator()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.menu_add_field -> {
|
R.id.menu_add_field -> {
|
||||||
addNewCustomField()
|
addNewCustomField()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.menu_add_attachment -> {
|
||||||
|
addNewAttachment(item)
|
||||||
|
true
|
||||||
|
}
|
||||||
R.id.menu_add_otp -> {
|
R.id.menu_add_otp -> {
|
||||||
setupOTP()
|
setupOTP()
|
||||||
true
|
true
|
||||||
@@ -228,15 +262,19 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// To retrieve attachment
|
||||||
|
mSelectFileHelper = SelectFileHelper(this)
|
||||||
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
|
|
||||||
// Save button
|
// Save button
|
||||||
saveView = findViewById(R.id.entry_edit_validate)
|
validateButton = findViewById(R.id.entry_edit_validate)
|
||||||
saveView?.setOnClickListener { saveEntry() }
|
validateButton?.setOnClickListener { saveEntry() }
|
||||||
|
|
||||||
// Verify the education views
|
// Verify the education views
|
||||||
entryEditActivityEducation = EntryEditActivityEducation(this)
|
entryEditActivityEducation = EntryEditActivityEducation(this)
|
||||||
|
|
||||||
// Create progress dialog
|
// Create progress dialog
|
||||||
mProgressDialogThread?.onActionFinish = { actionTask, result ->
|
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
||||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||||
@@ -256,6 +294,57 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
} else {
|
} else {
|
||||||
View.GONE
|
View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Padding if lock button visible
|
||||||
|
entryEditAddToolBar?.updateLockPaddingLeft()
|
||||||
|
|
||||||
|
mAllowMultipleAttachments = mDatabase?.allowMultipleAttachments == true
|
||||||
|
mAttachmentFileBinderManager?.apply {
|
||||||
|
registerProgressTask()
|
||||||
|
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||||
|
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||||
|
when (entryAttachmentState.downloadState) {
|
||||||
|
AttachmentState.START -> {
|
||||||
|
entryEditContentsView?.apply {
|
||||||
|
// When only one attachment is allowed
|
||||||
|
if (!mAllowMultipleAttachments) {
|
||||||
|
clearAttachments()
|
||||||
|
}
|
||||||
|
putAttachment(entryAttachmentState)
|
||||||
|
requestLayout()
|
||||||
|
// Scroll to the attachment position
|
||||||
|
getAttachmentViewPosition(entryAttachmentState) {
|
||||||
|
scrollView?.smoothScrollTo(0, it.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AttachmentState.IN_PROGRESS -> {
|
||||||
|
entryEditContentsView?.putAttachment(entryAttachmentState)
|
||||||
|
}
|
||||||
|
AttachmentState.COMPLETE -> {
|
||||||
|
entryEditContentsView?.apply {
|
||||||
|
putAttachment(entryAttachmentState)
|
||||||
|
// Scroll to the attachment position
|
||||||
|
getAttachmentViewPosition(entryAttachmentState) {
|
||||||
|
scrollView?.smoothScrollTo(0, it.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AttachmentState.ERROR -> {
|
||||||
|
mDatabase?.removeAttachmentIfNotUsed(entryAttachmentState.attachment)
|
||||||
|
entryEditContentsView?.removeAttachment(entryAttachmentState)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
mAttachmentFileBinderManager?.unregisterProgressTask()
|
||||||
|
|
||||||
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun populateViewsWithEntry(newEntry: Entry) {
|
private fun populateViewsWithEntry(newEntry: Entry) {
|
||||||
@@ -278,9 +367,15 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
if (expires)
|
if (expires)
|
||||||
expiresDate = newEntry.expiryTime
|
expiresDate = newEntry.expiryTime
|
||||||
notes = newEntry.notes
|
notes = newEntry.notes
|
||||||
for (entry in newEntry.customFields.entries) {
|
assignExtraFields(newEntry.customFields.mapTo(ArrayList()) {
|
||||||
post {
|
Field(it.key, it.value)
|
||||||
putCustomField(entry.key, entry.value)
|
}, {
|
||||||
|
editCustomField(it)
|
||||||
|
}, mFocusedEditExtraField)
|
||||||
|
|
||||||
|
mDatabase?.binaryPool?.let { binaryPool ->
|
||||||
|
assignAttachments(newEntry.getAttachments(binaryPool).toSet(), StreamDirection.UPLOAD) { attachment ->
|
||||||
|
newEntry.removeAttachment(attachment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,10 +397,16 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
if (entryView.expires) {
|
if (entryView.expires) {
|
||||||
expiryTime = entryView.expiresDate
|
expiryTime = entryView.expiresDate
|
||||||
}
|
}
|
||||||
notes = entryView. notes
|
notes = entryView.notes
|
||||||
entryView.customFields.forEach { customField ->
|
entryView.getExtraFields().forEach { customField ->
|
||||||
putExtraField(customField.name, customField.protectedValue)
|
putExtraField(customField.name, customField.protectedValue)
|
||||||
}
|
}
|
||||||
|
mDatabase?.binaryPool?.let { binaryPool ->
|
||||||
|
entryView.getAttachments().forEach {
|
||||||
|
putAttachment(it, binaryPool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mFocusedEditExtraField = entryView.getExtraFieldFocused()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,12 +428,90 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new customized field view and scroll to bottom
|
* Add a new customized field
|
||||||
*/
|
*/
|
||||||
private fun addNewCustomField() {
|
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) {
|
||||||
|
entryEditContentsView?.apply {
|
||||||
|
putExtraField(newField)
|
||||||
|
getExtraFieldViewPosition(newField) { position ->
|
||||||
|
scrollView?.smoothScrollTo(0, position.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEditCustomFieldApproved(oldField: Field, newField: Field) {
|
||||||
|
entryEditContentsView?.replaceExtraField(oldField, newField)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeleteCustomFieldApproved(oldField: Field) {
|
||||||
|
entryEditContentsView?.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?) {
|
||||||
|
if (attachmentToUploadUri != null && attachment != null) {
|
||||||
|
mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) {
|
||||||
|
val compression = mDatabase?.compressionForNewEntry() ?: false
|
||||||
|
mDatabase?.buildNewBinary(applicationContext.filesDir, false, compression)?.let { binaryAttachment ->
|
||||||
|
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||||
|
// Ask to replace the current attachment
|
||||||
|
if ((mDatabase?.allowMultipleAttachments != true && entryEditContentsView?.containsAttachment() == true) ||
|
||||||
|
entryEditContentsView?.containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD)) == true) {
|
||||||
|
ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment)
|
||||||
|
.show(supportFragmentManager, "replacementFileFragment")
|
||||||
|
} else {
|
||||||
|
mAttachmentFileBinderManager?.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() {
|
private fun setupOTP() {
|
||||||
// Retrieve the current otpElement if exists
|
// Retrieve the current otpElement if exists
|
||||||
// and open the dialog to set up the OTP
|
// and open the dialog to set up the OTP
|
||||||
@@ -344,7 +523,6 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
* Saves the new entry or update an existing entry in the database
|
* Saves the new entry or update an existing entry in the database
|
||||||
*/
|
*/
|
||||||
private fun saveEntry() {
|
private fun saveEntry() {
|
||||||
|
|
||||||
// Launch a validation and show the error if present
|
// Launch a validation and show the error if present
|
||||||
if (entryEditContentsView?.isValid() == true) {
|
if (entryEditContentsView?.isValid() == true) {
|
||||||
// Clone the entry
|
// Clone the entry
|
||||||
@@ -361,7 +539,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
// Open a progress dialog and save entry
|
// Open a progress dialog and save entry
|
||||||
if (mIsNew) {
|
if (mIsNew) {
|
||||||
mParent?.let { parent ->
|
mParent?.let { parent ->
|
||||||
mProgressDialogThread?.startDatabaseCreateEntry(
|
mProgressDatabaseTaskProvider?.startDatabaseCreateEntry(
|
||||||
newEntry,
|
newEntry,
|
||||||
parent,
|
parent,
|
||||||
!mReadOnly && mAutoSaveEnable
|
!mReadOnly && mAutoSaveEnable
|
||||||
@@ -369,7 +547,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mEntry?.let { oldEntry ->
|
mEntry?.let { oldEntry ->
|
||||||
mProgressDialogThread?.startDatabaseUpdateEntry(
|
mProgressDatabaseTaskProvider?.startDatabaseUpdateEntry(
|
||||||
oldEntry,
|
oldEntry,
|
||||||
newEntry,
|
newEntry,
|
||||||
!mReadOnly && mAutoSaveEnable
|
!mReadOnly && mAutoSaveEnable
|
||||||
@@ -397,7 +575,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||||
val passwordGeneratorView: View? = entryEditAddToolBar?.findViewById(R.id.menu_generate_password)
|
val passwordGeneratorView: View? = entryEditContentsView?.entryPasswordGeneratorView
|
||||||
val generatePasswordEducationPerformed = passwordGeneratorView != null
|
val generatePasswordEducationPerformed = passwordGeneratorView != null
|
||||||
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||||
passwordGeneratorView,
|
passwordGeneratorView,
|
||||||
@@ -411,8 +589,8 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
if (!generatePasswordEducationPerformed) {
|
if (!generatePasswordEducationPerformed) {
|
||||||
val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field)
|
val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field)
|
||||||
val addNewFieldEducationPerformed = mNewEntry != null
|
val addNewFieldEducationPerformed = mNewEntry != null
|
||||||
&& mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
|
&& mNewEntry!!.allowCustomFields() && addNewFieldView != null
|
||||||
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
|
&& addNewFieldView.visibility == View.VISIBLE
|
||||||
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
||||||
addNewFieldView,
|
addNewFieldView,
|
||||||
{
|
{
|
||||||
@@ -423,13 +601,27 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!addNewFieldEducationPerformed) {
|
if (!addNewFieldEducationPerformed) {
|
||||||
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
|
val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment)
|
||||||
setupOtpView != null && setupOtpView.visibility == View.VISIBLE
|
val addAttachmentEducationPerformed = attachmentView != null && attachmentView.visibility == View.VISIBLE
|
||||||
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
||||||
setupOtpView,
|
attachmentView,
|
||||||
{
|
{
|
||||||
setupOTP()
|
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(attachmentView)
|
||||||
})
|
},
|
||||||
|
{
|
||||||
|
performedNextEducation(entryEditActivityEducation)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (!addAttachmentEducationPerformed) {
|
||||||
|
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
|
||||||
|
setupOtpView != null && setupOtpView.visibility == View.VISIBLE
|
||||||
|
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
||||||
|
setupOtpView,
|
||||||
|
{
|
||||||
|
setupOTP()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -437,7 +629,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.menu_save_database -> {
|
R.id.menu_save_database -> {
|
||||||
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
|
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
||||||
}
|
}
|
||||||
R.id.menu_contribute -> {
|
R.id.menu_contribute -> {
|
||||||
MenuUtil.onContributionItemSelected(this)
|
MenuUtil.onContributionItemSelected(this)
|
||||||
@@ -455,8 +647,13 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
// Update the otp field with otpauth:// url
|
// Update the otp field with otpauth:// url
|
||||||
val otpField = OtpEntryFields.buildOtpField(otpElement,
|
val otpField = OtpEntryFields.buildOtpField(otpElement,
|
||||||
mEntry?.title, mEntry?.username)
|
mEntry?.title, mEntry?.username)
|
||||||
entryEditContentsView?.putCustomField(otpField.name, otpField.protectedValue)
|
|
||||||
mEntry?.putExtraField(otpField.name, otpField.protectedValue)
|
mEntry?.putExtraField(otpField.name, otpField.protectedValue)
|
||||||
|
entryEditContentsView?.apply {
|
||||||
|
putExtraField(otpField)
|
||||||
|
getExtraFieldViewPosition(otpField) { position ->
|
||||||
|
scrollView?.smoothScrollTo(0, position.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun iconPicked(bundle: Bundle) {
|
override fun iconPicked(bundle: Bundle) {
|
||||||
@@ -504,6 +701,10 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
outState.putParcelable(KEY_NEW_ENTRY, it)
|
outState.putParcelable(KEY_NEW_ENTRY, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mFocusedEditExtraField?.let {
|
||||||
|
outState.putParcelable(EXTRA_FIELD_FOCUSED_ENTRY, it)
|
||||||
|
}
|
||||||
|
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,6 +762,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
|
|
||||||
// SaveInstanceState
|
// SaveInstanceState
|
||||||
const val KEY_NEW_ENTRY = "new_entry"
|
const val KEY_NEW_ENTRY = "new_entry"
|
||||||
|
const val EXTRA_FIELD_FOCUSED_ENTRY = "EXTRA_FIELD_FOCUSED_ENTRY"
|
||||||
|
|
||||||
// Keys for callback
|
// Keys for callback
|
||||||
const val ADD_ENTRY_RESULT_CODE = 31
|
const val ADD_ENTRY_RESULT_CODE = 31
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
|
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity to search or select entry in database,
|
||||||
|
* Commonly used with Magikeyboard
|
||||||
|
*/
|
||||||
|
class EntrySelectionLauncherActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
|
var sharedWebDomain: String? = null
|
||||||
|
|
||||||
|
when (intent?.action) {
|
||||||
|
Intent.ACTION_SEND -> {
|
||||||
|
if ("text/plain" == intent.type) {
|
||||||
|
// Retrieve web domain
|
||||||
|
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||||
|
sharedWebDomain = Uri.parse(it).host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setting to integrate Magikeyboard
|
||||||
|
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
||||||
|
|
||||||
|
// Build search param
|
||||||
|
val searchInfo = SearchInfo().apply {
|
||||||
|
webDomain = sharedWebDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
// If database is open
|
||||||
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
Database.getInstance(),
|
||||||
|
searchInfo,
|
||||||
|
{ items ->
|
||||||
|
// Items found
|
||||||
|
if (searchShareForMagikeyboard) {
|
||||||
|
if (items.size == 1) {
|
||||||
|
// Automatically populate keyboard
|
||||||
|
val entryPopulate = items[0]
|
||||||
|
populateKeyboardAndMoveAppToBackground(this,
|
||||||
|
entryPopulate,
|
||||||
|
intent)
|
||||||
|
} else {
|
||||||
|
// Select the one we want
|
||||||
|
GroupActivity.launchForEntrySelectionResult(this,
|
||||||
|
true,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
GroupActivity.launch(this,
|
||||||
|
true,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
if (searchShareForMagikeyboard) {
|
||||||
|
GroupActivity.launchForEntrySelectionResult(this,
|
||||||
|
false,
|
||||||
|
searchInfo)
|
||||||
|
} else {
|
||||||
|
GroupActivity.launch(this,
|
||||||
|
false,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If database not open
|
||||||
|
if (searchShareForMagikeyboard) {
|
||||||
|
FileDatabaseSelectActivity.launchForEntrySelectionResult(this,
|
||||||
|
searchInfo)
|
||||||
|
} else {
|
||||||
|
FileDatabaseSelectActivity.launch(this,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
finish()
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
||||||
|
entry: EntryInfo,
|
||||||
|
intent: Intent,
|
||||||
|
toast: Boolean = true) {
|
||||||
|
// Populate Magikeyboard with entry
|
||||||
|
MagikIME.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
||||||
|
// Consume the selection mode
|
||||||
|
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
|
||||||
|
activity.moveTaskToBack(true)
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.activities
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -31,9 +32,11 @@ import android.util.Log
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
@@ -41,32 +44,36 @@ import com.google.android.material.snackbar.Snackbar
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper.KEY_SEARCH_INFO
|
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
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.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||||
import kotlinx.android.synthetic.main.activity_file_selection.*
|
import kotlinx.android.synthetic.main.activity_file_selection.*
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
class FileDatabaseSelectActivity : StylishActivity(),
|
class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var fileManagerExplanationButton: View? = null
|
private var createDatabaseButtonView: View? = null
|
||||||
private var createButtonView: View? = null
|
|
||||||
private var openDatabaseButtonView: View? = null
|
private var openDatabaseButtonView: View? = null
|
||||||
|
|
||||||
|
private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels()
|
||||||
|
|
||||||
// Adapter to manage database history list
|
// Adapter to manage database history list
|
||||||
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
||||||
|
|
||||||
@@ -74,9 +81,9 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -90,28 +97,15 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
toolbar.title = ""
|
toolbar.title = ""
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
|
|
||||||
fileManagerExplanationButton = findViewById(R.id.file_manager_explanation_button)
|
// Create database button
|
||||||
fileManagerExplanationButton?.setOnClickListener {
|
createDatabaseButtonView = findViewById(R.id.create_database_button)
|
||||||
UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
|
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
||||||
}
|
|
||||||
|
|
||||||
// Create button
|
// Open database button
|
||||||
createButtonView = findViewById(R.id.create_database_button)
|
mSelectFileHelper = SelectFileHelper(this)
|
||||||
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
|
||||||
// There is an activity which can handle this intent.
|
|
||||||
createButtonView?.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
// No Activity found that can handle this intent.
|
|
||||||
createButtonView?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
createButtonView?.setOnClickListener { createNewFile() }
|
|
||||||
|
|
||||||
mOpenFileHelper = OpenFileHelper(this)
|
|
||||||
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
||||||
openDatabaseButtonView?.apply {
|
openDatabaseButtonView?.apply {
|
||||||
mOpenFileHelper?.openFileOnClickViewListener?.let {
|
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
||||||
setOnClickListener(it)
|
setOnClickListener(it)
|
||||||
setOnLongClickListener(it)
|
setOnLongClickListener(it)
|
||||||
}
|
}
|
||||||
@@ -124,26 +118,25 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
(fileDatabaseHistoryRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
(fileDatabaseHistoryRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
// Construct adapter with listeners
|
// Construct adapter with listeners
|
||||||
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this)
|
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this)
|
||||||
|
mAdapterDatabaseHistory?.setOnDefaultDatabaseListener { databaseFile ->
|
||||||
|
databaseFilesViewModel.setDefaultDatabase(databaseFile)
|
||||||
|
}
|
||||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||||
UriUtil.parse(fileDatabaseHistoryEntityToOpen.databaseUri)?.let { databaseFileUri ->
|
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
||||||
launchPasswordActivity(
|
launchPasswordActivity(
|
||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
UriUtil.parse(fileDatabaseHistoryEntityToOpen.keyFileUri))
|
fileDatabaseHistoryEntityToOpen.keyFileUri
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
||||||
// Remove from app database
|
// Remove from app database
|
||||||
mFileDatabaseHistoryAction?.deleteFileDatabaseHistory(fileDatabaseHistoryToDelete) { fileHistoryDeleted ->
|
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
|
||||||
// Remove from adapter
|
|
||||||
fileHistoryDeleted?.let { databaseFileHistoryDeleted ->
|
|
||||||
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileHistoryDeleted)
|
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
mAdapterDatabaseHistory?.setOnSaveAliasListener { fileDatabaseHistoryWithNewAlias ->
|
mAdapterDatabaseHistory?.setOnSaveAliasListener { fileDatabaseHistoryWithNewAlias ->
|
||||||
mFileDatabaseHistoryAction?.addOrUpdateFileDatabaseHistory(fileDatabaseHistoryWithNewAlias)
|
// Update in app database
|
||||||
|
databaseFilesViewModel.updateDatabaseFile(fileDatabaseHistoryWithNewAlias)
|
||||||
}
|
}
|
||||||
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
|
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
|
||||||
|
|
||||||
@@ -156,7 +149,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
UriUtil.parse(databasePath)?.let { databaseFileUri ->
|
UriUtil.parse(databasePath)?.let { databaseFileUri ->
|
||||||
launchPasswordActivityWithPath(databaseFileUri)
|
launchPasswordActivityWithPath(databaseFileUri)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
Log.i(TAG, "Unable to launch Password Activity")
|
Log.i(TAG, "No default database to prepare")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,12 +159,47 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
|
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
|
// Attach the dialog thread to this activity
|
||||||
mProgressDialogThread = ProgressDialogThread(this).apply {
|
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
||||||
onActionFinish = { actionTask, _ ->
|
onActionFinish = { actionTask, result ->
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_CREATE_TASK -> {
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,23 +224,30 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
||||||
|
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
PasswordActivity.launch(this@FileDatabaseSelectActivity,
|
PasswordActivity.launch(this@FileDatabaseSelectActivity,
|
||||||
databaseUri, keyFile)
|
databaseUri, keyFile,
|
||||||
|
searchInfo)
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
fileNoFoundAction(e)
|
fileNoFoundAction(e)
|
||||||
}
|
}
|
||||||
|
// Remove the search info from intent
|
||||||
|
if (searchInfo != null) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
|
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
|
||||||
databaseUri, keyFile)
|
databaseUri, keyFile,
|
||||||
finish()
|
searchInfo)
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
fileNoFoundAction(e)
|
fileNoFoundAction(e)
|
||||||
}
|
}
|
||||||
|
finish()
|
||||||
},
|
},
|
||||||
{ assistStructure ->
|
{ assistStructure ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
@@ -220,23 +255,27 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
||||||
databaseUri, keyFile,
|
databaseUri, keyFile,
|
||||||
assistStructure,
|
assistStructure,
|
||||||
intent.getParcelableExtra(KEY_SEARCH_INFO))
|
searchInfo)
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
fileNoFoundAction(e)
|
fileNoFoundAction(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivity(readOnly: Boolean) {
|
private fun launchGroupActivity(readOnly: Boolean) {
|
||||||
|
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||||
{
|
{
|
||||||
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
||||||
|
false,
|
||||||
|
searchInfo,
|
||||||
readOnly)
|
readOnly)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
GroupActivity.launchForKeyboardSelection(this@FileDatabaseSelectActivity,
|
GroupActivity.launchForEntrySelectionResult(this@FileDatabaseSelectActivity,
|
||||||
|
false,
|
||||||
|
searchInfo,
|
||||||
readOnly)
|
readOnly)
|
||||||
// Do not keep history
|
// Do not keep history
|
||||||
finish()
|
finish()
|
||||||
@@ -245,7 +284,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
||||||
assistStructure,
|
assistStructure,
|
||||||
intent.getParcelableExtra(KEY_SEARCH_INFO),
|
false,
|
||||||
|
searchInfo,
|
||||||
readOnly)
|
readOnly)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -259,42 +299,42 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// Show open and create button or special mode
|
||||||
|
if (mSelectionMode) {
|
||||||
|
// Disable create button if in selection mode or request for autofill
|
||||||
|
createDatabaseButtonView?.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
||||||
|
// There is an activity which can handle this intent.
|
||||||
|
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||||
|
} else{
|
||||||
|
// No Activity found that can handle this intent.
|
||||||
|
createDatabaseButtonView?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val database = Database.getInstance()
|
val database = Database.getInstance()
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
launchGroupActivity(database.isReadOnly)
|
launchGroupActivity(database.isReadOnly)
|
||||||
}
|
|
||||||
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
// Construct adapter with listeners
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
if (PreferencesUtil.showRecentFiles(this)) {
|
||||||
}
|
databaseFilesViewModel.loadListOfDatabases()
|
||||||
|
} else {
|
||||||
|
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
||||||
|
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
// Register progress task
|
// Register progress task
|
||||||
mProgressDialogThread?.registerProgressTask()
|
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
// Unregister progress task
|
// Unregister progress task
|
||||||
mProgressDialogThread?.unregisterProgressTask()
|
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
@@ -315,7 +355,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
mDatabaseFileUri?.let { databaseUri ->
|
mDatabaseFileUri?.let { databaseUri ->
|
||||||
|
|
||||||
// Create the new database
|
// Create the new database
|
||||||
mProgressDialogThread?.startDatabaseCreate(
|
mProgressDatabaseTaskProvider?.startDatabaseCreate(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
masterPasswordChecked,
|
masterPasswordChecked,
|
||||||
masterPassword,
|
masterPassword,
|
||||||
@@ -343,8 +383,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||||
) { uri ->
|
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
launchPasswordActivityWithPath(uri)
|
launchPasswordActivityWithPath(uri)
|
||||||
}
|
}
|
||||||
@@ -368,7 +407,10 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
|
||||||
|
if (!mSelectionMode) {
|
||||||
|
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
||||||
|
}
|
||||||
|
|
||||||
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
||||||
|
|
||||||
@@ -377,11 +419,12 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
|
|
||||||
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
||||||
// If no recent files
|
// If no recent files
|
||||||
val createDatabaseEducationPerformed = createButtonView != null && createButtonView!!.visibility == View.VISIBLE
|
val createDatabaseEducationPerformed =
|
||||||
|
createDatabaseButtonView != null && createDatabaseButtonView!!.visibility == View.VISIBLE
|
||||||
&& mAdapterDatabaseHistory != null
|
&& mAdapterDatabaseHistory != null
|
||||||
&& mAdapterDatabaseHistory!!.itemCount > 0
|
&& mAdapterDatabaseHistory!!.itemCount > 0
|
||||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
||||||
createButtonView!!,
|
createDatabaseButtonView!!,
|
||||||
{
|
{
|
||||||
createNewFile()
|
createNewFile()
|
||||||
},
|
},
|
||||||
@@ -396,7 +439,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
openDatabaseButtonView!!,
|
openDatabaseButtonView!!,
|
||||||
{tapTargetView ->
|
{tapTargetView ->
|
||||||
tapTargetView?.let {
|
tapTargetView?.let {
|
||||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
|
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
@@ -405,6 +448,10 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
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)
|
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,18 +463,30 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* No Standard Launch, pass by PasswordActivity
|
* Launch only to standard search, else pass by PasswordActivity
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
fun launch(context: Context,
|
||||||
|
searchInfo: SearchInfo? = null) {
|
||||||
|
val intent = Intent(context, FileDatabaseSelectActivity::class.java)
|
||||||
|
searchInfo?.let {
|
||||||
|
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Keyboard Launch
|
* Keyboard Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun launchForKeyboardSelection(activity: Activity) {
|
fun launchForEntrySelectionResult(activity: Activity,
|
||||||
EntrySelectionHelper.startActivityForEntrySelection(activity, Intent(activity, FileDatabaseSelectActivity::class.java))
|
searchInfo: SearchInfo? = null) {
|
||||||
|
EntrySelectionHelper.startActivityForEntrySelectionResult(activity,
|
||||||
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
|
searchInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -439,7 +498,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: Activity,
|
fun launchForAutofillResult(activity: Activity,
|
||||||
assistStructure: AssistStructure,
|
assistStructure: AssistStructure,
|
||||||
searchInfo: SearchInfo?) {
|
searchInfo: SearchInfo? = null) {
|
||||||
AutofillHelper.startActivityForAutofillResult(activity,
|
AutofillHelper.startActivityForAutofillResult(activity,
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
assistStructure,
|
assistStructure,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import android.util.Log
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
@@ -47,6 +48,7 @@ import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
|
|||||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
|
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
|
||||||
@@ -61,8 +63,8 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
|||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.model.getSearchString
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||||
@@ -74,10 +76,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
|
|||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.view.AddNodeButtonView
|
import com.kunzisoft.keepass.view.*
|
||||||
import com.kunzisoft.keepass.view.ToolbarAction
|
|
||||||
import com.kunzisoft.keepass.view.asError
|
|
||||||
import com.kunzisoft.keepass.view.showActionError
|
|
||||||
|
|
||||||
class GroupActivity : LockingActivity(),
|
class GroupActivity : LockingActivity(),
|
||||||
GroupEditDialogFragment.EditGroupListener,
|
GroupEditDialogFragment.EditGroupListener,
|
||||||
@@ -89,6 +88,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
SortDialogFragment.SortSelectionListener {
|
SortDialogFragment.SortSelectionListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
|
private var rootContainerView: ViewGroup? = null
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var lockView: View? = null
|
private var lockView: View? = null
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
@@ -96,7 +96,6 @@ class GroupActivity : LockingActivity(),
|
|||||||
private var toolbarAction: ToolbarAction? = null
|
private var toolbarAction: ToolbarAction? = null
|
||||||
private var iconView: ImageView? = null
|
private var iconView: ImageView? = null
|
||||||
private var numberChildrenView: TextView? = null
|
private var numberChildrenView: TextView? = null
|
||||||
private var modeTitleView: TextView? = null
|
|
||||||
private var addNodeButtonView: AddNodeButtonView? = null
|
private var addNodeButtonView: AddNodeButtonView? = null
|
||||||
private var groupNameView: TextView? = null
|
private var groupNameView: TextView? = null
|
||||||
|
|
||||||
@@ -106,6 +105,11 @@ class GroupActivity : LockingActivity(),
|
|||||||
private var mCurrentGroupIsASearch: Boolean = false
|
private var mCurrentGroupIsASearch: Boolean = false
|
||||||
private var mRequestStartupSearch = true
|
private var mRequestStartupSearch = true
|
||||||
|
|
||||||
|
private var actionNodeMode: ActionMode? = null
|
||||||
|
|
||||||
|
// To manage history in selection mode
|
||||||
|
private var mSelectionModeCountBackStack = 0
|
||||||
|
|
||||||
// Nodes
|
// Nodes
|
||||||
private var mRootGroup: Group? = null
|
private var mRootGroup: Group? = null
|
||||||
private var mCurrentGroup: Group? = null
|
private var mCurrentGroup: Group? = null
|
||||||
@@ -118,15 +122,13 @@ class GroupActivity : LockingActivity(),
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
if (isFinishing) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mDatabase = Database.getInstance()
|
mDatabase = Database.getInstance()
|
||||||
|
|
||||||
// Construct main view
|
// Construct main view
|
||||||
setContentView(layoutInflater.inflate(R.layout.activity_group, null))
|
setContentView(layoutInflater.inflate(R.layout.activity_group, null))
|
||||||
|
|
||||||
// Initialize views
|
// Initialize views
|
||||||
|
rootContainerView = findViewById(R.id.activity_group_container_view)
|
||||||
coordinatorLayout = findViewById(R.id.group_coordinator)
|
coordinatorLayout = findViewById(R.id.group_coordinator)
|
||||||
iconView = findViewById(R.id.group_icon)
|
iconView = findViewById(R.id.group_icon)
|
||||||
numberChildrenView = findViewById(R.id.group_numbers)
|
numberChildrenView = findViewById(R.id.group_numbers)
|
||||||
@@ -135,7 +137,6 @@ class GroupActivity : LockingActivity(),
|
|||||||
searchTitleView = findViewById(R.id.search_title)
|
searchTitleView = findViewById(R.id.search_title)
|
||||||
groupNameView = findViewById(R.id.group_name)
|
groupNameView = findViewById(R.id.group_name)
|
||||||
toolbarAction = findViewById(R.id.toolbar_action)
|
toolbarAction = findViewById(R.id.toolbar_action)
|
||||||
modeTitleView = findViewById(R.id.mode_title_view)
|
|
||||||
lockView = findViewById(R.id.lock_button)
|
lockView = findViewById(R.id.lock_button)
|
||||||
|
|
||||||
lockView?.setOnClickListener {
|
lockView?.setOnClickListener {
|
||||||
@@ -145,8 +146,13 @@ class GroupActivity : LockingActivity(),
|
|||||||
toolbar?.title = ""
|
toolbar?.title = ""
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
|
|
||||||
|
// Retrieve the textColor to tint the icon
|
||||||
|
val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||||
|
mIconColor = taTextColor.getColor(0, Color.WHITE)
|
||||||
|
taTextColor.recycle()
|
||||||
|
|
||||||
// Focus view to reinitialize timeout
|
// Focus view to reinitialize timeout
|
||||||
resetAppTimeoutWhenViewFocusedOrChanged(addNodeButtonView)
|
resetAppTimeoutWhenViewFocusedOrChanged(rootContainerView)
|
||||||
|
|
||||||
// Retrieve elements after an orientation change
|
// Retrieve elements after an orientation change
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
@@ -171,14 +177,6 @@ class GroupActivity : LockingActivity(),
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update last access time.
|
|
||||||
mCurrentGroup?.touch(modified = false, touchParents = false)
|
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
|
||||||
val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
|
||||||
mIconColor = taTextColor.getColor(0, Color.WHITE)
|
|
||||||
taTextColor.recycle()
|
|
||||||
|
|
||||||
var fragmentTag = LIST_NODES_FRAGMENT_TAG
|
var fragmentTag = LIST_NODES_FRAGMENT_TAG
|
||||||
if (mCurrentGroupIsASearch)
|
if (mCurrentGroupIsASearch)
|
||||||
fragmentTag = SEARCH_FRAGMENT_TAG
|
fragmentTag = SEARCH_FRAGMENT_TAG
|
||||||
@@ -195,6 +193,14 @@ class GroupActivity : LockingActivity(),
|
|||||||
fragmentTag)
|
fragmentTag)
|
||||||
.commit()
|
.commit()
|
||||||
|
|
||||||
|
// Update last access time.
|
||||||
|
mCurrentGroup?.touch(modified = false, touchParents = false)
|
||||||
|
|
||||||
|
// To relaunch the activity with ACTION_SEARCH
|
||||||
|
if (manageSearchInfoIntent(intent)) {
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
// Add listeners to the add buttons
|
// Add listeners to the add buttons
|
||||||
addNodeButtonView?.setAddGroupClickListener(View.OnClickListener {
|
addNodeButtonView?.setAddGroupClickListener(View.OnClickListener {
|
||||||
GroupEditDialogFragment.build()
|
GroupEditDialogFragment.build()
|
||||||
@@ -212,7 +218,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, database)
|
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, database)
|
||||||
|
|
||||||
// Init dialog thread
|
// Init dialog thread
|
||||||
mProgressDialogThread?.onActionFinish = { actionTask, result ->
|
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
||||||
|
|
||||||
var oldNodes: List<Node> = ArrayList()
|
var oldNodes: List<Node> = ArrayList()
|
||||||
result.data?.getBundle(OLD_NODES_KEY)?.let { oldNodesBundle ->
|
result.data?.getBundle(OLD_NODES_KEY)?.let { oldNodesBundle ->
|
||||||
@@ -223,6 +229,8 @@ class GroupActivity : LockingActivity(),
|
|||||||
newNodes = getListNodesFromBundle(database, newNodesBundle)
|
newNodes = getListNodesFromBundle(database, newNodesBundle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshSearchGroup()
|
||||||
|
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_UPDATE_GROUP_TASK -> {
|
ACTION_DATABASE_UPDATE_GROUP_TASK -> {
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
@@ -277,11 +285,14 @@ class GroupActivity : LockingActivity(),
|
|||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
|
|
||||||
intent?.let { intentNotNull ->
|
intent?.let { intentNotNull ->
|
||||||
|
// To transform KEY_SEARCH_INFO in ACTION_SEARCH
|
||||||
|
manageSearchInfoIntent(intentNotNull)
|
||||||
Log.d(TAG, "setNewIntent: $intentNotNull")
|
Log.d(TAG, "setNewIntent: $intentNotNull")
|
||||||
setIntent(intentNotNull)
|
setIntent(intentNotNull)
|
||||||
mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intentNotNull.action) {
|
mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intentNotNull.action) {
|
||||||
// only one instance of search in backstack
|
// only one instance of search in backstack
|
||||||
openSearchGroup(retrieveCurrentGroup(intentNotNull, null))
|
deletePreviousSearchGroup()
|
||||||
|
openGroup(retrieveCurrentGroup(intentNotNull, null), true)
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
@@ -289,20 +300,34 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openSearchGroup(group: Group?) {
|
/**
|
||||||
// Delete the previous search fragment
|
* Transform the KEY_SEARCH_INFO in ACTION_SEARCH, return true if KEY_SEARCH_INFO was present
|
||||||
val searchFragment = supportFragmentManager.findFragmentByTag(SEARCH_FRAGMENT_TAG)
|
*/
|
||||||
if (searchFragment != null) {
|
private fun manageSearchInfoIntent(intent: Intent): Boolean {
|
||||||
if (supportFragmentManager
|
// To relaunch the activity as ACTION_SEARCH
|
||||||
.popBackStackImmediate(SEARCH_FRAGMENT_TAG,
|
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||||
FragmentManager.POP_BACK_STACK_INCLUSIVE))
|
val autoSearch = intent.getBooleanExtra(AUTO_SEARCH_KEY, false)
|
||||||
supportFragmentManager.beginTransaction().remove(searchFragment).commit()
|
if (searchInfo != null && autoSearch) {
|
||||||
|
intent.action = Intent.ACTION_SEARCH
|
||||||
|
intent.putExtra(SearchManager.QUERY, searchInfo.getSearchString(this))
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
openGroup(group, true)
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openChildGroup(group: Group) {
|
private fun deletePreviousSearchGroup() {
|
||||||
openGroup(group, false)
|
// Delete the previous search fragment
|
||||||
|
try {
|
||||||
|
val searchFragment = supportFragmentManager.findFragmentByTag(SEARCH_FRAGMENT_TAG)
|
||||||
|
if (searchFragment != null) {
|
||||||
|
if (supportFragmentManager
|
||||||
|
.popBackStackImmediate(SEARCH_FRAGMENT_TAG,
|
||||||
|
FragmentManager.POP_BACK_STACK_INCLUSIVE))
|
||||||
|
supportFragmentManager.beginTransaction().remove(searchFragment).commit()
|
||||||
|
}
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.e(TAG, "unable to remove previous search fragment", exception)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openGroup(group: Group?, isASearch: Boolean) {
|
private fun openGroup(group: Group?, isASearch: Boolean) {
|
||||||
@@ -329,6 +354,12 @@ class GroupActivity : LockingActivity(),
|
|||||||
fragmentTransaction.addToBackStack(fragmentTag)
|
fragmentTransaction.addToBackStack(fragmentTag)
|
||||||
fragmentTransaction.commit()
|
fragmentTransaction.commit()
|
||||||
|
|
||||||
|
if (mSelectionMode)
|
||||||
|
mSelectionModeCountBackStack++
|
||||||
|
|
||||||
|
// Update last access time.
|
||||||
|
group?.touch(modified = false, touchParents = false)
|
||||||
|
|
||||||
mListNodesFragment = newListNodeFragment
|
mListNodesFragment = newListNodeFragment
|
||||||
mCurrentGroup = group
|
mCurrentGroup = group
|
||||||
assignGroupViewElements()
|
assignGroupViewElements()
|
||||||
@@ -346,6 +377,12 @@ class GroupActivity : LockingActivity(),
|
|||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun refreshSearchGroup() {
|
||||||
|
deletePreviousSearchGroup()
|
||||||
|
if (mCurrentGroupIsASearch)
|
||||||
|
openGroup(retrieveCurrentGroup(intent, null), true)
|
||||||
|
}
|
||||||
|
|
||||||
private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): Group? {
|
private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): Group? {
|
||||||
|
|
||||||
// Force read only if the database is like that
|
// Force read only if the database is like that
|
||||||
@@ -354,7 +391,8 @@ class GroupActivity : LockingActivity(),
|
|||||||
// If it's a search
|
// If it's a search
|
||||||
if (Intent.ACTION_SEARCH == intent.action) {
|
if (Intent.ACTION_SEARCH == intent.action) {
|
||||||
val searchString = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
|
val searchString = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
|
||||||
return mDatabase?.createVirtualGroupFromSearch(searchString)
|
return mDatabase?.createVirtualGroupFromSearch(searchString,
|
||||||
|
PreferencesUtil.omitBackup(this))
|
||||||
}
|
}
|
||||||
// else a real group
|
// else a real group
|
||||||
else {
|
else {
|
||||||
@@ -426,13 +464,6 @@ class GroupActivity : LockingActivity(),
|
|||||||
// Assign number of children
|
// Assign number of children
|
||||||
refreshNumberOfChildren()
|
refreshNumberOfChildren()
|
||||||
|
|
||||||
// Show selection mode message if needed
|
|
||||||
if (mSelectionMode) {
|
|
||||||
modeTitleView?.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
modeTitleView?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show button if allowed
|
// Show button if allowed
|
||||||
addNodeButtonView?.apply {
|
addNodeButtonView?.apply {
|
||||||
|
|
||||||
@@ -446,14 +477,29 @@ class GroupActivity : LockingActivity(),
|
|||||||
enableAddGroup(addGroupEnabled)
|
enableAddGroup(addGroupEnabled)
|
||||||
enableAddEntry(addEntryEnabled)
|
enableAddEntry(addEntryEnabled)
|
||||||
|
|
||||||
showButton()
|
if (actionNodeMode == null)
|
||||||
|
showButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCancelSpecialMode() {
|
||||||
|
// To remove the navigation history and
|
||||||
|
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
|
||||||
|
val fragmentManager = supportFragmentManager
|
||||||
|
if (mSelectionModeCountBackStack > 0) {
|
||||||
|
for (selectionMode in 0 .. mSelectionModeCountBackStack) {
|
||||||
|
fragmentManager.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Reinit the counter for navigation history
|
||||||
|
mSelectionModeCountBackStack = 0
|
||||||
|
backToTheAppCaller()
|
||||||
|
}
|
||||||
|
|
||||||
private fun refreshNumberOfChildren() {
|
private fun refreshNumberOfChildren() {
|
||||||
numberChildrenView?.apply {
|
numberChildrenView?.apply {
|
||||||
if (PreferencesUtil.showNumberEntries(context)) {
|
if (PreferencesUtil.showNumberEntries(context)) {
|
||||||
text = mCurrentGroup?.getNumberOfChildEntries(*Group.ChildFilter.getDefaults(context))?.toString() ?: ""
|
text = mCurrentGroup?.getNumberOfChildEntries(Group.ChildFilter.getDefaults(context))?.toString() ?: ""
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
@@ -462,13 +508,15 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onScrolled(dy: Int) {
|
override fun onScrolled(dy: Int) {
|
||||||
addNodeButtonView?.hideButtonOnScrollListener(dy)
|
if (actionNodeMode == null)
|
||||||
|
addNodeButtonView?.hideOrShowButtonOnScrollListener(dy)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNodeClick(node: Node) {
|
override fun onNodeClick(node: Node) {
|
||||||
when (node.type) {
|
when (node.type) {
|
||||||
Type.GROUP -> try {
|
Type.GROUP -> try {
|
||||||
openChildGroup(node as Group)
|
// Open child group
|
||||||
|
openGroup(node as Group, false)
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
Log.e(TAG, "Node can't be cast in Group")
|
Log.e(TAG, "Node can't be cast in Group")
|
||||||
}
|
}
|
||||||
@@ -480,14 +528,13 @@ class GroupActivity : LockingActivity(),
|
|||||||
EntryActivity.launch(this@GroupActivity, entryVersioned, mReadOnly)
|
EntryActivity.launch(this@GroupActivity, entryVersioned, mReadOnly)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
rebuildListNodes()
|
||||||
// Populate Magikeyboard with entry
|
// Populate Magikeyboard with entry
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
MagikIME.addEntryAndLaunchNotificationIfAllowed(this@GroupActivity,
|
populateKeyboardAndMoveAppToBackground(this@GroupActivity,
|
||||||
entryVersioned.getEntryInfo(database))
|
entryVersioned.getEntryInfo(database),
|
||||||
|
intent)
|
||||||
}
|
}
|
||||||
// Consume the selection mode
|
|
||||||
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
|
|
||||||
moveTaskToBack(true)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Build response with the entry selected
|
// Build response with the entry selected
|
||||||
@@ -505,18 +552,28 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var actionNodeMode: ActionMode? = null
|
|
||||||
|
|
||||||
private fun finishNodeAction() {
|
private fun finishNodeAction() {
|
||||||
actionNodeMode?.finish()
|
actionNodeMode?.finish()
|
||||||
actionNodeMode = null
|
|
||||||
addNodeButtonView?.showButton()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNodeSelected(nodes: List<Node>): Boolean {
|
override fun onNodeSelected(nodes: List<Node>): Boolean {
|
||||||
if (nodes.isNotEmpty()) {
|
if (nodes.isNotEmpty()) {
|
||||||
if (actionNodeMode == null || toolbarAction?.getSupportActionModeCallback() == null) {
|
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)
|
actionNodeMode = toolbarAction?.startSupportActionMode(it)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -573,7 +630,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
|
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
|
||||||
// Copy
|
// Copy
|
||||||
mCurrentGroup?.let { newParent ->
|
mCurrentGroup?.let { newParent ->
|
||||||
mProgressDialogThread?.startDatabaseCopyNodes(
|
mProgressDatabaseTaskProvider?.startDatabaseCopyNodes(
|
||||||
nodes,
|
nodes,
|
||||||
newParent,
|
newParent,
|
||||||
!mReadOnly && mAutoSaveEnable
|
!mReadOnly && mAutoSaveEnable
|
||||||
@@ -583,7 +640,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
|
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
|
||||||
// Move
|
// Move
|
||||||
mCurrentGroup?.let { newParent ->
|
mCurrentGroup?.let { newParent ->
|
||||||
mProgressDialogThread?.startDatabaseMoveNodes(
|
mProgressDatabaseTaskProvider?.startDatabaseMoveNodes(
|
||||||
nodes,
|
nodes,
|
||||||
newParent,
|
newParent,
|
||||||
!mReadOnly && mAutoSaveEnable
|
!mReadOnly && mAutoSaveEnable
|
||||||
@@ -616,7 +673,8 @@ class GroupActivity : LockingActivity(),
|
|||||||
if (database != null
|
if (database != null
|
||||||
&& database.isRecycleBinEnabled
|
&& database.isRecycleBinEnabled
|
||||||
&& database.recycleBin != mCurrentGroup) {
|
&& database.recycleBin != mCurrentGroup) {
|
||||||
mProgressDialogThread?.startDatabaseDeleteNodes(
|
|
||||||
|
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
|
||||||
nodes,
|
nodes,
|
||||||
!mReadOnly && mAutoSaveEnable
|
!mReadOnly && mAutoSaveEnable
|
||||||
)
|
)
|
||||||
@@ -631,7 +689,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun permanentlyDeleteNodes(nodes: List<Node>) {
|
override fun permanentlyDeleteNodes(nodes: List<Node>) {
|
||||||
mProgressDialogThread?.startDatabaseDeleteNodes(
|
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
|
||||||
nodes,
|
nodes,
|
||||||
!mReadOnly && mAutoSaveEnable
|
!mReadOnly && mAutoSaveEnable
|
||||||
)
|
)
|
||||||
@@ -650,6 +708,8 @@ class GroupActivity : LockingActivity(),
|
|||||||
assignGroupViewElements()
|
assignGroupViewElements()
|
||||||
// Refresh suggestions to change preferences
|
// Refresh suggestions to change preferences
|
||||||
mSearchSuggestionAdapter?.reInit(this)
|
mSearchSuggestionAdapter?.reInit(this)
|
||||||
|
// Padding if lock button visible
|
||||||
|
toolbarAction?.updateLockPaddingLeft()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -667,8 +727,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||||
}
|
}
|
||||||
if (!mSelectionMode) {
|
if (!mSelectionMode) {
|
||||||
inflater.inflate(R.menu.default_menu, menu)
|
MenuUtil.defaultMenuInflater(inflater, menu)
|
||||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menu for recycle bin
|
// Menu for recycle bin
|
||||||
@@ -791,7 +850,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
//onSearchRequested();
|
//onSearchRequested();
|
||||||
return true
|
return true
|
||||||
R.id.menu_save_database -> {
|
R.id.menu_save_database -> {
|
||||||
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
|
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_empty_recycle_bin -> {
|
R.id.menu_empty_recycle_bin -> {
|
||||||
@@ -825,7 +884,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
// Not really needed here because added in runnable but safe
|
// Not really needed here because added in runnable but safe
|
||||||
newGroup.parent = currentGroup
|
newGroup.parent = currentGroup
|
||||||
|
|
||||||
mProgressDialogThread?.startDatabaseCreateGroup(
|
mProgressDatabaseTaskProvider?.startDatabaseCreateGroup(
|
||||||
newGroup,
|
newGroup,
|
||||||
currentGroup,
|
currentGroup,
|
||||||
!mReadOnly && mAutoSaveEnable
|
!mReadOnly && mAutoSaveEnable
|
||||||
@@ -847,7 +906,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If group updated save it in the database
|
// If group updated save it in the database
|
||||||
mProgressDialogThread?.startDatabaseUpdateGroup(
|
mProgressDatabaseTaskProvider?.startDatabaseUpdateGroup(
|
||||||
oldGroupToUpdate,
|
oldGroupToUpdate,
|
||||||
updateGroup,
|
updateGroup,
|
||||||
!mReadOnly && mAutoSaveEnable
|
!mReadOnly && mAutoSaveEnable
|
||||||
@@ -877,19 +936,16 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun startActivity(intent: Intent) {
|
override fun startActivity(intent: Intent) {
|
||||||
|
|
||||||
// Get the intent, verify the action and get the query
|
// Get the intent, verify the action and get the query
|
||||||
if (Intent.ACTION_SEARCH == intent.action) {
|
if (Intent.ACTION_SEARCH == intent.action) {
|
||||||
// manually launch the real search activity
|
// manually launch the same search activity
|
||||||
val searchIntent = Intent(applicationContext, GroupActivity::class.java).apply {
|
val searchIntent = getIntent().apply {
|
||||||
// Add bundle of current intent
|
|
||||||
putExtras(this@GroupActivity.intent)
|
|
||||||
// add query to the Intent Extras
|
// add query to the Intent Extras
|
||||||
action = Intent.ACTION_SEARCH
|
action = Intent.ACTION_SEARCH
|
||||||
putExtra(SearchManager.QUERY, intent.getStringExtra(SearchManager.QUERY))
|
putExtra(SearchManager.QUERY, intent.getStringExtra(SearchManager.QUERY))
|
||||||
}
|
}
|
||||||
|
setIntent(searchIntent)
|
||||||
super.startActivity(searchIntent)
|
onNewIntent(searchIntent)
|
||||||
} else {
|
} else {
|
||||||
super.startActivity(intent)
|
super.startActivity(intent)
|
||||||
}
|
}
|
||||||
@@ -921,12 +977,29 @@ class GroupActivity : LockingActivity(),
|
|||||||
mListNodesFragment?.onActivityResult(requestCode, resultCode, data)
|
mListNodesFragment?.onActivityResult(requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeSearchInIntent(intent: Intent) {
|
private fun rebuildListNodes() {
|
||||||
|
mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment?
|
||||||
|
// to refresh fragment
|
||||||
|
mListNodesFragment?.rebuildList()
|
||||||
|
mCurrentGroup = mListNodesFragment?.mainGroup
|
||||||
|
// Remove search in intent
|
||||||
|
deletePreviousSearchGroup()
|
||||||
|
mCurrentGroupIsASearch = false
|
||||||
if (Intent.ACTION_SEARCH == intent.action) {
|
if (Intent.ACTION_SEARCH == intent.action) {
|
||||||
mCurrentGroupIsASearch = false
|
|
||||||
intent.action = Intent.ACTION_DEFAULT
|
intent.action = Intent.ACTION_DEFAULT
|
||||||
intent.removeExtra(SearchManager.QUERY)
|
intent.removeExtra(SearchManager.QUERY)
|
||||||
}
|
}
|
||||||
|
assignGroupViewElements()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backToTheAppCaller() {
|
||||||
|
if (mAutofillSelection) {
|
||||||
|
// To get the app caller, only for autofill
|
||||||
|
super.onBackPressed()
|
||||||
|
} else {
|
||||||
|
// To move the app in background
|
||||||
|
moveTaskToBack(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
@@ -934,24 +1007,23 @@ class GroupActivity : LockingActivity(),
|
|||||||
finishNodeAction()
|
finishNodeAction()
|
||||||
} else {
|
} else {
|
||||||
// Normal way when we are not in root
|
// Normal way when we are not in root
|
||||||
if (mRootGroup != null && mRootGroup != mCurrentGroup)
|
if (mRootGroup != null && mRootGroup != mCurrentGroup) {
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
// Else lock if needed
|
rebuildListNodes()
|
||||||
|
}
|
||||||
|
// Else in root, lock if needed
|
||||||
else {
|
else {
|
||||||
|
intent.removeExtra(AUTO_SEARCH_KEY)
|
||||||
|
intent.removeExtra(KEY_SEARCH_INFO)
|
||||||
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
|
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
|
||||||
lockAndExit()
|
lockAndExit()
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
} else {
|
} else {
|
||||||
moveTaskToBack(true)
|
// To restore standard mode
|
||||||
|
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
|
||||||
|
backToTheAppCaller()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment
|
|
||||||
// to refresh fragment
|
|
||||||
mListNodesFragment?.rebuildList()
|
|
||||||
mCurrentGroup = mListNodesFragment?.mainGroup
|
|
||||||
removeSearchInIntent(intent)
|
|
||||||
assignGroupViewElements()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -964,42 +1036,35 @@ class GroupActivity : LockingActivity(),
|
|||||||
private const val LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG"
|
private const val LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG"
|
||||||
private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
|
private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
|
||||||
private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"
|
private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"
|
||||||
|
private const val AUTO_SEARCH_KEY = "AUTO_SEARCH_KEY"
|
||||||
|
|
||||||
private fun buildIntent(context: Context,
|
private fun buildIntent(context: Context,
|
||||||
group: Group?,
|
group: Group?,
|
||||||
searchInfo: SearchInfo?,
|
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
intentBuildLauncher: (Intent) -> Unit) {
|
intentBuildLauncher: (Intent) -> Unit) {
|
||||||
val intent = Intent(context, GroupActivity::class.java)
|
val intent = Intent(context, GroupActivity::class.java)
|
||||||
if (group != null) {
|
if (group != null) {
|
||||||
intent.putExtra(GROUP_ID_KEY, group.nodeId)
|
intent.putExtra(GROUP_ID_KEY, group.nodeId)
|
||||||
}
|
}
|
||||||
if (searchInfo != null) {
|
|
||||||
intent.action = Intent.ACTION_SEARCH
|
|
||||||
val searchQuery = searchInfo.webDomain ?: searchInfo.applicationId
|
|
||||||
intent.putExtra(SearchManager.QUERY, searchQuery)
|
|
||||||
}
|
|
||||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
||||||
intentBuildLauncher.invoke(intent)
|
intentBuildLauncher.invoke(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkTimeAndBuildIntent(activity: Activity,
|
private fun checkTimeAndBuildIntent(activity: Activity,
|
||||||
group: Group?,
|
group: Group?,
|
||||||
searchInfo: SearchInfo?,
|
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
intentBuildLauncher: (Intent) -> Unit) {
|
intentBuildLauncher: (Intent) -> Unit) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
buildIntent(activity, group, searchInfo, readOnly, intentBuildLauncher)
|
buildIntent(activity, group, readOnly, intentBuildLauncher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkTimeAndBuildIntent(context: Context,
|
private fun checkTimeAndBuildIntent(context: Context,
|
||||||
group: Group?,
|
group: Group?,
|
||||||
searchInfo: SearchInfo?,
|
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
intentBuildLauncher: (Intent) -> Unit) {
|
intentBuildLauncher: (Intent) -> Unit) {
|
||||||
if (TimeoutHelper.checkTime(context)) {
|
if (TimeoutHelper.checkTime(context)) {
|
||||||
buildIntent(context, group, searchInfo, readOnly, intentBuildLauncher)
|
buildIntent(context, group, readOnly, intentBuildLauncher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1009,8 +1074,14 @@ class GroupActivity : LockingActivity(),
|
|||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launch(context: Context,
|
fun launch(context: Context,
|
||||||
|
autoSearch: Boolean = false,
|
||||||
|
searchInfo: SearchInfo? = null,
|
||||||
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
||||||
checkTimeAndBuildIntent(context, null, null, readOnly) { intent ->
|
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
|
||||||
|
searchInfo?.let {
|
||||||
|
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||||
|
}
|
||||||
|
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1020,11 +1091,13 @@ class GroupActivity : LockingActivity(),
|
|||||||
* Keyboard Launch
|
* Keyboard Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
// TODO implement pre search to directly open the direct group #280
|
fun launchForEntrySelectionResult(context: Context,
|
||||||
fun launchForKeyboardSelection(context: Context,
|
autoSearch: Boolean = false,
|
||||||
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
searchInfo: SearchInfo? = null,
|
||||||
checkTimeAndBuildIntent(context, null, null, readOnly) { intent ->
|
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
||||||
EntrySelectionHelper.startActivityForEntrySelection(context, intent)
|
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
|
||||||
|
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||||
|
EntrySelectionHelper.startActivityForEntrySelectionResult(context, intent, searchInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1036,9 +1109,11 @@ class GroupActivity : LockingActivity(),
|
|||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: Activity,
|
fun launchForAutofillResult(activity: Activity,
|
||||||
assistStructure: AssistStructure,
|
assistStructure: AssistStructure,
|
||||||
|
autoSearch: Boolean = false,
|
||||||
searchInfo: SearchInfo? = null,
|
searchInfo: SearchInfo? = null,
|
||||||
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) {
|
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) {
|
||||||
checkTimeAndBuildIntent(activity, null, searchInfo, readOnly) { intent ->
|
checkTimeAndBuildIntent(activity, null, readOnly) { intent ->
|
||||||
|
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||||
AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure, searchInfo)
|
AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure, searchInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,14 +266,15 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun actionNodesCallback(nodes: List<Node>,
|
fun actionNodesCallback(nodes: List<Node>,
|
||||||
menuListener: NodesActionMenuListener?) : ActionMode.Callback {
|
menuListener: NodesActionMenuListener?,
|
||||||
|
actionModeCallback: ActionMode.Callback) : ActionMode.Callback {
|
||||||
|
|
||||||
return object : ActionMode.Callback {
|
return object : ActionMode.Callback {
|
||||||
|
|
||||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||||
return true
|
return actionModeCallback.onCreateActionMode(mode, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||||
@@ -318,7 +319,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
// Add the number of items selected in title
|
// Add the number of items selected in title
|
||||||
mode?.title = nodes.size.toString()
|
mode?.title = nodes.size.toString()
|
||||||
|
|
||||||
return true
|
return actionModeCallback.onPrepareActionMode(mode, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||||
@@ -348,7 +349,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
returnValue
|
returnValue
|
||||||
}
|
}
|
||||||
else -> false
|
else -> actionModeCallback.onActionItemClicked(mode, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +359,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
mAdapter?.unselectActionNodes()
|
mAdapter?.unselectActionNodes()
|
||||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
|
actionModeCallback.onDestroyActionMode(mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePassDX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
@@ -17,24 +17,31 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.magikeyboard
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
|
||||||
import com.kunzisoft.keepass.activities.GroupActivity
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
|
|
||||||
class KeyboardLauncherActivity : AppCompatActivity() {
|
/**
|
||||||
|
* Activity to select entry in database and populate it in Magikeyboard
|
||||||
|
*/
|
||||||
|
class MagikeyboardLauncherActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
if (Database.getInstance().loaded && TimeoutHelper.checkTime(this))
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
GroupActivity.launchForKeyboardSelection(this)
|
Database.getInstance(),
|
||||||
else {
|
null,
|
||||||
// Pass extra to get entry
|
{},
|
||||||
FileDatabaseSelectActivity.launchForKeyboardSelection(this)
|
{
|
||||||
}
|
GroupActivity.launchForEntrySelectionResult(this)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Pass extra to get entry
|
||||||
|
FileDatabaseSelectActivity.launchForEntrySelectionResult(this)
|
||||||
|
}
|
||||||
|
)
|
||||||
finish()
|
finish()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.activities
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.app.backup.BackupManager
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -34,65 +33,68 @@ import android.util.Log
|
|||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper.KEY_SEARCH_INFO
|
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
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.element.Database
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
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.CIPHER_ENTITY_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_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.MASTER_PASSWORD_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
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.MenuUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||||
import kotlinx.android.synthetic.main.activity_password.*
|
import kotlinx.android.synthetic.main.activity_password.*
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
open class PasswordActivity : StylishActivity() {
|
open class PasswordActivity : SpecialModeActivity() {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
private var containerView: View? = null
|
|
||||||
private var filenameView: TextView? = null
|
private var filenameView: TextView? = null
|
||||||
private var passwordView: EditText? = null
|
private var passwordView: EditText? = null
|
||||||
private var keyFileSelectionView: KeyFileSelectionView? = null
|
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||||
private var confirmButtonView: Button? = null
|
private var confirmButtonView: Button? = null
|
||||||
private var checkboxPasswordView: CompoundButton? = null
|
private var checkboxPasswordView: CompoundButton? = null
|
||||||
private var checkboxKeyFileView: CompoundButton? = null
|
private var checkboxKeyFileView: CompoundButton? = null
|
||||||
private var checkboxDefaultDatabaseView: CompoundButton? = null
|
|
||||||
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
|
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
|
||||||
private var infoContainerView: ViewGroup? = null
|
private var infoContainerView: ViewGroup? = null
|
||||||
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
||||||
|
|
||||||
|
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||||
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
private var mDatabaseKeyFileUri: Uri? = null
|
private var mDatabaseKeyFileUri: Uri? = null
|
||||||
|
|
||||||
private var mRememberKeyFile: Boolean = false
|
private var mRememberKeyFile: Boolean = false
|
||||||
private var mOpenFileHelper: OpenFileHelper? = null
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
|
||||||
private var mPermissionAsked = false
|
private var mPermissionAsked = false
|
||||||
private var readOnly: Boolean = false
|
private var readOnly: Boolean = false
|
||||||
@@ -107,9 +109,10 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mProgressDialogThread: ProgressDialogThread? = null
|
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||||
|
|
||||||
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
|
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
|
||||||
|
private var mAllowAutoOpenBiometricPrompt: Boolean = true
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -122,23 +125,22 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
containerView = findViewById(R.id.container)
|
|
||||||
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||||
filenameView = findViewById(R.id.filename)
|
filenameView = findViewById(R.id.filename)
|
||||||
passwordView = findViewById(R.id.password)
|
passwordView = findViewById(R.id.password)
|
||||||
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
||||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
||||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||||
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
|
|
||||||
advancedUnlockInfoView = findViewById(R.id.biometric_info)
|
advancedUnlockInfoView = findViewById(R.id.biometric_info)
|
||||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||||
|
|
||||||
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
||||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||||
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
|
|
||||||
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
|
mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
|
||||||
keyFileSelectionView?.apply {
|
keyFileSelectionView?.apply {
|
||||||
mOpenFileHelper?.openFileOnClickViewListener?.let {
|
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
||||||
setOnClickListener(it)
|
setOnClickListener(it)
|
||||||
setOnLongClickListener(it)
|
setOnLongClickListener(it)
|
||||||
}
|
}
|
||||||
@@ -165,8 +167,34 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
||||||
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
|
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
|
||||||
}
|
}
|
||||||
|
if (savedInstanceState?.containsKey(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) == true) {
|
||||||
|
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
|
||||||
|
}
|
||||||
|
|
||||||
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 ->
|
onActionFinish = { actionTask, result ->
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
@@ -203,7 +231,7 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
result.data?.let { resultData ->
|
result.data?.let { resultData ->
|
||||||
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||||
masterPassword = resultData.getString(MASTER_PASSWORD_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)
|
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||||
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
||||||
}
|
}
|
||||||
@@ -225,7 +253,7 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||||
resultError = "$resultError $resultMessage"
|
resultError = "$resultError $resultMessage"
|
||||||
}
|
}
|
||||||
Log.e(TAG, resultError, resultException)
|
Log.e(TAG, resultError)
|
||||||
Snackbar.make(activity_password_coordinator_layout,
|
Snackbar.make(activity_password_coordinator_layout,
|
||||||
resultError,
|
resultError,
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
@@ -255,21 +283,52 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivity() {
|
private fun launchGroupActivity() {
|
||||||
|
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||||
{
|
{
|
||||||
GroupActivity.launch(this@PasswordActivity,
|
GroupActivity.launch(this@PasswordActivity,
|
||||||
|
true,
|
||||||
|
searchInfo,
|
||||||
readOnly)
|
readOnly)
|
||||||
|
// Finish activity if no search info
|
||||||
|
if (searchInfo != null) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
GroupActivity.launchForKeyboardSelection(this@PasswordActivity,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
readOnly)
|
Database.getInstance(),
|
||||||
|
searchInfo,
|
||||||
|
{ items ->
|
||||||
|
// Response is build
|
||||||
|
if (items.size == 1) {
|
||||||
|
populateKeyboardAndMoveAppToBackground(this@PasswordActivity,
|
||||||
|
items[0],
|
||||||
|
intent)
|
||||||
|
} else {
|
||||||
|
// Select the one we want
|
||||||
|
GroupActivity.launchForEntrySelectionResult(this,
|
||||||
|
true,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Here no search info found, disable auto search
|
||||||
|
GroupActivity.launchForEntrySelectionResult(this@PasswordActivity,
|
||||||
|
false,
|
||||||
|
searchInfo,
|
||||||
|
readOnly)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Simply close if database not opened, normally not happened
|
||||||
|
}
|
||||||
|
)
|
||||||
// Do not keep history
|
// Do not keep history
|
||||||
finish()
|
finish()
|
||||||
},
|
},
|
||||||
{ assistStructure ->
|
{ assistStructure ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
AutofillHelper.checkAutoSearchInfo(this,
|
|
||||||
Database.getInstance(),
|
Database.getInstance(),
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ items ->
|
{ items ->
|
||||||
@@ -278,10 +337,11 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
finish()
|
finish()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Here no search info found
|
// Here no search info found, disable auto search
|
||||||
GroupActivity.launchForAutofillResult(this@PasswordActivity,
|
GroupActivity.launchForAutofillResult(this@PasswordActivity,
|
||||||
assistStructure,
|
assistStructure,
|
||||||
null,
|
false,
|
||||||
|
searchInfo,
|
||||||
readOnly)
|
readOnly)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -304,97 +364,49 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
|
||||||
if (Database.getInstance().loaded)
|
|
||||||
launchGroupActivity()
|
|
||||||
|
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
|
||||||
|
|
||||||
// If the database isn't accessible make sure to clear the password field, if it
|
|
||||||
// was saved in the instance state
|
|
||||||
if (Database.getInstance().loaded) {
|
|
||||||
clearCredentialsViews()
|
|
||||||
}
|
|
||||||
|
|
||||||
// For check shutdown
|
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
mProgressDialogThread?.registerProgressTask()
|
if (Database.getInstance().loaded) {
|
||||||
|
launchGroupActivity()
|
||||||
initUriFromIntent()
|
|
||||||
|
|
||||||
checkPermission()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
|
|
||||||
mDatabaseKeyFileUri?.let {
|
|
||||||
outState.putString(KEY_KEYFILE, it.toString())
|
|
||||||
}
|
|
||||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
onPostInitUri(mDatabaseFileUri, mDatabaseKeyFileUri)
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
|
|
||||||
|
// If the database isn't accessible make sure to clear the password field, if it
|
||||||
|
// was saved in the instance state
|
||||||
|
if (Database.getInstance().loaded) {
|
||||||
|
clearCredentialsViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
|
||||||
|
// Back to previous keyboard is setting activated
|
||||||
|
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this)) {
|
||||||
|
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow auto open prompt if lock become when UI visible
|
||||||
|
mAllowAutoOpenBiometricPrompt = if (LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
|
||||||
|
false
|
||||||
|
else
|
||||||
|
mAllowAutoOpenBiometricPrompt
|
||||||
|
|
||||||
|
mDatabaseFileUri?.let { databaseFileUri ->
|
||||||
|
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPermission()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||||
// Define title
|
|
||||||
databaseFileUri?.let {
|
|
||||||
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
|
|
||||||
filenameView?.text = title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define Key File text
|
// Define Key File text
|
||||||
if (mRememberKeyFile) {
|
if (mRememberKeyFile) {
|
||||||
populateKeyFileTextView(keyFileUri)
|
populateKeyFileTextView(keyFileUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define listeners for default database checkbox and validate button
|
// Define listener for 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()
|
|
||||||
}
|
|
||||||
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
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
|
// If Activity is launch with a password and want to open directly
|
||||||
val intent = intent
|
val intent = intent
|
||||||
val password = intent.getStringExtra(KEY_PASSWORD)
|
val password = intent.getStringExtra(KEY_PASSWORD)
|
||||||
@@ -411,7 +423,6 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
var biometricInitialize = false
|
var biometricInitialize = false
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
|
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
|
||||||
|
|
||||||
if (advancedUnlockedManager == null && databaseFileUri != null) {
|
if (advancedUnlockedManager == null && databaseFileUri != null) {
|
||||||
advancedUnlockedManager = AdvancedUnlockedManager(this,
|
advancedUnlockedManager = AdvancedUnlockedManager(this,
|
||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
@@ -438,10 +449,12 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
advancedUnlockedManager?.isBiometricPromptAutoOpenEnable = mAllowAutoOpenBiometricPrompt
|
||||||
advancedUnlockedManager?.checkBiometricAvailability()
|
advancedUnlockedManager?.checkBiometricAvailability()
|
||||||
biometricInitialize = true
|
biometricInitialize = true
|
||||||
} else {
|
} else {
|
||||||
advancedUnlockedManager?.destroy()
|
advancedUnlockedManager?.destroy()
|
||||||
|
advancedUnlockInfoView?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!biometricInitialize) {
|
if (!biometricInitialize) {
|
||||||
@@ -497,16 +510,28 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
mProgressDialogThread?.unregisterProgressTask()
|
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
advancedUnlockedManager?.destroy()
|
||||||
|
advancedUnlockedManager = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reinit locking activity UI variable
|
||||||
|
LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
||||||
|
mAllowAutoOpenBiometricPrompt = true
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
|
||||||
advancedUnlockedManager?.destroy()
|
mDatabaseKeyFileUri?.let {
|
||||||
|
outState.putString(KEY_KEYFILE, it.toString())
|
||||||
}
|
}
|
||||||
super.onDestroy()
|
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
||||||
|
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||||
@@ -560,7 +585,7 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
cipherDatabaseEntity: CipherDatabaseEntity?,
|
cipherDatabaseEntity: CipherDatabaseEntity?,
|
||||||
fixDuplicateUUID: Boolean) {
|
fixDuplicateUUID: Boolean) {
|
||||||
mProgressDialogThread?.startDatabaseLoad(
|
mProgressDatabaseTaskProvider?.startDatabaseLoad(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
password,
|
password,
|
||||||
keyFile,
|
keyFile,
|
||||||
@@ -580,14 +605,15 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
val inflater = menuInflater
|
val inflater = menuInflater
|
||||||
// Read menu
|
// Read menu
|
||||||
inflater.inflate(R.menu.open_file, menu)
|
inflater.inflate(R.menu.open_file, menu)
|
||||||
|
if (mSelectionMode || mForceReadOnly) {
|
||||||
if (mForceReadOnly) {
|
|
||||||
menu.removeItem(R.id.menu_open_file_read_mode_key)
|
menu.removeItem(R.id.menu_open_file_read_mode_key)
|
||||||
} else {
|
} else {
|
||||||
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
|
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
if (!mSelectionMode) {
|
||||||
|
MenuUtil.defaultMenuInflater(inflater, menu)
|
||||||
|
}
|
||||||
|
|
||||||
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
// biometric menu
|
// biometric menu
|
||||||
@@ -715,13 +741,15 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
data: Intent?) {
|
data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
mAllowAutoOpenBiometricPrompt = false
|
||||||
|
|
||||||
// To get entry in result
|
// To get entry in result
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyFileResult = false
|
var keyFileResult = false
|
||||||
mOpenFileHelper?.let {
|
mSelectFileHelper?.let {
|
||||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
||||||
) { uri ->
|
) { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
@@ -733,10 +761,13 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
if (!keyFileResult) {
|
if (!keyFileResult) {
|
||||||
// this block if not a key file response
|
// this block if not a key file response
|
||||||
when (resultCode) {
|
when (resultCode) {
|
||||||
LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> {
|
LockingActivity.RESULT_EXIT_LOCK -> {
|
||||||
clearCredentialsViews()
|
clearCredentialsViews()
|
||||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
||||||
}
|
}
|
||||||
|
Activity.RESULT_CANCELED -> {
|
||||||
|
clearCredentialsViews()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -754,6 +785,8 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
|
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
|
||||||
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
|
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
|
||||||
|
|
||||||
|
private const val ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT = "ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT"
|
||||||
|
|
||||||
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
||||||
intentBuildLauncher: (Intent) -> Unit) {
|
intentBuildLauncher: (Intent) -> Unit) {
|
||||||
val intent = Intent(activity, PasswordActivity::class.java)
|
val intent = Intent(activity, PasswordActivity::class.java)
|
||||||
@@ -773,8 +806,12 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
fun launch(
|
fun launch(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?) {
|
keyFile: Uri?,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
searchInfo?.let {
|
||||||
|
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||||
|
}
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -789,9 +826,13 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
fun launchForKeyboardResult(
|
fun launchForKeyboardResult(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?) {
|
keyFile: Uri?,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
EntrySelectionHelper.startActivityForEntrySelection(activity, intent)
|
EntrySelectionHelper.startActivityForEntrySelectionResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
searchInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -818,7 +859,7 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
launch(activity, databaseFile, keyFile)
|
launch(activity, databaseFile, keyFile, searchInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,16 +25,16 @@ import android.content.DialogInterface
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
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.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.TextView
|
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.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
|
|
||||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||||
@@ -56,7 +56,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
private var mListener: AssignPasswordDialogListener? = null
|
private var mListener: AssignPasswordDialogListener? = null
|
||||||
|
|
||||||
private var mOpenFileHelper: OpenFileHelper? = null
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
|
||||||
private val passwordTextWatcher = object : TextWatcher {
|
private val passwordTextWatcher = object : TextWatcher {
|
||||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
@@ -113,10 +113,10 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||||
|
|
||||||
mOpenFileHelper = OpenFileHelper(this)
|
mSelectFileHelper = SelectFileHelper(this)
|
||||||
keyFileSelectionView?.apply {
|
keyFileSelectionView?.apply {
|
||||||
setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
||||||
setOnLongClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
val dialog = builder.create()
|
val dialog = builder.create()
|
||||||
@@ -249,8 +249,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||||
) { uri ->
|
|
||||||
uri?.let { pathUri ->
|
uri?.let { pathUri ->
|
||||||
keyFileCheckBox?.isChecked = true
|
keyFileCheckBox?.isChecked = true
|
||||||
keyFileSelectionView?.uri = pathUri
|
keyFileSelectionView?.uri = pathUri
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
/*
|
||||||
|
* 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 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,92 @@
|
|||||||
|
/*
|
||||||
|
* 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 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.yes) { _, _ ->
|
||||||
|
mActionChooseListener?.onValidateUploadFileTooBig(
|
||||||
|
arguments?.getParcelable(KEY_FILE_URI),
|
||||||
|
arguments?.getString(KEY_FILE_NAME))
|
||||||
|
}
|
||||||
|
builder.setNegativeButton(android.R.string.no) { _, _ ->
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,17 +24,17 @@ import android.content.Context
|
|||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.core.widget.ImageViewCompat
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.BaseAdapter
|
import android.widget.BaseAdapter
|
||||||
import android.widget.GridView
|
import android.widget.GridView
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.widget.ImageViewCompat
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.icons.IconPack
|
import com.kunzisoft.keepass.icons.IconPack
|
||||||
import com.kunzisoft.keepass.icons.IconPackChooser
|
import com.kunzisoft.keepass.icons.IconPackChooser
|
||||||
@@ -60,7 +60,7 @@ class IconPickerDialogFragment : DialogFragment() {
|
|||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
iconPack = IconPackChooser.getSelectedIconPack(context!!)
|
iconPack = IconPackChooser.getSelectedIconPack(requireContext())
|
||||||
|
|
||||||
// Inflate and set the layout for the dialog
|
// Inflate and set the layout for the dialog
|
||||||
// Pass null as the parent view because its going in the dialog layout
|
// Pass null as the parent view because its going in the dialog layout
|
||||||
@@ -132,7 +132,7 @@ class IconPickerDialogFragment : DialogFragment() {
|
|||||||
return bundle.getParcelable(KEY_ICON_STANDARD)
|
return bundle.getParcelable(KEY_ICON_STANDARD)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launch(activity: StylishActivity) {
|
fun launch(activity: AppCompatActivity) {
|
||||||
// Create an instance of the dialog fragment and show it
|
// Create an instance of the dialog fragment and show it
|
||||||
val dialog = IconPickerDialogFragment()
|
val dialog = IconPickerDialogFragment()
|
||||||
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
|
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* 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 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.yes) { _, _ ->
|
||||||
|
mActionChooseListener?.onValidateReplaceFile(
|
||||||
|
arguments?.getParcelable(KEY_FILE_URI),
|
||||||
|
arguments?.getParcelable(KEY_ENTRY_ATTACHMENT))
|
||||||
|
}
|
||||||
|
builder.setNegativeButton(android.R.string.no) { _, _ ->
|
||||||
|
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.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
@@ -89,9 +90,9 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var mSecretWellFormed = false
|
private var mSecretWellFormed = false
|
||||||
private var mCounterWellFormed = true
|
private var mCounterWellFormed = false
|
||||||
private var mPeriodWellFormed = true
|
private var mPeriodWellFormed = false
|
||||||
private var mDigitsWellFormed = true
|
private var mDigitsWellFormed = false
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
@@ -152,6 +153,28 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
otpCounterTextView?.setOnTouchListener(mOnTouchListener)
|
otpCounterTextView?.setOnTouchListener(mOnTouchListener)
|
||||||
otpDigitsTextView?.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
|
// HOTP / TOTP Type selection
|
||||||
val otpTypeArray = OtpType.values()
|
val otpTypeArray = OtpType.values()
|
||||||
@@ -365,14 +388,26 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
private fun upgradeParameters() {
|
private fun upgradeParameters() {
|
||||||
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
|
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
|
||||||
.indexOf(mOtpElement.algorithm))
|
.indexOf(mOtpElement.algorithm))
|
||||||
|
|
||||||
|
val secret = mOtpElement.getBase32Secret()
|
||||||
otpSecretTextView?.apply {
|
otpSecretTextView?.apply {
|
||||||
setText(mOtpElement.getBase32Secret())
|
setText(secret)
|
||||||
// Cursor at end
|
// Cursor at end
|
||||||
setSelection(this.text.length)
|
setSelection(this.text.length)
|
||||||
}
|
}
|
||||||
otpCounterTextView?.setText(mOtpElement.counter.toString())
|
mSecretWellFormed = OtpElement.isValidBase32(secret)
|
||||||
otpPeriodTextView?.setText(mOtpElement.period.toString())
|
|
||||||
otpDigitsTextView?.setText(mOtpElement.digits.toString())
|
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) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.text.Html
|
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
|||||||
@@ -24,15 +24,22 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
|
||||||
object EntrySelectionHelper {
|
object EntrySelectionHelper {
|
||||||
|
|
||||||
private const val EXTRA_ENTRY_SELECTION_MODE = "com.kunzisoft.keepass.extra.ENTRY_SELECTION_MODE"
|
private const val EXTRA_ENTRY_SELECTION_MODE = "com.kunzisoft.keepass.extra.ENTRY_SELECTION_MODE"
|
||||||
private const val DEFAULT_ENTRY_SELECTION_MODE = false
|
private const val DEFAULT_ENTRY_SELECTION_MODE = false
|
||||||
|
// Key to retrieve search in intent
|
||||||
|
const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||||
|
|
||||||
fun startActivityForEntrySelection(context: Context, intent: Intent) {
|
fun startActivityForEntrySelectionResult(context: Context,
|
||||||
|
intent: Intent,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
addEntrySelectionModeExtraInIntent(intent)
|
addEntrySelectionModeExtraInIntent(intent)
|
||||||
// only to avoid visible flickering when redirecting
|
searchInfo?.let {
|
||||||
|
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||||
|
}
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,19 +28,20 @@ import android.content.pm.PackageManager
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class OpenFileHelper {
|
class SelectFileHelper {
|
||||||
|
|
||||||
private var activity: Activity? = null
|
private var activity: Activity? = null
|
||||||
private var fragment: Fragment? = null
|
private var fragment: Fragment? = null
|
||||||
|
|
||||||
val openFileOnClickViewListener: OpenFileOnClickViewListener
|
val selectFileOnClickViewListener: SelectFileOnClickViewListener
|
||||||
get() = OpenFileOnClickViewListener()
|
get() = SelectFileOnClickViewListener()
|
||||||
|
|
||||||
constructor(context: Activity) {
|
constructor(context: Activity) {
|
||||||
this.activity = context
|
this.activity = context
|
||||||
@@ -52,7 +53,10 @@ class OpenFileHelper {
|
|||||||
this.fragment = context
|
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) {
|
private fun onAbstractClick(longClick: Boolean = false) {
|
||||||
try {
|
try {
|
||||||
@@ -85,17 +89,22 @@ class OpenFileHelper {
|
|||||||
onAbstractClick(true)
|
onAbstractClick(true)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||||
|
onAbstractClick()
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
private fun openActivityWithActionOpenDocument() {
|
private fun openActivityWithActionOpenDocument() {
|
||||||
val intentOpenDocument = Intent(APP_ACTION_OPEN_DOCUMENT).apply {
|
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
type = "*/*"
|
type = "*/*"
|
||||||
flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
|
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
}
|
}
|
||||||
if (fragment != null)
|
if (fragment != null)
|
||||||
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||||
@@ -108,10 +117,10 @@ class OpenFileHelper {
|
|||||||
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
type = "*/*"
|
type = "*/*"
|
||||||
flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
|
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
}
|
}
|
||||||
if (fragment != null)
|
if (fragment != null)
|
||||||
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||||
@@ -226,12 +235,6 @@ class OpenFileHelper {
|
|||||||
|
|
||||||
private const val TAG = "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"
|
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
|
||||||
|
|
||||||
private const val GET_CONTENT = 25745
|
private const val GET_CONTENT = 25745
|
||||||
@@ -19,31 +19,21 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.lock
|
package com.kunzisoft.keepass.activities.lock
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
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.database.element.Database
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
|
|
||||||
abstract class LockingActivity : StylishActivity() {
|
abstract class LockingActivity : SpecialModeActivity() {
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val TAG = "LockingActivity"
|
|
||||||
|
|
||||||
const val RESULT_EXIT_LOCK = 1450
|
|
||||||
|
|
||||||
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
|
||||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
|
||||||
}
|
|
||||||
|
|
||||||
protected var mTimeoutEnable: Boolean = true
|
protected var mTimeoutEnable: Boolean = true
|
||||||
|
|
||||||
@@ -51,14 +41,17 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
private var mExitLock: Boolean = false
|
private var mExitLock: Boolean = false
|
||||||
|
|
||||||
// Force readOnly if Entry Selection mode
|
// Force readOnly if Entry Selection mode
|
||||||
protected var mReadOnly: Boolean = false
|
protected var mReadOnly: Boolean
|
||||||
get() {
|
get() {
|
||||||
return field || mSelectionMode
|
return mReadOnlyToSave || mSelectionMode
|
||||||
}
|
}
|
||||||
protected var mSelectionMode: Boolean = false
|
set(value) {
|
||||||
|
mReadOnlyToSave = value
|
||||||
|
}
|
||||||
|
private var mReadOnlyToSave: Boolean = false
|
||||||
protected var mAutoSaveEnable: Boolean = true
|
protected var mAutoSaveEnable: Boolean = true
|
||||||
|
|
||||||
var mProgressDialogThread: ProgressDialogThread? = null
|
var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -75,17 +68,19 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
if (mTimeoutEnable) {
|
if (mTimeoutEnable) {
|
||||||
mLockReceiver = LockReceiver {
|
mLockReceiver = LockReceiver {
|
||||||
closeDatabase()
|
closeDatabase()
|
||||||
|
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
||||||
|
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
||||||
// Add onActivityForResult response
|
// Add onActivityForResult response
|
||||||
setResult(RESULT_EXIT_LOCK)
|
setResult(RESULT_EXIT_LOCK)
|
||||||
|
closeOptionsMenu()
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
registerLockReceiver(mLockReceiver)
|
registerLockReceiver(mLockReceiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
mExitLock = false
|
mExitLock = false
|
||||||
mReadOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState, intent)
|
|
||||||
|
|
||||||
mProgressDialogThread = ProgressDialogThread(this)
|
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
@@ -101,10 +96,10 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
mProgressDialogThread?.registerProgressTask()
|
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
|
||||||
// To refresh when back to normal workflow from selection workflow
|
// To refresh when back to normal workflow from selection workflow
|
||||||
mSelectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
|
mReadOnlyToSave = ReadOnlyHelper.retrieveReadOnlyFromIntent(intent)
|
||||||
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
|
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
|
||||||
|
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
@@ -124,16 +119,19 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
if (!mExitLock)
|
if (!mExitLock)
|
||||||
TimeoutHelper.recordTime(this)
|
TimeoutHelper.recordTime(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOCKING_ACTIVITY_UI_VISIBLE = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
ReadOnlyHelper.onSaveInstanceState(outState, mReadOnly)
|
|
||||||
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
|
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
mProgressDialogThread?.unregisterProgressTask()
|
LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||||
|
|
||||||
|
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
@@ -155,11 +153,21 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
/**
|
/**
|
||||||
* To reset the app timeout when a view is focused or changed
|
* To reset the app timeout when a view is focused or changed
|
||||||
*/
|
*/
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View?) {
|
protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View?) {
|
||||||
views.forEach {
|
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 ->
|
it?.setOnFocusChangeListener { _, hasFocus ->
|
||||||
if (hasFocus) {
|
if (hasFocus) {
|
||||||
Log.d(TAG, "View focused, reset app timeout")
|
// Log.d(TAG, "View focused, try to reset app timeout")
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,4 +188,17 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val TAG = "LockingActivity"
|
||||||
|
|
||||||
|
const val RESULT_EXIT_LOCK = 1450
|
||||||
|
|
||||||
|
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||||
|
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||||
|
|
||||||
|
private var LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||||
|
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.selection
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.view.SpecialModeView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity to manage special mode (ie: selection mode)
|
||||||
|
*/
|
||||||
|
abstract class SpecialModeActivity : StylishActivity() {
|
||||||
|
|
||||||
|
protected var mSelectionMode: Boolean = false
|
||||||
|
|
||||||
|
protected var mAutofillSelection: Boolean = false
|
||||||
|
|
||||||
|
private var specialModeView: SpecialModeView? = null
|
||||||
|
|
||||||
|
open fun onCancelSpecialMode() {
|
||||||
|
onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
mSelectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
mAutofillSelection = AutofillHelper.retrieveAssistStructure(intent) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
val searchInfo: SearchInfo? = intent.getParcelableExtra(EntrySelectionHelper.KEY_SEARCH_INFO)
|
||||||
|
|
||||||
|
// To show the selection mode
|
||||||
|
specialModeView = findViewById(R.id.special_mode_view)
|
||||||
|
specialModeView?.apply {
|
||||||
|
// Populate title
|
||||||
|
val typeModeId = if (mAutofillSelection)
|
||||||
|
R.string.autofill
|
||||||
|
else
|
||||||
|
R.string.magic_keyboard_title
|
||||||
|
title = "${resources.getString(R.string.selection_mode)} (${getString(typeModeId)})"
|
||||||
|
// Populate subtitle
|
||||||
|
subtitle = searchInfo?.getName(resources)
|
||||||
|
|
||||||
|
// Show the toolbar or not
|
||||||
|
visible = mSelectionMode
|
||||||
|
|
||||||
|
// Add back listener
|
||||||
|
onCancelButtonClickListener = View.OnClickListener {
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create menu
|
||||||
|
menu.clear()
|
||||||
|
if (mAutofillSelection) {
|
||||||
|
menuInflater.inflate(R.menu.autofill, menu)
|
||||||
|
setOnMenuItemClickListener { menuItem ->
|
||||||
|
when (menuItem.itemId) {
|
||||||
|
R.id.menu_block_autofill -> {
|
||||||
|
blockAutofill(searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun blockAutofill(searchInfo: SearchInfo?) {
|
||||||
|
val webDomain = searchInfo?.webDomain
|
||||||
|
val applicationId = searchInfo?.applicationId
|
||||||
|
if (webDomain != null) {
|
||||||
|
PreferencesUtil.addWebDomainToBlocklist(this,
|
||||||
|
webDomain)
|
||||||
|
} else if (applicationId != null) {
|
||||||
|
PreferencesUtil.addApplicationIdToBlocklist(this,
|
||||||
|
applicationId)
|
||||||
|
}
|
||||||
|
onCancelSpecialMode()
|
||||||
|
Toast.makeText(this.applicationContext,
|
||||||
|
R.string.autofill_block_restart,
|
||||||
|
Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,19 +53,19 @@ abstract class StylishActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
this.themeId = Stylish.getThemeId(this)
|
this.themeId = Stylish.getThemeId(this)
|
||||||
setTheme(themeId)
|
setTheme(themeId)
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
// Several gingerbread devices have problems with FLAG_SECURE
|
// Several gingerbread devices have problems with FLAG_SECURE
|
||||||
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
if (Stylish.getThemeId(this) != this.themeId) {
|
if (Stylish.getThemeId(this) != this.themeId) {
|
||||||
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
||||||
this.recreate()
|
this.recreate()
|
||||||
}
|
}
|
||||||
super.onResume()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,12 +46,19 @@ abstract class StylishFragment : Fragment() {
|
|||||||
// To fix status bar color
|
// To fix status bar color
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
val window = requireActivity().window
|
val window = requireActivity().window
|
||||||
|
|
||||||
val attrColorPrimaryDark = intArrayOf(android.R.attr.colorPrimaryDark)
|
|
||||||
val taColorPrimaryDark = contextThemed?.theme?.obtainStyledAttributes(attrColorPrimaryDark)
|
|
||||||
val defaultColor = Color.BLACK
|
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)
|
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,118 @@
|
|||||||
|
/*
|
||||||
|
* 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.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
|
||||||
|
|
||||||
|
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.binaryFileTitle.text = entryAttachmentState.attachment.name
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.widget.EditText
|
||||||
|
import androidx.core.widget.doOnTextChanged
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.model.Field
|
||||||
|
import com.kunzisoft.keepass.model.FocusedEditField
|
||||||
|
import com.kunzisoft.keepass.view.EditTextSelectable
|
||||||
|
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||||
|
|
||||||
|
class EntryExtraFieldsItemsAdapter(context: Context)
|
||||||
|
: AnimatedItemsAdapter<Field, EntryExtraFieldsItemsAdapter.EntryExtraFieldViewHolder>(context) {
|
||||||
|
|
||||||
|
var applyFontVisibility = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
private var mValueViewInputType: Int = 0
|
||||||
|
private var mLastFocusedEditField = FocusedEditField()
|
||||||
|
private var mLastFocusedTimestamp: Long = 0L
|
||||||
|
|
||||||
|
var onEditButtonClickListener: ((item: Field)->Unit)? = null
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryExtraFieldViewHolder {
|
||||||
|
val view = EntryExtraFieldViewHolder(
|
||||||
|
inflater.inflate(R.layout.item_entry_edit_extra_field, parent, false)
|
||||||
|
)
|
||||||
|
mValueViewInputType = view.extraFieldValue.inputType
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: EntryExtraFieldViewHolder, position: Int) {
|
||||||
|
val extraField = itemsList[position]
|
||||||
|
|
||||||
|
holder.itemView.visibility = View.VISIBLE
|
||||||
|
if (extraField.protectedValue.isProtected) {
|
||||||
|
holder.extraFieldValueContainer.isPasswordVisibilityToggleEnabled = true
|
||||||
|
holder.extraFieldValue.inputType = EditorInfo.TYPE_TEXT_VARIATION_PASSWORD or mValueViewInputType
|
||||||
|
} else {
|
||||||
|
holder.extraFieldValueContainer.isPasswordVisibilityToggleEnabled = false
|
||||||
|
holder.extraFieldValue.inputType = mValueViewInputType
|
||||||
|
}
|
||||||
|
holder.extraFieldValueContainer.hint = extraField.name
|
||||||
|
holder.extraFieldValue.apply {
|
||||||
|
setText(extraField.protectedValue.toString())
|
||||||
|
// To Fix focus in RecyclerView
|
||||||
|
setOnFocusChangeListener { _, hasFocus ->
|
||||||
|
if (hasFocus) {
|
||||||
|
setFocusField(extraField, selectionStart, selectionEnd)
|
||||||
|
} else {
|
||||||
|
// request focus on last text focused
|
||||||
|
if (focusedTimestampNotExpired())
|
||||||
|
requestFocusField(this, extraField, false)
|
||||||
|
else
|
||||||
|
removeFocusField(extraField)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addOnSelectionChangedListener(object: EditTextSelectable.OnSelectionChangedListener {
|
||||||
|
override fun onSelectionChanged(start: Int, end: Int) {
|
||||||
|
mLastFocusedEditField.apply {
|
||||||
|
cursorSelectionStart = start
|
||||||
|
cursorSelectionEnd = end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
requestFocusField(this, extraField, true)
|
||||||
|
doOnTextChanged { text, _, _, _ ->
|
||||||
|
extraField.protectedValue.stringValue = text.toString()
|
||||||
|
}
|
||||||
|
if (applyFontVisibility)
|
||||||
|
applyFontVisibility()
|
||||||
|
}
|
||||||
|
holder.extraFieldEditButton.setOnClickListener {
|
||||||
|
onEditButtonClickListener?.invoke(extraField)
|
||||||
|
}
|
||||||
|
performDeletion(holder, extraField)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assignItems(items: List<Field>, focusedEditField: FocusedEditField?) {
|
||||||
|
focusedEditField?.let {
|
||||||
|
setFocusField(it, true)
|
||||||
|
}
|
||||||
|
super.assignItems(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setFocusField(field: Field,
|
||||||
|
selectionStart: Int,
|
||||||
|
selectionEnd: Int,
|
||||||
|
force: Boolean = false) {
|
||||||
|
mLastFocusedEditField.apply {
|
||||||
|
this.field = field
|
||||||
|
this.cursorSelectionStart = selectionStart
|
||||||
|
this.cursorSelectionEnd = selectionEnd
|
||||||
|
}
|
||||||
|
setFocusField(mLastFocusedEditField, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setFocusField(field: FocusedEditField, force: Boolean = false) {
|
||||||
|
mLastFocusedEditField = field
|
||||||
|
mLastFocusedTimestamp = if (force) 0L else System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeFocusField(field: Field? = null) {
|
||||||
|
if (field == null || mLastFocusedEditField.field == field) {
|
||||||
|
mLastFocusedEditField.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestFocusField(editText: EditText, field: Field, setSelection: Boolean) {
|
||||||
|
if (field == mLastFocusedEditField.field) {
|
||||||
|
editText.apply {
|
||||||
|
post {
|
||||||
|
if (setSelection) {
|
||||||
|
setEditTextSelection(editText)
|
||||||
|
}
|
||||||
|
requestFocus()
|
||||||
|
removeFocusField(field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setEditTextSelection(editText: EditText) {
|
||||||
|
try {
|
||||||
|
var newCursorPositionStart = mLastFocusedEditField.cursorSelectionStart
|
||||||
|
var newCursorPositionEnd = mLastFocusedEditField.cursorSelectionEnd
|
||||||
|
// Cursor at end if 0 or less
|
||||||
|
if (newCursorPositionStart < 0 || newCursorPositionEnd < 0) {
|
||||||
|
newCursorPositionStart = (editText.text?:"").length
|
||||||
|
newCursorPositionEnd = newCursorPositionStart
|
||||||
|
}
|
||||||
|
editText.setSelection(newCursorPositionStart, newCursorPositionEnd)
|
||||||
|
} catch (ignoredException: Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun focusedTimestampNotExpired(): Boolean {
|
||||||
|
return mLastFocusedTimestamp == 0L || (mLastFocusedTimestamp + FOCUS_TIMESTAMP) > System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFocusedField(): FocusedEditField {
|
||||||
|
return mLastFocusedEditField
|
||||||
|
}
|
||||||
|
|
||||||
|
class EntryExtraFieldViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
var extraFieldValueContainer: TextInputLayout = itemView.findViewById(R.id.entry_extra_field_value_container)
|
||||||
|
var extraFieldValue: EditTextSelectable = itemView.findViewById(R.id.entry_extra_field_value)
|
||||||
|
var extraFieldEditButton: View = itemView.findViewById(R.id.entry_extra_field_edit)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// time to focus element when a keyboard appears
|
||||||
|
private const val FOCUS_TIMESTAMP = 400L
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,31 +22,33 @@ package com.kunzisoft.keepass.adapters
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.PorterDuff
|
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.annotation.ColorInt
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
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.R
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryEntity
|
import com.kunzisoft.keepass.model.DatabaseFile
|
||||||
import com.kunzisoft.keepass.utils.FileDatabaseInfo
|
import com.kunzisoft.keepass.view.collapse
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.view.expand
|
||||||
|
|
||||||
class FileDatabaseHistoryAdapter(private val context: Context)
|
class FileDatabaseHistoryAdapter(context: Context)
|
||||||
: RecyclerView.Adapter<FileDatabaseHistoryAdapter.FileDatabaseHistoryViewHolder>() {
|
: RecyclerView.Adapter<FileDatabaseHistoryAdapter.FileDatabaseHistoryViewHolder>() {
|
||||||
|
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
private var fileItemOpenListener: ((FileDatabaseHistoryEntity)->Unit)? = null
|
private var defaultDatabaseListener: ((DatabaseFile?) -> Unit)? = null
|
||||||
private var fileSelectClearListener: ((FileDatabaseHistoryEntity)->Boolean)? = null
|
private var fileItemOpenListener: ((DatabaseFile)->Unit)? = null
|
||||||
private var saveAliasListener: ((FileDatabaseHistoryEntity)->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 mDefaultDatabaseFile: DatabaseFile? = null
|
||||||
private var mPreviousExpandedPosition = -1
|
private var mExpandedDatabaseFile: DatabaseFile? = null
|
||||||
|
private var mPreviousExpandedDatabaseFile: DatabaseFile? = null
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private val defaultColor: Int
|
private val defaultColor: Int
|
||||||
@@ -63,43 +65,49 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileDatabaseHistoryViewHolder {
|
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)
|
return FileDatabaseHistoryViewHolder(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
|
||||||
// Get info from position
|
// Get info from position
|
||||||
val fileHistoryEntity = listDatabaseFiles[position]
|
val databaseFile = listDatabaseFiles[position]
|
||||||
val fileDatabaseInfo = FileDatabaseInfo(context, fileHistoryEntity.databaseUri)
|
|
||||||
|
|
||||||
// Click item to open file
|
// Click item to open file
|
||||||
if (fileItemOpenListener != null)
|
holder.fileContainer.setOnClickListener {
|
||||||
holder.fileContainer.setOnClickListener {
|
fileItemOpenListener?.invoke(databaseFile)
|
||||||
fileItemOpenListener?.invoke(fileHistoryEntity)
|
}
|
||||||
|
|
||||||
|
// Default database
|
||||||
|
holder.defaultFileButton.apply {
|
||||||
|
this.isChecked = mDefaultDatabaseFile == databaseFile
|
||||||
|
setOnClickListener {
|
||||||
|
defaultDatabaseListener?.invoke(if (isChecked) databaseFile else null)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// File alias
|
// File alias
|
||||||
holder.fileAlias.text = fileDatabaseInfo.retrieveDatabaseAlias(fileHistoryEntity.databaseAlias)
|
holder.fileAlias.text = databaseFile.databaseAlias
|
||||||
|
|
||||||
// File path
|
// File path
|
||||||
holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString())
|
holder.filePath.text = databaseFile.databaseDecodedPath
|
||||||
|
|
||||||
if (fileDatabaseInfo.exists) {
|
if (databaseFile.databaseFileExists) {
|
||||||
holder.fileInformation.clearColorFilter()
|
holder.fileInformationButton.clearColorFilter()
|
||||||
} else {
|
} else {
|
||||||
holder.fileInformation.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
|
holder.fileInformationButton.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modification
|
// Modification
|
||||||
fileDatabaseInfo.getModificationString()?.let {
|
databaseFile.databaseLastModified?.let {
|
||||||
holder.fileModification.text = it
|
holder.fileModification.text = it
|
||||||
holder.fileModification.visibility = View.VISIBLE
|
holder.fileModificationContainer.visibility = View.VISIBLE
|
||||||
} ?: run {
|
} ?: run {
|
||||||
holder.fileModification.visibility = View.GONE
|
holder.fileModificationContainer.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size
|
// Size
|
||||||
fileDatabaseInfo.getSizeString()?.let {
|
databaseFile.databaseSize?.let {
|
||||||
holder.fileSize.text = it
|
holder.fileSize.text = it
|
||||||
holder.fileSize.visibility = View.VISIBLE
|
holder.fileSize.visibility = View.VISIBLE
|
||||||
} ?: run {
|
} ?: run {
|
||||||
@@ -107,15 +115,24 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Click on information
|
// Click on information
|
||||||
val isExpanded = position == mExpandedPosition
|
val isExpanded = databaseFile == mExpandedDatabaseFile
|
||||||
//This line hides or shows the layout in question
|
// Hides or shows info
|
||||||
holder.fileExpandContainer.visibility = if (isExpanded) View.VISIBLE else View.GONE
|
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
|
// Save alias modification
|
||||||
holder.fileAliasCloseButton.setOnClickListener {
|
holder.fileAliasCloseButton.setOnClickListener {
|
||||||
// Change the alias
|
// Change the alias
|
||||||
fileHistoryEntity.databaseAlias = holder.fileAliasEdit.text.toString()
|
databaseFile.databaseAlias = holder.fileAliasEdit.text.toString()
|
||||||
saveAliasListener?.invoke(fileHistoryEntity)
|
saveAliasListener?.invoke(databaseFile)
|
||||||
|
|
||||||
// Finish save mode
|
// Finish save mode
|
||||||
holder.fileMainSwitcher.showPrevious()
|
holder.fileMainSwitcher.showPrevious()
|
||||||
@@ -130,20 +147,22 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
holder.fileDeleteButton.setOnClickListener {
|
holder.fileDeleteButton.setOnClickListener {
|
||||||
fileSelectClearListener?.invoke(fileHistoryEntity)
|
fileSelectClearListener?.invoke(databaseFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
mPreviousExpandedPosition = position
|
mPreviousExpandedDatabaseFile = databaseFile
|
||||||
}
|
}
|
||||||
|
holder.fileInformationButton.apply {
|
||||||
holder.fileInformation.setOnClickListener {
|
animate().rotation(if (isExpanded) 180F else 0F).start()
|
||||||
mExpandedPosition = if (isExpanded) -1 else position
|
setOnClickListener {
|
||||||
|
mExpandedDatabaseFile = if (isExpanded) null else databaseFile
|
||||||
// Notify change
|
// Notify change
|
||||||
if (mPreviousExpandedPosition < itemCount)
|
val previousExpandedPosition = listDatabaseFiles.indexOf(mPreviousExpandedDatabaseFile)
|
||||||
notifyItemChanged(mPreviousExpandedPosition)
|
notifyItemChanged(previousExpandedPosition)
|
||||||
notifyItemChanged(position)
|
val expandedPosition = listDatabaseFiles.indexOf(mExpandedDatabaseFile)
|
||||||
|
notifyItemChanged(expandedPosition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh View / Close alias modification if not contains fileAlias
|
// Refresh View / Close alias modification if not contains fileAlias
|
||||||
@@ -160,33 +179,68 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
listDatabaseFiles.clear()
|
listDatabaseFiles.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<FileDatabaseHistoryEntity>) {
|
fun addDatabaseFileHistory(fileDatabaseHistoryToAdd: DatabaseFile) {
|
||||||
listDatabaseFiles.clear()
|
listDatabaseFiles.add(0, fileDatabaseHistoryToAdd)
|
||||||
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
|
notifyItemInserted(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: FileDatabaseHistoryEntity) {
|
fun updateDatabaseFileHistory(fileDatabaseHistoryToUpdate: DatabaseFile) {
|
||||||
listDatabaseFiles.remove(fileDatabaseHistoryToDelete)
|
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
|
this.fileItemOpenListener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnFileDatabaseHistoryDeleteListener(listener : ((FileDatabaseHistoryEntity)->Boolean)?) {
|
fun setOnFileDatabaseHistoryDeleteListener(listener : ((DatabaseFile)->Boolean)?) {
|
||||||
this.fileSelectClearListener = listener
|
this.fileSelectClearListener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnSaveAliasListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
|
fun setOnSaveAliasListener(listener : ((DatabaseFile)->Unit)?) {
|
||||||
this.saveAliasListener = listener
|
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 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 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 fileMainSwitcher: ViewSwitcher = itemView.findViewById(R.id.file_main_switcher)
|
||||||
var fileAliasEdit: EditText = itemView.findViewById(R.id.file_alias_edit)
|
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 fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
|
||||||
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
|
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
|
||||||
var filePath: TextView = itemView.findViewById(R.id.file_path)
|
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 fileModification: TextView = itemView.findViewById(R.id.file_modification)
|
||||||
var fileSize: TextView = itemView.findViewById(R.id.file_size)
|
var fileSize: TextView = itemView.findViewById(R.id.file_size)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SortedList
|
import androidx.recyclerview.widget.SortedList
|
||||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||||
@@ -73,7 +74,11 @@ class NodeAdapter (private val context: Context)
|
|||||||
|
|
||||||
private val mDatabase: Database
|
private val mDatabase: Database
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
private val contentSelectionColor: Int
|
||||||
|
@ColorInt
|
||||||
private val iconGroupColor: Int
|
private val iconGroupColor: Int
|
||||||
|
@ColorInt
|
||||||
private val iconEntryColor: Int
|
private val iconEntryColor: Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,6 +102,10 @@ class NodeAdapter (private val context: Context)
|
|||||||
// Database
|
// Database
|
||||||
this.mDatabase = Database.getInstance()
|
this.mDatabase = Database.getInstance()
|
||||||
|
|
||||||
|
// Color of content selection
|
||||||
|
val taContentSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||||
|
this.contentSelectionColor = taContentSelectionColor.getColor(0, Color.WHITE)
|
||||||
|
taContentSelectionColor.recycle()
|
||||||
// Retrieve the color to tint the icon
|
// Retrieve the color to tint the icon
|
||||||
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||||
this.iconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
this.iconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||||
@@ -133,8 +142,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
*/
|
*/
|
||||||
fun rebuildList(group: Group) {
|
fun rebuildList(group: Group) {
|
||||||
assignPreferences()
|
assignPreferences()
|
||||||
nodeSortedList.replaceAll(group.getFilteredChildren(*entryFilters)
|
nodeSortedList.replaceAll(group.getFilteredChildren(entryFilters))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
|
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
|
||||||
@@ -281,11 +289,18 @@ class NodeAdapter (private val context: Context)
|
|||||||
|
|
||||||
override fun onBindViewHolder(holder: NodeViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: NodeViewHolder, position: Int) {
|
||||||
val subNode = nodeSortedList.get(position)
|
val subNode = nodeSortedList.get(position)
|
||||||
|
|
||||||
|
// Node selection
|
||||||
|
holder.container.isSelected = actionNodesList.contains(subNode)
|
||||||
|
|
||||||
// Assign image
|
// Assign image
|
||||||
val iconColor = when (subNode.type) {
|
val iconColor = if (holder.container.isSelected)
|
||||||
|
contentSelectionColor
|
||||||
|
else when (subNode.type) {
|
||||||
Type.GROUP -> iconGroupColor
|
Type.GROUP -> iconGroupColor
|
||||||
Type.ENTRY -> iconEntryColor
|
Type.ENTRY -> iconEntryColor
|
||||||
}
|
}
|
||||||
|
holder.imageIdentifier?.setColorFilter(iconColor)
|
||||||
holder.icon.apply {
|
holder.icon.apply {
|
||||||
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
||||||
// Relative size of the icon
|
// Relative size of the icon
|
||||||
@@ -323,6 +338,9 @@ class NodeAdapter (private val context: Context)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
holder.attachmentIcon?.visibility =
|
||||||
|
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
mDatabase.stopManageEntry(entry)
|
mDatabase.stopManageEntry(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +349,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
if (showNumberEntries) {
|
if (showNumberEntries) {
|
||||||
holder.numberChildren?.apply {
|
holder.numberChildren?.apply {
|
||||||
text = (subNode as Group)
|
text = (subNode as Group)
|
||||||
.getNumberOfChildEntries(*entryFilters)
|
.getNumberOfChildEntries(entryFilters)
|
||||||
.toString()
|
.toString()
|
||||||
setTextSize(textSizeUnit, numberChildrenTextDefaultDimension, prefSizeMultiplier)
|
setTextSize(textSizeUnit, numberChildrenTextDefaultDimension, prefSizeMultiplier)
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
@@ -348,8 +366,6 @@ class NodeAdapter (private val context: Context)
|
|||||||
holder.container.setOnLongClickListener {
|
holder.container.setOnLongClickListener {
|
||||||
nodeClickCallback?.onNodeLongClick(subNode) ?: false
|
nodeClickCallback?.onNodeLongClick(subNode) ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.container.isSelected = actionNodesList.contains(subNode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
@@ -373,10 +389,12 @@ class NodeAdapter (private val context: Context)
|
|||||||
|
|
||||||
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
var container: View = itemView.findViewById(R.id.node_container)
|
var container: View = itemView.findViewById(R.id.node_container)
|
||||||
|
var imageIdentifier: ImageView? = itemView.findViewById(R.id.node_image_identifier)
|
||||||
var icon: ImageView = itemView.findViewById(R.id.node_icon)
|
var icon: ImageView = itemView.findViewById(R.id.node_icon)
|
||||||
var text: TextView = itemView.findViewById(R.id.node_text)
|
var text: TextView = itemView.findViewById(R.id.node_text)
|
||||||
var subText: TextView = itemView.findViewById(R.id.node_subtext)
|
var subText: TextView = itemView.findViewById(R.id.node_subtext)
|
||||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||||
|
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
|
|
||||||
private val cursorInflater: LayoutInflater? = context.getSystemService(
|
private val cursorInflater: LayoutInflater? = context.getSystemService(
|
||||||
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
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
|
private val iconColor: Int
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -59,7 +60,8 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun reInit(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 {
|
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
|
||||||
@@ -93,7 +95,7 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
// Assign subtitle
|
// Assign subtitle
|
||||||
viewHolder.textViewSubTitle?.apply {
|
viewHolder.textViewSubTitle?.apply {
|
||||||
val entryUsername = currentEntry.username
|
val entryUsername = currentEntry.username
|
||||||
text = if (displayUsername && entryUsername.isNotEmpty()) {
|
text = if (mDisplayUsername && entryUsername.isNotEmpty()) {
|
||||||
String.format("(%s)", entryUsername)
|
String.format("(%s)", entryUsername)
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
@@ -129,10 +131,12 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
if (database.type == DatabaseKDBX.TYPE)
|
if (database.type == DatabaseKDBX.TYPE)
|
||||||
cursorKDBX = EntryCursorKDBX()
|
cursorKDBX = EntryCursorKDBX()
|
||||||
|
|
||||||
val searchGroup = database.createVirtualGroupFromSearch(query, SearchHelper.MAX_SEARCH_ENTRY)
|
val searchGroup = database.createVirtualGroupFromSearch(query,
|
||||||
|
mOmitBackup,
|
||||||
|
SearchHelper.MAX_SEARCH_ENTRY)
|
||||||
if (searchGroup != null) {
|
if (searchGroup != null) {
|
||||||
// Search in hide entries but not meta-stream
|
// Search in hide entries but not meta-stream
|
||||||
for (entry in searchGroup.getFilteredChildEntries(*Group.ChildFilter.getDefaults(context))) {
|
for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) {
|
||||||
entry.entryKDB?.let {
|
entry.entryKDB?.let {
|
||||||
cursorKDB?.addEntry(it)
|
cursorKDB?.addEntry(it)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class CipherDatabaseAction(applicationContext: Context) {
|
|||||||
|
|
||||||
fun getCipherDatabase(databaseUri: Uri,
|
fun getCipherDatabase(databaseUri: Uri,
|
||||||
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
|
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
||||||
},
|
},
|
||||||
@@ -51,7 +51,7 @@ class CipherDatabaseAction(applicationContext: Context) {
|
|||||||
|
|
||||||
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
||||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
|
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ class CipherDatabaseAction(applicationContext: Context) {
|
|||||||
|
|
||||||
fun deleteByDatabaseUri(databaseUri: Uri,
|
fun deleteByDatabaseUri(databaseUri: Uri,
|
||||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
||||||
},
|
},
|
||||||
@@ -81,7 +81,7 @@ class CipherDatabaseAction(applicationContext: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAll() {
|
fun deleteAll() {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
cipherDatabaseDao.deleteAll()
|
cipherDatabaseDao.deleteAll()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,31 +21,44 @@ package com.kunzisoft.keepass.app.database
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
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.SingletonHolderParameter
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
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 =
|
private val databaseFileHistoryDao =
|
||||||
AppDatabase
|
AppDatabase
|
||||||
.getDatabase(applicationContext)
|
.getDatabase(applicationContext)
|
||||||
.fileDatabaseHistoryDao()
|
.fileDatabaseHistoryDao()
|
||||||
|
|
||||||
fun getFileDatabaseHistory(databaseUri: Uri,
|
fun getDatabaseFile(databaseUri: Uri,
|
||||||
fileHistoryResultListener: (fileDatabaseHistoryResult: FileDatabaseHistoryEntity?) -> Unit) {
|
databaseFileResult: (DatabaseFile?) -> Unit) {
|
||||||
ActionDatabaseAsyncTask(
|
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()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getKeyFileUriByDatabaseUri(databaseUri: Uri,
|
fun getKeyFileUriByDatabaseUri(databaseUri: Uri,
|
||||||
keyFileUriResultListener: (Uri?) -> Unit) {
|
keyFileUriResultListener: (Uri?) -> Unit) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||||
},
|
},
|
||||||
@@ -59,61 +72,120 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
|||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllFileDatabaseHistories(fileHistoryResultListener: (fileDatabaseHistoryResult: List<FileDatabaseHistoryEntity>?) -> Unit) {
|
fun getDatabaseFileList(databaseFileListResult: (List<DatabaseFile>) -> Unit) {
|
||||||
ActionDatabaseAsyncTask(
|
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)
|
databaseFileList ->
|
||||||
}
|
databaseFileList?.let {
|
||||||
).execute()
|
databaseFileListResult.invoke(it)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteFileDatabaseHistory(fileDatabaseHistory: FileDatabaseHistoryEntity,
|
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null,
|
||||||
fileHistoryDeletedResult: (FileDatabaseHistoryEntity?) -> Unit) {
|
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
|
||||||
ActionDatabaseAsyncTask(
|
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)
|
databaseFileAddedOrUpdatedResult?.invoke(it)
|
||||||
fileHistoryDeletedResult.invoke(fileDatabaseHistory)
|
}
|
||||||
else
|
).execute()
|
||||||
fileHistoryDeletedResult.invoke(null)
|
}
|
||||||
|
|
||||||
|
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()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
|
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
|
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
|
||||||
}
|
}
|
||||||
@@ -121,7 +193,7 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAllKeyFiles() {
|
fun deleteAllKeyFiles() {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.deleteAllKeyFiles()
|
databaseFileHistoryDao.deleteAllKeyFiles()
|
||||||
}
|
}
|
||||||
@@ -129,7 +201,7 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAll() {
|
fun deleteAll() {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.deleteAll()
|
databaseFileHistoryDao.deleteAll()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,21 +19,27 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.app.database
|
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 action: () -> T ,
|
||||||
private val afterActionDatabaseListener: ((T?) -> Unit)? = null
|
private val afterActionDatabaseListener: ((T?) -> Unit)? = null) {
|
||||||
) : AsyncTask<Void, Void, T>() {
|
|
||||||
|
|
||||||
override fun doInBackground(vararg args: Void?): T? {
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
return action.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPostExecute(result: T?) {
|
fun execute() {
|
||||||
afterActionDatabaseListener?.invoke(result)
|
mainScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val asyncResult: Deferred<T?> = async {
|
||||||
|
action.invoke()
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
afterActionDatabaseListener?.invoke(asyncResult.await())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,14 +34,12 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
@@ -50,7 +48,6 @@ object AutofillHelper {
|
|||||||
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
|
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
|
||||||
|
|
||||||
private const val ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
private const val ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
||||||
const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
|
||||||
|
|
||||||
fun retrieveAssistStructure(intent: Intent?): AssistStructure? {
|
fun retrieveAssistStructure(intent: Intent?): AssistStructure? {
|
||||||
intent?.let {
|
intent?.let {
|
||||||
@@ -122,60 +119,34 @@ object AutofillHelper {
|
|||||||
* Build the Autofill response for many entry
|
* Build the Autofill response for many entry
|
||||||
*/
|
*/
|
||||||
fun buildResponse(activity: Activity, entriesInfo: List<EntryInfo>) {
|
fun buildResponse(activity: Activity, entriesInfo: List<EntryInfo>) {
|
||||||
var setResultOk = false
|
if (entriesInfo.isEmpty()) {
|
||||||
activity.intent?.extras?.let { extras ->
|
activity.setResult(Activity.RESULT_CANCELED)
|
||||||
if (extras.containsKey(ASSIST_STRUCTURE)) {
|
|
||||||
activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure ->
|
|
||||||
StructureParser(structure).parse()?.let { result ->
|
|
||||||
// New Response
|
|
||||||
val responseBuilder = FillResponse.Builder()
|
|
||||||
entriesInfo.forEach {
|
|
||||||
responseBuilder.addDataset(buildDataset(activity, it, result))
|
|
||||||
}
|
|
||||||
val mReplyIntent = Intent()
|
|
||||||
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
|
||||||
mReplyIntent.putExtra(
|
|
||||||
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
|
||||||
responseBuilder.build())
|
|
||||||
setResultOk = true
|
|
||||||
activity.setResult(Activity.RESULT_OK, mReplyIntent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!setResultOk) {
|
|
||||||
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method to perform actions if item is found or not after an auto search in [database]
|
|
||||||
*/
|
|
||||||
fun checkAutoSearchInfo(context: Context,
|
|
||||||
database: Database,
|
|
||||||
searchInfo: SearchInfo?,
|
|
||||||
onItemsFound: (items: List<EntryInfo>) -> Unit,
|
|
||||||
onItemNotFound: () -> Unit,
|
|
||||||
onDatabaseClosed: () -> Unit) {
|
|
||||||
if (database.loaded && TimeoutHelper.checkTime(context)) {
|
|
||||||
var searchWithoutUI = false
|
|
||||||
if (PreferencesUtil.isAutofillAutoSearchEnable(context)
|
|
||||||
&& searchInfo != null) {
|
|
||||||
// If search provide results
|
|
||||||
database.createVirtualGroupFromSearch(searchInfo, SearchHelper.MAX_SEARCH_ENTRY)?.let { searchGroup ->
|
|
||||||
if (searchGroup.getNumberOfChildEntries() > 0) {
|
|
||||||
searchWithoutUI = true
|
|
||||||
onItemsFound.invoke(
|
|
||||||
searchGroup.getChildEntriesInfo(database))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!searchWithoutUI) {
|
|
||||||
onItemNotFound.invoke()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
onDatabaseClosed.invoke()
|
var setResultOk = false
|
||||||
|
activity.intent?.extras?.let { extras ->
|
||||||
|
if (extras.containsKey(ASSIST_STRUCTURE)) {
|
||||||
|
activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure ->
|
||||||
|
StructureParser(structure).parse()?.let { result ->
|
||||||
|
// New Response
|
||||||
|
val responseBuilder = FillResponse.Builder()
|
||||||
|
entriesInfo.forEach {
|
||||||
|
responseBuilder.addDataset(buildDataset(activity, it, result))
|
||||||
|
}
|
||||||
|
val mReplyIntent = Intent()
|
||||||
|
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
||||||
|
mReplyIntent.putExtra(
|
||||||
|
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
||||||
|
responseBuilder.build())
|
||||||
|
setResultOk = true
|
||||||
|
activity.setResult(Activity.RESULT_OK, mReplyIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!setResultOk) {
|
||||||
|
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
||||||
|
activity.setResult(Activity.RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,12 +26,25 @@ import android.util.Log
|
|||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class KeeAutofillService : AutofillService() {
|
class KeeAutofillService : AutofillService() {
|
||||||
|
|
||||||
|
var applicationIdBlocklist: Set<String>? = null
|
||||||
|
var webDomainBlocklist: Set<String>? = null
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
|
||||||
|
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFillRequest(request: FillRequest,
|
override fun onFillRequest(request: FillRequest,
|
||||||
cancellationSignal: CancellationSignal,
|
cancellationSignal: CancellationSignal,
|
||||||
callback: FillCallback) {
|
callback: FillCallback) {
|
||||||
@@ -43,32 +56,36 @@ class KeeAutofillService : AutofillService() {
|
|||||||
// Check user's settings for authenticating Responses and Datasets.
|
// Check user's settings for authenticating Responses and Datasets.
|
||||||
StructureParser(latestStructure).parse()?.let { parseResult ->
|
StructureParser(latestStructure).parse()?.let { parseResult ->
|
||||||
|
|
||||||
val searchInfo = SearchInfo().apply {
|
// Build search info only if applicationId or webDomain are not blocked
|
||||||
applicationId = parseResult.applicationId
|
if (searchAllowedFor(parseResult.applicationId, applicationIdBlocklist)
|
||||||
webDomain = parseResult.domain
|
&& searchAllowedFor(parseResult.domain, webDomainBlocklist)) {
|
||||||
}
|
val searchInfo = SearchInfo().apply {
|
||||||
|
applicationId = parseResult.applicationId
|
||||||
|
webDomain = parseResult.domain
|
||||||
|
}
|
||||||
|
|
||||||
AutofillHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
Database.getInstance(),
|
Database.getInstance(),
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ items ->
|
{ items ->
|
||||||
val responseBuilder = FillResponse.Builder()
|
val responseBuilder = FillResponse.Builder()
|
||||||
AutofillHelper.addHeader(responseBuilder, packageName,
|
AutofillHelper.addHeader(responseBuilder, packageName,
|
||||||
parseResult.domain, parseResult.applicationId)
|
parseResult.domain, parseResult.applicationId)
|
||||||
items.forEach {
|
items.forEach {
|
||||||
responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult))
|
responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult))
|
||||||
|
}
|
||||||
|
callback.onSuccess(responseBuilder.build())
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Show UI if no search result
|
||||||
|
showUIForEntrySelection(parseResult, searchInfo, callback)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Show UI if database not open
|
||||||
|
showUIForEntrySelection(parseResult, searchInfo, callback)
|
||||||
}
|
}
|
||||||
callback.onSuccess(responseBuilder.build())
|
)
|
||||||
},
|
}
|
||||||
{
|
|
||||||
// Show UI if no search result
|
|
||||||
showUIForEntrySelection(parseResult, searchInfo, callback)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Show UI if database not open
|
|
||||||
showUIForEntrySelection(parseResult, searchInfo, callback)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,5 +131,18 @@ class KeeAutofillService : AutofillService() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = KeeAutofillService::class.java.name
|
private val TAG = KeeAutofillService::class.java.name
|
||||||
|
|
||||||
|
fun searchAllowedFor(element: String?, blockList: Set<String>?): Boolean {
|
||||||
|
element?.let { elementNotNull ->
|
||||||
|
if (blockList?.any { appIdBlocked ->
|
||||||
|
elementNotNull.contains(appIdBlocked)
|
||||||
|
} == true
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "Autofill not allowed for $elementNotNull")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,9 +102,12 @@ internal class StructureParser(private val structure: AssistStructure) {
|
|||||||
when {
|
when {
|
||||||
it.equals(View.AUTOFILL_HINT_USERNAME, true)
|
it.equals(View.AUTOFILL_HINT_USERNAME, true)
|
||||||
|| it.equals(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
|| it.equals(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
||||||
|| it.equals(View.AUTOFILL_HINT_PHONE, true)
|
|
||||||
|| it.equals("email", true)
|
|| it.equals("email", true)
|
||||||
|| it.equals("usernameOrEmail", true)-> {
|
|| it.equals(View.AUTOFILL_HINT_PHONE, true)
|
||||||
|
|| it.contains("OrUsername", true)
|
||||||
|
|| it.contains("OrEmailAddress", true)
|
||||||
|
|| it.contains("OrEmail", true)
|
||||||
|
|| it.contains("OrPhone", true)-> {
|
||||||
result?.usernameId = autofillId
|
result?.usernameId = autofillId
|
||||||
Log.d(TAG, "Autofill username hint")
|
Log.d(TAG, "Autofill username hint")
|
||||||
}
|
}
|
||||||
@@ -112,7 +115,7 @@ internal class StructureParser(private val structure: AssistStructure) {
|
|||||||
|| it.contains("password", true) -> {
|
|| it.contains("password", true) -> {
|
||||||
result?.passwordId = autofillId
|
result?.passwordId = autofillId
|
||||||
Log.d(TAG, "Autofill password hint")
|
Log.d(TAG, "Autofill password hint")
|
||||||
// Username not needed in this specific case
|
// Username not needed in this case
|
||||||
usernameNeeded = false
|
usernameNeeded = false
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -160,42 +163,78 @@ internal class StructureParser(private val structure: AssistStructure) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun inputIsVariationType(inputType: Int, vararg type: Int): Boolean {
|
||||||
|
type.forEach {
|
||||||
|
if (inputType and InputType.TYPE_MASK_VARIATION == it)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showHexInputType(inputType: Int): String {
|
||||||
|
return "0x${"%08x".format(inputType)}"
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseNodeByAndroidInput(node: AssistStructure.ViewNode): Boolean {
|
private fun parseNodeByAndroidInput(node: AssistStructure.ViewNode): Boolean {
|
||||||
val autofillId = node.autofillId
|
val autofillId = node.autofillId
|
||||||
val inputType = node.inputType
|
val inputType = node.inputType
|
||||||
if (inputType and InputType.TYPE_CLASS_TEXT != 0) {
|
when (inputType and InputType.TYPE_MASK_CLASS) {
|
||||||
when {
|
InputType.TYPE_CLASS_TEXT -> {
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS != 0 -> {
|
when {
|
||||||
result?.usernameId = autofillId
|
inputIsVariationType(inputType,
|
||||||
Log.d(TAG, "Autofill username android type: $inputType")
|
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS) -> {
|
||||||
|
result?.usernameId = autofillId
|
||||||
|
Log.d(TAG, "Autofill username android text type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_NORMAL,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
|
||||||
|
usernameCandidate = autofillId
|
||||||
|
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_PASSWORD,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
|
||||||
|
result?.passwordId = autofillId
|
||||||
|
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
|
||||||
|
usernameNeeded = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_FILTER,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_PHONETIC,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_URI) -> {
|
||||||
|
// Type not used
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.d(TAG, "Autofill unknown android text type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_NORMAL != 0 ||
|
}
|
||||||
inputType and InputType.TYPE_NUMBER_VARIATION_NORMAL != 0 ||
|
InputType.TYPE_CLASS_NUMBER -> {
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_PERSON_NAME != 0 -> {
|
when {
|
||||||
usernameCandidate = autofillId
|
inputIsVariationType(inputType,
|
||||||
Log.d(TAG, "Autofill username candidate android type: $inputType")
|
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
|
||||||
}
|
usernameCandidate = autofillId
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD != 0 ||
|
Log.d(TAG, "Autofill usernale candidate android number type: ${showHexInputType(inputType)}")
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD != 0 ||
|
}
|
||||||
inputType and InputType.TYPE_NUMBER_VARIATION_PASSWORD != 0 -> {
|
inputIsVariationType(inputType,
|
||||||
result?.passwordId = autofillId
|
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
|
||||||
Log.d(TAG, "Autofill password android type: $inputType")
|
result?.passwordId = autofillId
|
||||||
return true
|
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
|
||||||
}
|
usernameNeeded = false
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT != 0 ||
|
return true
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_FILTER != 0 ||
|
}
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE != 0 ||
|
else -> {
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_PHONETIC != 0 ||
|
Log.d(TAG, "Autofill unknown android number type: ${showHexInputType(inputType)}")
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS != 0 ||
|
}
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_URI != 0 ||
|
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT != 0 ||
|
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS != 0 ||
|
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD != 0 -> {
|
|
||||||
// Type not used
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Log.d(TAG, "Autofill unknown android type: $inputType")
|
|
||||||
usernameCandidate = autofillId
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import android.provider.Settings
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
@@ -52,7 +53,18 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
|
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
|
||||||
private var biometricMode: Mode = Mode.UNAVAILABLE
|
private var biometricMode: Mode = Mode.UNAVAILABLE
|
||||||
|
|
||||||
private var isBiometricPromptAutoOpenEnable = PreferencesUtil.isBiometricPromptAutoOpenEnable(context)
|
/**
|
||||||
|
* Manage setting to auto open biometric prompt
|
||||||
|
*/
|
||||||
|
private var biometricPromptAutoOpenPreference = PreferencesUtil.isBiometricPromptAutoOpenEnable(context)
|
||||||
|
var isBiometricPromptAutoOpenEnable: Boolean = true
|
||||||
|
get() {
|
||||||
|
return field && biometricPromptAutoOpenPreference
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variable to check if the prompt can be open (if the right activity is currently shown)
|
||||||
|
// checkBiometricAvailability() allows open biometric prompt and onDestroy() removes the authorization
|
||||||
|
private var allowOpenBiometricPrompt = false
|
||||||
|
|
||||||
private var cipherDatabaseAction = CipherDatabaseAction.getInstance(context.applicationContext)
|
private var cipherDatabaseAction = CipherDatabaseAction.getInstance(context.applicationContext)
|
||||||
|
|
||||||
@@ -73,6 +85,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
// biometric not supported (by API level or hardware) so keep option hidden
|
// biometric not supported (by API level or hardware) so keep option hidden
|
||||||
// or manually disable
|
// or manually disable
|
||||||
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate()
|
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate()
|
||||||
|
allowOpenBiometricPrompt = true
|
||||||
|
|
||||||
if (!PreferencesUtil.isBiometricUnlockEnable(context)
|
if (!PreferencesUtil.isBiometricUnlockEnable(context)
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||||
@@ -206,7 +219,8 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
cryptoObject: BiometricPrompt.CryptoObject,
|
cryptoObject: BiometricPrompt.CryptoObject,
|
||||||
promptInfo: BiometricPrompt.PromptInfo) {
|
promptInfo: BiometricPrompt.PromptInfo) {
|
||||||
context.runOnUiThread {
|
context.runOnUiThread {
|
||||||
biometricPrompt?.authenticate(promptInfo, cryptoObject)
|
if (allowOpenBiometricPrompt)
|
||||||
|
biometricPrompt?.authenticate(promptInfo, cryptoObject)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,6 +286,9 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
|
// Close the biometric prompt
|
||||||
|
allowOpenBiometricPrompt = false
|
||||||
|
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
|
||||||
// Restore the checked listener
|
// Restore the checked listener
|
||||||
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
|
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
|
||||||
}
|
}
|
||||||
@@ -318,7 +335,9 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showFingerPrintViews(show: Boolean) {
|
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) {
|
private fun setAdvancedUnlockedTitleView(textId: Int) {
|
||||||
|
|||||||
@@ -271,6 +271,10 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun closeBiometricPrompt() {
|
||||||
|
biometricPrompt?.cancelAuthentication()
|
||||||
|
}
|
||||||
|
|
||||||
interface BiometricUnlockErrorCallback {
|
interface BiometricUnlockErrorCallback {
|
||||||
fun onInvalidKeyException(e: Exception)
|
fun onInvalidKeyException(e: Exception)
|
||||||
fun onBiometricException(e: Exception)
|
fun onBiometricException(e: Exception)
|
||||||
|
|||||||
@@ -41,17 +41,6 @@ object CipherFactory {
|
|||||||
Security.addProvider(BouncyCastleProvider())
|
Security.addProvider(BouncyCastleProvider())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class)
|
|
||||||
@JvmOverloads
|
|
||||||
fun getInstance(transformation: String, androidOverride: Boolean = false): Cipher {
|
|
||||||
// Return the native AES if it is possible
|
|
||||||
return if (!deviceBlacklisted() && !androidOverride && hasNativeImplementation(transformation) && NativeLib.loaded()) {
|
|
||||||
Cipher.getInstance(transformation, AESProvider())
|
|
||||||
} else {
|
|
||||||
Cipher.getInstance(transformation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deviceBlacklisted(): Boolean {
|
fun deviceBlacklisted(): Boolean {
|
||||||
if (!blacklistInit) {
|
if (!blacklistInit) {
|
||||||
blacklistInit = true
|
blacklistInit = true
|
||||||
@@ -65,6 +54,16 @@ object CipherFactory {
|
|||||||
return transformation == "AES/CBC/PKCS5Padding"
|
return transformation == "AES/CBC/PKCS5Padding"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class)
|
||||||
|
fun getInstance(transformation: String, androidOverride: Boolean = false): Cipher {
|
||||||
|
// Return the native AES if it is possible
|
||||||
|
return if (!deviceBlacklisted() && !androidOverride && hasNativeImplementation(transformation) && NativeLib.loaded()) {
|
||||||
|
Cipher.getInstance(transformation, AESProvider())
|
||||||
|
} else {
|
||||||
|
Cipher.getInstance(transformation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate appropriate cipher based on KeePass 2.x UUID's
|
* Generate appropriate cipher based on KeePass 2.x UUID's
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -74,12 +74,10 @@ object CryptoUtil {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
fun hashSha256(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
|
fun hashSha256(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
|
||||||
return hashGen("SHA-256", data, offset, count)
|
return hashGen("SHA-256", data, offset, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
fun hashSha512(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
|
fun hashSha512(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
|
||||||
return hashGen("SHA-512", data, offset, count)
|
return hashGen("SHA-512", data, offset, count)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,12 +20,10 @@
|
|||||||
package com.kunzisoft.keepass.crypto.finalkey
|
package com.kunzisoft.keepass.crypto.finalkey
|
||||||
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.lang.Exception
|
|
||||||
import java.security.InvalidKeyException
|
import java.security.InvalidKeyException
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.NoSuchPaddingException
|
|
||||||
import javax.crypto.ShortBufferException
|
import javax.crypto.ShortBufferException
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
|||||||
@@ -61,10 +61,10 @@ class Argon2Kdf internal constructor() : KdfEngine() {
|
|||||||
UnsignedInt(it)
|
UnsignedInt(it)
|
||||||
}
|
}
|
||||||
val memory = kdfParameters.getUInt64(PARAM_MEMORY)?.div(MEMORY_BLOCK_SIZE)?.let {
|
val memory = kdfParameters.getUInt64(PARAM_MEMORY)?.div(MEMORY_BLOCK_SIZE)?.let {
|
||||||
UnsignedInt.fromLong(it)
|
UnsignedInt.fromKotlinLong(it)
|
||||||
}
|
}
|
||||||
val iterations = kdfParameters.getUInt64(PARAM_ITERATIONS)?.let {
|
val iterations = kdfParameters.getUInt64(PARAM_ITERATIONS)?.let {
|
||||||
UnsignedInt.fromLong(it)
|
UnsignedInt.fromKotlinLong(it)
|
||||||
}
|
}
|
||||||
val version = kdfParameters.getUInt32(PARAM_VERSION)?.let {
|
val version = kdfParameters.getUInt32(PARAM_VERSION)?.let {
|
||||||
UnsignedInt(it)
|
UnsignedInt(it)
|
||||||
@@ -124,16 +124,16 @@ class Argon2Kdf internal constructor() : KdfEngine() {
|
|||||||
|
|
||||||
override fun getParallelism(kdfParameters: KdfParameters): Long {
|
override fun getParallelism(kdfParameters: KdfParameters): Long {
|
||||||
return kdfParameters.getUInt32(PARAM_PARALLELISM)?.let {
|
return kdfParameters.getUInt32(PARAM_PARALLELISM)?.let {
|
||||||
UnsignedInt(it).toLong()
|
UnsignedInt(it).toKotlinLong()
|
||||||
} ?: defaultParallelism
|
} ?: defaultParallelism
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setParallelism(kdfParameters: KdfParameters, parallelism: Long) {
|
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
|
override val defaultParallelism: Long
|
||||||
get() = DEFAULT_PARALLELISM.toLong()
|
get() = DEFAULT_PARALLELISM.toKotlinLong()
|
||||||
|
|
||||||
override val minParallelism: Long
|
override val minParallelism: Long
|
||||||
get() = MIN_PARALLELISM
|
get() = MIN_PARALLELISM
|
||||||
@@ -173,13 +173,13 @@ class Argon2Kdf internal constructor() : KdfEngine() {
|
|||||||
private val MAX_VERSION = UnsignedInt(0x13)
|
private val MAX_VERSION = UnsignedInt(0x13)
|
||||||
|
|
||||||
private const val MIN_SALT = 8
|
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 MIN_ITERATIONS: Long = 1L
|
||||||
private const val MAX_ITERATIONS = 4294967295L
|
private const val MAX_ITERATIONS = 4294967295L
|
||||||
|
|
||||||
private const val MIN_MEMORY = (1024 * 8).toLong()
|
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 MEMORY_BLOCK_SIZE: Long = 1024L
|
||||||
|
|
||||||
private const val MIN_PARALLELISM: Long = 1L
|
private const val MIN_PARALLELISM: Long = 1L
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ public class Argon2Native {
|
|||||||
return nTransformMasterKey(
|
return nTransformMasterKey(
|
||||||
password,
|
password,
|
||||||
salt,
|
salt,
|
||||||
parallelism.toInt(),
|
parallelism.toKotlinInt(),
|
||||||
memory.toInt(),
|
memory.toKotlinInt(),
|
||||||
iterations.toInt(),
|
iterations.toKotlinInt(),
|
||||||
secretKey,
|
secretKey,
|
||||||
associatedData,
|
associatedData,
|
||||||
version.toInt());
|
version.toKotlinInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static native byte[] nTransformMasterKey(byte[] password, byte[] salt, int parallelism,
|
private static native byte[] nTransformMasterKey(byte[] password, byte[] salt, int parallelism,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ abstract class KdfEngine : ObjectNameResource, Serializable {
|
|||||||
get() = 1
|
get() = 1
|
||||||
|
|
||||||
open val maxKeyRounds: Long
|
open val maxKeyRounds: Long
|
||||||
get() = UnsignedInt.MAX_VALUE.toLong()
|
get() = UnsignedInt.MAX_VALUE.toKotlinLong()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* MEMORY
|
* MEMORY
|
||||||
@@ -73,7 +73,7 @@ abstract class KdfEngine : ObjectNameResource, Serializable {
|
|||||||
get() = 1
|
get() = 1
|
||||||
|
|
||||||
open val maxMemoryUsage: Long
|
open val maxMemoryUsage: Long
|
||||||
get() = UnsignedInt.MAX_VALUE.toLong()
|
get() = UnsignedInt.MAX_VALUE.toKotlinLong()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* PARALLELISM
|
* PARALLELISM
|
||||||
@@ -94,7 +94,7 @@ abstract class KdfEngine : ObjectNameResource, Serializable {
|
|||||||
get() = 1L
|
get() = 1L
|
||||||
|
|
||||||
open val maxParallelism: Long
|
open val maxParallelism: Long
|
||||||
get() = UnsignedInt.MAX_VALUE.toLong()
|
get() = UnsignedInt.MAX_VALUE.toKotlinLong()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val UNKNOWN_VALUE: Long = -1L
|
const val UNKNOWN_VALUE: Long = -1L
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ open class AssignPasswordInDatabaseRunnable (
|
|||||||
: SaveDatabaseRunnable(context, database, true) {
|
: SaveDatabaseRunnable(context, database, true) {
|
||||||
|
|
||||||
private var mMasterPassword: String? = null
|
private var mMasterPassword: String? = null
|
||||||
protected var mKeyFile: Uri? = null
|
protected var mKeyFileUri: Uri? = null
|
||||||
|
|
||||||
private var mBackupKey: ByteArray? = null
|
private var mBackupKey: ByteArray? = null
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ open class AssignPasswordInDatabaseRunnable (
|
|||||||
if (withMasterPassword)
|
if (withMasterPassword)
|
||||||
this.mMasterPassword = masterPassword
|
this.mMasterPassword = masterPassword
|
||||||
if (withKeyFile)
|
if (withKeyFile)
|
||||||
this.mKeyFile = keyFile
|
this.mKeyFileUri = keyFile
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
@@ -55,7 +55,7 @@ open class AssignPasswordInDatabaseRunnable (
|
|||||||
mBackupKey = ByteArray(database.masterKey.size)
|
mBackupKey = ByteArray(database.masterKey.size)
|
||||||
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.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)
|
database.retrieveMasterKey(mMasterPassword, uriInputStream)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
erase(mBackupKey)
|
erase(mBackupKey)
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ class CreateDatabaseRunnable(context: Context,
|
|||||||
withMasterPassword: Boolean,
|
withMasterPassword: Boolean,
|
||||||
masterPassword: String?,
|
masterPassword: String?,
|
||||||
withKeyFile: Boolean,
|
withKeyFile: Boolean,
|
||||||
keyFile: Uri?)
|
keyFile: Uri?,
|
||||||
|
private val createDatabaseResult: ((Result) -> Unit)?)
|
||||||
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile) {
|
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile) {
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
@@ -42,29 +43,36 @@ class CreateDatabaseRunnable(context: Context,
|
|||||||
// Create new database record
|
// Create new database record
|
||||||
mDatabase.apply {
|
mDatabase.apply {
|
||||||
createData(mDatabaseUri, databaseName, rootName)
|
createData(mDatabaseUri, databaseName, rootName)
|
||||||
// Set Database state
|
|
||||||
loaded = true
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
mDatabase.closeAndClear()
|
mDatabase.closeAndClear(context.applicationContext.filesDir)
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onStartRun()
|
super.onStartRun()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFinishRun() {
|
override fun onActionRun() {
|
||||||
super.onFinishRun()
|
super.onActionRun()
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
// Add database to recent files
|
// Add database to recent files
|
||||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||||
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
||||||
.addOrUpdateDatabaseUri(mDatabaseUri,
|
.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 {
|
} else {
|
||||||
Log.e("CreateDatabaseRunnable", "Unable to create the database")
|
Log.e("CreateDatabaseRunnable", "Unable to create the database")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFinishRun() {
|
||||||
|
super.onFinishRun()
|
||||||
|
|
||||||
|
createDatabaseResult?.invoke(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
|||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
@@ -39,17 +38,14 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
private val mKey: Uri?,
|
private val mKey: Uri?,
|
||||||
private val mReadonly: Boolean,
|
private val mReadonly: Boolean,
|
||||||
private val mCipherEntity: CipherDatabaseEntity?,
|
private val mCipherEntity: CipherDatabaseEntity?,
|
||||||
private val mOmitBackup: Boolean,
|
|
||||||
private val mFixDuplicateUUID: Boolean,
|
private val mFixDuplicateUUID: Boolean,
|
||||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
private val mDuplicateUuidAction: ((Result) -> Unit)?)
|
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||||
: ActionRunnable() {
|
: ActionRunnable() {
|
||||||
|
|
||||||
private val cacheDirectory = context.applicationContext.filesDir
|
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
// Clear before we load
|
// Clear before we load
|
||||||
mDatabase.closeAndClear(cacheDirectory)
|
mDatabase.closeAndClear(context.applicationContext.filesDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionRun() {
|
override fun onActionRun() {
|
||||||
@@ -57,21 +53,17 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
mDatabase.loadData(mUri, mPass, mKey,
|
mDatabase.loadData(mUri, mPass, mKey,
|
||||||
mReadonly,
|
mReadonly,
|
||||||
context.contentResolver,
|
context.contentResolver,
|
||||||
cacheDirectory,
|
context.applicationContext.filesDir,
|
||||||
mOmitBackup,
|
|
||||||
mFixDuplicateUUID,
|
mFixDuplicateUUID,
|
||||||
progressTaskUpdater)
|
progressTaskUpdater)
|
||||||
}
|
}
|
||||||
catch (e: DuplicateUuidDatabaseException) {
|
catch (e: DuplicateUuidDatabaseException) {
|
||||||
mDuplicateUuidAction?.invoke(result)
|
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
catch (e: LoadDatabaseException) {
|
catch (e: LoadDatabaseException) {
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinishRun() {
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
// Save keyFile in app database
|
// Save keyFile in app database
|
||||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||||
@@ -88,11 +80,12 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
|
|
||||||
// Register the current time to init the lock timer
|
// Register the current time to init the lock timer
|
||||||
PreferencesUtil.saveCurrentTime(context)
|
PreferencesUtil.saveCurrentTime(context)
|
||||||
|
|
||||||
// Start the opening notification
|
|
||||||
DatabaseOpenNotificationService.start(context)
|
|
||||||
} else {
|
} else {
|
||||||
mDatabase.closeAndClear(cacheDirectory)
|
mDatabase.closeAndClear(context.applicationContext.filesDir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFinishRun() {
|
||||||
|
mLoadDatabaseResult?.invoke(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
@@ -37,7 +36,6 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
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
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||||
@@ -68,18 +66,17 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
|
|||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
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_START_TASK_ACTION
|
||||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class ProgressDialogThread(private val activity: FragmentActivity) {
|
class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||||
|
|
||||||
var onActionFinish: ((actionTask: String,
|
var onActionFinish: ((actionTask: String,
|
||||||
result: ActionRunnable.Result) -> Unit)? = null
|
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 databaseTaskBroadcastReceiver: BroadcastReceiver? = null
|
||||||
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
|
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
|
||||||
@@ -90,37 +87,23 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
|
|
||||||
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
||||||
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||||
TimeoutHelper.temporarilyDisableTimeout()
|
startDialog(titleId, messageId, warningId)
|
||||||
// Stop the opening notification
|
|
||||||
DatabaseOpenNotificationService.stop(activity)
|
|
||||||
startOrUpdateDialog(titleId, messageId, warningId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||||
startOrUpdateDialog(titleId, messageId, warningId)
|
updateDialog(titleId, messageId, warningId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStopAction(actionTask: String, result: ActionRunnable.Result) {
|
override fun onStopAction(actionTask: String, result: ActionRunnable.Result) {
|
||||||
onActionFinish?.invoke(actionTask, result)
|
onActionFinish?.invoke(actionTask, result)
|
||||||
|
|
||||||
// Remove the progress task
|
// Remove the progress task
|
||||||
stopDialog()
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startOrUpdateDialog(titleId: Int?, messageId: Int?, warningId: Int?) {
|
private fun startDialog(titleId: Int? = null,
|
||||||
|
messageId: Int? = null,
|
||||||
|
warningId: Int? = null) {
|
||||||
if (progressTaskDialogFragment == null) {
|
if (progressTaskDialogFragment == null) {
|
||||||
progressTaskDialogFragment = activity.supportFragmentManager
|
progressTaskDialogFragment = activity.supportFragmentManager
|
||||||
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
|
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
|
||||||
@@ -129,6 +112,10 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
progressTaskDialogFragment = ProgressTaskDialogFragment()
|
progressTaskDialogFragment = ProgressTaskDialogFragment()
|
||||||
progressTaskDialogFragment?.show(activity.supportFragmentManager, PROGRESS_TASK_DIALOG_TAG)
|
progressTaskDialogFragment?.show(activity.supportFragmentManager, PROGRESS_TASK_DIALOG_TAG)
|
||||||
}
|
}
|
||||||
|
updateDialog(titleId, messageId, warningId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDialog(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||||
progressTaskDialogFragment?.apply {
|
progressTaskDialogFragment?.apply {
|
||||||
titleId?.let {
|
titleId?.let {
|
||||||
updateTitle(it)
|
updateTitle(it)
|
||||||
@@ -194,6 +181,8 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
bindService()
|
bindService()
|
||||||
}
|
}
|
||||||
DATABASE_STOP_TASK_ACTION -> {
|
DATABASE_STOP_TASK_ACTION -> {
|
||||||
|
// Remove the progress task
|
||||||
|
stopDialog()
|
||||||
unBindService()
|
unBindService()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,12 +218,8 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
activity.stopService(intentDatabaseTask)
|
activity.stopService(intentDatabaseTask)
|
||||||
if (bundle != null)
|
if (bundle != null)
|
||||||
intentDatabaseTask.putExtras(bundle)
|
intentDatabaseTask.putExtras(bundle)
|
||||||
intentDatabaseTask.action = actionTask
|
intentDatabaseTask.action = actionTask
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
activity.startService(intentDatabaseTask)
|
||||||
activity.startForegroundService(intentDatabaseTask)
|
|
||||||
} else {
|
|
||||||
activity.startService(intentDatabaseTask)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -253,7 +238,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
||||||
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
||||||
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
|
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
|
||||||
putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile)
|
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_CREATE_TASK)
|
, ACTION_DATABASE_CREATE_TASK)
|
||||||
}
|
}
|
||||||
@@ -267,7 +252,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
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)
|
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
||||||
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
|
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
|
||||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||||
@@ -286,7 +271,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
||||||
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
||||||
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
|
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)
|
, ACTION_DATABASE_ASSIGN_PASSWORD_TASK)
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ class DeleteEntryHistoryDatabaseRunnable (
|
|||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
try {
|
try {
|
||||||
mainEntry.removeEntryFromHistory(entryHistoryPosition)
|
database.removeEntryHistory(mainEntry, entryHistoryPosition)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ class DeleteNodesRunnable(context: Context,
|
|||||||
} else {
|
} else {
|
||||||
database.deleteEntry(currentNode)
|
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
|
package com.kunzisoft.keepass.database.action.node
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
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
|
// WARNING : Re attribute parent removed in entry edit activity to save memory
|
||||||
mNewEntry.addParentFrom(mOldEntry)
|
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
|
// Update entry with new values
|
||||||
mOldEntry.updateWith(mNewEntry)
|
mOldEntry.updateWith(mNewEntry)
|
||||||
mNewEntry.touch(modified = true, touchParents = true)
|
mNewEntry.touch(modified = true, touchParents = true)
|
||||||
|
|
||||||
// Create an entry history (an entry history don't have history)
|
// Create an entry history (an entry history don't have history)
|
||||||
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
|
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
|
||||||
database.removeOldestEntryHistory(mOldEntry)
|
database.removeOldestEntryHistory(mOldEntry, database.binaryPool)
|
||||||
|
|
||||||
// Only change data in index
|
// Only change data in index
|
||||||
database.updateEntry(mOldEntry)
|
database.updateEntry(mOldEntry)
|
||||||
|
|
||||||
|
// Remove oldest attachments
|
||||||
|
attachmentsToRemove.forEach {
|
||||||
|
database.removeAttachmentIfNotUsed(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nodeFinish(): ActionNodesValues {
|
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 android.util.Log
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.*
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||||
@@ -46,14 +44,13 @@ import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
|
|||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
|
||||||
import com.kunzisoft.keepass.stream.readBytes4ToUInt
|
import com.kunzisoft.keepass.stream.readBytes4ToUInt
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolder
|
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import org.apache.commons.io.FileUtils
|
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
@@ -72,6 +69,13 @@ class Database {
|
|||||||
val drawFactory = IconDrawableFactory()
|
val drawFactory = IconDrawableFactory()
|
||||||
|
|
||||||
var loaded = false
|
var loaded = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
loadTimestamp = if (field) System.currentTimeMillis() else null
|
||||||
|
}
|
||||||
|
|
||||||
|
var loadTimestamp: Long? = null
|
||||||
|
private set
|
||||||
|
|
||||||
val iconFactory: IconImageFactory
|
val iconFactory: IconImageFactory
|
||||||
get() {
|
get() {
|
||||||
@@ -152,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,
|
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
|
||||||
newCompression: CompressionAlgorithm) {
|
newCompression: CompressionAlgorithm) {
|
||||||
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
|
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
|
||||||
@@ -263,14 +278,14 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if RecycleBin is available or not for this version of database
|
* Determine if a configurable RecycleBin is available or not for this version of database
|
||||||
* @return true if RecycleBin available
|
* @return true if a configurable RecycleBin available
|
||||||
*/
|
*/
|
||||||
val allowRecycleBin: Boolean
|
val allowConfigurableRecycleBin: Boolean
|
||||||
get() = mDatabaseKDBX != null
|
get() = mDatabaseKDBX != null
|
||||||
|
|
||||||
var isRecycleBinEnabled: Boolean
|
var isRecycleBinEnabled: Boolean
|
||||||
// TODO #394 isRecycleBinEnabled mDatabaseKDB
|
// Backup is always enabled in KDB database
|
||||||
get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false
|
get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false
|
||||||
set(value) {
|
set(value) {
|
||||||
mDatabaseKDBX?.isRecycleBinEnabled = value
|
mDatabaseKDBX?.isRecycleBinEnabled = value
|
||||||
@@ -288,12 +303,12 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun ensureRecycleBinExists(resources: Resources) {
|
fun ensureRecycleBinExists(resources: Resources) {
|
||||||
mDatabaseKDB?.ensureRecycleBinExists()
|
mDatabaseKDB?.ensureBackupExists()
|
||||||
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeRecycleBin() {
|
fun removeRecycleBin() {
|
||||||
// TODO #394 delete backup mDatabaseKDB?.removeRecycleBin()
|
// Don't allow remove backup in KDB
|
||||||
mDatabaseKDBX?.removeRecycleBin()
|
mDatabaseKDBX?.removeRecycleBin()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +325,8 @@ class Database {
|
|||||||
fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
|
fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
|
||||||
setDatabaseKDBX(DatabaseKDBX(databaseName, rootName))
|
setDatabaseKDBX(DatabaseKDBX(databaseName, rootName))
|
||||||
this.fileUri = databaseUri
|
this.fileUri = databaseUri
|
||||||
|
// Set Database state
|
||||||
|
this.loaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
@@ -317,7 +334,6 @@ class Database {
|
|||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
contentResolver: ContentResolver,
|
contentResolver: ContentResolver,
|
||||||
cacheDirectory: File,
|
cacheDirectory: File,
|
||||||
omitBackup: Boolean,
|
|
||||||
fixDuplicateUUID: Boolean,
|
fixDuplicateUUID: Boolean,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||||
|
|
||||||
@@ -379,14 +395,12 @@ class Database {
|
|||||||
else -> throw SignatureDatabaseException()
|
else -> throw SignatureDatabaseException()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mSearchHelper = SearchHelper(omitBackup)
|
this.mSearchHelper = SearchHelper()
|
||||||
loaded = true
|
loaded = true
|
||||||
|
|
||||||
} catch (e: LoadDatabaseException) {
|
} catch (e: LoadDatabaseException) {
|
||||||
Log.e("KPD", "Database::loadData", e)
|
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("KPD", "Database::loadData", e)
|
|
||||||
throw FileNotFoundDatabaseException()
|
throw FileNotFoundDatabaseException()
|
||||||
} finally {
|
} finally {
|
||||||
keyFileInputStream?.close()
|
keyFileInputStream?.close()
|
||||||
@@ -394,25 +408,24 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isGroupSearchable(group: Group, isOmitBackup: Boolean): Boolean {
|
fun isGroupSearchable(group: Group, omitBackup: Boolean): Boolean {
|
||||||
return mDatabaseKDB?.isGroupSearchable(group.groupKDB, isOmitBackup) ?:
|
return mDatabaseKDB?.isGroupSearchable(group.groupKDB, omitBackup) ?:
|
||||||
mDatabaseKDBX?.isGroupSearchable(group.groupKDBX, isOmitBackup) ?:
|
mDatabaseKDBX?.isGroupSearchable(group.groupKDBX, omitBackup) ?:
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createVirtualGroupFromSearch(searchQuery: String,
|
fun createVirtualGroupFromSearch(searchQuery: String,
|
||||||
|
omitBackup: Boolean,
|
||||||
max: Int = Integer.MAX_VALUE): Group? {
|
max: Int = Integer.MAX_VALUE): Group? {
|
||||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this, searchQuery, SearchParameters(), max)
|
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
||||||
|
searchQuery, SearchParameters(), omitBackup, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createVirtualGroupFromSearch(searchInfo: SearchInfo,
|
fun createVirtualGroupFromSearchInfo(searchInfoString: String,
|
||||||
max: Int = Integer.MAX_VALUE): Group? {
|
omitBackup: Boolean,
|
||||||
val query = (if (searchInfo.webDomain != null)
|
max: Int = Integer.MAX_VALUE): Group? {
|
||||||
searchInfo.webDomain
|
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
||||||
else
|
searchInfoString, SearchParameters().apply {
|
||||||
searchInfo.applicationId)
|
|
||||||
?: return null
|
|
||||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this, query, SearchParameters().apply {
|
|
||||||
searchInTitles = false
|
searchInTitles = false
|
||||||
searchInUserNames = false
|
searchInUserNames = false
|
||||||
searchInPasswords = false
|
searchInPasswords = false
|
||||||
@@ -422,7 +435,38 @@ class Database {
|
|||||||
searchInUUIDs = false
|
searchInUUIDs = false
|
||||||
searchInTags = false
|
searchInTags = false
|
||||||
ignoreCase = true
|
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,
|
||||||
|
enableProtection: Boolean = false,
|
||||||
|
compressed: Boolean = false): BinaryAttachment? {
|
||||||
|
return mDatabaseKDB?.buildNewBinary(cacheDirectory)
|
||||||
|
?: mDatabaseKDBX?.buildNewBinary(cacheDirectory, enableProtection, compressed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAttachmentIfNotUsed(attachment: Attachment) {
|
||||||
|
// No need in KDB database because unique attachment by entry
|
||||||
|
mDatabaseKDBX?.removeAttachmentIfNotUsed(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeUnlinkedAttachments() {
|
||||||
|
// No check in database KDB because unique attachment by entry
|
||||||
|
mDatabaseKDBX?.removeUnlinkedAttachments()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(DatabaseOutputException::class)
|
@Throws(DatabaseOutputException::class)
|
||||||
@@ -470,7 +514,7 @@ class Database {
|
|||||||
} else {
|
} else {
|
||||||
var outputStream: OutputStream? = null
|
var outputStream: OutputStream? = null
|
||||||
try {
|
try {
|
||||||
outputStream = contentResolver.openOutputStream(uri)
|
outputStream = contentResolver.openOutputStream(uri, "rwt")
|
||||||
outputStream?.let { definedOutputStream ->
|
outputStream?.let { definedOutputStream ->
|
||||||
val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
|
val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
|
||||||
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
|
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
|
||||||
@@ -492,7 +536,9 @@ class Database {
|
|||||||
mDatabaseKDBX?.clearCache()
|
mDatabaseKDBX?.clearCache()
|
||||||
// In all cases, delete all the files in the temp dir
|
// In all cases, delete all the files in the temp dir
|
||||||
try {
|
try {
|
||||||
FileUtils.cleanDirectory(filesDirectory)
|
filesDirectory?.let { directory ->
|
||||||
|
cleanDirectory(directory)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to clear the directory cache.", e)
|
Log.e(TAG, "Unable to clear the directory cache.", e)
|
||||||
}
|
}
|
||||||
@@ -503,6 +549,17 @@ class Database {
|
|||||||
this.loaded = false
|
this.loaded = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun cleanDirectory(directory: File) {
|
||||||
|
directory.listFiles()?.let { files ->
|
||||||
|
for (file in files) {
|
||||||
|
if (file.isDirectory) {
|
||||||
|
cleanDirectory(file)
|
||||||
|
}
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
||||||
return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile)
|
return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile)
|
||||||
?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile)
|
?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile)
|
||||||
@@ -702,7 +759,7 @@ class Database {
|
|||||||
fun canRecycle(entry: Entry): Boolean {
|
fun canRecycle(entry: Entry): Boolean {
|
||||||
var canRecycle: Boolean? = null
|
var canRecycle: Boolean? = null
|
||||||
entry.entryKDB?.let {
|
entry.entryKDB?.let {
|
||||||
canRecycle = mDatabaseKDB?.canRecycle()
|
canRecycle = mDatabaseKDB?.canRecycle(it)
|
||||||
}
|
}
|
||||||
entry.entryKDBX?.let {
|
entry.entryKDBX?.let {
|
||||||
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
||||||
@@ -713,7 +770,7 @@ class Database {
|
|||||||
fun canRecycle(group: Group): Boolean {
|
fun canRecycle(group: Group): Boolean {
|
||||||
var canRecycle: Boolean? = null
|
var canRecycle: Boolean? = null
|
||||||
group.groupKDB?.let {
|
group.groupKDB?.let {
|
||||||
canRecycle = mDatabaseKDB?.canRecycle()
|
canRecycle = mDatabaseKDB?.canRecycle(it)
|
||||||
}
|
}
|
||||||
group.groupKDBX?.let {
|
group.groupKDBX?.let {
|
||||||
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
||||||
@@ -784,7 +841,7 @@ class Database {
|
|||||||
rootGroup?.doForEachChildAndForIt(
|
rootGroup?.doForEachChildAndForIt(
|
||||||
object : NodeHandler<Entry>() {
|
object : NodeHandler<Entry>() {
|
||||||
override fun operate(node: Entry): Boolean {
|
override fun operate(node: Entry): Boolean {
|
||||||
removeOldestEntryHistory(node)
|
removeOldestEntryHistory(node, binaryPool)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -792,34 +849,19 @@ class Database {
|
|||||||
override fun operate(node: Group): Boolean {
|
override fun operate(node: Group): Boolean {
|
||||||
return true
|
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
|
* Remove oldest history if more than max items or max memory
|
||||||
*/
|
*/
|
||||||
fun removeOldestEntryHistory(entry: Entry) {
|
fun removeOldestEntryHistory(entry: Entry, binaryPool: BinaryPool) {
|
||||||
mDatabaseKDBX?.let {
|
mDatabaseKDBX?.let {
|
||||||
|
|
||||||
val maxItems = historyMaxItems
|
val maxItems = historyMaxItems
|
||||||
if (maxItems >= 0) {
|
if (maxItems >= 0) {
|
||||||
while (entry.getHistory().size > maxItems) {
|
while (entry.getHistory().size > maxItems) {
|
||||||
entry.removeOldestEntryFromHistory()
|
removeOldestEntryHistory(entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -828,11 +870,10 @@ class Database {
|
|||||||
while (true) {
|
while (true) {
|
||||||
var historySize: Long = 0
|
var historySize: Long = 0
|
||||||
for (entryHistory in entry.getHistory()) {
|
for (entryHistory in entry.getHistory()) {
|
||||||
historySize += entryHistory.getSize()
|
historySize += entryHistory.getSize(binaryPool)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (historySize > maxSize) {
|
if (historySize > maxSize) {
|
||||||
entry.removeOldestEntryFromHistory()
|
removeOldestEntryHistory(entry)
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -841,6 +882,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) {
|
companion object : SingletonHolder<Database>(::Database) {
|
||||||
|
|
||||||
private val TAG = Database::class.java.name
|
private val TAG = Database::class.java.name
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ class DeletedObject {
|
|||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
constructor(uuid: UUID, deletionTime: Date = Date()) {
|
constructor(uuid: UUID, deletionTime: Date = Date()) {
|
||||||
this.uuid = uuid
|
this.uuid = uuid
|
||||||
this.mDeletionTime = deletionTime
|
this.mDeletionTime = deletionTime
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.database.element
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
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.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
@@ -32,9 +33,7 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
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.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.model.EntryAttachment
|
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.Field
|
import com.kunzisoft.keepass.model.Field
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
@@ -318,33 +317,40 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startToManageFieldReferences(db: DatabaseKDBX) {
|
fun startToManageFieldReferences(database: DatabaseKDBX) {
|
||||||
entryKDBX?.startToManageFieldReferences(db)
|
entryKDBX?.startToManageFieldReferences(database)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopToManageFieldReferences() {
|
fun stopToManageFieldReferences() {
|
||||||
entryKDBX?.stopToManageFieldReferences()
|
entryKDBX?.stopToManageFieldReferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAttachments(): ArrayList<EntryAttachment> {
|
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> {
|
||||||
val attachments = ArrayList<EntryAttachment>()
|
val attachments = ArrayList<Attachment>()
|
||||||
|
entryKDB?.getAttachment()?.let {
|
||||||
val binaryDescriptionKDB = entryKDB?.binaryDescription ?: ""
|
attachments.add(it)
|
||||||
val binaryKDB = entryKDB?.binaryData
|
|
||||||
if (binaryKDB != null) {
|
|
||||||
attachments.add(EntryAttachment(binaryDescriptionKDB, binaryKDB))
|
|
||||||
}
|
}
|
||||||
|
entryKDBX?.getAttachments(binaryPool, inHistory)?.let {
|
||||||
val actionEach = object : (Map.Entry<String, BinaryAttachment>)->Unit {
|
attachments.addAll(it)
|
||||||
override fun invoke(mapEntry: Map.Entry<String, BinaryAttachment>) {
|
|
||||||
attachments.add(EntryAttachment(mapEntry.key, mapEntry.value))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
entryKDBX?.binaries?.forEach(actionEach)
|
|
||||||
|
|
||||||
return attachments
|
return attachments
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun containsAttachment(): Boolean {
|
||||||
|
return entryKDB?.containsAttachment() == true
|
||||||
|
|| entryKDBX?.containsAttachment() == true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
|
||||||
|
entryKDB?.putAttachment(attachment)
|
||||||
|
entryKDBX?.putAttachment(attachment, binaryPool)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAttachment(attachment: Attachment) {
|
||||||
|
entryKDB?.removeAttachment(attachment)
|
||||||
|
entryKDBX?.removeAttachment(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
fun getHistory(): ArrayList<Entry> {
|
fun getHistory(): ArrayList<Entry> {
|
||||||
val history = ArrayList<Entry>()
|
val history = ArrayList<Entry>()
|
||||||
val entryKDBXHistory = entryKDBX?.history ?: ArrayList()
|
val entryKDBXHistory = entryKDBX?.history ?: ArrayList()
|
||||||
@@ -360,20 +366,22 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeEntryFromHistory(position: Int) {
|
fun removeEntryFromHistory(position: Int): Entry? {
|
||||||
entryKDBX?.removeEntryFromHistory(position)
|
entryKDBX?.removeEntryFromHistory(position)?.let {
|
||||||
|
return Entry(it)
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAllHistory() {
|
fun removeOldestEntryFromHistory(): Entry? {
|
||||||
entryKDBX?.removeAllHistory()
|
entryKDBX?.removeOldestEntryFromHistory()?.let {
|
||||||
|
return Entry(it)
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeOldestEntryFromHistory() {
|
fun getSize(binaryPool: BinaryPool): Long {
|
||||||
entryKDBX?.removeOldestEntryFromHistory()
|
return entryKDBX?.getSize(binaryPool) ?: 0L
|
||||||
}
|
|
||||||
|
|
||||||
fun getSize(): Long {
|
|
||||||
return entryKDBX?.size ?: 0L
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun containsCustomData(): Boolean {
|
fun containsCustomData(): Boolean {
|
||||||
|
|||||||
@@ -260,9 +260,9 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
return entriesInfo
|
return entriesInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFilteredChildEntries(vararg filter: ChildFilter): List<Entry> {
|
fun getFilteredChildEntries(filters: Array<ChildFilter>): List<Entry> {
|
||||||
val withoutMetaStream = filter.contains(ChildFilter.META_STREAM)
|
val withoutMetaStream = filters.contains(ChildFilter.META_STREAM)
|
||||||
val showExpiredEntries = !filter.contains(ChildFilter.EXPIRED)
|
val showExpiredEntries = !filters.contains(ChildFilter.EXPIRED)
|
||||||
|
|
||||||
return groupKDB?.getChildEntries()?.filter {
|
return groupKDB?.getChildEntries()?.filter {
|
||||||
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream))
|
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream))
|
||||||
@@ -278,8 +278,8 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
ArrayList()
|
ArrayList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNumberOfChildEntries(vararg filter: ChildFilter): Int {
|
fun getNumberOfChildEntries(filters: Array<ChildFilter> = emptyArray()): Int {
|
||||||
return getFilteredChildEntries(*filter).size
|
return getFilteredChildEntries(filters).size
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -290,8 +290,8 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
return getChildGroups() + getChildEntries()
|
return getChildGroups() + getChildEntries()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFilteredChildren(vararg filter: ChildFilter): List<Node> {
|
fun getFilteredChildren(filters: Array<ChildFilter>): List<Node> {
|
||||||
return getChildGroups() + getFilteredChildEntries(*filter)
|
return getChildGroups() + getFilteredChildEntries(filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addChildGroup(group: Group) {
|
override fun addChildGroup(group: Group) {
|
||||||
|
|||||||
@@ -17,10 +17,8 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* 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.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.stream.readBytes
|
import com.kunzisoft.keepass.stream.readBytes
|
||||||
@@ -30,7 +28,7 @@ import java.util.zip.GZIPOutputStream
|
|||||||
|
|
||||||
class BinaryAttachment : Parcelable {
|
class BinaryAttachment : Parcelable {
|
||||||
|
|
||||||
var isCompressed: Boolean? = null
|
var isCompressed: Boolean = false
|
||||||
private set
|
private set
|
||||||
var isProtected: Boolean = false
|
var isProtected: Boolean = false
|
||||||
private set
|
private set
|
||||||
@@ -46,12 +44,12 @@ class BinaryAttachment : Parcelable {
|
|||||||
* Empty protected binary
|
* Empty protected binary
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
this.isCompressed = null
|
this.isCompressed = false
|
||||||
this.isProtected = false
|
this.isProtected = false
|
||||||
this.dataFile = null
|
this.dataFile = null
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(dataFile: File, enableProtection: Boolean = false, compressed: Boolean? = null) {
|
constructor(dataFile: File, enableProtection: Boolean = false, compressed: Boolean = false) {
|
||||||
this.isCompressed = compressed
|
this.isCompressed = compressed
|
||||||
this.isProtected = enableProtection
|
this.isProtected = enableProtection
|
||||||
this.dataFile = dataFile
|
this.dataFile = dataFile
|
||||||
@@ -59,7 +57,7 @@ class BinaryAttachment : Parcelable {
|
|||||||
|
|
||||||
private constructor(parcel: Parcel) {
|
private constructor(parcel: Parcel) {
|
||||||
val compressedByte = parcel.readByte().toInt()
|
val compressedByte = parcel.readByte().toInt()
|
||||||
isCompressed = if (compressedByte == 2) null else compressedByte != 0
|
isCompressed = compressedByte != 0
|
||||||
isProtected = parcel.readByte().toInt() != 0
|
isProtected = parcel.readByte().toInt() != 0
|
||||||
parcel.readString()?.let {
|
parcel.readString()?.let {
|
||||||
dataFile = File(it)
|
dataFile = File(it)
|
||||||
@@ -74,32 +72,51 @@ class BinaryAttachment : Parcelable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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)
|
@Throws(IOException::class)
|
||||||
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
||||||
dataFile?.let { concreteDataFile ->
|
dataFile?.let { concreteDataFile ->
|
||||||
// To compress, create a new binary with file
|
// To compress, create a new binary with file
|
||||||
if (isCompressed != true) {
|
if (!isCompressed) {
|
||||||
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
||||||
var outputStream: GZIPOutputStream? = null
|
GZIPOutputStream(FileOutputStream(fileBinaryCompress)).use { outputStream ->
|
||||||
var inputStream: InputStream? = null
|
getInputDataStream().use { inputStream ->
|
||||||
try {
|
inputStream.readBytes(bufferSize) { buffer ->
|
||||||
outputStream = GZIPOutputStream(FileOutputStream(fileBinaryCompress))
|
outputStream.write(buffer)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Remove unGzip file
|
||||||
|
if (concreteDataFile.delete()) {
|
||||||
|
if (fileBinaryCompress.renameTo(concreteDataFile)) {
|
||||||
|
// Harmonize with database compression
|
||||||
|
isCompressed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,52 +124,20 @@ class BinaryAttachment : Parcelable {
|
|||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
||||||
dataFile?.let { concreteDataFile ->
|
dataFile?.let { concreteDataFile ->
|
||||||
if (isCompressed != false) {
|
if (isCompressed) {
|
||||||
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
||||||
var outputStream: FileOutputStream? = null
|
FileOutputStream(fileBinaryDecompress).use { outputStream ->
|
||||||
var inputStream: GZIPInputStream? = null
|
getUnGzipInputDataStream().use { inputStream ->
|
||||||
try {
|
inputStream.readBytes(bufferSize) { buffer ->
|
||||||
outputStream = FileOutputStream(fileBinaryDecompress)
|
outputStream.write(buffer)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// Remove gzip file
|
||||||
}
|
if (concreteDataFile.delete()) {
|
||||||
}
|
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
|
||||||
|
// Harmonize with database compression
|
||||||
fun download(createdFileUri: Uri,
|
isCompressed = false
|
||||||
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) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,18 +170,22 @@ class BinaryAttachment : Parcelable {
|
|||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
|
|
||||||
var result = 0
|
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 (isProtected) 1 else 0
|
||||||
result = 31 * result + dataFile!!.hashCode()
|
result = 31 * result + dataFile!!.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return dataFile.toString()
|
||||||
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
dest.writeByte((if (isCompressed == null) 2 else if (isCompressed!!) 1 else 0).toByte())
|
dest.writeByte((if (isCompressed) 1 else 0).toByte())
|
||||||
dest.writeByte((if (isProtected) 1 else 0).toByte())
|
dest.writeByte((if (isProtected) 1 else 0).toByte())
|
||||||
dest.writeString(dataFile?.absolutePath)
|
dest.writeString(dataFile?.absolutePath)
|
||||||
}
|
}
|
||||||
@@ -19,52 +19,126 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element.database
|
package com.kunzisoft.keepass.database.element.database
|
||||||
|
|
||||||
import android.util.SparseArray
|
|
||||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class BinaryPool {
|
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? {
|
operator fun get(key: Int): BinaryAttachment? {
|
||||||
return pool[key]
|
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()) {
|
* To put a [binaryAttachment] in the pool,
|
||||||
action.invoke(i, pool.get(pool.keyAt(i)))
|
* 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)
|
@Throws(IOException::class)
|
||||||
fun clear() {
|
fun remove(binaryAttachment: BinaryAttachment) {
|
||||||
doForEachBinary { _, binary ->
|
findKey(binaryAttachment)?.let {
|
||||||
binary.clear()
|
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) {
|
* Utility method to find an unused key in the pool
|
||||||
pool.put(findUnusedKey(), fileBinary)
|
*/
|
||||||
}
|
private fun findUnusedKey(): Int {
|
||||||
}
|
var unusedKey = 0
|
||||||
|
while (pool[unusedKey] != null)
|
||||||
fun findUnusedKey(): Int {
|
|
||||||
var unusedKey = pool.size()
|
|
||||||
while (get(unusedKey) != null)
|
|
||||||
unusedKey++
|
unusedKey++
|
||||||
return unusedKey
|
return unusedKey
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findKey(pb: BinaryAttachment): Int? {
|
/**
|
||||||
for (i in 0 until pool.size()) {
|
* Return key of [binaryAttachmentToRetrieve] or null if not found
|
||||||
if (pool.get(pool.keyAt(i)) == pb) return i
|
*/
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.group.GroupKDB
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
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.database.element.security.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
import com.kunzisoft.keepass.stream.NullOutputStream
|
||||||
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.security.DigestOutputStream
|
import java.security.DigestOutputStream
|
||||||
@@ -38,7 +40,7 @@ import kotlin.collections.ArrayList
|
|||||||
|
|
||||||
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
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 kdfListV3: MutableList<KdfEngine> = ArrayList()
|
||||||
|
|
||||||
@@ -57,7 +59,14 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
|
|
||||||
// Retrieve backup group in index
|
// Retrieve backup group in index
|
||||||
val backupGroup: GroupKDB?
|
val backupGroup: GroupKDB?
|
||||||
get() = if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID) null else getGroupById(backupGroupId)
|
get() {
|
||||||
|
if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
|
||||||
|
ensureBackupExists()
|
||||||
|
return if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
getGroupById(backupGroupId)
|
||||||
|
}
|
||||||
|
|
||||||
override val kdfEngine: KdfEngine?
|
override val kdfEngine: KdfEngine?
|
||||||
get() = kdfListV3[0]
|
get() = kdfListV3[0]
|
||||||
@@ -192,10 +201,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
|
* if it doesn't exist
|
||||||
*/
|
*/
|
||||||
fun ensureRecycleBinExists() {
|
fun ensureBackupExists() {
|
||||||
rootGroups.forEach { currentGroup ->
|
rootGroups.forEach { currentGroup ->
|
||||||
if (currentGroup.level == 0
|
if (currentGroup.level == 0
|
||||||
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
|
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
|
||||||
@@ -219,21 +228,25 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
* @param node Node to remove
|
* @param node Node to remove
|
||||||
* @return true if node can be recycle, false elsewhere
|
* @return true if node can be recycle, false elsewhere
|
||||||
*/
|
*/
|
||||||
// TODO #394 Backup KDB
|
fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
|
||||||
// fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
|
if (node == backupGroup)
|
||||||
fun canRecycle(): Boolean {
|
return false
|
||||||
|
backupGroup?.let {
|
||||||
|
if (node.isContainedIn(it))
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recycle(group: GroupKDB) {
|
fun recycle(group: GroupKDB) {
|
||||||
ensureRecycleBinExists()
|
ensureBackupExists()
|
||||||
removeGroupFrom(group, group.parent)
|
removeGroupFrom(group, group.parent)
|
||||||
addGroupTo(group, backupGroup)
|
addGroupTo(group, backupGroup)
|
||||||
group.afterAssignNewParent()
|
group.afterAssignNewParent()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recycle(entry: EntryKDB) {
|
fun recycle(entry: EntryKDB) {
|
||||||
ensureRecycleBinExists()
|
ensureBackupExists()
|
||||||
removeEntryFrom(entry, entry.parent)
|
removeEntryFrom(entry, entry.parent)
|
||||||
addEntryTo(entry, backupGroup)
|
addEntryTo(entry, backupGroup)
|
||||||
entry.afterAssignNewParent()
|
entry.afterAssignNewParent()
|
||||||
@@ -249,6 +262,12 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
addEntryTo(entry, origParent)
|
addEntryTo(entry, origParent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun buildNewBinary(cacheDirectory: File): BinaryAttachment {
|
||||||
|
// Generate an unique new file with timestamp
|
||||||
|
val fileInCache = File(cacheDirectory, System.currentTimeMillis().toString())
|
||||||
|
return BinaryAttachment(fileInCache)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TYPE = DatabaseKDB::class.java
|
val TYPE = DatabaseKDB::class.java
|
||||||
|
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
|||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
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.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
@@ -40,12 +42,14 @@ import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
|||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
||||||
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
||||||
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
import com.kunzisoft.keepass.utils.VariantDictionary
|
||||||
import org.w3c.dom.Node
|
import org.w3c.dom.Node
|
||||||
import org.w3c.dom.Text
|
import org.w3c.dom.Text
|
||||||
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
@@ -173,33 +177,51 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
|
|
||||||
fun changeBinaryCompression(oldCompression: CompressionAlgorithm,
|
fun changeBinaryCompression(oldCompression: CompressionAlgorithm,
|
||||||
newCompression: CompressionAlgorithm) {
|
newCompression: CompressionAlgorithm) {
|
||||||
binaryPool.doForEachBinary { key, binary ->
|
when (oldCompression) {
|
||||||
|
CompressionAlgorithm.None -> {
|
||||||
try {
|
when (newCompression) {
|
||||||
when (oldCompression) {
|
CompressionAlgorithm.None -> {}
|
||||||
CompressionAlgorithm.None -> {
|
|
||||||
when (newCompression) {
|
|
||||||
CompressionAlgorithm.None -> {
|
|
||||||
}
|
|
||||||
CompressionAlgorithm.GZip -> {
|
|
||||||
// To compress, create a new binary with file
|
|
||||||
binary.compress(BUFFER_SIZE_BYTES)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CompressionAlgorithm.GZip -> {
|
CompressionAlgorithm.GZip -> {
|
||||||
when (newCompression) {
|
// Only in databaseV3.1, in databaseV4 the header is zipped during the save
|
||||||
CompressionAlgorithm.None -> {
|
if (kdbxVersion.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) {
|
||||||
// To decompress, create a new binary with file
|
compressAllBinaries()
|
||||||
binary.decompress(BUFFER_SIZE_BYTES)
|
|
||||||
}
|
|
||||||
CompressionAlgorithm.GZip -> {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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) {
|
} 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 +558,52 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
return publicCustomData.size() > 0
|
return publicCustomData.size() > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun buildNewBinary(cacheDirectory: File,
|
||||||
|
protection: Boolean,
|
||||||
|
compression: Boolean,
|
||||||
|
binaryPoolId: Int? = null): BinaryAttachment {
|
||||||
|
// New file with current time
|
||||||
|
val fileInCache = File(cacheDirectory, System.currentTimeMillis().toString())
|
||||||
|
val binaryAttachment = BinaryAttachment(fileInCache, protection, compression)
|
||||||
|
// add attachment to pool
|
||||||
|
binaryPool.put(binaryPoolId, binaryAttachment)
|
||||||
|
return binaryAttachment
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAttachmentIfNotUsed(attachment: Attachment) {
|
||||||
|
// Remove attachment from pool
|
||||||
|
removeUnlinkedAttachments(attachment.binaryAttachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeUnlinkedAttachments(vararg binaries: BinaryAttachment) {
|
||||||
|
// 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)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Unable to clean binaries", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
override fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
||||||
if (password == null)
|
if (password == null)
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -20,15 +20,14 @@
|
|||||||
package com.kunzisoft.keepass.database.element.database
|
package com.kunzisoft.keepass.database.element.database
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupVersioned
|
import com.kunzisoft.keepass.database.element.group.GroupVersioned
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
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.DuplicateUuidDatabaseException
|
||||||
import com.kunzisoft.keepass.database.exception.KeyFileEmptyDatabaseException
|
import com.kunzisoft.keepass.database.exception.KeyFileEmptyDatabaseException
|
||||||
import org.apache.commons.io.IOUtils
|
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
@@ -127,7 +126,7 @@ abstract class DatabaseVersioned<
|
|||||||
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
|
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
|
||||||
|
|
||||||
val keyByteArrayOutputStream = ByteArrayOutputStream()
|
val keyByteArrayOutputStream = ByteArrayOutputStream()
|
||||||
IOUtils.copy(keyInputStream, keyByteArrayOutputStream)
|
keyInputStream.copyTo(keyByteArrayOutputStream)
|
||||||
val keyData = keyByteArrayOutputStream.toByteArray()
|
val keyData = keyByteArrayOutputStream.toByteArray()
|
||||||
|
|
||||||
val keyByteArrayInputStream = ByteArrayInputStream(keyData)
|
val keyByteArrayInputStream = ByteArrayInputStream(keyData)
|
||||||
|
|||||||
@@ -25,14 +25,12 @@ import android.os.Parcelable
|
|||||||
import com.kunzisoft.keepass.utils.ParcelableUtil
|
import com.kunzisoft.keepass.utils.ParcelableUtil
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
|
||||||
import java.util.HashMap
|
|
||||||
|
|
||||||
class AutoType : Parcelable {
|
class AutoType : Parcelable {
|
||||||
|
|
||||||
var enabled = true
|
var enabled = true
|
||||||
var obfuscationOptions = OBF_OPT_NONE
|
var obfuscationOptions = OBF_OPT_NONE
|
||||||
var defaultSequence = ""
|
var defaultSequence = ""
|
||||||
private var windowSeqPairs = HashMap<String, String>()
|
private var windowSeqPairs = LinkedHashMap<String, String>()
|
||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
|
|
||||||
@@ -58,7 +56,7 @@ class AutoType : Parcelable {
|
|||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
dest.writeByte((if (enabled) 1 else 0).toByte())
|
dest.writeByte((if (enabled) 1 else 0).toByte())
|
||||||
dest.writeInt(obfuscationOptions.toInt())
|
dest.writeInt(obfuscationOptions.toKotlinInt())
|
||||||
dest.writeString(defaultSequence)
|
dest.writeString(defaultSequence)
|
||||||
ParcelableUtil.writeStringParcelableMap(dest, windowSeqPairs)
|
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.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeKDBInterface
|
import com.kunzisoft.keepass.database.element.node.NodeKDBInterface
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
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 java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Structure containing information about one entry.
|
* Structure containing information about one entry.
|
||||||
@@ -135,6 +137,29 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
override val type: Type
|
override val type: Type
|
||||||
get() = Type.ENTRY
|
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) {
|
||||||
|
if (this.binaryDescription == attachment.name) {
|
||||||
|
this.binaryDescription = ""
|
||||||
|
this.binaryData = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
/** Size of byte buffer needed to hold this struct. */
|
/** 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.Parcel
|
||||||
import android.os.Parcelable
|
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.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
@@ -31,11 +33,13 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
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.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.utils.ParcelableUtil
|
import com.kunzisoft.keepass.utils.ParcelableUtil
|
||||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
import com.kunzisoft.keepass.utils.UnsignedLong
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
import kotlin.collections.HashSet
|
||||||
|
import kotlin.collections.LinkedHashMap
|
||||||
|
|
||||||
class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
|
class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
|
||||||
|
|
||||||
@@ -58,9 +62,10 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
super.icon = value
|
super.icon = value
|
||||||
}
|
}
|
||||||
var iconCustom = IconImageCustom.UNKNOWN_ICON
|
var iconCustom = IconImageCustom.UNKNOWN_ICON
|
||||||
private var customData = HashMap<String, String>()
|
private var customData = LinkedHashMap<String, String>()
|
||||||
var fields = HashMap<String, ProtectedString>()
|
// TODO Private
|
||||||
var binaries = HashMap<String, BinaryAttachment>()
|
var fields = LinkedHashMap<String, ProtectedString>()
|
||||||
|
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
|
||||||
var foregroundColor = ""
|
var foregroundColor = ""
|
||||||
var backgroundColor = ""
|
var backgroundColor = ""
|
||||||
var overrideURL = ""
|
var overrideURL = ""
|
||||||
@@ -69,36 +74,32 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
var additional = ""
|
var additional = ""
|
||||||
var tags = ""
|
var tags = ""
|
||||||
|
|
||||||
val size: Long
|
fun getSize(binaryPool: BinaryPool): Long {
|
||||||
get() {
|
var size = FIXED_LENGTH_SIZE
|
||||||
var size = FIXED_LENGTH_SIZE
|
|
||||||
|
|
||||||
for (entry in fields.entries) {
|
for (entry in fields.entries) {
|
||||||
size += entry.key.length.toLong()
|
size += entry.key.length.toLong()
|
||||||
size += entry.value.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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
override var expires: Boolean = false
|
||||||
|
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
@@ -109,7 +110,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
|
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
|
||||||
customData = ParcelableUtil.readStringParcelableMap(parcel)
|
customData = ParcelableUtil.readStringParcelableMap(parcel)
|
||||||
fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java)
|
fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java)
|
||||||
binaries = ParcelableUtil.readStringParcelableMap(parcel, BinaryAttachment::class.java)
|
binaries = ParcelableUtil.readStringIntMap(parcel)
|
||||||
foregroundColor = parcel.readString() ?: foregroundColor
|
foregroundColor = parcel.readString() ?: foregroundColor
|
||||||
backgroundColor = parcel.readString() ?: backgroundColor
|
backgroundColor = parcel.readString() ?: backgroundColor
|
||||||
overrideURL = parcel.readString() ?: overrideURL
|
overrideURL = parcel.readString() ?: overrideURL
|
||||||
@@ -123,11 +124,11 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
super.writeToParcel(dest, flags)
|
super.writeToParcel(dest, flags)
|
||||||
dest.writeParcelable(iconCustom, flags)
|
dest.writeParcelable(iconCustom, flags)
|
||||||
dest.writeLong(usageCount.toLong())
|
dest.writeLong(usageCount.toKotlinLong())
|
||||||
dest.writeParcelable(locationChanged, flags)
|
dest.writeParcelable(locationChanged, flags)
|
||||||
ParcelableUtil.writeStringParcelableMap(dest, customData)
|
ParcelableUtil.writeStringParcelableMap(dest, customData)
|
||||||
ParcelableUtil.writeStringParcelableMap(dest, flags, fields)
|
ParcelableUtil.writeStringParcelableMap(dest, flags, fields)
|
||||||
ParcelableUtil.writeStringParcelableMap(dest, flags, binaries)
|
ParcelableUtil.writeStringIntMap(dest, binaries)
|
||||||
dest.writeString(foregroundColor)
|
dest.writeString(foregroundColor)
|
||||||
dest.writeString(backgroundColor)
|
dest.writeString(backgroundColor)
|
||||||
dest.writeString(overrideURL)
|
dest.writeString(overrideURL)
|
||||||
@@ -166,8 +167,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
tags = source.tags
|
tags = source.tags
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startToManageFieldReferences(db: DatabaseKDBX) {
|
fun startToManageFieldReferences(database: DatabaseKDBX) {
|
||||||
this.mDatabase = db
|
this.mDatabase = database
|
||||||
this.mDecodeRef = true
|
this.mDecodeRef = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,13 +261,11 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
|| key == STR_NOTES)
|
|| key == STR_NOTES)
|
||||||
}
|
}
|
||||||
|
|
||||||
var customFields = HashMap<String, ProtectedString>()
|
var customFields = LinkedHashMap<String, ProtectedString>()
|
||||||
get() {
|
get() {
|
||||||
field.clear()
|
field.clear()
|
||||||
for (entry in fields.entries) {
|
for ((key, value) in fields) {
|
||||||
val key = entry.key
|
if (!isStandardField(key)) {
|
||||||
val value = entry.value
|
|
||||||
if (!isStandardField(entry.key)) {
|
|
||||||
field[key] = ProtectedString(value.isProtected, decodeRefKey(mDecodeRef, key))
|
field[key] = ProtectedString(value.isProtected, decodeRefKey(mDecodeRef, key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -285,10 +284,46 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
fields[label] = value
|
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 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Remove ?
|
||||||
fun sizeOfHistory(): Int {
|
fun sizeOfHistory(): Int {
|
||||||
return history.size
|
return history.size
|
||||||
}
|
}
|
||||||
@@ -305,15 +340,11 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
history.add(entry)
|
history.add(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeEntryFromHistory(position: Int) {
|
fun removeEntryFromHistory(position: Int): EntryKDBX? {
|
||||||
history.removeAt(position)
|
return history.removeAt(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAllHistory() {
|
fun removeOldestEntryFromHistory(): EntryKDBX? {
|
||||||
history.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeOldestEntryFromHistory() {
|
|
||||||
var min: Date? = null
|
var min: Date? = null
|
||||||
var index = -1
|
var index = -1
|
||||||
|
|
||||||
@@ -326,15 +357,15 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index != -1) {
|
return if (index != -1) {
|
||||||
history.removeAt(index)
|
history.removeAt(index)
|
||||||
}
|
} else null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun touch(modified: Boolean, touchParents: Boolean) {
|
override fun touch(modified: Boolean, touchParents: Boolean) {
|
||||||
super.touch(modified, touchParents)
|
super.touch(modified, touchParents)
|
||||||
// TODO unsigned long
|
// TODO unsigned long
|
||||||
usageCount = UnsignedLong(usageCount.toLong() + 1)
|
usageCount = UnsignedLong(usageCount.toKotlinLong() + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class FieldReferencesEngine {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
val fullRef = text.substring(start, end - start + 1)
|
val fullRef = text.substring(start, end + 1)
|
||||||
val result = findRefTarget(fullRef, contextV4)
|
val result = findRefTarget(fullRef, contextV4)
|
||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
@@ -127,7 +127,7 @@ class FieldReferencesEngine {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val ref = fullRef.substring(STR_REF_START.length, fullRef.length - STR_REF_START.length - STR_REF_END.length)
|
val ref = fullRef.substring(STR_REF_START.length, fullRef.length - STR_REF_END.length)
|
||||||
if (ref.length <= 4) {
|
if (ref.length <= 4) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
super.writeToParcel(dest, flags)
|
super.writeToParcel(dest, flags)
|
||||||
dest.writeParcelable(iconCustom, flags)
|
dest.writeParcelable(iconCustom, flags)
|
||||||
dest.writeLong(usageCount.toLong())
|
dest.writeLong(usageCount.toKotlinLong())
|
||||||
dest.writeParcelable(locationChanged, flags)
|
dest.writeParcelable(locationChanged, flags)
|
||||||
// TODO ParcelableUtil.writeStringParcelableMap(dest, customData);
|
// TODO ParcelableUtil.writeStringParcelableMap(dest, customData);
|
||||||
dest.writeString(notes)
|
dest.writeString(notes)
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ class NodeIdInt : NodeId<Int> {
|
|||||||
|
|
||||||
constructor(source: NodeIdInt) : this(source.id)
|
constructor(source: NodeIdInt) : this(source.id)
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
constructor(groupId: Int = Random().nextInt()) : super() {
|
constructor(groupId: Int = Random().nextInt()) : super() {
|
||||||
this.id = groupId
|
this.id = groupId
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ class NodeIdUUID : NodeId<UUID> {
|
|||||||
|
|
||||||
constructor(source: NodeIdUUID) : this(source.id)
|
constructor(source: NodeIdUUID) : this(source.id)
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
constructor(uuid: UUID = UUID.randomUUID()) : super() {
|
constructor(uuid: UUID = UUID.randomUUID()) : super() {
|
||||||
this.id = uuid
|
this.id = uuid
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,14 +26,13 @@ class ProtectedString : Parcelable {
|
|||||||
|
|
||||||
var isProtected: Boolean = false
|
var isProtected: Boolean = false
|
||||||
private set
|
private set
|
||||||
private var stringValue: String = ""
|
var stringValue: String = ""
|
||||||
|
|
||||||
constructor(toCopy: ProtectedString) {
|
constructor(toCopy: ProtectedString) {
|
||||||
this.isProtected = toCopy.isProtected
|
this.isProtected = toCopy.isProtected
|
||||||
this.stringValue = toCopy.stringValue
|
this.stringValue = toCopy.stringValue
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
constructor(enableProtection: Boolean = false, string: String = "") {
|
constructor(enableProtection: Boolean = false, string: String = "") {
|
||||||
this.isProtected = enableProtection
|
this.isProtected = enableProtection
|
||||||
this.stringValue = string
|
this.stringValue = string
|
||||||
|
|||||||
@@ -97,11 +97,11 @@ class DatabaseHeaderKDB : DatabaseHeader() {
|
|||||||
const val BUF_SIZE = 124
|
const val BUF_SIZE = 124
|
||||||
|
|
||||||
fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean {
|
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 {
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,10 +176,10 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
|||||||
private fun readHeaderField(dis: LittleEndianDataInputStream): Boolean {
|
private fun readHeaderField(dis: LittleEndianDataInputStream): Boolean {
|
||||||
val fieldID = dis.read().toByte()
|
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()
|
dis.readUShort()
|
||||||
} else {
|
} else {
|
||||||
dis.readUInt().toInt()
|
dis.readUInt().toKotlinInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
var fieldData: ByteArray? = null
|
var fieldData: ByteArray? = null
|
||||||
@@ -192,30 +192,31 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fieldID == PwDbHeaderV4Fields.EndOfHeader)
|
||||||
|
return true
|
||||||
|
|
||||||
if (fieldData != null)
|
if (fieldData != null)
|
||||||
when (fieldID) {
|
when (fieldID) {
|
||||||
PwDbHeaderV4Fields.EndOfHeader -> return true
|
|
||||||
|
|
||||||
PwDbHeaderV4Fields.CipherID -> setCipher(fieldData)
|
PwDbHeaderV4Fields.CipherID -> setCipher(fieldData)
|
||||||
|
|
||||||
PwDbHeaderV4Fields.CompressionFlags -> setCompressionFlags(fieldData)
|
PwDbHeaderV4Fields.CompressionFlags -> setCompressionFlags(fieldData)
|
||||||
|
|
||||||
PwDbHeaderV4Fields.MasterSeed -> masterSeed = 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
|
transformSeed = fieldData
|
||||||
|
|
||||||
PwDbHeaderV4Fields.TransformRounds -> if (version.toLong() < FILE_VERSION_32_4.toLong())
|
PwDbHeaderV4Fields.TransformRounds -> if (version.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong())
|
||||||
setTransformRound(fieldData)
|
setTransformRound(fieldData)
|
||||||
|
|
||||||
PwDbHeaderV4Fields.EncryptionIV -> encryptionIV = 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
|
innerRandomStreamKey = fieldData
|
||||||
|
|
||||||
PwDbHeaderV4Fields.StreamStartBytes -> streamStartBytes = 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)
|
setRandomStreamID(fieldData)
|
||||||
|
|
||||||
PwDbHeaderV4Fields.KdfParameters -> databaseV4.kdfParameters = KdfParameters.deserialize(fieldData)
|
PwDbHeaderV4Fields.KdfParameters -> databaseV4.kdfParameters = KdfParameters.deserialize(fieldData)
|
||||||
@@ -261,7 +262,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val flag = bytes4ToUInt(pbFlags)
|
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.")
|
throw IOException("Unrecognized compression flag.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,7 +278,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val id = bytes4ToUInt(streamID)
|
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.")
|
throw IOException("Invalid stream id.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,8 +293,8 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
|||||||
* @return true if it's a supported version
|
* @return true if it's a supported version
|
||||||
*/
|
*/
|
||||||
private fun validVersion(version: UnsignedInt): Boolean {
|
private fun validVersion(version: UnsignedInt): Boolean {
|
||||||
return version.toInt() and FILE_VERSION_CRITICAL_MASK.toInt() <=
|
return version.toKotlinInt() and FILE_VERSION_CRITICAL_MASK.toKotlinInt() <=
|
||||||
FILE_VERSION_32_4.toInt() and FILE_VERSION_CRITICAL_MASK.toInt()
|
FILE_VERSION_32_4.toKotlinInt() and FILE_VERSION_CRITICAL_MASK.toKotlinInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -306,7 +307,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
|||||||
val FILE_VERSION_32_4 = UnsignedInt(0x00040000)
|
val FILE_VERSION_32_4 = UnsignedInt(0x00040000)
|
||||||
|
|
||||||
fun getCompressionFromFlag(flag: UnsignedInt): CompressionAlgorithm? {
|
fun getCompressionFromFlag(flag: UnsignedInt): CompressionAlgorithm? {
|
||||||
return when (flag.toInt()) {
|
return when (flag.toKotlinInt()) {
|
||||||
0 -> CompressionAlgorithm.None
|
0 -> CompressionAlgorithm.None
|
||||||
1 -> CompressionAlgorithm.GZip
|
1 -> CompressionAlgorithm.GZip
|
||||||
else -> null
|
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.group.GroupKDB
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
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.element.security.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.exception.*
|
import com.kunzisoft.keepass.database.exception.*
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||||
import com.kunzisoft.keepass.stream.*
|
import com.kunzisoft.keepass.stream.*
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import org.joda.time.Instant
|
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.security.*
|
import java.security.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -90,16 +88,16 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
|
|
||||||
// Select algorithm
|
// Select algorithm
|
||||||
when {
|
when {
|
||||||
header.flags.toInt() and DatabaseHeaderKDB.FLAG_RIJNDAEL.toInt() != 0 -> {
|
header.flags.toKotlinInt() and DatabaseHeaderKDB.FLAG_RIJNDAEL.toKotlinInt() != 0 -> {
|
||||||
mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.AESRijndael
|
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
|
mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.Twofish
|
||||||
}
|
}
|
||||||
else -> throw InvalidAlgorithmDatabaseException()
|
else -> throw InvalidAlgorithmDatabaseException()
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabaseToOpen.numberKeyEncryptionRounds = header.numKeyEncRounds.toLong()
|
mDatabaseToOpen.numberKeyEncryptionRounds = header.numKeyEncRounds.toKotlinLong()
|
||||||
|
|
||||||
// Generate transformedMasterKey from masterKey
|
// Generate transformedMasterKey from masterKey
|
||||||
mDatabaseToOpen.makeFinalKey(
|
mDatabaseToOpen.makeFinalKey(
|
||||||
@@ -160,11 +158,11 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
var newEntry: EntryKDB? = null
|
var newEntry: EntryKDB? = null
|
||||||
var currentGroupNumber = 0
|
var currentGroupNumber = 0
|
||||||
var currentEntryNumber = 0
|
var currentEntryNumber = 0
|
||||||
while (currentGroupNumber < header.numGroups.toLong()
|
while (currentGroupNumber < header.numGroups.toKotlinLong()
|
||||||
|| currentEntryNumber < header.numEntries.toLong()) {
|
|| currentEntryNumber < header.numEntries.toKotlinLong()) {
|
||||||
|
|
||||||
val fieldType = cipherInputStream.readBytes2ToUShort()
|
val fieldType = cipherInputStream.readBytes2ToUShort()
|
||||||
val fieldSize = cipherInputStream.readBytes4ToUInt().toInt()
|
val fieldSize = cipherInputStream.readBytes4ToUInt().toKotlinInt()
|
||||||
|
|
||||||
when (fieldType) {
|
when (fieldType) {
|
||||||
0x0000 -> {
|
0x0000 -> {
|
||||||
@@ -175,7 +173,7 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
when (fieldSize) {
|
when (fieldSize) {
|
||||||
4 -> {
|
4 -> {
|
||||||
newGroup = mDatabaseToOpen.createGroup().apply {
|
newGroup = mDatabaseToOpen.createGroup().apply {
|
||||||
setGroupId(cipherInputStream.readBytes4ToUInt().toInt())
|
setGroupId(cipherInputStream.readBytes4ToUInt().toKotlinInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
16 -> {
|
16 -> {
|
||||||
@@ -194,7 +192,7 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
} ?:
|
} ?:
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
val groupKDB = mDatabaseToOpen.createGroup()
|
val groupKDB = mDatabaseToOpen.createGroup()
|
||||||
groupKDB.nodeId = NodeIdInt(cipherInputStream.readBytes4ToUInt().toInt())
|
groupKDB.nodeId = NodeIdInt(cipherInputStream.readBytes4ToUInt().toKotlinInt())
|
||||||
entry.parent = groupKDB
|
entry.parent = groupKDB
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,7 +201,7 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
group.creationTime = cipherInputStream.readBytes5ToDate()
|
group.creationTime = cipherInputStream.readBytes5ToDate()
|
||||||
} ?:
|
} ?:
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
var iconId = cipherInputStream.readBytes4ToUInt().toInt()
|
var iconId = cipherInputStream.readBytes4ToUInt().toKotlinInt()
|
||||||
// Clean up after bug that set icon ids to -1
|
// Clean up after bug that set icon ids to -1
|
||||||
if (iconId == -1) {
|
if (iconId == -1) {
|
||||||
iconId = 0
|
iconId = 0
|
||||||
@@ -237,7 +235,7 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
}
|
}
|
||||||
0x0007 -> {
|
0x0007 -> {
|
||||||
newGroup?.let { group ->
|
newGroup?.let { group ->
|
||||||
group.icon = mDatabaseToOpen.iconFactory.getIcon(cipherInputStream.readBytes4ToUInt().toInt())
|
group.icon = mDatabaseToOpen.iconFactory.getIcon(cipherInputStream.readBytes4ToUInt().toKotlinInt())
|
||||||
} ?:
|
} ?:
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
entry.password = cipherInputStream.readBytesToString(fieldSize,false)
|
entry.password = cipherInputStream.readBytesToString(fieldSize,false)
|
||||||
@@ -253,7 +251,7 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
}
|
}
|
||||||
0x0009 -> {
|
0x0009 -> {
|
||||||
newGroup?.let { group ->
|
newGroup?.let { group ->
|
||||||
group.groupFlags = cipherInputStream.readBytes4ToUInt().toInt()
|
group.groupFlags = cipherInputStream.readBytes4ToUInt().toKotlinInt()
|
||||||
} ?:
|
} ?:
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
entry.creationTime = cipherInputStream.readBytes5ToDate()
|
entry.creationTime = cipherInputStream.readBytes5ToDate()
|
||||||
@@ -282,11 +280,9 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
0x000E -> {
|
0x000E -> {
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
if (fieldSize > 0) {
|
if (fieldSize > 0) {
|
||||||
// Generate an unique new file with timestamp
|
val binaryAttachment = mDatabaseToOpen.buildNewBinary(cacheDirectory)
|
||||||
val binaryFile = File(cacheDirectory,
|
entry.binaryData = binaryAttachment
|
||||||
Instant.now().millis.toString())
|
BufferedOutputStream(binaryAttachment.getOutputDataStream()).use { outputStream ->
|
||||||
entry.binaryData = BinaryAttachment(binaryFile)
|
|
||||||
BufferedOutputStream(FileOutputStream(binaryFile)).use { outputStream ->
|
|
||||||
cipherInputStream.readBytes(fieldSize,
|
cipherInputStream.readBytes(fieldSize,
|
||||||
DatabaseKDB.BUFFER_SIZE_BYTES) { buffer ->
|
DatabaseKDB.BUFFER_SIZE_BYTES) { buffer ->
|
||||||
outputStream.write(buffer)
|
outputStream.write(buffer)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import com.kunzisoft.keepass.crypto.StreamCipherFactory
|
|||||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
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.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
||||||
@@ -35,7 +36,7 @@ import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
|||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
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.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.database.exception.*
|
import com.kunzisoft.keepass.database.exception.*
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||||
@@ -49,12 +50,14 @@ import org.bouncycastle.crypto.StreamCipher
|
|||||||
import org.xmlpull.v1.XmlPullParser
|
import org.xmlpull.v1.XmlPullParser
|
||||||
import org.xmlpull.v1.XmlPullParserException
|
import org.xmlpull.v1.XmlPullParserException
|
||||||
import org.xmlpull.v1.XmlPullParserFactory
|
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.nio.charset.Charset
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.zip.GZIPInputStream
|
import java.util.zip.GZIPInputStream
|
||||||
import java.util.zip.GZIPOutputStream
|
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.CipherInputStream
|
import javax.crypto.CipherInputStream
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -68,9 +71,6 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
|
|
||||||
private var hashOfHeader: ByteArray? = null
|
private var hashOfHeader: ByteArray? = null
|
||||||
|
|
||||||
private val unusedCacheFileName: String
|
|
||||||
get() = mDatabase.binaryPool.findUnusedKey().toString()
|
|
||||||
|
|
||||||
private var readNextNode = true
|
private var readNextNode = true
|
||||||
private val ctxGroups = Stack<GroupKDBX>()
|
private val ctxGroups = Stack<GroupKDBX>()
|
||||||
private var ctxGroup: GroupKDBX? = null
|
private var ctxGroup: GroupKDBX? = null
|
||||||
@@ -132,7 +132,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
}
|
}
|
||||||
|
|
||||||
val isPlain: InputStream
|
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 decrypted = attachCipherStream(databaseInputStream, cipher)
|
||||||
val dataDecrypted = LittleEndianDataInputStream(decrypted)
|
val dataDecrypted = LittleEndianDataInputStream(decrypted)
|
||||||
@@ -180,7 +180,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
else -> isPlain
|
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)
|
loadInnerHeader(inputStreamXml, header)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,13 +228,15 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
header: DatabaseHeaderKDBX): Boolean {
|
header: DatabaseHeaderKDBX): Boolean {
|
||||||
val fieldId = dataInputStream.read().toByte()
|
val fieldId = dataInputStream.read().toByte()
|
||||||
|
|
||||||
val size = dataInputStream.readUInt().toInt()
|
val size = dataInputStream.readUInt().toKotlinInt()
|
||||||
if (size < 0) throw IOException("Corrupted file")
|
if (size < 0) throw IOException("Corrupted file")
|
||||||
|
|
||||||
var data = ByteArray(0)
|
var data = ByteArray(0)
|
||||||
if (size > 0) {
|
if (size > 0) {
|
||||||
if (fieldId != DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
|
if (fieldId != DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) {
|
||||||
|
// TODO OOM here
|
||||||
data = dataInputStream.readBytes(size)
|
data = dataInputStream.readBytes(size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = true
|
var result = true
|
||||||
@@ -249,18 +251,16 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
header.innerRandomStreamKey = data
|
header.innerRandomStreamKey = data
|
||||||
}
|
}
|
||||||
DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary -> {
|
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
|
// Read in a file
|
||||||
val file = File(cacheDirectory, unusedCacheFileName)
|
val protectedFlag = dataInputStream.readBytes(1)[0].toInt() != 0
|
||||||
FileOutputStream(file).use { outputStream ->
|
val byteLength = size - 1
|
||||||
|
// No compression at this level
|
||||||
|
val protectedBinary = mDatabase.buildNewBinary(cacheDirectory, protectedFlag, false)
|
||||||
|
protectedBinary.getOutputDataStream().use { outputStream ->
|
||||||
dataInputStream.readBytes(byteLength, DatabaseKDBX.BUFFER_SIZE_BYTES) { buffer ->
|
dataInputStream.readBytes(byteLength, DatabaseKDBX.BUFFER_SIZE_BYTES) { buffer ->
|
||||||
outputStream.write(buffer)
|
outputStream.write(buffer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val protectedBinary = BinaryAttachment(file, protectedFlag)
|
|
||||||
mDatabase.binaryPool.add(protectedBinary)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,14 +443,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
}
|
}
|
||||||
|
|
||||||
KdbContext.Binaries -> if (name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
|
KdbContext.Binaries -> if (name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
|
||||||
val key = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrId)
|
readBinary(xpp)
|
||||||
if (key != null) {
|
|
||||||
val pbData = readBinary(xpp)
|
|
||||||
val id = Integer.parseInt(key)
|
|
||||||
mDatabase.binaryPool.put(id, pbData!!)
|
|
||||||
} else {
|
|
||||||
readUnknown(xpp)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
readUnknown(xpp)
|
readUnknown(xpp)
|
||||||
}
|
}
|
||||||
@@ -492,7 +485,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
} else if (name.equals(DatabaseKDBXXML.ElemNotes, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemNotes, ignoreCase = true)) {
|
||||||
ctxGroup?.notes = readString(xpp)
|
ctxGroup?.notes = readString(xpp)
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
|
} 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)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
|
||||||
ctxGroup?.iconCustom = mDatabase.iconFactory.getIcon(readUuid(xpp))
|
ctxGroup?.iconCustom = mDatabase.iconFactory.getIcon(readUuid(xpp))
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemTimes, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemTimes, ignoreCase = true)) {
|
||||||
@@ -546,7 +539,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
KdbContext.Entry -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
|
KdbContext.Entry -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
|
||||||
ctxEntry?.nodeId = NodeIdUUID(readUuid(xpp))
|
ctxEntry?.nodeId = NodeIdUUID(readUuid(xpp))
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
|
} 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)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
|
||||||
ctxEntry?.iconCustom = mDatabase.iconFactory.getIcon(readUuid(xpp))
|
ctxEntry?.iconCustom = mDatabase.iconFactory.getIcon(readUuid(xpp))
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemFgColor, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemFgColor, ignoreCase = true)) {
|
||||||
@@ -766,8 +759,9 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
|
|
||||||
return KdbContext.Entry
|
return KdbContext.Entry
|
||||||
} else if (ctx == KdbContext.EntryBinary && name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
|
} else if (ctx == KdbContext.EntryBinary && name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
|
||||||
if (ctxBinaryName != null && ctxBinaryValue != null)
|
if (ctxBinaryName != null && ctxBinaryValue != null) {
|
||||||
ctxEntry?.putProtectedBinary(ctxBinaryName!!, ctxBinaryValue!!)
|
ctxEntry?.putAttachment(Attachment(ctxBinaryName!!, ctxBinaryValue!!), mDatabase.binaryPool)
|
||||||
|
}
|
||||||
ctxBinaryName = null
|
ctxBinaryName = null
|
||||||
ctxBinaryValue = null
|
ctxBinaryValue = null
|
||||||
|
|
||||||
@@ -819,7 +813,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
val sDate = readString(xpp)
|
val sDate = readString(xpp)
|
||||||
var utcDate: Date? = null
|
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)
|
var buf = Base64.decode(sDate, BASE_64_FLAG)
|
||||||
if (buf.size != 8) {
|
if (buf.size != 8) {
|
||||||
val buf8 = ByteArray(8)
|
val buf8 = ByteArray(8)
|
||||||
@@ -947,50 +941,56 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
|
|
||||||
// Reference Id to a binary already present in binary pool
|
// Reference Id to a binary already present in binary pool
|
||||||
val ref = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrRef)
|
val ref = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrRef)
|
||||||
if (ref != null) {
|
// New id to a binary
|
||||||
xpp.next() // Consume end tag
|
val key = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrId)
|
||||||
|
|
||||||
val id = Integer.parseInt(ref)
|
return when {
|
||||||
return mDatabase.binaryPool[id]
|
ref != null -> {
|
||||||
}
|
xpp.next() // Consume end tag
|
||||||
|
val id = Integer.parseInt(ref)
|
||||||
// New binary to retrieve
|
// A ref is not necessarily an index in Database V3.1
|
||||||
else {
|
mDatabase.binaryPool[id]
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
key != null -> {
|
||||||
val base64 = readString(xpp)
|
createBinary(key.toIntOrNull(), xpp)
|
||||||
if (base64.isEmpty())
|
}
|
||||||
return BinaryAttachment()
|
else -> {
|
||||||
val data = Base64.decode(base64, BASE_64_FLAG)
|
// New binary to retrieve
|
||||||
|
createBinary(null, xpp)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class, XmlPullParserException::class)
|
||||||
|
private fun createBinary(binaryId: Int?, xpp: XmlPullParser): BinaryAttachment? {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val base64 = readString(xpp)
|
||||||
|
if (base64.isEmpty())
|
||||||
|
return null
|
||||||
|
val data = Base64.decode(base64, BASE_64_FLAG)
|
||||||
|
|
||||||
|
// Build the new binary and compress
|
||||||
|
val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory, protected, compressed, binaryId)
|
||||||
|
binaryAttachment.getOutputDataStream().use { outputStream ->
|
||||||
|
outputStream.write(data)
|
||||||
|
}
|
||||||
|
return binaryAttachment
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IOException::class, XmlPullParserException::class)
|
@Throws(IOException::class, XmlPullParserException::class)
|
||||||
private fun readString(xpp: XmlPullParser): String {
|
private fun readString(xpp: XmlPullParser): String {
|
||||||
val buf = readProtectedBase64String(xpp)
|
val buf = readProtectedBase64String(xpp)
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ constructor(private val databaseKDBX: DatabaseKDBX,
|
|||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CompressionFlags, uIntTo4Bytes(DatabaseHeaderKDBX.getFlagFromCompression(databaseKDBX.compressionAlgorithm)))
|
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CompressionFlags, uIntTo4Bytes(DatabaseHeaderKDBX.getFlagFromCompression(databaseKDBX.compressionAlgorithm)))
|
||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.MasterSeed, header.masterSeed)
|
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.TransformSeed, header.transformSeed)
|
||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.TransformRounds, longTo8Bytes(databaseKDBX.numberKeyEncryptionRounds))
|
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.TransformRounds, longTo8Bytes(databaseKDBX.numberKeyEncryptionRounds))
|
||||||
} else {
|
} else {
|
||||||
@@ -101,7 +101,7 @@ constructor(private val databaseKDBX: DatabaseKDBX,
|
|||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.EncryptionIV, header.encryptionIV)
|
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.InnerRandomstreamKey, header.innerRandomStreamKey)
|
||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.StreamStartBytes, header.streamStartBytes)
|
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.StreamStartBytes, header.streamStartBytes)
|
||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomStreamID, uIntTo4Bytes(header.innerRandomStream!!.id))
|
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomStreamID, uIntTo4Bytes(header.innerRandomStream!!.id))
|
||||||
@@ -136,7 +136,7 @@ constructor(private val databaseKDBX: DatabaseKDBX,
|
|||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun writeHeaderFieldSize(size: Int) {
|
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)
|
los.writeUShort(size)
|
||||||
} else {
|
} else {
|
||||||
los.writeInt(size)
|
los.writeInt(size)
|
||||||
|
|||||||
@@ -40,25 +40,32 @@ class DatabaseInnerHeaderOutputKDBX(private val database: DatabaseKDBX,
|
|||||||
dataOutputStream.writeInt(4)
|
dataOutputStream.writeInt(4)
|
||||||
if (header.innerRandomStream == null)
|
if (header.innerRandomStream == null)
|
||||||
throw IOException("Can't write innerRandomStream")
|
throw IOException("Can't write innerRandomStream")
|
||||||
dataOutputStream.writeInt(header.innerRandomStream!!.id.toInt())
|
dataOutputStream.writeInt(header.innerRandomStream!!.id.toKotlinInt())
|
||||||
|
|
||||||
val streamKeySize = header.innerRandomStreamKey.size
|
val streamKeySize = header.innerRandomStreamKey.size
|
||||||
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey.toInt())
|
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey.toInt())
|
||||||
dataOutputStream.writeInt(streamKeySize)
|
dataOutputStream.writeInt(streamKeySize)
|
||||||
dataOutputStream.write(header.innerRandomStreamKey)
|
dataOutputStream.write(header.innerRandomStreamKey)
|
||||||
|
|
||||||
database.binaryPool.doForEachBinary { _, protectedBinary ->
|
database.binaryPool.doForEachOrderedBinary { _, keyBinary ->
|
||||||
|
val protectedBinary = keyBinary.binary
|
||||||
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
|
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
|
||||||
if (protectedBinary.isProtected) {
|
if (protectedBinary.isProtected) {
|
||||||
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force decompression to add binary in header
|
||||||
|
protectedBinary.decompress()
|
||||||
|
|
||||||
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary.toInt())
|
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary.toInt())
|
||||||
dataOutputStream.writeInt(protectedBinary.length().toInt() + 1) // TODO verify
|
dataOutputStream.writeInt(protectedBinary.length().toInt() + 1)
|
||||||
dataOutputStream.write(flag.toInt())
|
dataOutputStream.write(flag.toInt())
|
||||||
|
|
||||||
protectedBinary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
// if was compressed in cache, uncompress it
|
||||||
dataOutputStream.write(buffer)
|
protectedBinary.getInputDataStream().use { inputStream ->
|
||||||
|
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||||
|
dataOutputStream.write(buffer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,10 +118,10 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
|
|
||||||
when {
|
when {
|
||||||
mDatabaseKDB.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael -> {
|
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 -> {
|
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.")
|
else -> throw DatabaseOutputException("Unsupported algorithm.")
|
||||||
}
|
}
|
||||||
@@ -129,7 +129,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
header.version = DatabaseHeaderKDB.DBVER_DW
|
header.version = DatabaseHeaderKDB.DBVER_DW
|
||||||
header.numGroups = UnsignedInt(mDatabaseKDB.numberOfGroups())
|
header.numGroups = UnsignedInt(mDatabaseKDB.numberOfGroups())
|
||||||
header.numEntries = UnsignedInt(mDatabaseKDB.numberOfEntries())
|
header.numEntries = UnsignedInt(mDatabaseKDB.numberOfEntries())
|
||||||
header.numKeyEncRounds = UnsignedInt.fromLong(mDatabaseKDB.numberKeyEncryptionRounds)
|
header.numKeyEncRounds = UnsignedInt.fromKotlinLong(mDatabaseKDB.numberKeyEncryptionRounds)
|
||||||
|
|
||||||
setIVs(header)
|
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.group.GroupKDBX
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
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.MemoryProtectionConfig
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||||
@@ -55,7 +55,6 @@ import java.io.OutputStream
|
|||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.zip.GZIPInputStream
|
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.CipherOutputStream
|
import javax.crypto.CipherOutputStream
|
||||||
@@ -85,7 +84,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
header = outputHeader(mOS)
|
header = outputHeader(mOS)
|
||||||
|
|
||||||
val osPlain: OutputStream
|
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)
|
val cos = attachStreamEncryptor(header!!, mOS)
|
||||||
cos.write(header!!.streamStartBytes)
|
cos.write(header!!.streamStartBytes)
|
||||||
|
|
||||||
@@ -105,7 +104,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
else -> osPlain
|
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)
|
val ihOut = DatabaseInnerHeaderOutputKDBX(mDatabaseKDBX, header!!, osXml)
|
||||||
ihOut.output()
|
ihOut.output()
|
||||||
}
|
}
|
||||||
@@ -209,7 +208,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
writeObject(DatabaseKDBXXML.ElemDbDescChanged, mDatabaseKDBX.descriptionChanged.date)
|
writeObject(DatabaseKDBXXML.ElemDbDescChanged, mDatabaseKDBX.descriptionChanged.date)
|
||||||
writeObject(DatabaseKDBXXML.ElemDbDefaultUser, mDatabaseKDBX.defaultUserName, true)
|
writeObject(DatabaseKDBXXML.ElemDbDefaultUser, mDatabaseKDBX.defaultUserName, true)
|
||||||
writeObject(DatabaseKDBXXML.ElemDbDefaultUserChanged, mDatabaseKDBX.defaultUserNameChanged.date)
|
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.ElemDbColor, mDatabaseKDBX.color)
|
||||||
writeObject(DatabaseKDBXXML.ElemDbKeyChanged, mDatabaseKDBX.keyLastChanged.date)
|
writeObject(DatabaseKDBXXML.ElemDbKeyChanged, mDatabaseKDBX.keyLastChanged.date)
|
||||||
writeObject(DatabaseKDBXXML.ElemDbKeyChangeRec, mDatabaseKDBX.keyChangeRecDays)
|
writeObject(DatabaseKDBXXML.ElemDbKeyChangeRec, mDatabaseKDBX.keyChangeRecDays)
|
||||||
@@ -230,7 +229,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
writeUuid(DatabaseKDBXXML.ElemLastTopVisibleGroup, mDatabaseKDBX.lastTopVisibleGroupUUID)
|
writeUuid(DatabaseKDBXXML.ElemLastTopVisibleGroup, mDatabaseKDBX.lastTopVisibleGroupUUID)
|
||||||
|
|
||||||
// Seem to work properly if always in meta
|
// 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()
|
writeMetaBinaries()
|
||||||
|
|
||||||
writeCustomData(mDatabaseKDBX.customData)
|
writeCustomData(mDatabaseKDBX.customData)
|
||||||
@@ -274,7 +273,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
Log.e(TAG, "Unable to retrieve header", unknownKDF)
|
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.innerRandomStream = CrsAlgorithm.Salsa20
|
||||||
header.innerRandomStreamKey = ByteArray(32)
|
header.innerRandomStreamKey = ByteArray(32)
|
||||||
} else {
|
} else {
|
||||||
@@ -288,7 +287,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
throw DatabaseOutputException("Invalid random cipher")
|
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)
|
random.nextBytes(header.streamStartBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +384,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
private fun writeObject(name: String, value: Date) {
|
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))
|
writeObject(name, DatabaseKDBXXML.DateFormatter.format(value))
|
||||||
} else {
|
} else {
|
||||||
val dt = DateTime(value)
|
val dt = DateTime(value)
|
||||||
@@ -422,7 +421,6 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
private fun writeBinary(binary : BinaryAttachment) {
|
private fun writeBinary(binary : BinaryAttachment) {
|
||||||
val binaryLength = binary.length()
|
val binaryLength = binary.length()
|
||||||
if (binaryLength > 0) {
|
if (binaryLength > 0) {
|
||||||
|
|
||||||
if (binary.isProtected) {
|
if (binary.isProtected) {
|
||||||
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
|
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
|
||||||
|
|
||||||
@@ -433,21 +431,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
xml.text(charArray, 0, charArray.size)
|
xml.text(charArray, 0, charArray.size)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Force binary compression from database (compression was harmonized during import)
|
if (binary.isCompressed) {
|
||||||
if (mDatabaseKDBX.compressionAlgorithm === CompressionAlgorithm.GZip) {
|
|
||||||
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
|
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
|
// 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()
|
val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray()
|
||||||
xml.text(charArray, 0, charArray.size)
|
xml.text(charArray, 0, charArray.size)
|
||||||
}
|
}
|
||||||
@@ -459,10 +447,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
private fun writeMetaBinaries() {
|
private fun writeMetaBinaries() {
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
|
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.startTag(null, DatabaseKDBXXML.ElemBinary)
|
||||||
xml.attribute(null, DatabaseKDBXXML.AttrId, key.toString())
|
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
|
||||||
writeBinary(binary)
|
writeBinary(keyBinary.binary)
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,7 +478,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
xml.startTag(null, DatabaseKDBXXML.ElemAutoType)
|
xml.startTag(null, DatabaseKDBXXML.ElemAutoType)
|
||||||
|
|
||||||
writeObject(DatabaseKDBXXML.ElemAutoTypeEnabled, autoType.enabled)
|
writeObject(DatabaseKDBXXML.ElemAutoTypeEnabled, autoType.enabled)
|
||||||
writeObject(DatabaseKDBXXML.ElemAutoTypeObfuscation, autoType.obfuscationOptions.toLong())
|
writeObject(DatabaseKDBXXML.ElemAutoTypeObfuscation, autoType.obfuscationOptions.toKotlinLong())
|
||||||
|
|
||||||
if (autoType.defaultSequence.isNotEmpty()) {
|
if (autoType.defaultSequence.isNotEmpty()) {
|
||||||
writeObject(DatabaseKDBXXML.ElemAutoTypeDefaultSeq, autoType.defaultSequence, true)
|
writeObject(DatabaseKDBXXML.ElemAutoTypeDefaultSeq, autoType.defaultSequence, true)
|
||||||
@@ -559,23 +548,22 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
private fun writeEntryBinaries(binaries: Map<String, BinaryAttachment>) {
|
private fun writeEntryBinaries(binaries: LinkedHashMap<String, Int>) {
|
||||||
for ((key, binary) in binaries) {
|
for ((label, poolId) in binaries) {
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
// Retrieve the right index with the poolId, don't use ref because of header in DatabaseV4
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemKey)
|
mDatabaseKDBX.binaryPool.getBinaryIndexFromKey(poolId)?.toString()?.let { indexString ->
|
||||||
xml.text(safeXmlString(key))
|
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemKey)
|
xml.startTag(null, DatabaseKDBXXML.ElemKey)
|
||||||
|
xml.text(safeXmlString(label))
|
||||||
|
xml.endTag(null, DatabaseKDBXXML.ElemKey)
|
||||||
|
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemValue)
|
xml.startTag(null, DatabaseKDBXXML.ElemValue)
|
||||||
val ref = mDatabaseKDBX.binaryPool.findKey(binary)
|
// Use only pool data in Meta to save binaries
|
||||||
if (ref != null) {
|
xml.attribute(null, DatabaseKDBXXML.AttrRef, indexString)
|
||||||
xml.attribute(null, DatabaseKDBXXML.AttrRef, ref.toString())
|
xml.endTag(null, DatabaseKDBXXML.ElemValue)
|
||||||
} else {
|
|
||||||
writeBinary(binary)
|
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
||||||
}
|
}
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemValue)
|
|
||||||
|
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,7 +617,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
writeObject(DatabaseKDBXXML.ElemLastAccessTime, node.lastAccessTime.date)
|
writeObject(DatabaseKDBXXML.ElemLastAccessTime, node.lastAccessTime.date)
|
||||||
writeObject(DatabaseKDBXXML.ElemExpiryTime, node.expiryTime.date)
|
writeObject(DatabaseKDBXXML.ElemExpiryTime, node.expiryTime.date)
|
||||||
writeObject(DatabaseKDBXXML.ElemExpires, node.expires)
|
writeObject(DatabaseKDBXXML.ElemExpires, node.expires)
|
||||||
writeObject(DatabaseKDBXXML.ElemUsageCount, node.usageCount.toLong())
|
writeObject(DatabaseKDBXXML.ElemUsageCount, node.usageCount.toKotlinLong())
|
||||||
writeObject(DatabaseKDBXXML.ElemLocationChanged, node.locationChanged.date)
|
writeObject(DatabaseKDBXXML.ElemLocationChanged, node.locationChanged.date)
|
||||||
|
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemTimes)
|
xml.endTag(null, DatabaseKDBXXML.ElemTimes)
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ class EntryOutputKDB
|
|||||||
val binaryData = mEntry.binaryData
|
val binaryData = mEntry.binaryData
|
||||||
val binaryDataLength = binaryData?.length() ?: 0L
|
val binaryDataLength = binaryData?.length() ?: 0L
|
||||||
// Write data length
|
// Write data length
|
||||||
mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromLong(binaryDataLength)))
|
mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength)))
|
||||||
// Write data
|
// Write data
|
||||||
if (binaryDataLength > 0) {
|
if (binaryDataLength > 0) {
|
||||||
binaryData?.getInputDataStream().use { inputStream ->
|
binaryData?.getInputDataStream().use { inputStream ->
|
||||||
|
|||||||
@@ -19,17 +19,58 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.search
|
package com.kunzisoft.keepass.database.search
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDB
|
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDB
|
||||||
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDBX
|
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDBX
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
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 {
|
companion object {
|
||||||
const val MAX_SEARCH_ENTRY = 6
|
const val MAX_SEARCH_ENTRY = 6
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to perform actions if item is found or not after an auto search in [database]
|
||||||
|
*/
|
||||||
|
fun checkAutoSearchInfo(context: Context,
|
||||||
|
database: Database,
|
||||||
|
searchInfo: SearchInfo?,
|
||||||
|
onItemsFound: (items: List<EntryInfo>) -> Unit,
|
||||||
|
onItemNotFound: () -> Unit,
|
||||||
|
onDatabaseClosed: () -> Unit) {
|
||||||
|
if (database.loaded && TimeoutHelper.checkTime(context)) {
|
||||||
|
var searchWithoutUI = false
|
||||||
|
if (PreferencesUtil.isAutofillAutoSearchEnable(context)
|
||||||
|
&& searchInfo != null
|
||||||
|
&& !searchInfo.containsOnlyNullValues()) {
|
||||||
|
// If search provide results
|
||||||
|
database.createVirtualGroupFromSearchInfo(
|
||||||
|
searchInfo.getSearchString(context),
|
||||||
|
PreferencesUtil.omitBackup(context),
|
||||||
|
MAX_SEARCH_ENTRY
|
||||||
|
)?.let { searchGroup ->
|
||||||
|
if (searchGroup.getNumberOfChildEntries() > 0) {
|
||||||
|
searchWithoutUI = true
|
||||||
|
onItemsFound.invoke(
|
||||||
|
searchGroup.getChildEntriesInfo(database))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!searchWithoutUI) {
|
||||||
|
onItemNotFound.invoke()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onDatabaseClosed.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var incrementEntry = 0
|
private var incrementEntry = 0
|
||||||
@@ -37,6 +78,7 @@ class SearchHelper(private val isOmitBackup: Boolean) {
|
|||||||
fun createVirtualGroupWithSearchResult(database: Database,
|
fun createVirtualGroupWithSearchResult(database: Database,
|
||||||
searchQuery: String,
|
searchQuery: String,
|
||||||
searchParameters: SearchParameters,
|
searchParameters: SearchParameters,
|
||||||
|
omitBackup: Boolean,
|
||||||
max: Int): Group? {
|
max: Int): Group? {
|
||||||
|
|
||||||
val searchGroup = database.createGroup()
|
val searchGroup = database.createGroup()
|
||||||
@@ -61,7 +103,7 @@ class SearchHelper(private val isOmitBackup: Boolean) {
|
|||||||
override fun operate(node: Group): Boolean {
|
override fun operate(node: Group): Boolean {
|
||||||
return when {
|
return when {
|
||||||
incrementEntry >= max -> false
|
incrementEntry >= max -> false
|
||||||
database.isGroupSearchable(node, isOmitBackup) -> true
|
database.isGroupSearchable(node, omitBackup) -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,9 +95,9 @@ open class Education(val activity: Activity) {
|
|||||||
R.string.education_entry_edit_key,
|
R.string.education_entry_edit_key,
|
||||||
R.string.education_password_generator_key,
|
R.string.education_password_generator_key,
|
||||||
R.string.education_entry_new_field_key,
|
R.string.education_entry_new_field_key,
|
||||||
|
R.string.education_add_attachment_key,
|
||||||
R.string.education_setup_OTP_key)
|
R.string.education_setup_OTP_key)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get preferences bundle for education
|
* 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))
|
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.
|
* 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)
|
class EntryEditActivityEducation(activity: Activity)
|
||||||
: Education(activity) {
|
: Education(activity) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and display learning views
|
||||||
|
* Displays the explanation for the password generator
|
||||||
|
*/
|
||||||
fun checkAndPerformedGeneratePasswordEducation(educationView: View,
|
fun checkAndPerformedGeneratePasswordEducation(educationView: View,
|
||||||
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
||||||
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
|
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
|
||||||
@@ -56,7 +60,7 @@ class EntryEditActivityEducation(activity: Activity)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check and display learning views
|
* 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,
|
fun checkAndPerformedEntryNewFieldEducation(educationView: View,
|
||||||
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
||||||
@@ -83,6 +87,35 @@ class EntryEditActivityEducation(activity: Activity)
|
|||||||
R.string.education_entry_new_field_key)
|
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
|
* Check and display learning views
|
||||||
* Displays the explanation to setup OTP
|
* Displays the explanation to setup OTP
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import android.content.res.Resources
|
|||||||
import android.util.SparseIntArray
|
import android.util.SparseIntArray
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class who construct dynamically database icons contains in a separate library
|
* 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
|
* See *icon-pack-classic* module as sample
|
||||||
*
|
*
|
||||||
*
|
|
||||||
*/
|
|
||||||
class IconPack
|
|
||||||
/**
|
|
||||||
* Construct dynamically the icon pack provide by the string resource id
|
* 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 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)
|
* @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()
|
private val icons: SparseIntArray = SparseIntArray()
|
||||||
/**
|
/**
|
||||||
@@ -84,7 +81,7 @@ internal constructor(packageName: String, resources: Resources, resourceId: Int)
|
|||||||
while (num <= NB_ICONS) {
|
while (num <= NB_ICONS) {
|
||||||
// To construct the id with name_ic_XX_32dp (ex : classic_ic_08_32dp )
|
// To construct the id with name_ic_XX_32dp (ex : classic_ic_08_32dp )
|
||||||
val resId = resources.getIdentifier(
|
val resId = resources.getIdentifier(
|
||||||
id + "_" + DecimalFormat("00").format(num.toLong()) + "_32dp",
|
id + "_" + String.format(Locale.ENGLISH, "%02d", num) + "_32dp",
|
||||||
"drawable",
|
"drawable",
|
||||||
packageName)
|
packageName)
|
||||||
icons.put(num, resId)
|
icons.put(num, resId)
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import android.widget.FrameLayout
|
|||||||
import android.widget.PopupWindow
|
import android.widget.PopupWindow
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity
|
||||||
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
@@ -64,6 +65,9 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
|||||||
removeEntryInfo()
|
removeEntryInfo()
|
||||||
assignKeyboardView()
|
assignKeyboardView()
|
||||||
}
|
}
|
||||||
|
lockReceiver?.backToPreviousKeyboardAction = {
|
||||||
|
switchToPreviousKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
registerLockReceiver(lockReceiver, true)
|
registerLockReceiver(lockReceiver, true)
|
||||||
}
|
}
|
||||||
@@ -104,8 +108,17 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
|||||||
val closeView = popupFieldsView.findViewById<View>(R.id.keyboard_popup_close)
|
val closeView = popupFieldsView.findViewById<View>(R.id.keyboard_popup_close)
|
||||||
closeView.setOnClickListener { popupCustomKeys?.dismiss() }
|
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()
|
removeEntryInfo()
|
||||||
|
}
|
||||||
assignKeyboardView()
|
assignKeyboardView()
|
||||||
keyboardView?.setOnKeyboardActionListener(this)
|
keyboardView?.setOnKeyboardActionListener(this)
|
||||||
keyboardView?.isPreviewEnabled = false
|
keyboardView?.isPreviewEnabled = false
|
||||||
@@ -211,7 +224,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
|||||||
// Stop current service and reinit entry
|
// Stop current service and reinit entry
|
||||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
||||||
removeEntryInfo()
|
removeEntryInfo()
|
||||||
val intent = Intent(this, KeyboardLauncherActivity::class.java)
|
val intent = Intent(this, MagikeyboardLauncherActivity::class.java)
|
||||||
// New task needed because don't launch from an Activity context
|
// New task needed because don't launch from an Activity context
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
@@ -248,7 +261,9 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
|||||||
}
|
}
|
||||||
popupCustomKeys?.showAtLocation(keyboardView, Gravity.END or Gravity.TOP, 0, 0)
|
popupCustomKeys?.showAtLocation(keyboardView, Gravity.END or Gravity.TOP, 0, 0)
|
||||||
}
|
}
|
||||||
Keyboard.KEYCODE_DELETE -> inputConnection.deleteSurroundingText(1, 0)
|
Keyboard.KEYCODE_DELETE -> {
|
||||||
|
inputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
|
||||||
|
}
|
||||||
Keyboard.KEYCODE_DONE -> inputConnection.performEditorAction(EditorInfo.IME_ACTION_GO)
|
Keyboard.KEYCODE_DONE -> inputConnection.performEditorAction(EditorInfo.IME_ACTION_GO)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,8 +274,12 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun actionGoAutomatically() {
|
private fun actionGoAutomatically() {
|
||||||
if (PreferencesUtil.isAutoGoActionEnable(this))
|
if (PreferencesUtil.isAutoGoActionEnable(this)) {
|
||||||
currentInputConnection.performEditorAction(EditorInfo.IME_ACTION_GO)
|
currentInputConnection.performEditorAction(EditorInfo.IME_ACTION_GO)
|
||||||
|
if (PreferencesUtil.isKeyboardPreviousFillInEnable(this)) {
|
||||||
|
switchToPreviousKeyboard()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPress(primaryCode: Int) {
|
override fun onPress(primaryCode: Int) {
|
||||||
@@ -311,21 +330,25 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
|||||||
private const val KEY_URL = 520
|
private const val KEY_URL = 520
|
||||||
private const val KEY_FIELDS = 530
|
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 entryInfoKey: EntryInfo? = null
|
||||||
|
private var entryInfoTimestamp: Long? = null
|
||||||
|
|
||||||
private fun removeEntryInfo() {
|
private fun removeEntryInfo() {
|
||||||
entryInfoKey = null
|
entryInfoKey = null
|
||||||
|
entryInfoTimestamp = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeEntry(context: Context) {
|
fun removeEntry(context: Context) {
|
||||||
context.sendBroadcast(Intent(REMOVE_ENTRY_MAGIKEYBOARD_ACTION))
|
context.sendBroadcast(Intent(REMOVE_ENTRY_MAGIKEYBOARD_ACTION))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addEntryAndLaunchNotificationIfAllowed(context: Context, entry: EntryInfo) {
|
fun addEntryAndLaunchNotificationIfAllowed(context: Context, entry: EntryInfo, toast: Boolean = false) {
|
||||||
// Add a new entry
|
// Add a new entry
|
||||||
entryInfoKey = entry
|
entryInfoKey = entry
|
||||||
|
entryInfoTimestamp = System.currentTimeMillis()
|
||||||
// Launch notification if allowed
|
// Launch notification if allowed
|
||||||
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry)
|
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.Parcel
|
||||||
import android.os.Parcelable
|
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.readEnum
|
||||||
import com.kunzisoft.keepass.utils.writeEnum
|
import com.kunzisoft.keepass.utils.writeEnum
|
||||||
|
|
||||||
data class EntryAttachment(var name: String,
|
data class EntryAttachmentState(var attachment: Attachment,
|
||||||
var binaryAttachment: BinaryAttachment,
|
var streamDirection: StreamDirection,
|
||||||
var downloadState: AttachmentState = AttachmentState.NULL,
|
var downloadState: AttachmentState = AttachmentState.NULL,
|
||||||
var downloadProgression: Int = 0) : Parcelable {
|
var downloadProgression: Int = 0) : Parcelable {
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(
|
constructor(parcel: Parcel) : this(
|
||||||
parcel.readString() ?: "",
|
parcel.readParcelable(Attachment::class.java.classLoader) ?: Attachment("", BinaryAttachment()),
|
||||||
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment(),
|
parcel.readEnum<StreamDirection>() ?: StreamDirection.DOWNLOAD,
|
||||||
parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL,
|
parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL,
|
||||||
parcel.readInt())
|
parcel.readInt())
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
parcel.writeString(name)
|
parcel.writeParcelable(attachment, flags)
|
||||||
parcel.writeParcelable(binaryAttachment, flags)
|
parcel.writeEnum(streamDirection)
|
||||||
parcel.writeEnum(downloadState)
|
parcel.writeEnum(downloadState)
|
||||||
parcel.writeInt(downloadProgression)
|
parcel.writeInt(downloadProgression)
|
||||||
}
|
}
|
||||||
@@ -47,12 +48,25 @@ data class EntryAttachment(var name: String,
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<EntryAttachment> {
|
override fun equals(other: Any?): Boolean {
|
||||||
override fun createFromParcel(parcel: Parcel): EntryAttachment {
|
if (this === other) return true
|
||||||
return EntryAttachment(parcel)
|
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)
|
return arrayOfNulls(size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,9 +49,7 @@ class Field : Parcelable {
|
|||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (other !is Field) return false
|
||||||
|
|
||||||
other as Field
|
|
||||||
|
|
||||||
if (name != other.name) 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,31 @@
|
|||||||
package com.kunzisoft.keepass.model
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Resources
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.ObjectNameResource
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class SearchInfo : Parcelable {
|
class SearchInfo : ObjectNameResource, Parcelable {
|
||||||
|
|
||||||
var applicationId: String? = null
|
var applicationId: String? = null
|
||||||
|
set(value) {
|
||||||
|
field = when {
|
||||||
|
value == null -> null
|
||||||
|
Regex(APPLICATION_ID_REGEX).matches(value) -> value
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
var webDomain: String? = null
|
var webDomain: String? = null
|
||||||
|
set(value) {
|
||||||
|
field = when {
|
||||||
|
value == null -> null
|
||||||
|
Regex(WEB_DOMAIN_REGEX).matches(value) -> value
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
|
|
||||||
@@ -26,7 +45,40 @@ class SearchInfo : Parcelable {
|
|||||||
parcel.writeString(webDomain ?: "")
|
parcel.writeString(webDomain ?: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getName(resources: Resources): String {
|
||||||
|
return toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun containsOnlyNullValues(): Boolean {
|
||||||
|
return applicationId == null && webDomain == null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as SearchInfo
|
||||||
|
|
||||||
|
if (applicationId != other.applicationId) return false
|
||||||
|
if (webDomain != other.webDomain) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = applicationId?.hashCode() ?: 0
|
||||||
|
result = 31 * result + (webDomain?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return webDomain ?: applicationId ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
// https://gist.github.com/rishabhmhjn/8663966
|
||||||
|
const val APPLICATION_ID_REGEX = "^(?:[a-zA-Z]+(?:\\d*[a-zA-Z_]*)*)(?:\\.[a-zA-Z]+(?:\\d*[a-zA-Z_]*)*)+\$"
|
||||||
|
const val WEB_DOMAIN_REGEX = "^(?!://)([a-zA-Z0-9-_]+\\.)*[a-zA-Z0-9][a-zA-Z0-9-_]+\\.[a-zA-Z]{2,11}?\$"
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val CREATOR: Parcelable.Creator<SearchInfo> = object : Parcelable.Creator<SearchInfo> {
|
val CREATOR: Parcelable.Creator<SearchInfo> = object : Parcelable.Creator<SearchInfo> {
|
||||||
@@ -39,4 +91,15 @@ class SearchInfo : Parcelable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SearchInfo.getSearchString(context: Context): String {
|
||||||
|
return run {
|
||||||
|
if (!PreferencesUtil.searchSubdomains(context))
|
||||||
|
UriUtil.getWebDomainWithoutSubDomain(webDomain)
|
||||||
|
else
|
||||||
|
webDomain
|
||||||
|
}
|
||||||
|
?: applicationId
|
||||||
|
?: ""
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
enum class StreamDirection {
|
||||||
|
UPLOAD, DOWNLOAD
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
package com.kunzisoft.keepass.notifications
|
package com.kunzisoft.keepass.notifications
|
||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
@@ -27,53 +28,61 @@ import android.os.IBinder
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.kunzisoft.keepass.R
|
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.AttachmentState
|
||||||
import com.kunzisoft.keepass.model.EntryAttachment
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileAsyncTask
|
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 java.util.*
|
||||||
import kotlin.collections.HashMap
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
|
|
||||||
class AttachmentFileNotificationService: LockNotificationService() {
|
class AttachmentFileNotificationService: LockNotificationService() {
|
||||||
|
|
||||||
override val notificationId: Int = 10000
|
override val notificationId: Int = 10000
|
||||||
|
private val attachmentNotificationList = CopyOnWriteArrayList<AttachmentNotification>()
|
||||||
|
|
||||||
private var mActionTaskBinder = ActionTaskBinder()
|
private var mActionTaskBinder = ActionTaskBinder()
|
||||||
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
|
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
|
||||||
|
|
||||||
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
inner class ActionTaskBinder: Binder() {
|
inner class ActionTaskBinder: Binder() {
|
||||||
|
|
||||||
fun getService(): AttachmentFileNotificationService = this@AttachmentFileNotificationService
|
fun getService(): AttachmentFileNotificationService = this@AttachmentFileNotificationService
|
||||||
|
|
||||||
fun addActionTaskListener(actionTaskListener: ActionTaskListener) {
|
fun addActionTaskListener(actionTaskListener: ActionTaskListener) {
|
||||||
mActionTaskListeners.add(actionTaskListener)
|
mActionTaskListeners.add(actionTaskListener)
|
||||||
|
attachmentNotificationList.forEach {
|
||||||
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
|
it.attachmentFileAction?.listener = attachmentFileActionListener
|
||||||
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) {
|
fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
|
||||||
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
|
attachmentNotificationList.forEach {
|
||||||
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
|
it.attachmentFileAction?.listener = null
|
||||||
entry.value.attachmentTask?.onUpdate = null
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
mActionTaskListeners.remove(actionTaskListener)
|
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 {
|
interface ActionTaskListener {
|
||||||
fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment)
|
fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
@@ -81,47 +90,31 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
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) {
|
val downloadFileUri: Uri? = if (intent?.hasExtra(FILE_URI_KEY) == true) {
|
||||||
intent.getParcelableExtra(DOWNLOAD_FILE_URI_KEY)
|
intent.getParcelableExtra(FILE_URI_KEY)
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
when(intent?.action) {
|
when(intent?.action) {
|
||||||
|
ACTION_ATTACHMENT_FILE_START_UPLOAD -> {
|
||||||
|
actionUploadOrDownload(downloadFileUri,
|
||||||
|
intent,
|
||||||
|
StreamDirection.UPLOAD)
|
||||||
|
}
|
||||||
ACTION_ATTACHMENT_FILE_START_DOWNLOAD -> {
|
ACTION_ATTACHMENT_FILE_START_DOWNLOAD -> {
|
||||||
if (downloadFileUri != null
|
actionUploadOrDownload(downloadFileUri,
|
||||||
&& intent.hasExtra(ATTACHMENT_KEY)) {
|
intent,
|
||||||
|
StreamDirection.DOWNLOAD)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
if (downloadFileUri != null) {
|
if (downloadFileUri != null) {
|
||||||
downloadFileUris[downloadFileUri]?.notificationId?.let {
|
attachmentNotificationList.firstOrNull { it.uri == downloadFileUri }?.let { elementToRemove ->
|
||||||
notificationManager?.cancel(it)
|
notificationManager?.cancel(elementToRemove.notificationId)
|
||||||
downloadFileUris.remove(downloadFileUri)
|
attachmentNotificationList.remove(elementToRemove)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (downloadFileUris.isEmpty()) {
|
if (attachmentNotificationList.isEmpty()) {
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,25 +123,35 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
return START_REDELIVER_INTENT
|
return START_REDELIVER_INTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
fun checkCurrentAttachmentProgress() {
|
fun checkCurrentAttachmentProgress() {
|
||||||
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
|
attachmentNotificationList.forEach { attachmentNotification ->
|
||||||
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
|
mActionTaskListeners.forEach { actionListener ->
|
||||||
mActionTaskListeners.forEach { actionListener ->
|
actionListener.onAttachmentAction(
|
||||||
actionListener.onAttachmentProgress(entry.key, entry.value.entryAttachment)
|
attachmentNotification.uri,
|
||||||
}
|
attachmentNotification.entryAttachmentState
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newNotification(downloadFileUri: Uri,
|
@Synchronized
|
||||||
entryAttachment: EntryAttachment,
|
fun removeAttachmentAction(entryAttachment: EntryAttachmentState) {
|
||||||
notificationIdAttachment: Int) {
|
attachmentNotificationList.firstOrNull {
|
||||||
|
it.entryAttachmentState == entryAttachment
|
||||||
|
}?.let {
|
||||||
|
attachmentNotificationList.remove(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newNotification(attachmentNotification: AttachmentNotification) {
|
||||||
|
|
||||||
val pendingContentIntent = PendingIntent.getActivity(this,
|
val pendingContentIntent = PendingIntent.getActivity(this,
|
||||||
0,
|
0,
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
action = Intent.ACTION_VIEW
|
action = Intent.ACTION_VIEW
|
||||||
setDataAndType(downloadFileUri, contentResolver.getType(downloadFileUri))
|
setDataAndType(attachmentNotification.uri,
|
||||||
|
contentResolver.getType(attachmentNotification.uri))
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
||||||
|
|
||||||
@@ -156,54 +159,84 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
0,
|
0,
|
||||||
Intent(this, AttachmentFileNotificationService::class.java).apply {
|
Intent(this, AttachmentFileNotificationService::class.java).apply {
|
||||||
// No action to delete the service
|
// No action to delete the service
|
||||||
putExtra(DOWNLOAD_FILE_URI_KEY, downloadFileUri)
|
putExtra(FILE_URI_KEY, attachmentNotification.uri)
|
||||||
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
||||||
|
|
||||||
val fileName = DocumentFile.fromSingleUri(this, downloadFileUri)?.name ?: ""
|
val fileName = DocumentFile.fromSingleUri(this, attachmentNotification.uri)?.name ?: ""
|
||||||
|
|
||||||
val builder = buildNewNotification().apply {
|
val builder = buildNewNotification().apply {
|
||||||
setSmallIcon(R.drawable.ic_file_download_white_24dp)
|
when (attachmentNotification.entryAttachmentState.streamDirection) {
|
||||||
setContentTitle(getString(R.string.download_attachment, fileName))
|
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)
|
setAutoCancel(false)
|
||||||
when (entryAttachment.downloadState) {
|
when (attachmentNotification.entryAttachmentState.downloadState) {
|
||||||
AttachmentState.NULL, AttachmentState.START -> {
|
AttachmentState.NULL, AttachmentState.START -> {
|
||||||
setContentText(getString(R.string.download_initialization))
|
setContentText(getString(R.string.download_initialization))
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
}
|
}
|
||||||
AttachmentState.IN_PROGRESS -> {
|
AttachmentState.IN_PROGRESS -> {
|
||||||
if (entryAttachment.downloadProgression > 100) {
|
if (attachmentNotification.entryAttachmentState.downloadProgression > 100) {
|
||||||
setContentText(getString(R.string.download_finalization))
|
setContentText(getString(R.string.download_finalization))
|
||||||
} else {
|
} else {
|
||||||
setProgress(100, entryAttachment.downloadProgression, false)
|
setProgress(100,
|
||||||
setContentText(getString(R.string.download_progression, entryAttachment.downloadProgression))
|
attachmentNotification.entryAttachmentState.downloadProgression,
|
||||||
|
false)
|
||||||
|
setContentText(getString(R.string.download_progression,
|
||||||
|
attachmentNotification.entryAttachmentState.downloadProgression))
|
||||||
}
|
}
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
}
|
}
|
||||||
AttachmentState.COMPLETE, AttachmentState.ERROR -> {
|
AttachmentState.COMPLETE -> {
|
||||||
setContentText(getString(R.string.download_complete))
|
setContentText(getString(R.string.download_complete))
|
||||||
setContentIntent(pendingContentIntent)
|
when (attachmentNotification.entryAttachmentState.streamDirection) {
|
||||||
|
StreamDirection.UPLOAD -> {
|
||||||
|
|
||||||
|
}
|
||||||
|
StreamDirection.DOWNLOAD -> {
|
||||||
|
setContentIntent(pendingContentIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
setDeleteIntent(pendingDeleteIntent)
|
setDeleteIntent(pendingDeleteIntent)
|
||||||
setOngoing(false)
|
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() {
|
override fun onDestroy() {
|
||||||
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
|
attachmentNotificationList.forEach { attachmentNotification ->
|
||||||
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
|
attachmentNotification.attachmentFileAction?.listener = null
|
||||||
entry.value.attachmentTask?.onUpdate = null
|
notificationManager?.cancel(attachmentNotification.notificationId)
|
||||||
notificationManager?.cancel(entry.value.notificationId)
|
}
|
||||||
}
|
attachmentNotificationList.clear()
|
||||||
})
|
|
||||||
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AttachmentNotification(var notificationId: Int,
|
private data class AttachmentNotification(var uri: Uri,
|
||||||
var entryAttachment: EntryAttachment,
|
var notificationId: Int,
|
||||||
var attachmentTask: AttachmentFileAsyncTask? = null) {
|
var entryAttachmentState: EntryAttachmentState,
|
||||||
|
var attachmentFileAction: AttachmentFileAction? = null) {
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
@@ -220,15 +253,182 @@ 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)
|
||||||
|
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() {
|
||||||
|
TimeoutHelper.temporarilyDisableTimeout()
|
||||||
|
|
||||||
|
// on pre execute
|
||||||
|
attachmentNotification.attachmentFileAction = this
|
||||||
|
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 {
|
companion object {
|
||||||
private val TAG = AttachmentFileNotificationService::javaClass.name
|
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_FILE_START_DOWNLOAD = "ACTION_ATTACHMENT_FILE_START_DOWNLOAD"
|
||||||
|
|
||||||
const val DOWNLOAD_FILE_URI_KEY = "DOWNLOAD_FILE_URI_KEY"
|
const val FILE_URI_KEY = "FILE_URI_KEY"
|
||||||
const val ATTACHMENT_KEY = "ATTACHMENT_KEY"
|
const val ATTACHMENT_KEY = "ATTACHMENT_KEY"
|
||||||
|
|
||||||
private val downloadFileUris = HashMap<Uri, AttachmentNotification>()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -55,6 +55,8 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
super.onStartCommand(intent, flags, startId)
|
||||||
|
|
||||||
// Get entry info from intent
|
// Get entry info from intent
|
||||||
mEntryInfo = intent?.getParcelableExtra(EXTRA_ENTRY_INFO)
|
mEntryInfo = intent?.getParcelableExtra(EXTRA_ENTRY_INFO)
|
||||||
|
|
||||||
@@ -151,9 +153,6 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
val nextField = nextFields[0]
|
val nextField = nextFields[0]
|
||||||
builder.setContentText(getString(R.string.select_to_copy, nextField.label))
|
builder.setContentText(getString(R.string.select_to_copy, nextField.label))
|
||||||
builder.setContentIntent(getCopyPendingIntent(nextField, nextFields))
|
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)
|
val cleanIntent = Intent(this, ClipboardEntryNotificationService::class.java)
|
||||||
|
|||||||
@@ -1,108 +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 {
|
|
||||||
|
|
||||||
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,39 +19,54 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.notifications
|
package com.kunzisoft.keepass.notifications
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
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.R
|
||||||
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.database.action.*
|
import com.kunzisoft.keepass.database.action.*
|
||||||
import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseRunnable
|
import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseRunnable
|
||||||
import com.kunzisoft.keepass.database.action.history.RestoreEntryHistoryDatabaseRunnable
|
import com.kunzisoft.keepass.database.action.history.RestoreEntryHistoryDatabaseRunnable
|
||||||
import com.kunzisoft.keepass.database.action.node.*
|
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.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
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.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
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_START_TASK_ACTION
|
||||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_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.*
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdater {
|
open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater {
|
||||||
|
|
||||||
override val notificationId: Int = 575
|
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 mActionTaskBinder = ActionTaskBinder()
|
||||||
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
|
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 mMessageId: Int? = null
|
||||||
private var mWarningId: Int? = null
|
private var mWarningId: Int? = null
|
||||||
|
|
||||||
@@ -60,13 +75,15 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
fun getService(): DatabaseTaskNotificationService = this@DatabaseTaskNotificationService
|
fun getService(): DatabaseTaskNotificationService = this@DatabaseTaskNotificationService
|
||||||
|
|
||||||
fun addActionTaskListener(actionTaskListener: ActionTaskListener) {
|
fun addActionTaskListener(actionTaskListener: ActionTaskListener) {
|
||||||
|
mAllowFinishAction.set(true)
|
||||||
mActionTaskListeners.add(actionTaskListener)
|
mActionTaskListeners.add(actionTaskListener)
|
||||||
// To prevent task dialog to be unbound before the display
|
|
||||||
actionRunnableAsyncTask?.allowFinishTask?.set(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
|
fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
|
||||||
mActionTaskListeners.remove(actionTaskListener)
|
mActionTaskListeners.remove(actionTaskListener)
|
||||||
|
if (mActionTaskListeners.size == 0) {
|
||||||
|
mAllowFinishAction.set(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,49 +93,41 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
fun onStopAction(actionTask: String, result: ActionRunnable.Result)
|
fun onStopAction(actionTask: String, result: ActionRunnable.Result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force to call [ActionTaskListener.onStartAction] if the action is still running
|
||||||
|
*/
|
||||||
fun checkAction() {
|
fun checkAction() {
|
||||||
mActionTaskListeners.forEach { actionTaskListener ->
|
if (mActionRunning) {
|
||||||
actionTaskListener.onUpdateAction(mTitleId, mMessageId, mWarningId)
|
mActionTaskListeners.forEach { actionTaskListener ->
|
||||||
|
actionTaskListener.onStartAction(mTitleId, mMessageId, mWarningId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
super.onBind(intent)
|
||||||
return mActionTaskBinder
|
return mActionTaskBinder
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
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
|
val intentAction = intent?.action
|
||||||
if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
|
||||||
saveAction = intent.getBooleanExtra(SAVE_DATABASE_KEY, saveAction)
|
if (intentAction == null && !mDatabase.loaded) {
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
if (intentAction == ACTION_DATABASE_CLOSE) {
|
||||||
|
// Send lock action
|
||||||
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
}
|
}
|
||||||
|
|
||||||
val titleId: Int = when (intentAction) {
|
val actionRunnable: ActionRunnable? = 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) {
|
|
||||||
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent)
|
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent)
|
||||||
ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent)
|
ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent)
|
||||||
ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent)
|
ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent)
|
||||||
@@ -147,52 +156,212 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
actionRunnable?.let { actionRunnableNotNull ->
|
// Build and launch the action
|
||||||
// Assign elements for updates
|
if (actionRunnable != null) {
|
||||||
mTitleId = titleId
|
mainScope.launch {
|
||||||
mMessageId = messageId
|
executeAction(this@DatabaseTaskNotificationService,
|
||||||
mWarningId = warningId
|
{
|
||||||
|
mActionRunning = true
|
||||||
|
|
||||||
// Create the notification
|
sendBroadcast(Intent(DATABASE_START_TASK_ACTION).apply {
|
||||||
newNotification(intent.getIntExtra(DATABASE_TASK_TITLE_KEY, titleId))
|
putExtra(DATABASE_TASK_TITLE_KEY, mTitleId)
|
||||||
|
putExtra(DATABASE_TASK_MESSAGE_KEY, mMessageId)
|
||||||
|
putExtra(DATABASE_TASK_WARNING_KEY, mWarningId)
|
||||||
|
})
|
||||||
|
|
||||||
// Build and launch the action
|
mActionTaskListeners.forEach { actionTaskListener ->
|
||||||
actionRunnableAsyncTask = ActionRunnableAsyncTask(this,
|
actionTaskListener.onStartAction(mTitleId, mMessageId, mWarningId)
|
||||||
{
|
}
|
||||||
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(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 ->
|
sendBroadcast(Intent(DATABASE_STOP_TASK_ACTION))
|
||||||
mActionTaskListeners.forEach { actionTaskListener ->
|
|
||||||
actionTaskListener.onStopAction(intentAction!!, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
sendBroadcast(Intent(DATABASE_STOP_TASK_ACTION))
|
mActionRunning = false
|
||||||
|
}
|
||||||
stopSelf()
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
actionRunnableAsyncTask?.execute({ actionRunnableNotNull })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
var saveAction = false
|
||||||
.setSmallIcon(R.drawable.notification_ic_database_load)
|
if (intent != null && intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||||
.setContentTitle(getString(title))
|
saveAction = intent.getBooleanExtra(SAVE_DATABASE_KEY, saveAction)
|
||||||
.setAutoCancel(false)
|
}
|
||||||
.setContentIntent(null)
|
|
||||||
startForeground(notificationId, builder.build())
|
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) {
|
override fun updateMessage(resId: Int) {
|
||||||
@@ -202,22 +371,32 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun actionOnLock() {
|
||||||
|
if (!TimeoutHelper.temporarilyDisableTimeout) {
|
||||||
|
closeDatabase()
|
||||||
|
// Remove the lock timer (no more needed if it exists)
|
||||||
|
TimeoutHelper.cancelLockTimer(this)
|
||||||
|
// Service is stopped after receive the broadcast
|
||||||
|
super.actionOnLock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildDatabaseCreateActionTask(intent: Intent): ActionRunnable? {
|
private fun buildDatabaseCreateActionTask(intent: Intent): ActionRunnable? {
|
||||||
|
|
||||||
if (intent.hasExtra(DATABASE_URI_KEY)
|
if (intent.hasExtra(DATABASE_URI_KEY)
|
||||||
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
|
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
|
||||||
&& intent.hasExtra(MASTER_PASSWORD_KEY)
|
&& intent.hasExtra(MASTER_PASSWORD_KEY)
|
||||||
&& intent.hasExtra(KEY_FILE_CHECKED_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 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)
|
if (databaseUri == null)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
return CreateDatabaseRunnable(this,
|
return CreateDatabaseRunnable(this,
|
||||||
Database.getInstance(),
|
mDatabase,
|
||||||
databaseUri,
|
databaseUri,
|
||||||
getString(R.string.database_default_name),
|
getString(R.string.database_default_name),
|
||||||
getString(R.string.database),
|
getString(R.string.database),
|
||||||
@@ -225,7 +404,12 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
intent.getStringExtra(MASTER_PASSWORD_KEY),
|
intent.getStringExtra(MASTER_PASSWORD_KEY),
|
||||||
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
|
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
|
||||||
keyFileUri
|
keyFileUri
|
||||||
)
|
) { result ->
|
||||||
|
result.data = Bundle().apply {
|
||||||
|
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||||
|
putParcelable(KEY_FILE_URI_KEY, keyFileUri)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -235,15 +419,14 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
|
|
||||||
if (intent.hasExtra(DATABASE_URI_KEY)
|
if (intent.hasExtra(DATABASE_URI_KEY)
|
||||||
&& intent.hasExtra(MASTER_PASSWORD_KEY)
|
&& intent.hasExtra(MASTER_PASSWORD_KEY)
|
||||||
&& intent.hasExtra(KEY_FILE_KEY)
|
&& intent.hasExtra(KEY_FILE_URI_KEY)
|
||||||
&& intent.hasExtra(READ_ONLY_KEY)
|
&& intent.hasExtra(READ_ONLY_KEY)
|
||||||
&& intent.hasExtra(CIPHER_ENTITY_KEY)
|
&& intent.hasExtra(CIPHER_ENTITY_KEY)
|
||||||
&& intent.hasExtra(FIX_DUPLICATE_UUID_KEY)
|
&& intent.hasExtra(FIX_DUPLICATE_UUID_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
|
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
|
||||||
val masterPassword: String? = intent.getStringExtra(MASTER_PASSWORD_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 readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true)
|
||||||
val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY)
|
val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY)
|
||||||
|
|
||||||
@@ -252,13 +435,12 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
|
|
||||||
return LoadDatabaseRunnable(
|
return LoadDatabaseRunnable(
|
||||||
this,
|
this,
|
||||||
database,
|
mDatabase,
|
||||||
databaseUri,
|
databaseUri,
|
||||||
masterPassword,
|
masterPassword,
|
||||||
keyFileUri,
|
keyFileUri,
|
||||||
readOnly,
|
readOnly,
|
||||||
cipherEntity,
|
cipherEntity,
|
||||||
PreferencesUtil.omitBackup(this),
|
|
||||||
intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false),
|
intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false),
|
||||||
this
|
this
|
||||||
) { result ->
|
) { result ->
|
||||||
@@ -266,7 +448,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
result.data = Bundle().apply {
|
result.data = Bundle().apply {
|
||||||
putParcelable(DATABASE_URI_KEY, databaseUri)
|
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||||
putString(MASTER_PASSWORD_KEY, masterPassword)
|
putString(MASTER_PASSWORD_KEY, masterPassword)
|
||||||
putParcelable(KEY_FILE_KEY, keyFileUri)
|
putParcelable(KEY_FILE_URI_KEY, keyFileUri)
|
||||||
putBoolean(READ_ONLY_KEY, readOnly)
|
putBoolean(READ_ONLY_KEY, readOnly)
|
||||||
putParcelable(CIPHER_ENTITY_KEY, cipherEntity)
|
putParcelable(CIPHER_ENTITY_KEY, cipherEntity)
|
||||||
}
|
}
|
||||||
@@ -281,16 +463,16 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
|
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
|
||||||
&& intent.hasExtra(MASTER_PASSWORD_KEY)
|
&& intent.hasExtra(MASTER_PASSWORD_KEY)
|
||||||
&& intent.hasExtra(KEY_FILE_CHECKED_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
|
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null
|
||||||
AssignPasswordInDatabaseRunnable(this,
|
AssignPasswordInDatabaseRunnable(this,
|
||||||
Database.getInstance(),
|
mDatabase,
|
||||||
databaseUri,
|
databaseUri,
|
||||||
intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false),
|
intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false),
|
||||||
intent.getStringExtra(MASTER_PASSWORD_KEY),
|
intent.getStringExtra(MASTER_PASSWORD_KEY),
|
||||||
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
|
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
|
||||||
intent.getParcelableExtra(KEY_FILE_KEY)
|
intent.getParcelableExtra(KEY_FILE_URI_KEY)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -312,7 +494,6 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(PARENT_ID_KEY)
|
&& intent.hasExtra(PARENT_ID_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
|
val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
|
||||||
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
|
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
|
||||||
|
|
||||||
@@ -320,9 +501,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
|| newGroup == null)
|
|| newGroup == null)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
database.getGroupById(parentId)?.let { parent ->
|
mDatabase.getGroupById(parentId)?.let { parent ->
|
||||||
AddGroupRunnable(this,
|
AddGroupRunnable(this,
|
||||||
database,
|
mDatabase,
|
||||||
newGroup,
|
newGroup,
|
||||||
parent,
|
parent,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||||
@@ -338,7 +519,6 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(GROUP_KEY)
|
&& intent.hasExtra(GROUP_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
val groupId: NodeId<*>? = intent.getParcelableExtra(GROUP_ID_KEY)
|
val groupId: NodeId<*>? = intent.getParcelableExtra(GROUP_ID_KEY)
|
||||||
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
|
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
|
||||||
|
|
||||||
@@ -346,9 +526,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
|| newGroup == null)
|
|| newGroup == null)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
database.getGroupById(groupId)?.let { oldGroup ->
|
mDatabase.getGroupById(groupId)?.let { oldGroup ->
|
||||||
UpdateGroupRunnable(this,
|
UpdateGroupRunnable(this,
|
||||||
database,
|
mDatabase,
|
||||||
oldGroup,
|
oldGroup,
|
||||||
newGroup,
|
newGroup,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||||
@@ -364,7 +544,6 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(PARENT_ID_KEY)
|
&& intent.hasExtra(PARENT_ID_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
|
val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
|
||||||
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
|
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
|
||||||
|
|
||||||
@@ -372,9 +551,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
|| newEntry == null)
|
|| newEntry == null)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
database.getGroupById(parentId)?.let { parent ->
|
mDatabase.getGroupById(parentId)?.let { parent ->
|
||||||
AddEntryRunnable(this,
|
AddEntryRunnable(this,
|
||||||
database,
|
mDatabase,
|
||||||
newEntry,
|
newEntry,
|
||||||
parent,
|
parent,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||||
@@ -390,7 +569,6 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(ENTRY_KEY)
|
&& intent.hasExtra(ENTRY_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
val entryId: NodeId<UUID>? = intent.getParcelableExtra(ENTRY_ID_KEY)
|
val entryId: NodeId<UUID>? = intent.getParcelableExtra(ENTRY_ID_KEY)
|
||||||
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
|
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
|
||||||
|
|
||||||
@@ -398,9 +576,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
|| newEntry == null)
|
|| newEntry == null)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
database.getEntryById(entryId)?.let { oldEntry ->
|
mDatabase.getEntryById(entryId)?.let { oldEntry ->
|
||||||
UpdateEntryRunnable(this,
|
UpdateEntryRunnable(this,
|
||||||
database,
|
mDatabase,
|
||||||
oldEntry,
|
oldEntry,
|
||||||
newEntry,
|
newEntry,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||||
@@ -417,13 +595,12 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(PARENT_ID_KEY)
|
&& intent.hasExtra(PARENT_ID_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
|
val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
|
||||||
|
|
||||||
database.getGroupById(parentId)?.let { newParent ->
|
mDatabase.getGroupById(parentId)?.let { newParent ->
|
||||||
CopyNodesRunnable(this,
|
CopyNodesRunnable(this,
|
||||||
database,
|
mDatabase,
|
||||||
getListNodesFromBundle(database, intent.extras!!),
|
getListNodesFromBundle(mDatabase, intent.extras!!),
|
||||||
newParent,
|
newParent,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||||
AfterActionNodesRunnable())
|
AfterActionNodesRunnable())
|
||||||
@@ -439,13 +616,12 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(PARENT_ID_KEY)
|
&& intent.hasExtra(PARENT_ID_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
|
val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
|
||||||
|
|
||||||
database.getGroupById(parentId)?.let { newParent ->
|
mDatabase.getGroupById(parentId)?.let { newParent ->
|
||||||
MoveNodesRunnable(this,
|
MoveNodesRunnable(this,
|
||||||
database,
|
mDatabase,
|
||||||
getListNodesFromBundle(database, intent.extras!!),
|
getListNodesFromBundle(mDatabase, intent.extras!!),
|
||||||
newParent,
|
newParent,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||||
AfterActionNodesRunnable())
|
AfterActionNodesRunnable())
|
||||||
@@ -460,10 +636,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(ENTRIES_ID_KEY)
|
&& intent.hasExtra(ENTRIES_ID_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
DeleteNodesRunnable(this,
|
DeleteNodesRunnable(this,
|
||||||
database,
|
mDatabase,
|
||||||
getListNodesFromBundle(database, intent.extras!!),
|
getListNodesFromBundle(mDatabase, intent.extras!!),
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||||
AfterActionNodesRunnable())
|
AfterActionNodesRunnable())
|
||||||
} else {
|
} else {
|
||||||
@@ -476,12 +651,11 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(ENTRY_HISTORY_POSITION_KEY)
|
&& intent.hasExtra(ENTRY_HISTORY_POSITION_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
|
val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
|
||||||
|
|
||||||
database.getEntryById(entryId)?.let { mainEntry ->
|
mDatabase.getEntryById(entryId)?.let { mainEntry ->
|
||||||
RestoreEntryHistoryDatabaseRunnable(this,
|
RestoreEntryHistoryDatabaseRunnable(this,
|
||||||
database,
|
mDatabase,
|
||||||
mainEntry,
|
mainEntry,
|
||||||
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
|
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
|
||||||
@@ -496,12 +670,11 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(ENTRY_HISTORY_POSITION_KEY)
|
&& intent.hasExtra(ENTRY_HISTORY_POSITION_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
|
val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
|
||||||
|
|
||||||
database.getEntryById(entryId)?.let { mainEntry ->
|
mDatabase.getEntryById(entryId)?.let { mainEntry ->
|
||||||
DeleteEntryHistoryDatabaseRunnable(this,
|
DeleteEntryHistoryDatabaseRunnable(this,
|
||||||
database,
|
mDatabase,
|
||||||
mainEntry,
|
mainEntry,
|
||||||
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
|
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
|
||||||
@@ -524,7 +697,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
return UpdateCompressionBinariesDatabaseRunnable(this,
|
return UpdateCompressionBinariesDatabaseRunnable(this,
|
||||||
Database.getInstance(),
|
mDatabase,
|
||||||
oldElement,
|
oldElement,
|
||||||
newElement,
|
newElement,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
@@ -541,7 +714,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
private fun buildDatabaseUpdateElementActionTask(intent: Intent): ActionRunnable? {
|
private fun buildDatabaseUpdateElementActionTask(intent: Intent): ActionRunnable? {
|
||||||
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||||
return SaveDatabaseRunnable(this,
|
return SaveDatabaseRunnable(this,
|
||||||
Database.getInstance(),
|
mDatabase,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
).apply {
|
).apply {
|
||||||
mAfterSaveDatabase = { result ->
|
mAfterSaveDatabase = { result ->
|
||||||
@@ -559,54 +732,17 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
private fun buildDatabaseSave(intent: Intent): ActionRunnable? {
|
private fun buildDatabaseSave(intent: Intent): ActionRunnable? {
|
||||||
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||||
SaveDatabaseRunnable(this,
|
SaveDatabaseRunnable(this,
|
||||||
Database.getInstance(),
|
mDatabase,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
|
||||||
} else {
|
} else {
|
||||||
null
|
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>() {
|
|
||||||
|
|
||||||
var allowFinishTask = AtomicBoolean(false)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Additional wait if the dialog take time to show
|
|
||||||
while(!allowFinishTask.get()) {
|
|
||||||
Thread.sleep(250)
|
|
||||||
}
|
|
||||||
return resultTask
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPostExecute(result: ActionRunnable.Result) {
|
|
||||||
super.onPostExecute(result)
|
|
||||||
onPostExecute.invoke(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val TAG = DatabaseTaskNotificationService::class.java.name
|
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_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK"
|
||||||
const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK"
|
const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK"
|
||||||
const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_TASK"
|
const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_TASK"
|
||||||
@@ -632,12 +768,17 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
const val ACTION_DATABASE_UPDATE_PARALLELISM_TASK = "ACTION_DATABASE_UPDATE_PARALLELISM_TASK"
|
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_UPDATE_ITERATIONS_TASK = "ACTION_DATABASE_UPDATE_ITERATIONS_TASK"
|
||||||
const val ACTION_DATABASE_SAVE = "ACTION_DATABASE_SAVE"
|
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 DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
||||||
const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
|
const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
|
||||||
const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
|
const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
|
||||||
const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_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 READ_ONLY_KEY = "READ_ONLY_KEY"
|
||||||
const val CIPHER_ENTITY_KEY = "CIPHER_ENTITY_KEY"
|
const val CIPHER_ENTITY_KEY = "CIPHER_ENTITY_KEY"
|
||||||
const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY"
|
const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user