mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
706 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
291ed44621 | ||
|
|
a7e8915ea0 | ||
|
|
a027c76af3 | ||
|
|
ec375bd068 | ||
|
|
f5a28c83f0 | ||
|
|
813240e233 | ||
|
|
051ac0e669 | ||
|
|
5c798c4569 | ||
|
|
8356514bf8 | ||
|
|
464bc10860 | ||
|
|
5436c8bed1 | ||
|
|
1b163b161b | ||
|
|
f0ee5fd946 | ||
|
|
066a9f871c | ||
|
|
e23841f5bb | ||
|
|
571e66fae5 | ||
|
|
402aa280e0 | ||
|
|
65de5df319 | ||
|
|
63168afc85 | ||
|
|
1c61f54df6 | ||
|
|
50b5ad1799 | ||
|
|
6457c02a35 | ||
|
|
7b242c9733 | ||
|
|
e0ab6137e7 | ||
|
|
49b23a33e7 | ||
|
|
c5e9d14199 | ||
|
|
f7e8662bdf | ||
|
|
7d356d1e34 | ||
|
|
f9bb70f395 | ||
|
|
4335809468 | ||
|
|
2a1b1d28bd | ||
|
|
38d69428d8 | ||
|
|
0c22a8135d | ||
|
|
2b50665f9e | ||
|
|
59607efa62 | ||
|
|
cd5cfbe009 | ||
|
|
84e6d96ce0 | ||
|
|
8f00b53fab | ||
|
|
e2164a1a9c | ||
|
|
6e5dcaf08d | ||
|
|
18a2aae66a | ||
|
|
4c0aab15fa | ||
|
|
75a7d4188b | ||
|
|
ff0da57aeb | ||
|
|
d716cba46b | ||
|
|
5d41e44141 | ||
|
|
281936ecd0 | ||
|
|
da23321c0d | ||
|
|
110e2b7580 | ||
|
|
546d4353e2 | ||
|
|
15b5d36cd6 | ||
|
|
6a935a49ea | ||
|
|
f7f515481f | ||
|
|
3aab37c0c0 | ||
|
|
123e626df6 | ||
|
|
758985675d | ||
|
|
631ebc657b | ||
|
|
d0ca714482 | ||
|
|
1fe3787186 | ||
|
|
c8a952616f | ||
|
|
efcbecc218 | ||
|
|
487bafa5cf | ||
|
|
93aed33e2a | ||
|
|
a7765cb635 | ||
|
|
e6a5be6c66 | ||
|
|
5443532266 | ||
|
|
990aca2e1a | ||
|
|
49297adf97 | ||
|
|
b4d26fd35a | ||
|
|
b31f580760 | ||
|
|
7dc33f1956 | ||
|
|
e3470dd68b | ||
|
|
410c653654 | ||
|
|
bef3cf4c93 | ||
|
|
d8961d2acb | ||
|
|
17873ef7aa | ||
|
|
1d6e7eabc1 | ||
|
|
ba956abb1c | ||
|
|
7a2c2df89e | ||
|
|
bd44e659a8 | ||
|
|
8a1a27f5a1 | ||
|
|
7825071f61 | ||
|
|
7047bcbb1e | ||
|
|
193ef74e63 | ||
|
|
8a193d4dcd | ||
|
|
35cc662b26 | ||
|
|
9173bcc742 | ||
|
|
267f4273ee | ||
|
|
f1e675d662 | ||
|
|
c228534218 | ||
|
|
181fa5f32a | ||
|
|
7d5c37ec33 | ||
|
|
2eb9736d23 | ||
|
|
9f89ad2a08 | ||
|
|
490db3a026 | ||
|
|
2428f073bc | ||
|
|
e1231585ce | ||
|
|
839adaf559 | ||
|
|
839e004c08 | ||
|
|
ecf98828ff | ||
|
|
4dbc3c2353 | ||
|
|
c953a337fe | ||
|
|
6f026e6043 | ||
|
|
a775e29aef | ||
|
|
cb7b37fca4 | ||
|
|
cc79a67a9f | ||
|
|
72e7e3a3c4 | ||
|
|
4ffbeebd85 | ||
|
|
2e4b4e4736 | ||
|
|
a0acb0b658 | ||
|
|
feeaea4d64 | ||
|
|
0b7eb96e48 | ||
|
|
f0c4b628bf | ||
|
|
c66fc102ee | ||
|
|
9947a23343 | ||
|
|
abc204b313 | ||
|
|
425a0812bd | ||
|
|
030d466b11 | ||
|
|
27e9cd04a9 | ||
|
|
e42aea9444 | ||
|
|
3e1ee720f1 | ||
|
|
cb9f12482e | ||
|
|
bcf273a435 | ||
|
|
6230ada2cc | ||
|
|
db4de65683 | ||
|
|
931f7f07ca | ||
|
|
0f764c9400 | ||
|
|
89c98ab257 | ||
|
|
a49255c471 | ||
|
|
39d5a4908f | ||
|
|
fbaeaddff7 | ||
|
|
bc97db982e | ||
|
|
9a2f260bc5 | ||
|
|
954f1e3c7f | ||
|
|
0626f8d678 | ||
|
|
aa8c9676fe | ||
|
|
bb2ab768a8 | ||
|
|
8192c68f38 | ||
|
|
454561c2a1 | ||
|
|
deabcc9605 | ||
|
|
c3bdb9dd16 | ||
|
|
86d77c908f | ||
|
|
c5cbba5971 | ||
|
|
aea5493180 | ||
|
|
431832e50e | ||
|
|
daf239c93c | ||
|
|
2c669deae5 | ||
|
|
81969de76e | ||
|
|
5a29e7a83e | ||
|
|
9ed5aef40f | ||
|
|
9e025b4329 | ||
|
|
543da4fe24 | ||
|
|
e82e50a57a | ||
|
|
288d62d666 | ||
|
|
8ed224eeab | ||
|
|
22bafff943 | ||
|
|
02403cf566 | ||
|
|
eabbc6f037 | ||
|
|
27e60edaf4 | ||
|
|
f7d0b64dea | ||
|
|
b874484101 | ||
|
|
0ee72db2e9 | ||
|
|
4f07bac9ec | ||
|
|
80abaf70f2 | ||
|
|
d6acbc2fb1 | ||
|
|
caf93c019a | ||
|
|
7044efa7c3 | ||
|
|
5b4d8f971f | ||
|
|
75e8feb9b8 | ||
|
|
5d59419a06 | ||
|
|
ff2e47bf70 | ||
|
|
4721ed7c15 | ||
|
|
d9da328088 | ||
|
|
2ee506ac06 | ||
|
|
16c4e4dc5b | ||
|
|
28b8fb1b97 | ||
|
|
c8bae9fba6 | ||
|
|
920764ad33 | ||
|
|
a45d114527 | ||
|
|
62cca09045 | ||
|
|
ea126b90e2 | ||
|
|
777a20182e | ||
|
|
df62a9d32b | ||
|
|
47e211bf3c | ||
|
|
650ca3844e | ||
|
|
8de49fa027 | ||
|
|
35b7633f4d | ||
|
|
2c96a80280 | ||
|
|
c7000c8ac6 | ||
|
|
1dca2b4387 | ||
|
|
ccbe9ad21e | ||
|
|
1fe6f11ef4 | ||
|
|
9d4ab60fc4 | ||
|
|
ea8c5ad6e1 | ||
|
|
63a0f5f91c | ||
|
|
e8bacbdb6f | ||
|
|
bcc8226ccc | ||
|
|
ddec91a0c5 | ||
|
|
2c262bb29d | ||
|
|
ae0afb7b53 | ||
|
|
84e7fe5b60 | ||
|
|
ccb32b045a | ||
|
|
b2e29ac4bd | ||
|
|
7df6309a68 | ||
|
|
a203ad9f64 | ||
|
|
44d6e09337 | ||
|
|
7c59ec019a | ||
|
|
b457f64ec8 | ||
|
|
e152e59a61 | ||
|
|
2c620ad69a | ||
|
|
c08b405fc2 | ||
|
|
585e39c591 | ||
|
|
60e857dba9 | ||
|
|
9a4aa2b08f | ||
|
|
2900b08b70 | ||
|
|
b0936563a2 | ||
|
|
3d327b245a | ||
|
|
9a14de3448 | ||
|
|
59f83545cf | ||
|
|
d9da1ef085 | ||
|
|
279a27f740 | ||
|
|
6ba822ee48 | ||
|
|
e4445949c8 | ||
|
|
8178ff583d | ||
|
|
0e0e19a802 | ||
|
|
1e2e7d841f | ||
|
|
263c2f00eb | ||
|
|
39c0b57652 | ||
|
|
677baf549c | ||
|
|
848478c28b | ||
|
|
98ad33a589 | ||
|
|
3f33733f40 | ||
|
|
cdc4ae4fb3 | ||
|
|
3edfa8a6ce | ||
|
|
6e99b667af | ||
|
|
23c8735568 | ||
|
|
f39065044f | ||
|
|
7d2ae47b0f | ||
|
|
7247de6908 | ||
|
|
4c2aef8504 | ||
|
|
ac8717cf1f | ||
|
|
7761928064 | ||
|
|
198047406b | ||
|
|
1d57309db9 | ||
|
|
07af9f36b2 | ||
|
|
d157fea9be | ||
|
|
7692caf622 | ||
|
|
dc65a8823f | ||
|
|
2210932fdf | ||
|
|
0cb1bf4b7f | ||
|
|
44d175dd40 | ||
|
|
097feb6437 | ||
|
|
250c7e5f20 | ||
|
|
83740f77bb | ||
|
|
8267e13d22 | ||
|
|
a7e9396f35 | ||
|
|
0f6c8601d2 | ||
|
|
66e68092d5 | ||
|
|
09616a594c | ||
|
|
7ae58ea7ea | ||
|
|
9f1f85a3c4 | ||
|
|
9992cdfce0 | ||
|
|
f5fa08ce94 | ||
|
|
67f70f7f85 | ||
|
|
840bd56a3d | ||
|
|
c1e2d31cfd | ||
|
|
c35322d44e | ||
|
|
9867e46c3f | ||
|
|
946a120038 | ||
|
|
2cd1a17aa2 | ||
|
|
6e29fe2932 | ||
|
|
e6c6bf6613 | ||
|
|
72c53169df | ||
|
|
b78cce7b4f | ||
|
|
c40499ec31 | ||
|
|
ee158e517e | ||
|
|
70aebcf9aa | ||
|
|
b15870d441 | ||
|
|
fe7074736a | ||
|
|
69c523ffad | ||
|
|
6aeefdf43d | ||
|
|
5f4c8be3d3 | ||
|
|
3092e4c557 | ||
|
|
ea119068da | ||
|
|
14371ecf94 | ||
|
|
45b0fcfe15 | ||
|
|
00aa5f5586 | ||
|
|
79fd53fd4c | ||
|
|
357ee3daf0 | ||
|
|
c2f7897f10 | ||
|
|
75455c0c48 | ||
|
|
a394bb9f8e | ||
|
|
2f5a846493 | ||
|
|
90376b361d | ||
|
|
229cf6bf5f | ||
|
|
bc46737353 | ||
|
|
1db2243a2e | ||
|
|
7cf836b3cb | ||
|
|
f7bbd295d6 | ||
|
|
62dbd95b48 | ||
|
|
df07e9c719 | ||
|
|
9aaf72726e | ||
|
|
5289927619 | ||
|
|
fa8c686f75 | ||
|
|
df5f28b7c4 | ||
|
|
280d8368fa | ||
|
|
80dbff1f21 | ||
|
|
7ee68a8481 | ||
|
|
ac8dd42c45 | ||
|
|
eed2148b2a | ||
|
|
dc5345b6d3 | ||
|
|
221af0b5bb | ||
|
|
a10ccc1eb0 | ||
|
|
b72d858480 | ||
|
|
60412cc90b | ||
|
|
512ac87dc9 | ||
|
|
d97020d1c5 | ||
|
|
f82cb617ba | ||
|
|
f79281a1a0 | ||
|
|
7be1dbb78b | ||
|
|
f875787799 | ||
|
|
f82c208556 | ||
|
|
f2150e3d85 | ||
|
|
ecc198e8a0 | ||
|
|
949bc58a80 | ||
|
|
7d79fff16f | ||
|
|
3aeb678292 | ||
|
|
160ac41bed | ||
|
|
9dfbcbe89c | ||
|
|
30da529348 | ||
|
|
dd8d114711 | ||
|
|
2191a4a848 | ||
|
|
8b9ea8d988 | ||
|
|
46dda8567d | ||
|
|
6953da4d9a | ||
|
|
e987d6647e | ||
|
|
359d85727e | ||
|
|
a994bf9dd8 | ||
|
|
14c4e095f6 | ||
|
|
59dce0e56f | ||
|
|
1f54a893a7 | ||
|
|
9489f1ee3d | ||
|
|
dc3d720e8d | ||
|
|
efe30b598b | ||
|
|
42515bfb2d | ||
|
|
acb3657d95 | ||
|
|
e7159c9d36 | ||
|
|
f3fdca368b | ||
|
|
4ea3e08a45 | ||
|
|
1eebc72b21 | ||
|
|
9a91be7e36 | ||
|
|
48476f9b88 | ||
|
|
68c991eb9b | ||
|
|
b51c77b01b | ||
|
|
57105db554 | ||
|
|
4bd3bdaddf | ||
|
|
9cf59b8d73 | ||
|
|
a793b0bb42 | ||
|
|
4d9e2e1471 | ||
|
|
1719887e55 | ||
|
|
2be00aca9d | ||
|
|
6fd05c5ad7 | ||
|
|
65e404374f | ||
|
|
0b78731bb3 | ||
|
|
65d318ed88 | ||
|
|
1bfcea55a9 | ||
|
|
6780eb004d | ||
|
|
0e12b2d021 | ||
|
|
1b356f87ec | ||
|
|
bf24d0bae1 | ||
|
|
97c831d4bb | ||
|
|
e3e48ffa6d | ||
|
|
b678416122 | ||
|
|
df722925fa | ||
|
|
4cbc0d9806 | ||
|
|
b0f3711b4e | ||
|
|
14ec6579b2 | ||
|
|
30bf039473 | ||
|
|
33bea317b0 | ||
|
|
c6814dc05e | ||
|
|
16808069ec | ||
|
|
e813974e29 | ||
|
|
7e0010b536 | ||
|
|
93171adcb3 | ||
|
|
9501cc76a4 | ||
|
|
5d7db046ac | ||
|
|
46c259bc3e | ||
|
|
3bacff91d3 | ||
|
|
bd79d483d2 | ||
|
|
16e31f4881 | ||
|
|
afa23c393d | ||
|
|
54d23cb781 | ||
|
|
c757e410e9 | ||
|
|
39dd25567d | ||
|
|
32cd998c2a | ||
|
|
691fc6335e | ||
|
|
b0025d1416 | ||
|
|
2467d8b0e7 | ||
|
|
28993c53e7 | ||
|
|
efdea870f0 | ||
|
|
b2995ec862 | ||
|
|
2bcc84dbb2 | ||
|
|
70cc98ce33 | ||
|
|
6e055f398d | ||
|
|
9f6234f032 | ||
|
|
6135544b72 | ||
|
|
5f32ec218b | ||
|
|
3b9a884db2 | ||
|
|
ada6b85868 | ||
|
|
2f8a4f447c | ||
|
|
9bd6499271 | ||
|
|
76fcc919ef | ||
|
|
a382297edf | ||
|
|
4987dfe4f6 | ||
|
|
d1a2e50b8d | ||
|
|
b9e44b6166 | ||
|
|
af601edc94 | ||
|
|
6640dcf9cd | ||
|
|
9b2734ed38 | ||
|
|
afcfce056f | ||
|
|
8f3af2f27b | ||
|
|
706aae47b3 | ||
|
|
d494295b21 | ||
|
|
c1600a253b | ||
|
|
a3bc29ad8f | ||
|
|
83225ed157 | ||
|
|
f13f6dc01f | ||
|
|
2d2489443a | ||
|
|
0e5b7fbfa2 | ||
|
|
d16a8068f7 | ||
|
|
c5ef11febc | ||
|
|
027f31447a | ||
|
|
41d1a4e5fb | ||
|
|
924e3191cb | ||
|
|
ebdc6b8fd9 | ||
|
|
5c05128cd7 | ||
|
|
5bf5685a12 | ||
|
|
f7391cb4c4 | ||
|
|
bad7bd8884 | ||
|
|
e0524a1656 | ||
|
|
f59af9baa3 | ||
|
|
b36890ca82 | ||
|
|
22703af08b | ||
|
|
c4108269b3 | ||
|
|
94c9d090cf | ||
|
|
28b9235a43 | ||
|
|
3aebae5e15 | ||
|
|
2631cb75d6 | ||
|
|
07b8b4156f | ||
|
|
7978967c1a | ||
|
|
3f24ff4de3 | ||
|
|
7253dd82a6 | ||
|
|
2b8eb3ae7e | ||
|
|
429eae71cd | ||
|
|
e5c552defb | ||
|
|
5c950a2e2c | ||
|
|
577ff78189 | ||
|
|
3f3cde05f7 | ||
|
|
b48f2c3276 | ||
|
|
8e818846f0 | ||
|
|
1554e37f8c | ||
|
|
affaabd011 | ||
|
|
f34a8f991c | ||
|
|
c2b14d610b | ||
|
|
02693d0cbb | ||
|
|
23155279ab | ||
|
|
2d23f7403d | ||
|
|
ae411c6fd5 | ||
|
|
ab8d6075a9 | ||
|
|
bc5ae29a67 | ||
|
|
1a8aabc30c | ||
|
|
8c51d7f713 | ||
|
|
8a1485e7ce | ||
|
|
614145431a | ||
|
|
db25f1999f | ||
|
|
4ed231b9bb | ||
|
|
25a5342c11 | ||
|
|
c7202e3ca9 | ||
|
|
89c2e94cea | ||
|
|
3dc46771b5 | ||
|
|
0eac4d4d7f | ||
|
|
a0ceb788db | ||
|
|
98fb36d03a | ||
|
|
a670006517 | ||
|
|
9cdbe67cd4 | ||
|
|
bbe8af452c | ||
|
|
f5edf28ce1 | ||
|
|
8fc30f590b | ||
|
|
e578f23ebe | ||
|
|
99c488fc9e | ||
|
|
f6a710660d | ||
|
|
a61744bb65 | ||
|
|
17c3078c24 | ||
|
|
5fa7731b56 | ||
|
|
c8e2be4d8c | ||
|
|
e3db613a07 | ||
|
|
0f7839027f | ||
|
|
31b322a108 | ||
|
|
b3c0494618 | ||
|
|
78ddb0533d | ||
|
|
da2158e7f2 | ||
|
|
d2a1efb6e7 | ||
|
|
98a880db2d | ||
|
|
cb82ef8703 | ||
|
|
d56246767b | ||
|
|
bb477984aa | ||
|
|
5a3e650e02 | ||
|
|
3c96dd2fac | ||
|
|
da051e3ff3 | ||
|
|
d15b6323c2 | ||
|
|
ec5f8fe4a4 | ||
|
|
71d84d76f8 | ||
|
|
d33ed52ec2 | ||
|
|
3a970544bb | ||
|
|
792ac3a2e8 | ||
|
|
60bbc27401 | ||
|
|
cf7cbcb6e6 | ||
|
|
c126a8eba9 | ||
|
|
66a60d0357 | ||
|
|
1acdadd027 | ||
|
|
200be9dadd | ||
|
|
a73e2872a4 | ||
|
|
ce6f7729c5 | ||
|
|
c285411371 | ||
|
|
46394c600e | ||
|
|
bea9cb3248 | ||
|
|
1087dcd714 | ||
|
|
0b63029b7e | ||
|
|
9679d24414 | ||
|
|
7947fd53e5 | ||
|
|
8dedf75565 | ||
|
|
b5b7c12b49 | ||
|
|
51f4e3cc3a | ||
|
|
24b0315d2e | ||
|
|
be446220eb | ||
|
|
a3ef2d332e | ||
|
|
ba6fe576e3 | ||
|
|
abcef38102 | ||
|
|
d5780b2f30 | ||
|
|
f7e498a0a2 | ||
|
|
51ac7ca2de | ||
|
|
c94535f6b5 | ||
|
|
07457ae368 | ||
|
|
4767fff08c | ||
|
|
f0c3498ecc | ||
|
|
1eca52d0fe | ||
|
|
7fbac9ad2f | ||
|
|
6fb80ed50b | ||
|
|
47f63ac81b | ||
|
|
42cc0b28ba | ||
|
|
993806f781 | ||
|
|
8a5af33aaa | ||
|
|
a974e36e9e | ||
|
|
17bcb2b39e | ||
|
|
cddf02d0c1 | ||
|
|
75ff7ece37 | ||
|
|
ec2b407a20 | ||
|
|
1dc9d78e54 | ||
|
|
5742a75c9d | ||
|
|
b5e9ad6d7e | ||
|
|
6393025219 | ||
|
|
9ab3e289bc | ||
|
|
6454474886 | ||
|
|
c5720a7a03 | ||
|
|
41b15adc6d | ||
|
|
05b962e718 | ||
|
|
1f01ca7b85 | ||
|
|
5d3b4fa5ec | ||
|
|
754d2b2dd3 | ||
|
|
ffad62e3dc | ||
|
|
1c11e16565 | ||
|
|
edc12990b4 | ||
|
|
12ea234d18 | ||
|
|
0461206a61 | ||
|
|
663f9e3962 | ||
|
|
34ee948c8e | ||
|
|
1bb9c2e4fe | ||
|
|
8ab18ce5cc | ||
|
|
71be16826e | ||
|
|
926c09d9df | ||
|
|
66c065ae7f | ||
|
|
083ed7775c | ||
|
|
5185452495 | ||
|
|
fa15f226ab | ||
|
|
fea4da2a33 | ||
|
|
055c933f4b | ||
|
|
9bcb867748 | ||
|
|
26e3c03f5f | ||
|
|
c195c3b2d1 | ||
|
|
f9e0aacfeb | ||
|
|
37fef66647 | ||
|
|
9f99b67563 | ||
|
|
fe902648ad | ||
|
|
13b933cd0b | ||
|
|
1d3b1d1d80 | ||
|
|
67a612af3a | ||
|
|
a891683806 | ||
|
|
440a72fc42 | ||
|
|
696d2e5197 | ||
|
|
2b17d56fc7 | ||
|
|
a410ef5d9f | ||
|
|
fe94769541 | ||
|
|
c63ae9c00c | ||
|
|
d5ece8d007 | ||
|
|
692a971dc0 | ||
|
|
05b8370cc0 | ||
|
|
b6111b35a2 | ||
|
|
4d72687628 | ||
|
|
8f125983ce | ||
|
|
4279825caa | ||
|
|
77ae3a4623 | ||
|
|
4c222dbc54 | ||
|
|
4e0f93ee8a | ||
|
|
e99f3e6627 | ||
|
|
f73877c34a | ||
|
|
abd3f12cae | ||
|
|
00117f5b7b | ||
|
|
d7d728f93e | ||
|
|
dc9217c4ec | ||
|
|
95acb13b93 | ||
|
|
234cc00d9f | ||
|
|
d6ba164799 | ||
|
|
910aa03dc8 | ||
|
|
a3e4a4c873 | ||
|
|
2d7c843447 | ||
|
|
e342b45473 | ||
|
|
f354bccd58 | ||
|
|
98073134db | ||
|
|
360666b00b | ||
|
|
ce4c807870 | ||
|
|
f2783bdac8 | ||
|
|
875ed16c3b | ||
|
|
383ba56d1f | ||
|
|
45eb54e624 | ||
|
|
5aff4e2ed6 | ||
|
|
e73e47dd94 | ||
|
|
1c8ac5efbc | ||
|
|
90fa5e1ecd | ||
|
|
348994917b | ||
|
|
60dbea1027 | ||
|
|
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 | ||
|
|
a8f712c335 | ||
|
|
ea8888a685 | ||
|
|
2aecf69b67 | ||
|
|
1a4c24dd86 | ||
|
|
6cf2b45051 | ||
|
|
38dd2bdf6e | ||
|
|
8784f1da70 | ||
|
|
b9208ea94e |
68
CHANGELOG
68
CHANGELOG
@@ -1,3 +1,71 @@
|
|||||||
|
KeePassDX(2.9)
|
||||||
|
* Upgrade to Android API 30 #723
|
||||||
|
* Save new credentials with Autofill #524
|
||||||
|
* Setting to close database after Autofill selection #755
|
||||||
|
* Setting to switch keyboard when database is locked #625
|
||||||
|
* Fix biometric issues #724 #740 #731
|
||||||
|
* Fix autofill #725 #551
|
||||||
|
* Fix subdomain search #728
|
||||||
|
* Fix backup search #759
|
||||||
|
* Small fixes and translations #732 #736 #737 #738 #742 #767
|
||||||
|
|
||||||
|
KeePassDX(2.8.7)
|
||||||
|
* Downgrade to Android API 29 (crash on startup with API 30 on some devices)
|
||||||
|
Sorry for the inconvenience
|
||||||
|
|
||||||
|
KeePassDX(2.8.6)
|
||||||
|
* Fix Autofill recognition #712
|
||||||
|
* Keep value after renaming custom field #709
|
||||||
|
* Prevent random binary bug #713
|
||||||
|
* Fix dialog background #717
|
||||||
|
* Better domain recognition for autofill #702
|
||||||
|
* Write custom data #651
|
||||||
|
* Fix autolink #720
|
||||||
|
|
||||||
|
KeePassDX(2.8.5)
|
||||||
|
* Fix Base 64 #708
|
||||||
|
|
||||||
|
KeePassDX(2.8.4)
|
||||||
|
* Fix incomplete attachment deletion #684
|
||||||
|
* Fix opening database v1 without backup folder #692
|
||||||
|
* Fix ANR during first entry education #685
|
||||||
|
* Entry edition as fragment and manual views to fix focus #686
|
||||||
|
* Fix opening database with corrupted attachment #691
|
||||||
|
* Manage empty keyfile #679
|
||||||
|
|
||||||
|
KeePassDX(2.8.3)
|
||||||
|
* Upload attachments
|
||||||
|
* Visibility button for each hidden field
|
||||||
|
* Fix read header file
|
||||||
|
* Fix deletion in KDB database
|
||||||
|
* Fix minor issues
|
||||||
|
|
||||||
|
KeePassDX(2.8.2)
|
||||||
|
* Fix themes / new UI
|
||||||
|
* Fix multiples notifications
|
||||||
|
* Fix entry in Magikeyboard memory
|
||||||
|
* Fix biometric view visibility
|
||||||
|
* Fix fields order
|
||||||
|
* Upgrade code with ViewModel and LiveData
|
||||||
|
|
||||||
|
KeePassDX(2.8.1)
|
||||||
|
* Capture exceptions in coroutines
|
||||||
|
|
||||||
|
KeePassDX(2.8)
|
||||||
|
* Fix TOTP period (> 60s)
|
||||||
|
* Fix searching in recycle bin
|
||||||
|
* Settings to back to the previous keyboard during database credentials and after form filling
|
||||||
|
* Improve action tasks
|
||||||
|
* Improve recognition to reset app timeout
|
||||||
|
* Fix minor issues
|
||||||
|
|
||||||
|
KeePassDX(2.7)
|
||||||
|
* Add blocklists for autofill
|
||||||
|
* Add autofill compatibility mode (usefull for Browser not compatible)
|
||||||
|
* Upgrade autofill recognition algorithm
|
||||||
|
* Setting to search through web subdomains
|
||||||
|
* Refactoring selection mode
|
||||||
|
|
||||||
KeePassDX(2.6)
|
KeePassDX(2.6)
|
||||||
* Share a web domain to automatically search for an entry
|
* Share a web domain to automatically search for an entry
|
||||||
* Default group icon for a new entry
|
* Default group icon for a new entry
|
||||||
|
|||||||
373
LICENSES/LICENSE_MOZILLA_COMPONENTS
Normal file
373
LICENSES/LICENSE_MOZILLA_COMPONENTS
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
||||||
@@ -4,15 +4,15 @@ apply plugin: 'kotlin-android-extensions'
|
|||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 29
|
compileSdkVersion 30
|
||||||
buildToolsVersion '29.0.3'
|
buildToolsVersion '30.0.2'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 14
|
minSdkVersion 14
|
||||||
targetSdkVersion 29
|
targetSdkVersion 30
|
||||||
versionCode = 34
|
versionCode = 44
|
||||||
versionName = "2.6"
|
versionName = "2.9"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
@@ -50,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 {
|
||||||
@@ -69,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" ]
|
||||||
}
|
}
|
||||||
@@ -95,14 +95,16 @@ def room_version = "2.2.5"
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$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.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.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.1.0-beta01'
|
||||||
implementation 'androidx.core:core-ktx:1.2.0'
|
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||||
// To upgrade with style
|
implementation "androidx.core:core-ktx:1.3.2"
|
||||||
|
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||||
|
// WARNING: To upgrade with style, bug in edit text
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
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"
|
||||||
@@ -112,7 +114,7 @@ dependencies {
|
|||||||
// Time
|
// Time
|
||||||
implementation 'joda-time:joda-time:2.10.6'
|
implementation 'joda-time:joda-time:2.10.6'
|
||||||
// Color
|
// Color
|
||||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3'
|
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
|
||||||
// Education
|
// Education
|
||||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
||||||
// Apache Commons Collections
|
// Apache Commons Collections
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
app/src/free/res/drawable/ic_launcher_foreground.png
Normal file
BIN
app/src/free/res/drawable/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
BIN
app/src/libre/res/drawable/ic_launcher_foreground.png
Normal file
BIN
app/src/libre/res/drawable/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
@@ -16,6 +16,8 @@
|
|||||||
android:name="android.permission.VIBRATE"/>
|
android:name="android.permission.VIBRATE"/>
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.QUERY_ALL_PACKAGES"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
@@ -47,7 +49,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
|
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize|stateUnchanged">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
@@ -124,8 +126,7 @@
|
|||||||
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"
|
||||||
@@ -161,10 +162,6 @@
|
|||||||
</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"
|
||||||
|
|||||||
BIN
app/src/main/assets/publicsuffixes
Normal file
BIN
app/src/main/assets/publicsuffixes
Normal file
Binary file not shown.
@@ -26,25 +26,79 @@ 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.R
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
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.database.search.SearchHelper
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
@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?) {
|
||||||
|
|
||||||
|
// Retrieve selection mode
|
||||||
|
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
||||||
|
when (specialMode) {
|
||||||
|
SpecialMode.SELECTION -> {
|
||||||
|
// Build search param
|
||||||
|
val searchInfo = SearchInfo().apply {
|
||||||
|
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
||||||
|
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
||||||
|
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
|
||||||
|
}
|
||||||
|
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
|
searchInfo.webDomain = concreteWebDomain
|
||||||
|
launchSelection(searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpecialMode.REGISTRATION -> {
|
||||||
|
// To register info
|
||||||
|
val registerInfo = intent.getParcelableExtra<RegisterInfo>(KEY_REGISTER_INFO)
|
||||||
|
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
||||||
|
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
|
searchInfo.webDomain = concreteWebDomain
|
||||||
|
launchRegistration(searchInfo, registerInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Not an autofill call
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchSelection(searchInfo: SearchInfo) {
|
||||||
// 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.autofillAllowedFor(searchInfo.applicationId,
|
||||||
}
|
PreferencesUtil.applicationIdBlocklist(this))
|
||||||
|
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
||||||
|
PreferencesUtil.webDomainBlocklist(this))) {
|
||||||
|
showBlockRestartMessage()
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
val database = Database.getInstance()
|
||||||
|
val readOnly = database.isReadOnly
|
||||||
// If database is open
|
// If database is open
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
Database.getInstance(),
|
Database.getInstance(),
|
||||||
@@ -57,24 +111,79 @@ 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)
|
readOnly,
|
||||||
|
assistStructure,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// 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)
|
private fun launchRegistration(searchInfo: SearchInfo, registerInfo: RegisterInfo?) {
|
||||||
|
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
||||||
|
PreferencesUtil.applicationIdBlocklist(this))
|
||||||
|
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
||||||
|
PreferencesUtil.webDomainBlocklist(this))) {
|
||||||
|
showBlockRestartMessage()
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
} else {
|
||||||
|
val database = Database.getInstance()
|
||||||
|
val readOnly = database.isReadOnly
|
||||||
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
database,
|
||||||
|
searchInfo,
|
||||||
|
{ _ ->
|
||||||
|
if (!readOnly) {
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
GroupActivity.launchForRegistration(this,
|
||||||
|
registerInfo)
|
||||||
|
} else {
|
||||||
|
showReadOnlySaveMessage()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
if (!readOnly) {
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
GroupActivity.launchForRegistration(this,
|
||||||
|
registerInfo)
|
||||||
|
} else {
|
||||||
|
showReadOnlySaveMessage()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If database not open
|
||||||
|
FileDatabaseSelectActivity.launchForRegistration(this,
|
||||||
|
registerInfo)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showBlockRestartMessage() {
|
||||||
|
// If item not allowed, show a toast
|
||||||
|
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showReadOnlySaveMessage() {
|
||||||
|
Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
|
|
||||||
|
if (PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
|
||||||
|
// Close the database
|
||||||
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
|
}
|
||||||
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,18 +191,41 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
||||||
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
||||||
|
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
|
||||||
|
|
||||||
fun getAuthIntentSenderForResponse(context: Context,
|
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
||||||
searchInfo: SearchInfo? = null): IntentSender {
|
|
||||||
|
fun getAuthIntentSenderForSelection(context: Context,
|
||||||
|
searchInfo: SearchInfo? = null): IntentSender {
|
||||||
return PendingIntent.getActivity(context, 0,
|
return PendingIntent.getActivity(context, 0,
|
||||||
// Doesn't work with Parcelable (don't know why?)
|
// Doesn't work with Parcelable (don't know why?)
|
||||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
searchInfo?.let {
|
searchInfo?.let {
|
||||||
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
|
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
|
||||||
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
||||||
|
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAuthIntentSenderForRegistration(context: Context,
|
||||||
|
registerInfo: RegisterInfo): IntentSender {
|
||||||
|
return PendingIntent.getActivity(context, 0,
|
||||||
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
|
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
||||||
|
putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||||
|
},
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchForRegistration(context: Context,
|
||||||
|
registerInfo: RegisterInfo) {
|
||||||
|
val intent = Intent(context, AutofillLauncherActivity::class.java)
|
||||||
|
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
||||||
|
intent.putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import android.graphics.drawable.ColorDrawable
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
@@ -39,14 +40,15 @@ import com.google.android.material.appbar.CollapsingToolbarLayout
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
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.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.NodeId
|
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.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.EntryAttachment
|
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 +88,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 +132,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 +201,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 +214,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 +242,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 +275,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 +307,22 @@ 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 (mDatabase?.allowEntryCustomFields() == true) {
|
||||||
entryContentsView?.clearExtraFields()
|
entryContentsView?.clearExtraFields()
|
||||||
|
entry.getExtraFields().forEach { field ->
|
||||||
for (element in entry.customFields.entries) {
|
val label = field.name
|
||||||
val label = element.key
|
val value = field.protectedValue
|
||||||
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 +333,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 +362,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 +393,6 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeShowPasswordIcon(togglePassword: MenuItem?) {
|
|
||||||
if (mShowPassword) {
|
|
||||||
togglePassword?.setTitle(R.string.menu_hide_password)
|
|
||||||
togglePassword?.setIcon(R.drawable.ic_visibility_off_white_24dp)
|
|
||||||
} else {
|
|
||||||
togglePassword?.setTitle(R.string.menu_showpass)
|
|
||||||
togglePassword?.setIcon(R.drawable.ic_visibility_white_24dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
@@ -433,15 +408,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
|
||||||
@@ -457,35 +423,38 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show education views
|
// Show education views
|
||||||
Handler().post { performedNextEducation(EntryActivityEducation(this), menu) }
|
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||||
menu: Menu) {
|
menu: Menu) {
|
||||||
val entryCopyEducationPerformed = entryContentsView?.isUserNamePresent == true
|
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
|
||||||
|
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 +464,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 +483,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 +491,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 +523,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"
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,546 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
|
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
|
import com.kunzisoft.keepass.model.*
|
||||||
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||||
|
import com.kunzisoft.keepass.view.collapse
|
||||||
|
import com.kunzisoft.keepass.view.expand
|
||||||
|
|
||||||
|
class EntryEditFragment: StylishFragment() {
|
||||||
|
|
||||||
|
private lateinit var entryTitleLayoutView: TextInputLayout
|
||||||
|
private lateinit var entryTitleView: EditText
|
||||||
|
private lateinit var entryIconView: ImageView
|
||||||
|
private lateinit var entryUserNameView: EditText
|
||||||
|
private lateinit var entryUrlView: EditText
|
||||||
|
private lateinit var entryPasswordLayoutView: TextInputLayout
|
||||||
|
private lateinit var entryPasswordView: EditText
|
||||||
|
private lateinit var entryPasswordGeneratorView: View
|
||||||
|
private lateinit var entryExpiresCheckBox: CompoundButton
|
||||||
|
private lateinit var entryExpiresTextView: TextView
|
||||||
|
private lateinit var entryNotesView: EditText
|
||||||
|
private lateinit var extraFieldsContainerView: View
|
||||||
|
private lateinit var extraFieldsListView: ViewGroup
|
||||||
|
private lateinit var attachmentsContainerView: View
|
||||||
|
private lateinit var attachmentsListView: RecyclerView
|
||||||
|
|
||||||
|
private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter
|
||||||
|
|
||||||
|
private var fontInVisibility: Boolean = false
|
||||||
|
private var iconColor: Int = 0
|
||||||
|
private var expiresInstant: DateInstant = DateInstant.IN_ONE_MONTH
|
||||||
|
|
||||||
|
var drawFactory: IconDrawableFactory? = null
|
||||||
|
var setOnDateClickListener: View.OnClickListener? = null
|
||||||
|
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
|
||||||
|
var setOnIconViewClickListener: View.OnClickListener? = null
|
||||||
|
var setOnEditCustomField: ((Field) -> Unit)? = null
|
||||||
|
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
|
||||||
|
|
||||||
|
// Elements to modify the current entry
|
||||||
|
private var mEntryInfo = EntryInfo()
|
||||||
|
private var mLastFocusedEditField: FocusedEditField? = null
|
||||||
|
private var mExtraViewToRequestFocus: EditText? = null
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
|
val rootView = inflater.cloneInContext(contextThemed)
|
||||||
|
.inflate(R.layout.fragment_entry_edit_contents, container, false)
|
||||||
|
|
||||||
|
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
|
||||||
|
|
||||||
|
entryTitleLayoutView = rootView.findViewById(R.id.entry_edit_container_title)
|
||||||
|
entryTitleView = rootView.findViewById(R.id.entry_edit_title)
|
||||||
|
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
|
||||||
|
entryIconView.setOnClickListener {
|
||||||
|
setOnIconViewClickListener?.onClick(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
|
||||||
|
entryUrlView = rootView.findViewById(R.id.entry_edit_url)
|
||||||
|
entryPasswordLayoutView = rootView.findViewById(R.id.entry_edit_container_password)
|
||||||
|
entryPasswordView = rootView.findViewById(R.id.entry_edit_password)
|
||||||
|
entryPasswordGeneratorView = rootView.findViewById(R.id.entry_edit_password_generator_button)
|
||||||
|
entryPasswordGeneratorView.setOnClickListener {
|
||||||
|
setOnPasswordGeneratorClickListener?.onClick(it)
|
||||||
|
}
|
||||||
|
entryExpiresCheckBox = rootView.findViewById(R.id.entry_edit_expires_checkbox)
|
||||||
|
entryExpiresTextView = rootView.findViewById(R.id.entry_edit_expires_text)
|
||||||
|
entryExpiresTextView.setOnClickListener {
|
||||||
|
if (entryExpiresCheckBox.isChecked)
|
||||||
|
setOnDateClickListener?.onClick(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
|
||||||
|
|
||||||
|
extraFieldsContainerView = rootView.findViewById(R.id.extra_fields_container)
|
||||||
|
extraFieldsListView = rootView.findViewById(R.id.extra_fields_list)
|
||||||
|
|
||||||
|
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
|
||||||
|
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
|
||||||
|
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
|
||||||
|
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
|
||||||
|
if (previousSize > 0 && newSize == 0) {
|
||||||
|
attachmentsContainerView.collapse(true)
|
||||||
|
} else if (previousSize == 0 && newSize == 1) {
|
||||||
|
attachmentsContainerView.expand(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attachmentsListView.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||||
|
adapter = attachmentsAdapter
|
||||||
|
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
}
|
||||||
|
|
||||||
|
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
|
||||||
|
assignExpiresDateText()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the textColor to tint the icon
|
||||||
|
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
|
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
|
||||||
|
taIconColor?.recycle()
|
||||||
|
|
||||||
|
// Retrieve the new entry after an orientation change
|
||||||
|
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
|
||||||
|
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
||||||
|
else if (savedInstanceState?.containsKey(KEY_TEMP_ENTRY_INFO) == true) {
|
||||||
|
mEntryInfo = savedInstanceState.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) {
|
||||||
|
mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD) ?: mLastFocusedEditField
|
||||||
|
}
|
||||||
|
|
||||||
|
populateViewsWithEntry()
|
||||||
|
|
||||||
|
return rootView
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
super.onDetach()
|
||||||
|
|
||||||
|
drawFactory = null
|
||||||
|
setOnDateClickListener = null
|
||||||
|
setOnPasswordGeneratorClickListener = null
|
||||||
|
setOnIconViewClickListener = null
|
||||||
|
setOnRemoveAttachment = null
|
||||||
|
setOnEditCustomField = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryInfo(): EntryInfo? {
|
||||||
|
populateEntryWithViews()
|
||||||
|
return mEntryInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generatePasswordEducationPerformed(entryEditActivityEducation: EntryEditActivityEducation): Boolean {
|
||||||
|
return entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||||
|
entryPasswordGeneratorView,
|
||||||
|
{
|
||||||
|
GeneratePasswordDialogFragment().show(parentFragmentManager, "PasswordGeneratorFragment")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
(activity as? EntryEditActivity?)?.performedNextEducation(entryEditActivityEducation)
|
||||||
|
} catch (ignore: Exception) {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateViewsWithEntry() {
|
||||||
|
// Set info in view
|
||||||
|
icon = mEntryInfo.icon
|
||||||
|
title = mEntryInfo.title
|
||||||
|
username = mEntryInfo.username
|
||||||
|
url = mEntryInfo.url
|
||||||
|
password = mEntryInfo.password
|
||||||
|
expires = mEntryInfo.expires
|
||||||
|
expiryTime = mEntryInfo.expiryTime
|
||||||
|
notes = mEntryInfo.notes
|
||||||
|
assignExtraFields(mEntryInfo.customFields) { fields ->
|
||||||
|
setOnEditCustomField?.invoke(fields)
|
||||||
|
}
|
||||||
|
assignAttachments(mEntryInfo.attachments, StreamDirection.UPLOAD) { attachment ->
|
||||||
|
setOnRemoveAttachment?.invoke(attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateEntryWithViews() {
|
||||||
|
// Icon already populate
|
||||||
|
mEntryInfo.title = title
|
||||||
|
mEntryInfo.username = username
|
||||||
|
mEntryInfo.url = url
|
||||||
|
mEntryInfo.password = password
|
||||||
|
mEntryInfo.expires = expires
|
||||||
|
mEntryInfo.expiryTime = expiryTime
|
||||||
|
mEntryInfo.notes = notes
|
||||||
|
mEntryInfo.customFields = getExtraFields()
|
||||||
|
mEntryInfo.otpModel = OtpEntryFields.parseFields { key ->
|
||||||
|
getExtraFields().firstOrNull { it.name == key }?.protectedValue?.toString()
|
||||||
|
}?.otpModel
|
||||||
|
mEntryInfo.attachments = getAttachments()
|
||||||
|
}
|
||||||
|
|
||||||
|
var title: String
|
||||||
|
get() {
|
||||||
|
return entryTitleView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryTitleView.setText(value)
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryTitleView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
var icon: IconImage
|
||||||
|
get() {
|
||||||
|
return mEntryInfo.icon
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
mEntryInfo.icon = value
|
||||||
|
drawFactory?.let { drawFactory ->
|
||||||
|
entryIconView.assignDatabaseIcon(drawFactory, value, iconColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var username: String
|
||||||
|
get() {
|
||||||
|
return entryUserNameView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryUserNameView.setText(value)
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryUserNameView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
var url: String
|
||||||
|
get() {
|
||||||
|
return entryUrlView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryUrlView.setText(value)
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryUrlView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
var password: String
|
||||||
|
get() {
|
||||||
|
return entryPasswordView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryPasswordView.setText(value)
|
||||||
|
if (fontInVisibility) {
|
||||||
|
entryPasswordView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignExpiresDateText() {
|
||||||
|
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
|
||||||
|
entryExpiresTextView.setOnClickListener(setOnDateClickListener)
|
||||||
|
expiresInstant.getDateTimeString(resources)
|
||||||
|
} else {
|
||||||
|
entryExpiresTextView.setOnClickListener(null)
|
||||||
|
resources.getString(R.string.never)
|
||||||
|
}
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryExpiresTextView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
var expires: Boolean
|
||||||
|
get() {
|
||||||
|
return entryExpiresCheckBox.isChecked
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
if (!value) {
|
||||||
|
expiresInstant = DateInstant.IN_ONE_MONTH
|
||||||
|
}
|
||||||
|
entryExpiresCheckBox.isChecked = value
|
||||||
|
assignExpiresDateText()
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiryTime: DateInstant
|
||||||
|
get() {
|
||||||
|
return if (expires)
|
||||||
|
expiresInstant
|
||||||
|
else
|
||||||
|
DateInstant.NEVER_EXPIRE
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
if (expires)
|
||||||
|
expiresInstant = value
|
||||||
|
assignExpiresDateText()
|
||||||
|
}
|
||||||
|
|
||||||
|
var notes: String
|
||||||
|
get() {
|
||||||
|
return entryNotesView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryNotesView.setText(value)
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryNotesView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Extra Fields
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
private var mExtraFieldsList: MutableList<Field> = ArrayList()
|
||||||
|
private var mOnEditButtonClickListener: ((item: Field)->Unit)? = null
|
||||||
|
|
||||||
|
private fun buildViewFromField(extraField: Field): View? {
|
||||||
|
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
|
val itemView: View? = inflater?.inflate(R.layout.item_entry_edit_extra_field, extraFieldsListView, false)
|
||||||
|
itemView?.id = View.NO_ID
|
||||||
|
|
||||||
|
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
|
||||||
|
extraFieldValueContainer?.isPasswordVisibilityToggleEnabled = extraField.protectedValue.isProtected
|
||||||
|
extraFieldValueContainer?.hint = extraField.name
|
||||||
|
extraFieldValueContainer?.id = View.NO_ID
|
||||||
|
|
||||||
|
val extraFieldValue: TextInputEditText? = itemView?.findViewById(R.id.entry_extra_field_value)
|
||||||
|
extraFieldValue?.apply {
|
||||||
|
if (extraField.protectedValue.isProtected) {
|
||||||
|
inputType = extraFieldValue.inputType or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
|
||||||
|
}
|
||||||
|
setText(extraField.protectedValue.toString())
|
||||||
|
if (fontInVisibility)
|
||||||
|
applyFontVisibility()
|
||||||
|
}
|
||||||
|
extraFieldValue?.id = View.NO_ID
|
||||||
|
extraFieldValue?.tag = "FIELD_VALUE_TAG"
|
||||||
|
if (mLastFocusedEditField?.field == extraField) {
|
||||||
|
mExtraViewToRequestFocus = extraFieldValue
|
||||||
|
}
|
||||||
|
|
||||||
|
val extraFieldEditButton: View? = itemView?.findViewById(R.id.entry_extra_field_edit)
|
||||||
|
extraFieldEditButton?.setOnClickListener {
|
||||||
|
mOnEditButtonClickListener?.invoke(extraField)
|
||||||
|
}
|
||||||
|
extraFieldEditButton?.id = View.NO_ID
|
||||||
|
|
||||||
|
return itemView
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getExtraFields(): List<Field> {
|
||||||
|
mLastFocusedEditField = null
|
||||||
|
for (index in 0 until extraFieldsListView.childCount) {
|
||||||
|
val extraFieldValue: EditText = extraFieldsListView.getChildAt(index)
|
||||||
|
.findViewWithTag("FIELD_VALUE_TAG")
|
||||||
|
val extraField = mExtraFieldsList[index]
|
||||||
|
extraField.protectedValue.stringValue = extraFieldValue.text?.toString() ?: ""
|
||||||
|
if (extraFieldValue.isFocused) {
|
||||||
|
mLastFocusedEditField = FocusedEditField().apply {
|
||||||
|
field = extraField
|
||||||
|
cursorSelectionStart = extraFieldValue.selectionStart
|
||||||
|
cursorSelectionEnd = extraFieldValue.selectionEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mExtraFieldsList
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all children and add new views for each field
|
||||||
|
*/
|
||||||
|
fun assignExtraFields(fields: List<Field>,
|
||||||
|
onEditButtonClickListener: ((item: Field)->Unit)?) {
|
||||||
|
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
// Reinit focused field
|
||||||
|
mExtraFieldsList.clear()
|
||||||
|
mExtraFieldsList.addAll(fields)
|
||||||
|
extraFieldsListView.removeAllViews()
|
||||||
|
fields.forEach {
|
||||||
|
extraFieldsListView.addView(buildViewFromField(it))
|
||||||
|
}
|
||||||
|
// Request last focus
|
||||||
|
mLastFocusedEditField?.let { focusField ->
|
||||||
|
mExtraViewToRequestFocus?.apply {
|
||||||
|
requestFocus()
|
||||||
|
setSelection(focusField.cursorSelectionStart,
|
||||||
|
focusField.cursorSelectionEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mLastFocusedEditField = null
|
||||||
|
mOnEditButtonClickListener = onEditButtonClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an extra field or create a new one if doesn't exists, the old value is lost
|
||||||
|
*/
|
||||||
|
fun putExtraField(extraField: Field) {
|
||||||
|
extraFieldsContainerView.visibility = View.VISIBLE
|
||||||
|
val oldField = mExtraFieldsList.firstOrNull { it.name == extraField.name }
|
||||||
|
oldField?.let {
|
||||||
|
val index = mExtraFieldsList.indexOf(oldField)
|
||||||
|
mExtraFieldsList.removeAt(index)
|
||||||
|
mExtraFieldsList.add(index, extraField)
|
||||||
|
extraFieldsListView.removeViewAt(index)
|
||||||
|
val newView = buildViewFromField(extraField)
|
||||||
|
extraFieldsListView.addView(newView, index)
|
||||||
|
newView?.requestFocus()
|
||||||
|
} ?: kotlin.run {
|
||||||
|
mExtraFieldsList.add(extraField)
|
||||||
|
val newView = buildViewFromField(extraField)
|
||||||
|
extraFieldsListView.addView(newView)
|
||||||
|
newView?.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an extra field and keep the old value
|
||||||
|
*/
|
||||||
|
fun replaceExtraField(oldExtraField: Field, newExtraField: Field) {
|
||||||
|
extraFieldsContainerView.visibility = View.VISIBLE
|
||||||
|
val index = mExtraFieldsList.indexOf(oldExtraField)
|
||||||
|
val oldValueEditText: EditText = extraFieldsListView.getChildAt(index)
|
||||||
|
.findViewWithTag("FIELD_VALUE_TAG")
|
||||||
|
val oldValue = oldValueEditText.text.toString()
|
||||||
|
val newExtraFieldWithOldValue = Field(newExtraField).apply {
|
||||||
|
this.protectedValue.stringValue = oldValue
|
||||||
|
}
|
||||||
|
mExtraFieldsList.removeAt(index)
|
||||||
|
mExtraFieldsList.add(index, newExtraFieldWithOldValue)
|
||||||
|
extraFieldsListView.removeViewAt(index)
|
||||||
|
val newView = buildViewFromField(newExtraFieldWithOldValue)
|
||||||
|
extraFieldsListView.addView(newView, index)
|
||||||
|
newView?.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeExtraField(oldExtraField: Field) {
|
||||||
|
val previousSize = mExtraFieldsList.size
|
||||||
|
val index = mExtraFieldsList.indexOf(oldExtraField)
|
||||||
|
extraFieldsListView.getChildAt(index)?.let {
|
||||||
|
it.collapse(true) {
|
||||||
|
mExtraFieldsList.removeAt(index)
|
||||||
|
extraFieldsListView.removeViewAt(index)
|
||||||
|
val newSize = mExtraFieldsList.size
|
||||||
|
|
||||||
|
if (previousSize > 0 && newSize == 0) {
|
||||||
|
extraFieldsContainerView.collapse(true)
|
||||||
|
} else if (previousSize == 0 && newSize == 1) {
|
||||||
|
extraFieldsContainerView.expand(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Attachments
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun getAttachments(): List<Attachment> {
|
||||||
|
return attachmentsAdapter.itemsList.map { it.attachment }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assignAttachments(attachments: List<Attachment>,
|
||||||
|
streamDirection: StreamDirection,
|
||||||
|
onDeleteItem: (attachment: Attachment)->Unit) {
|
||||||
|
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
|
||||||
|
attachmentsAdapter.onDeleteButtonClickListener = { item ->
|
||||||
|
onDeleteItem.invoke(item.attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun containsAttachment(): Boolean {
|
||||||
|
return !attachmentsAdapter.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun containsAttachment(attachment: EntryAttachmentState): Boolean {
|
||||||
|
return attachmentsAdapter.contains(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putAttachment(attachment: EntryAttachmentState) {
|
||||||
|
attachmentsContainerView.visibility = View.VISIBLE
|
||||||
|
attachmentsAdapter.putItem(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAttachment(attachment: EntryAttachmentState) {
|
||||||
|
attachmentsAdapter.removeItem(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearAttachments() {
|
||||||
|
attachmentsAdapter.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
|
||||||
|
attachmentsListView.postDelayed({
|
||||||
|
position.invoke(attachmentsContainerView.y
|
||||||
|
+ attachmentsListView.y
|
||||||
|
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
|
||||||
|
?: 0F)
|
||||||
|
)
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
populateEntryWithViews()
|
||||||
|
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
|
||||||
|
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
|
||||||
|
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
|
||||||
|
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
|
||||||
|
|
||||||
|
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
|
||||||
|
return EntryEditFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -23,9 +23,7 @@ import android.app.Activity
|
|||||||
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 android.widget.Toast
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
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.database.search.SearchHelper
|
||||||
@@ -33,6 +31,7 @@ import com.kunzisoft.keepass.magikeyboard.MagikIME
|
|||||||
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.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity to search or select entry in database,
|
* Activity to search or select entry in database,
|
||||||
@@ -49,9 +48,8 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
|
|||||||
if ("text/plain" == intent.type) {
|
if ("text/plain" == intent.type) {
|
||||||
// Retrieve web domain
|
// Retrieve web domain
|
||||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||||
sharedWebDomain = Uri.parse(it).authority
|
sharedWebDomain = Uri.parse(it).host
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
@@ -61,13 +59,23 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
|
|||||||
val searchInfo = SearchInfo().apply {
|
val searchInfo = SearchInfo().apply {
|
||||||
webDomain = sharedWebDomain
|
webDomain = sharedWebDomain
|
||||||
}
|
}
|
||||||
|
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
|
searchInfo.webDomain = concreteWebDomain
|
||||||
|
launch(searchInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launch(searchInfo: SearchInfo) {
|
||||||
// Setting to integrate Magikeyboard
|
// Setting to integrate Magikeyboard
|
||||||
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
||||||
|
|
||||||
// If database is open
|
// If database is open
|
||||||
|
val database = Database.getInstance()
|
||||||
|
val readOnly = database.isReadOnly
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
Database.getInstance(),
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ items ->
|
{ items ->
|
||||||
// Items found
|
// Items found
|
||||||
@@ -75,36 +83,48 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
|
|||||||
if (items.size == 1) {
|
if (items.size == 1) {
|
||||||
// Automatically populate keyboard
|
// Automatically populate keyboard
|
||||||
val entryPopulate = items[0]
|
val entryPopulate = items[0]
|
||||||
populateKeyboardAndMoveAppToBackground(this, entryPopulate, intent)
|
populateKeyboardAndMoveAppToBackground(this,
|
||||||
|
entryPopulate,
|
||||||
|
intent)
|
||||||
} else {
|
} else {
|
||||||
// Select the one we want
|
// Select the one we want
|
||||||
GroupActivity.launchForEntrySelectionResult(this, searchInfo)
|
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||||
|
readOnly,
|
||||||
|
searchInfo,
|
||||||
|
true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
GroupActivity.launch(this, searchInfo)
|
GroupActivity.launchForSearchResult(this,
|
||||||
|
readOnly,
|
||||||
|
searchInfo,
|
||||||
|
true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
if (searchShareForMagikeyboard) {
|
if (readOnly || searchShareForMagikeyboard) {
|
||||||
GroupActivity.launchForEntrySelectionResult(this)
|
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||||
|
readOnly,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
} else {
|
} else {
|
||||||
GroupActivity.launch(this)
|
GroupActivity.launchForSaveResult(this,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// If database not open
|
// If database not open
|
||||||
if (searchShareForMagikeyboard) {
|
if (searchShareForMagikeyboard) {
|
||||||
FileDatabaseSelectActivity.launchForEntrySelectionResult(this, searchInfo)
|
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
|
||||||
|
searchInfo)
|
||||||
} else {
|
} else {
|
||||||
FileDatabaseSelectActivity.launch(this, searchInfo)
|
FileDatabaseSelectActivity.launchForSearchResult(this,
|
||||||
|
searchInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
finish()
|
finish()
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +135,6 @@ fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
|||||||
// Populate Magikeyboard with entry
|
// Populate Magikeyboard with entry
|
||||||
MagikIME.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
MagikIME.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
||||||
// Consume the selection mode
|
// Consume the selection mode
|
||||||
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
activity.moveTaskToBack(true)
|
activity.moveTaskToBack(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
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.Context
|
||||||
@@ -28,13 +27,16 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.util.Log
|
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
|
||||||
@@ -42,32 +44,38 @@ 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.EntrySelectionHelper.KEY_SEARCH_INFO
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
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.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.education.FileDatabaseSelectActivityEducation
|
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
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.ACTION_DATABASE_LOAD_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
|
||||||
|
|
||||||
@@ -75,9 +83,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)
|
||||||
@@ -91,28 +99,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)
|
||||||
}
|
}
|
||||||
@@ -125,26 +120,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
|
||||||
|
|
||||||
@@ -157,7 +151,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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,12 +161,66 @@ 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,
|
||||||
|
PreferencesUtil.enableReadOnlyDatabase(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
|
val database = Database.getInstance()
|
||||||
|
if (result.isSuccess
|
||||||
|
&& database.loaded) {
|
||||||
|
launchGroupActivity(database)
|
||||||
|
} else {
|
||||||
|
var resultError = ""
|
||||||
|
val resultMessage = result.message
|
||||||
|
// Show error message
|
||||||
|
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||||
|
resultError = "$resultError $resultMessage"
|
||||||
|
}
|
||||||
|
Log.e(TAG, resultError)
|
||||||
|
Snackbar.make(activity_file_selection_coordinator_layout,
|
||||||
|
resultError,
|
||||||
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,7 +230,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
/**
|
/**
|
||||||
* Create a new file by calling the content provider
|
* Create a new file by calling the content provider
|
||||||
*/
|
*/
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
private fun createNewFile() {
|
private fun createNewFile() {
|
||||||
createDocument(this, getString(R.string.database_file_name_default) +
|
createDocument(this, getString(R.string.database_file_name_default) +
|
||||||
getString(R.string.database_file_extension_default), "application/x-keepass")
|
getString(R.string.database_file_extension_default), "application/x-keepass")
|
||||||
@@ -197,68 +244,32 @@ 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)
|
PasswordActivity.launch(this,
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
databaseUri,
|
||||||
{
|
keyFile,
|
||||||
try {
|
{ exception ->
|
||||||
PasswordActivity.launch(this@FileDatabaseSelectActivity,
|
fileNoFoundAction(exception)
|
||||||
databaseUri, keyFile,
|
|
||||||
searchInfo)
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
fileNoFoundAction(e)
|
|
||||||
}
|
|
||||||
// Remove the search info from intent
|
|
||||||
if (searchInfo != null) {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{ onCancelSpecialMode() },
|
||||||
try {
|
{ onLaunchActivitySpecialMode() })
|
||||||
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
|
|
||||||
databaseUri, keyFile,
|
|
||||||
searchInfo)
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
fileNoFoundAction(e)
|
|
||||||
}
|
|
||||||
finish()
|
|
||||||
},
|
|
||||||
{ assistStructure ->
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
try {
|
|
||||||
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
|
||||||
databaseUri, keyFile,
|
|
||||||
assistStructure,
|
|
||||||
searchInfo)
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
fileNoFoundAction(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivity(readOnly: Boolean) {
|
private fun launchGroupActivity(database: Database) {
|
||||||
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
GroupActivity.launch(this,
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
database.isReadOnly,
|
||||||
{
|
{ onValidateSpecialMode() },
|
||||||
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
{ onCancelSpecialMode() },
|
||||||
searchInfo,
|
{ onLaunchActivitySpecialMode() })
|
||||||
readOnly)
|
}
|
||||||
},
|
|
||||||
{
|
override fun onValidateSpecialMode() {
|
||||||
GroupActivity.launchForEntrySelectionResult(this@FileDatabaseSelectActivity,
|
super.onValidateSpecialMode()
|
||||||
searchInfo,
|
finish()
|
||||||
readOnly)
|
}
|
||||||
// Do not keep history
|
|
||||||
finish()
|
override fun onCancelSpecialMode() {
|
||||||
},
|
super.onCancelSpecialMode()
|
||||||
{ assistStructure ->
|
finish()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
|
||||||
assistStructure,
|
|
||||||
searchInfo,
|
|
||||||
readOnly)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||||
@@ -269,42 +280,45 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
val database = Database.getInstance()
|
|
||||||
if (database.loaded) {
|
|
||||||
launchGroupActivity(database.isReadOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
// Construct adapter with listeners
|
// Show open and create button or special mode
|
||||||
if (PreferencesUtil.showRecentFiles(this)) {
|
when (mSpecialMode) {
|
||||||
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
|
SpecialMode.DEFAULT -> {
|
||||||
databaseFileHistoryList?.let { historyList ->
|
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
||||||
val hideBrokenLocations = PreferencesUtil.hideBrokenLocations(this@FileDatabaseSelectActivity)
|
// There is an activity which can handle this intent.
|
||||||
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(
|
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||||
// Show only uri accessible
|
} else{
|
||||||
historyList.filter {
|
// No Activity found that can handle this intent.
|
||||||
if (hideBrokenLocations) {
|
createDatabaseButtonView?.visibility = View.GONE
|
||||||
FileDatabaseInfo(this@FileDatabaseSelectActivity,
|
|
||||||
it.databaseUri).exists
|
|
||||||
} else
|
|
||||||
true
|
|
||||||
})
|
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
else -> {
|
||||||
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
// Disable create button if in selection mode or request for autofill
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
createDatabaseButtonView?.visibility = View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register progress task
|
val database = Database.getInstance()
|
||||||
mProgressDialogThread?.registerProgressTask()
|
if (database.loaded) {
|
||||||
|
launchGroupActivity(database)
|
||||||
|
} else {
|
||||||
|
// Construct adapter with listeners
|
||||||
|
if (PreferencesUtil.showRecentFiles(this)) {
|
||||||
|
databaseFilesViewModel.loadListOfDatabases()
|
||||||
|
} else {
|
||||||
|
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
||||||
|
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register progress task
|
||||||
|
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
// Unregister progress task
|
// Unregister progress task
|
||||||
mProgressDialogThread?.unregisterProgressTask()
|
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
@@ -325,7 +339,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,
|
||||||
@@ -353,8 +367,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)
|
||||||
}
|
}
|
||||||
@@ -378,20 +391,24 @@ 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)
|
|
||||||
|
|
||||||
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||||
|
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler(Looper.getMainLooper()).post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
},
|
},
|
||||||
@@ -406,7 +423,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
openDatabaseButtonView!!,
|
openDatabaseButtonView!!,
|
||||||
{tapTargetView ->
|
{tapTargetView ->
|
||||||
tapTargetView?.let {
|
tapTargetView?.let {
|
||||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
|
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
@@ -415,6 +432,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,17 +447,25 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Launch only to standard search, else pass by PasswordActivity
|
* Standard Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun launch(context: Context,
|
fun launch(context: Context) {
|
||||||
searchInfo: SearchInfo? = null) {
|
context.startActivity(Intent(context, FileDatabaseSelectActivity::class.java))
|
||||||
val intent = Intent(context, FileDatabaseSelectActivity::class.java)
|
}
|
||||||
searchInfo?.let {
|
|
||||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
/*
|
||||||
}
|
* -------------------------
|
||||||
context.startActivity(intent)
|
* Search Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun launchForSearchResult(context: Context,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
EntrySelectionHelper.startActivityForSearchModeResult(context,
|
||||||
|
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
|
searchInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -445,9 +474,9 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun launchForEntrySelectionResult(activity: Activity,
|
fun launchForKeyboardSelectionResult(activity: Activity,
|
||||||
searchInfo: SearchInfo? = null) {
|
searchInfo: SearchInfo? = null) {
|
||||||
EntrySelectionHelper.startActivityForEntrySelectionResult(activity,
|
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(activity,
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
@@ -467,5 +496,17 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
assistStructure,
|
assistStructure,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Registration Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
fun launchForRegistration(context: Context,
|
||||||
|
registerInfo: RegisterInfo? = null) {
|
||||||
|
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
||||||
|
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
|
registerInfo)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,12 +28,15 @@ import android.graphics.Color
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.util.Log
|
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 android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
@@ -42,13 +45,10 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
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.DeleteNodesDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.*
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
|
|
||||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
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.SpecialMode
|
||||||
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
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
@@ -60,13 +60,16 @@ import com.kunzisoft.keepass.database.element.icon.IconImage
|
|||||||
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.database.search.SearchHelper
|
||||||
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.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
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
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY
|
||||||
@@ -74,10 +77,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 +89,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 +97,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 +106,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 +123,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 +138,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 {
|
||||||
@@ -151,7 +153,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
taTextColor.recycle()
|
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) {
|
||||||
@@ -201,23 +203,55 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add listeners to the add buttons
|
// Add listeners to the add buttons
|
||||||
addNodeButtonView?.setAddGroupClickListener(View.OnClickListener {
|
addNodeButtonView?.setAddGroupClickListener {
|
||||||
GroupEditDialogFragment.build()
|
GroupEditDialogFragment.build()
|
||||||
.show(supportFragmentManager,
|
.show(supportFragmentManager,
|
||||||
GroupEditDialogFragment.TAG_CREATE_GROUP)
|
GroupEditDialogFragment.TAG_CREATE_GROUP)
|
||||||
})
|
}
|
||||||
addNodeButtonView?.setAddEntryClickListener(View.OnClickListener {
|
addNodeButtonView?.setAddEntryClickListener {
|
||||||
mCurrentGroup?.let { currentGroup ->
|
mCurrentGroup?.let { currentGroup ->
|
||||||
EntryEditActivity.launch(this@GroupActivity, currentGroup)
|
EntrySelectionHelper.doSpecialAction(intent,
|
||||||
|
{
|
||||||
|
EntryEditActivity.launch(this@GroupActivity, currentGroup)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Search not used
|
||||||
|
},
|
||||||
|
{ searchInfo ->
|
||||||
|
EntryEditActivity.launchForSave(this@GroupActivity,
|
||||||
|
currentGroup, searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{ searchInfo ->
|
||||||
|
EntryEditActivity.launchForKeyboardSelectionResult(this@GroupActivity,
|
||||||
|
currentGroup, searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{ searchInfo, assistStructure ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
EntryEditActivity.launchForAutofillResult(this@GroupActivity,
|
||||||
|
assistStructure,
|
||||||
|
currentGroup, searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
} else {
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ searchInfo ->
|
||||||
|
EntryEditActivity.launchForRegistration(this@GroupActivity,
|
||||||
|
currentGroup, searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
// Search suggestion
|
// Search suggestion
|
||||||
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 ->
|
||||||
@@ -231,6 +265,41 @@ class GroupActivity : LockingActivity(),
|
|||||||
refreshSearchGroup()
|
refreshSearchGroup()
|
||||||
|
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||||
|
if (result.isSuccess) {
|
||||||
|
mListNodesFragment?.updateNodes(oldNodes, newNodes)
|
||||||
|
EntrySelectionHelper.doSpecialAction(intent,
|
||||||
|
{
|
||||||
|
// Standard not used after task
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Search not used
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Save not used
|
||||||
|
},
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
val entry = newNodes[0] as Entry
|
||||||
|
entrySelectedForKeyboardSelection(entry)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to perform action for keyboard selection after entry update", e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ _, _ ->
|
||||||
|
try {
|
||||||
|
val entry = newNodes[0] as Entry
|
||||||
|
entrySelectedForAutofillSelection(entry)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to perform action for autofill selection after entry update", e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Not use
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
ACTION_DATABASE_UPDATE_GROUP_TASK -> {
|
ACTION_DATABASE_UPDATE_GROUP_TASK -> {
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
mListNodesFragment?.updateNodes(oldNodes, newNodes)
|
mListNodesFragment?.updateNodes(oldNodes, newNodes)
|
||||||
@@ -285,7 +354,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
|
|
||||||
intent?.let { intentNotNull ->
|
intent?.let { intentNotNull ->
|
||||||
// To transform KEY_SEARCH_INFO in ACTION_SEARCH
|
// To transform KEY_SEARCH_INFO in ACTION_SEARCH
|
||||||
manageSearchInfoIntent(intent)
|
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) {
|
||||||
@@ -304,12 +373,11 @@ class GroupActivity : LockingActivity(),
|
|||||||
*/
|
*/
|
||||||
private fun manageSearchInfoIntent(intent: Intent): Boolean {
|
private fun manageSearchInfoIntent(intent: Intent): Boolean {
|
||||||
// To relaunch the activity as ACTION_SEARCH
|
// To relaunch the activity as ACTION_SEARCH
|
||||||
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||||
if (searchInfo != null) {
|
val autoSearch = intent.getBooleanExtra(AUTO_SEARCH_KEY, false)
|
||||||
|
if (searchInfo != null && autoSearch) {
|
||||||
intent.action = Intent.ACTION_SEARCH
|
intent.action = Intent.ACTION_SEARCH
|
||||||
val searchQuery = searchInfo.webDomain ?: searchInfo.applicationId
|
intent.putExtra(SearchManager.QUERY, searchInfo.toString())
|
||||||
intent.removeExtra(KEY_SEARCH_INFO)
|
|
||||||
intent.putExtra(SearchManager.QUERY, searchQuery)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -354,6 +422,9 @@ class GroupActivity : LockingActivity(),
|
|||||||
fragmentTransaction.addToBackStack(fragmentTag)
|
fragmentTransaction.addToBackStack(fragmentTag)
|
||||||
fragmentTransaction.commit()
|
fragmentTransaction.commit()
|
||||||
|
|
||||||
|
if (mSpecialMode != SpecialMode.DEFAULT)
|
||||||
|
mSelectionModeCountBackStack++
|
||||||
|
|
||||||
// Update last access time.
|
// Update last access time.
|
||||||
group?.touch(modified = false, touchParents = false)
|
group?.touch(modified = false, touchParents = false)
|
||||||
|
|
||||||
@@ -388,7 +459,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 {
|
||||||
@@ -460,13 +532,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 {
|
||||||
|
|
||||||
@@ -480,7 +545,8 @@ class GroupActivity : LockingActivity(),
|
|||||||
enableAddGroup(addGroupEnabled)
|
enableAddGroup(addGroupEnabled)
|
||||||
enableAddEntry(addEntryEnabled)
|
enableAddEntry(addEntryEnabled)
|
||||||
|
|
||||||
showButton()
|
if (actionNodeMode == null)
|
||||||
|
showButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,7 +562,8 @@ 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) {
|
||||||
@@ -510,28 +577,42 @@ class GroupActivity : LockingActivity(),
|
|||||||
|
|
||||||
Type.ENTRY -> try {
|
Type.ENTRY -> try {
|
||||||
val entryVersioned = node as Entry
|
val entryVersioned = node as Entry
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
EntrySelectionHelper.doSpecialAction(intent,
|
||||||
{
|
{
|
||||||
EntryActivity.launch(this@GroupActivity, entryVersioned, mReadOnly)
|
EntryActivity.launch(this@GroupActivity, entryVersioned, mReadOnly)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rebuildListNodes()
|
// Nothing here, a search is simply performed
|
||||||
// Populate Magikeyboard with entry
|
},
|
||||||
mDatabase?.let { database ->
|
{ searchInfo ->
|
||||||
populateKeyboardAndMoveAppToBackground(this@GroupActivity,
|
if (!mReadOnly)
|
||||||
entryVersioned.getEntryInfo(database),
|
entrySelectedForSave(entryVersioned, searchInfo)
|
||||||
intent)
|
else
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
{ searchInfo ->
|
||||||
|
if (!mReadOnly
|
||||||
|
&& searchInfo != null
|
||||||
|
&& PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity)) {
|
||||||
|
updateEntryWithSearchInfo(entryVersioned, searchInfo)
|
||||||
|
} else {
|
||||||
|
entrySelectedForKeyboardSelection(entryVersioned)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{ searchInfo, _ ->
|
||||||
// Build response with the entry selected
|
if (!mReadOnly
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
|
&& searchInfo != null
|
||||||
mDatabase?.let { database ->
|
&& PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)) {
|
||||||
AutofillHelper.buildResponse(this@GroupActivity,
|
updateEntryWithSearchInfo(entryVersioned, searchInfo)
|
||||||
entryVersioned.getEntryInfo(database))
|
} else {
|
||||||
}
|
entrySelectedForAutofillSelection(entryVersioned)
|
||||||
}
|
}
|
||||||
finish()
|
},
|
||||||
|
{ registerInfo ->
|
||||||
|
if (!mReadOnly)
|
||||||
|
entrySelectedForRegistration(entryVersioned, registerInfo)
|
||||||
|
else
|
||||||
|
finish()
|
||||||
})
|
})
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
Log.e(TAG, "Node can't be cast in Entry")
|
Log.e(TAG, "Node can't be cast in Entry")
|
||||||
@@ -539,18 +620,79 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var actionNodeMode: ActionMode? = null
|
private fun entrySelectedForSave(entry: Entry, searchInfo: SearchInfo) {
|
||||||
|
rebuildListNodes()
|
||||||
|
// Save to update the entry
|
||||||
|
EntryEditActivity.launchForSave(this@GroupActivity,
|
||||||
|
entry, searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun entrySelectedForKeyboardSelection(entry: Entry) {
|
||||||
|
rebuildListNodes()
|
||||||
|
// Populate Magikeyboard with entry
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
populateKeyboardAndMoveAppToBackground(this,
|
||||||
|
entry.getEntryInfo(database),
|
||||||
|
intent)
|
||||||
|
}
|
||||||
|
onValidateSpecialMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun entrySelectedForAutofillSelection(entry: Entry) {
|
||||||
|
// Build response with the entry selected
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
AutofillHelper.buildResponse(this,
|
||||||
|
entry.getEntryInfo(database))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onValidateSpecialMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun entrySelectedForRegistration(entry: Entry, registerInfo: RegisterInfo?) {
|
||||||
|
rebuildListNodes()
|
||||||
|
// Registration to update the entry
|
||||||
|
EntryEditActivity.launchForRegistration(this@GroupActivity,
|
||||||
|
entry, registerInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateEntryWithSearchInfo(entry: Entry, searchInfo: SearchInfo) {
|
||||||
|
val newEntry = Entry(entry)
|
||||||
|
newEntry.setEntryInfo(mDatabase, newEntry.getEntryInfo(mDatabase).apply {
|
||||||
|
saveSearchInfo(mDatabase, searchInfo)
|
||||||
|
})
|
||||||
|
// In selection mode, it's forced read-only, so update not allowed
|
||||||
|
mProgressDatabaseTaskProvider?.startDatabaseUpdateEntry(
|
||||||
|
entry,
|
||||||
|
newEntry,
|
||||||
|
!mReadOnly && mAutoSaveEnable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -607,7 +749,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
|
||||||
@@ -617,7 +759,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
|
||||||
@@ -638,7 +780,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeleteMenuClick(nodes: List<Node>): Boolean {
|
private fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false): Boolean {
|
||||||
val database = mDatabase
|
val database = mDatabase
|
||||||
|
|
||||||
// If recycle bin enabled, ensure it exists
|
// If recycle bin enabled, ensure it exists
|
||||||
@@ -651,22 +793,31 @@ class GroupActivity : LockingActivity(),
|
|||||||
&& database.isRecycleBinEnabled
|
&& database.isRecycleBinEnabled
|
||||||
&& database.recycleBin != mCurrentGroup) {
|
&& database.recycleBin != mCurrentGroup) {
|
||||||
|
|
||||||
mProgressDialogThread?.startDatabaseDeleteNodes(
|
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
|
||||||
nodes,
|
nodes,
|
||||||
!mReadOnly && mAutoSaveEnable
|
!mReadOnly && mAutoSaveEnable
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// else open the dialog to confirm deletion
|
// else open the dialog to confirm deletion
|
||||||
else {
|
else {
|
||||||
DeleteNodesDialogFragment.getInstance(nodes)
|
val deleteNodesDialogFragment: DeleteNodesDialogFragment =
|
||||||
.show(supportFragmentManager, "deleteNodesDialogFragment")
|
if (recycleBin) {
|
||||||
|
EmptyRecycleBinDialogFragment.getInstance(nodes)
|
||||||
|
} else {
|
||||||
|
DeleteNodesDialogFragment.getInstance(nodes)
|
||||||
|
}
|
||||||
|
deleteNodesDialogFragment.show(supportFragmentManager, "deleteNodesDialogFragment")
|
||||||
}
|
}
|
||||||
finishNodeAction()
|
finishNodeAction()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDeleteMenuClick(nodes: List<Node>): Boolean {
|
||||||
|
return deleteNodes(nodes)
|
||||||
|
}
|
||||||
|
|
||||||
override fun permanentlyDeleteNodes(nodes: List<Node>) {
|
override fun permanentlyDeleteNodes(nodes: List<Node>) {
|
||||||
mProgressDialogThread?.startDatabaseDeleteNodes(
|
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
|
||||||
nodes,
|
nodes,
|
||||||
!mReadOnly && mAutoSaveEnable
|
!mReadOnly && mAutoSaveEnable
|
||||||
)
|
)
|
||||||
@@ -685,6 +836,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() {
|
||||||
@@ -701,9 +854,8 @@ class GroupActivity : LockingActivity(),
|
|||||||
if (mReadOnly) {
|
if (mReadOnly) {
|
||||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||||
}
|
}
|
||||||
if (!mSelectionMode) {
|
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||||
inflater.inflate(R.menu.default_menu, menu)
|
MenuUtil.defaultMenuInflater(inflater, menu)
|
||||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menu for recycle bin
|
// Menu for recycle bin
|
||||||
@@ -752,7 +904,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
// Launch education screen
|
// Launch education screen
|
||||||
Handler().post { performedNextEducation(GroupActivityEducation(this), menu) }
|
Handler(Looper.getMainLooper()).post { performedNextEducation(GroupActivityEducation(this), menu) }
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -826,13 +978,13 @@ 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 -> {
|
||||||
mCurrentGroup?.getChildren()?.let { listChildren ->
|
mCurrentGroup?.getChildren()?.let { listChildren ->
|
||||||
// Automatically delete all elements
|
// Automatically delete all elements
|
||||||
onDeleteMenuClick(listChildren)
|
deleteNodes(listChildren, true)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -860,7 +1012,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
|
||||||
@@ -882,7 +1034,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
|
||||||
@@ -912,19 +1064,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)
|
||||||
}
|
}
|
||||||
@@ -976,24 +1125,46 @@ 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.onRegularBackPressed()
|
||||||
// Else lock if needed
|
rebuildListNodes()
|
||||||
|
}
|
||||||
|
// Else in root, lock if needed
|
||||||
else {
|
else {
|
||||||
|
intent.removeExtra(AUTO_SEARCH_KEY)
|
||||||
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
|
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||||
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
|
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
|
||||||
lockAndExit()
|
lockAndExit()
|
||||||
super.onBackPressed()
|
super.onRegularBackPressed()
|
||||||
} else {
|
} else {
|
||||||
// To restore standard mode
|
backToTheAppCaller()
|
||||||
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
|
|
||||||
moveTaskToBack(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rebuildListNodes()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun removeFragmentHistory() {
|
||||||
|
val fragmentManager = supportFragmentManager
|
||||||
|
if (mSelectionModeCountBackStack > 0) {
|
||||||
|
for (selectionMode in 0 .. mSelectionModeCountBackStack) {
|
||||||
|
fragmentManager.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Reinit the counter for navigation history
|
||||||
|
mSelectionModeCountBackStack = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onValidateSpecialMode() {
|
||||||
|
removeFragmentHistory()
|
||||||
|
super.onValidateSpecialMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancelSpecialMode() {
|
||||||
|
removeFragmentHistory()
|
||||||
|
super.onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val TAG = GroupActivity::class.java.name
|
private val TAG = GroupActivity::class.java.name
|
||||||
@@ -1003,6 +1174,7 @@ 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?,
|
||||||
@@ -1040,26 +1212,62 @@ class GroupActivity : LockingActivity(),
|
|||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launch(context: Context,
|
fun launch(context: Context,
|
||||||
searchInfo: SearchInfo? = null,
|
readOnly: Boolean,
|
||||||
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
autoSearch: Boolean = false) {
|
||||||
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
|
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
|
||||||
searchInfo?.let {
|
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
|
||||||
}
|
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Search Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
fun launchForSearchResult(context: Context,
|
||||||
|
readOnly: Boolean,
|
||||||
|
searchInfo: SearchInfo,
|
||||||
|
autoSearch: Boolean = false) {
|
||||||
|
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
|
||||||
|
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||||
|
EntrySelectionHelper.addSearchInfoInIntent(
|
||||||
|
intent,
|
||||||
|
searchInfo)
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Search save Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
fun launchForSaveResult(context: Context,
|
||||||
|
searchInfo: SearchInfo,
|
||||||
|
autoSearch: Boolean = false) {
|
||||||
|
checkTimeAndBuildIntent(context, null, false) { intent ->
|
||||||
|
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||||
|
EntrySelectionHelper.startActivityForSaveModeResult(context,
|
||||||
|
intent,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Keyboard Launch
|
* Keyboard Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForEntrySelectionResult(context: Context,
|
fun launchForKeyboardSelectionResult(context: Context,
|
||||||
searchInfo: SearchInfo? = null,
|
readOnly: Boolean,
|
||||||
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
searchInfo: SearchInfo? = null,
|
||||||
|
autoSearch: Boolean = false) {
|
||||||
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
|
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
|
||||||
EntrySelectionHelper.startActivityForEntrySelectionResult(context, intent, searchInfo)
|
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||||
|
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(context,
|
||||||
|
intent,
|
||||||
|
searchInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1070,12 +1278,177 @@ class GroupActivity : LockingActivity(),
|
|||||||
*/
|
*/
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: Activity,
|
fun launchForAutofillResult(activity: Activity,
|
||||||
|
readOnly: Boolean,
|
||||||
assistStructure: AssistStructure,
|
assistStructure: AssistStructure,
|
||||||
searchInfo: SearchInfo? = null,
|
searchInfo: SearchInfo? = null,
|
||||||
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) {
|
autoSearch: Boolean = false) {
|
||||||
checkTimeAndBuildIntent(activity, null, readOnly) { intent ->
|
checkTimeAndBuildIntent(activity, null, readOnly) { intent ->
|
||||||
AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure, searchInfo)
|
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||||
|
AutofillHelper.startActivityForAutofillResult(activity,
|
||||||
|
intent,
|
||||||
|
assistStructure,
|
||||||
|
searchInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Registration Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
fun launchForRegistration(context: Context,
|
||||||
|
registerInfo: RegisterInfo? = null) {
|
||||||
|
checkTimeAndBuildIntent(context, null, false) { intent ->
|
||||||
|
intent.putExtra(AUTO_SEARCH_KEY, false)
|
||||||
|
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
||||||
|
intent,
|
||||||
|
registerInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Global Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
fun launch(activity: Activity,
|
||||||
|
readOnly: Boolean,
|
||||||
|
onValidateSpecialMode: () -> Unit,
|
||||||
|
onCancelSpecialMode: () -> Unit,
|
||||||
|
onLaunchActivitySpecialMode: () -> Unit) {
|
||||||
|
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||||
|
{
|
||||||
|
GroupActivity.launch(activity,
|
||||||
|
readOnly,
|
||||||
|
true)
|
||||||
|
},
|
||||||
|
{ searchInfo ->
|
||||||
|
SearchHelper.checkAutoSearchInfo(activity,
|
||||||
|
Database.getInstance(),
|
||||||
|
searchInfo,
|
||||||
|
{ _ ->
|
||||||
|
// Response is build
|
||||||
|
GroupActivity.launchForSearchResult(activity,
|
||||||
|
readOnly,
|
||||||
|
searchInfo,
|
||||||
|
true)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Here no search info found
|
||||||
|
if (readOnly) {
|
||||||
|
GroupActivity.launchForSearchResult(activity,
|
||||||
|
readOnly,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
} else {
|
||||||
|
GroupActivity.launchForSaveResult(activity,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
}
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Simply close if database not opened, normally not happened
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Nothing with Save Info, only pass by search first
|
||||||
|
},
|
||||||
|
{ searchInfo ->
|
||||||
|
SearchHelper.checkAutoSearchInfo(activity,
|
||||||
|
Database.getInstance(),
|
||||||
|
searchInfo,
|
||||||
|
{ items ->
|
||||||
|
// Response is build
|
||||||
|
if (items.size == 1) {
|
||||||
|
populateKeyboardAndMoveAppToBackground(activity,
|
||||||
|
items[0],
|
||||||
|
activity.intent)
|
||||||
|
onValidateSpecialMode()
|
||||||
|
} else {
|
||||||
|
// Select the one we want
|
||||||
|
GroupActivity.launchForKeyboardSelectionResult(activity,
|
||||||
|
readOnly,
|
||||||
|
searchInfo,
|
||||||
|
true)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Here no search info found, disable auto search
|
||||||
|
GroupActivity.launchForKeyboardSelectionResult(activity,
|
||||||
|
readOnly,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Simply close if database not opened, normally not happened
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ searchInfo, assistStructure ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
SearchHelper.checkAutoSearchInfo(activity,
|
||||||
|
Database.getInstance(),
|
||||||
|
searchInfo,
|
||||||
|
{ items ->
|
||||||
|
// Response is build
|
||||||
|
AutofillHelper.buildResponse(activity, items)
|
||||||
|
onValidateSpecialMode()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Here no search info found, disable auto search
|
||||||
|
GroupActivity.launchForAutofillResult(activity,
|
||||||
|
readOnly,
|
||||||
|
assistStructure,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Simply close if database not opened, normally not happened
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ registerInfo ->
|
||||||
|
if (!readOnly) {
|
||||||
|
SearchHelper.checkAutoSearchInfo(activity,
|
||||||
|
Database.getInstance(),
|
||||||
|
registerInfo?.searchInfo,
|
||||||
|
{ _ ->
|
||||||
|
// No auto search, it's a registration
|
||||||
|
GroupActivity.launchForRegistration(activity,
|
||||||
|
registerInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Here no search info found, disable auto search
|
||||||
|
GroupActivity.launchForRegistration(activity,
|
||||||
|
registerInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Simply close if database not opened, normally not happened
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(activity.applicationContext,
|
||||||
|
R.string.autofill_read_only_save,
|
||||||
|
Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -69,10 +70,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
|
|
||||||
|
|
||||||
private var readOnly: Boolean = false
|
private var readOnly: Boolean = false
|
||||||
get() {
|
private var specialMode: SpecialMode = SpecialMode.DEFAULT
|
||||||
return field || selectionMode
|
|
||||||
}
|
|
||||||
private var selectionMode: Boolean = false
|
|
||||||
|
|
||||||
val isEmpty: Boolean
|
val isEmpty: Boolean
|
||||||
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
||||||
@@ -97,6 +95,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
nodeClickListener = null
|
||||||
|
onScrollListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -189,7 +193,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
activity?.intent?.let {
|
activity?.intent?.let {
|
||||||
selectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(it)
|
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh data
|
// Refresh data
|
||||||
@@ -266,14 +270,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 +323,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 +353,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
returnValue
|
returnValue
|
||||||
}
|
}
|
||||||
else -> false
|
else -> actionModeCallback.onActionItemClicked(mode, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +363,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
mAdapter?.unselectActionNodes()
|
mAdapter?.unselectActionNodes()
|
||||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
|
actionModeCallback.onDestroyActionMode(mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,16 +30,22 @@ import com.kunzisoft.keepass.database.search.SearchHelper
|
|||||||
class MagikeyboardLauncherActivity : AppCompatActivity() {
|
class MagikeyboardLauncherActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
val database = Database.getInstance()
|
||||||
|
val readOnly = database.isReadOnly
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
Database.getInstance(),
|
database,
|
||||||
null,
|
null,
|
||||||
{},
|
|
||||||
{
|
{
|
||||||
GroupActivity.launchForEntrySelectionResult(this)
|
// Not called
|
||||||
|
// if items found directly returns before calling this activity
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Select if not found
|
||||||
|
GroupActivity.launchForKeyboardSelectionResult(this, readOnly)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Pass extra to get entry
|
// Pass extra to get entry
|
||||||
FileDatabaseSelectActivity.launchForEntrySelectionResult(this)
|
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
finish()
|
finish()
|
||||||
|
|||||||
@@ -21,79 +21,82 @@ 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
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.util.Log
|
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.EntrySelectionHelper.KEY_SEARCH_INFO
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
|
||||||
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.helpers.SpecialMode
|
||||||
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.biometric.AdvancedUnlockedManager
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper
|
||||||
|
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.RegisterInfo
|
||||||
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
|
||||||
@@ -108,7 +111,7 @@ 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
|
private var mAllowAutoOpenBiometricPrompt: Boolean = true
|
||||||
@@ -124,23 +127,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)
|
||||||
}
|
}
|
||||||
@@ -167,12 +169,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) {
|
if (savedInstanceState?.containsKey(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) == true) {
|
||||||
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
|
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 -> {
|
||||||
@@ -209,7 +233,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)
|
||||||
}
|
}
|
||||||
@@ -231,7 +255,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()
|
||||||
@@ -261,69 +285,22 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivity() {
|
private fun launchGroupActivity() {
|
||||||
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
GroupActivity.launch(this,
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
readOnly,
|
||||||
{
|
{ onValidateSpecialMode() },
|
||||||
GroupActivity.launch(this@PasswordActivity,
|
{ onCancelSpecialMode() },
|
||||||
searchInfo,
|
{ onLaunchActivitySpecialMode() }
|
||||||
readOnly)
|
)
|
||||||
// Remove the search info from intent
|
}
|
||||||
if (searchInfo != null) {
|
|
||||||
finish()
|
override fun onValidateSpecialMode() {
|
||||||
}
|
super.onValidateSpecialMode()
|
||||||
},
|
finish()
|
||||||
{
|
}
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
|
||||||
Database.getInstance(),
|
override fun onCancelSpecialMode() {
|
||||||
searchInfo,
|
super.onCancelSpecialMode()
|
||||||
{ items ->
|
finish()
|
||||||
// Response is build
|
|
||||||
if (items.size == 1) {
|
|
||||||
populateKeyboardAndMoveAppToBackground(this@PasswordActivity,
|
|
||||||
items[0],
|
|
||||||
intent)
|
|
||||||
} else {
|
|
||||||
// Select the one we want
|
|
||||||
GroupActivity.launchForEntrySelectionResult(this, searchInfo)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Here no search info found
|
|
||||||
GroupActivity.launchForEntrySelectionResult(this@PasswordActivity,
|
|
||||||
null,
|
|
||||||
readOnly)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Simply close if database not opened, normally not happened
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// Do not keep history
|
|
||||||
finish()
|
|
||||||
},
|
|
||||||
{ assistStructure ->
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
|
||||||
Database.getInstance(),
|
|
||||||
searchInfo,
|
|
||||||
{ items ->
|
|
||||||
// Response is build
|
|
||||||
AutofillHelper.buildResponse(this, items)
|
|
||||||
finish()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Here no search info found
|
|
||||||
GroupActivity.launchForAutofillResult(this@PasswordActivity,
|
|
||||||
assistStructure,
|
|
||||||
null,
|
|
||||||
readOnly)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Simply close if database not opened, normally not happened
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val onEditorActionListener = object : TextView.OnEditorActionListener {
|
private val onEditorActionListener = object : TextView.OnEditorActionListener {
|
||||||
@@ -337,6 +314,7 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
if (Database.getInstance().loaded) {
|
if (Database.getInstance().loaded) {
|
||||||
launchGroupActivity()
|
launchGroupActivity()
|
||||||
@@ -349,83 +327,35 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
clearCredentialsViews()
|
clearCredentialsViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
// For check shutdown
|
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
mProgressDialogThread?.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
|
// Don't allow auto open prompt if lock become when UI visible
|
||||||
mAllowAutoOpenBiometricPrompt = if (LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
|
mAllowAutoOpenBiometricPrompt = if (LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
|
||||||
false
|
false
|
||||||
else
|
else
|
||||||
mAllowAutoOpenBiometricPrompt
|
mAllowAutoOpenBiometricPrompt
|
||||||
|
mDatabaseFileUri?.let { databaseFileUri ->
|
||||||
initUriFromIntent()
|
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
checkPermission()
|
checkPermission()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initUriFromIntent() {
|
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||||
/*
|
|
||||||
// "canXrite" doesn't work with Google Drive, don't really know why?
|
|
||||||
mForceReadOnly = mDatabaseFileUri?.let {
|
|
||||||
!FileDatabaseInfo(this, it).canWrite
|
|
||||||
} ?: false
|
|
||||||
*/
|
|
||||||
mForceReadOnly = mDatabaseFileUri?.let {
|
|
||||||
!FileDatabaseInfo(this, it).exists
|
|
||||||
} ?: true
|
|
||||||
|
|
||||||
// Post init uri with KeyFile if needed
|
|
||||||
if (mRememberKeyFile && (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
|
|
||||||
// Retrieve KeyFile in a thread
|
|
||||||
mDatabaseFileUri?.let { databaseUri ->
|
|
||||||
FileDatabaseHistoryAction.getInstance(applicationContext)
|
|
||||||
.getKeyFileUriByDatabaseUri(databaseUri) {
|
|
||||||
onPostInitUri(databaseUri, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onPostInitUri(mDatabaseFileUri, mDatabaseKeyFileUri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
|
||||||
// Define title
|
|
||||||
databaseFileUri?.let {
|
|
||||||
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
|
|
||||||
filenameView?.text = title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
||||||
@@ -439,11 +369,10 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
||||||
} else {
|
} else {
|
||||||
// Init Biometric elements
|
// Init Biometric elements
|
||||||
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
|
||||||
if (advancedUnlockedManager == null && databaseFileUri != null) {
|
&& databaseFileUri != null) {
|
||||||
advancedUnlockedManager = AdvancedUnlockedManager(this,
|
advancedUnlockedManager = AdvancedUnlockedManager(this,
|
||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
advancedUnlockInfoView,
|
advancedUnlockInfoView,
|
||||||
@@ -469,14 +398,16 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
advancedUnlockedManager?.isBiometricPromptAutoOpenEnable = mAllowAutoOpenBiometricPrompt
|
advancedUnlockedManager?.isBiometricPromptAutoOpenEnable =
|
||||||
|
mAllowAutoOpenBiometricPrompt && mProgressDatabaseTaskProvider?.isBinded() != true
|
||||||
advancedUnlockedManager?.checkBiometricAvailability()
|
advancedUnlockedManager?.checkBiometricAvailability()
|
||||||
biometricInitialize = true
|
|
||||||
} else {
|
} else {
|
||||||
|
advancedUnlockInfoView?.visibility = View.GONE
|
||||||
advancedUnlockedManager?.destroy()
|
advancedUnlockedManager?.destroy()
|
||||||
|
advancedUnlockedManager = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!biometricInitialize) {
|
if (advancedUnlockedManager == null) {
|
||||||
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
||||||
}
|
}
|
||||||
checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
||||||
@@ -529,7 +460,7 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
mProgressDialogThread?.unregisterProgressTask()
|
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
advancedUnlockedManager?.destroy()
|
advancedUnlockedManager?.destroy()
|
||||||
@@ -586,15 +517,25 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
clearCredentialsViews()
|
clearCredentialsViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
databaseFileUri?.let { databaseUri ->
|
if (readOnly && (
|
||||||
// Show the progress dialog and load the database
|
mSpecialMode == SpecialMode.SAVE
|
||||||
showProgressDialogAndLoadDatabase(
|
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||||
databaseUri,
|
) {
|
||||||
password,
|
Log.e(TAG, getString(R.string.autofill_read_only_save))
|
||||||
keyFileUri,
|
Snackbar.make(activity_password_coordinator_layout,
|
||||||
readOnly,
|
R.string.autofill_read_only_save,
|
||||||
cipherDatabaseEntity,
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
false)
|
} else {
|
||||||
|
databaseFileUri?.let { databaseUri ->
|
||||||
|
// Show the progress dialog and load the database
|
||||||
|
showProgressDialogAndLoadDatabase(
|
||||||
|
databaseUri,
|
||||||
|
password,
|
||||||
|
keyFileUri,
|
||||||
|
readOnly,
|
||||||
|
cipherDatabaseEntity,
|
||||||
|
false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -604,7 +545,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,
|
||||||
@@ -624,14 +565,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 (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 (mSpecialMode == SpecialMode.DEFAULT) {
|
||||||
|
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
|
||||||
@@ -680,7 +622,7 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
if (!performedEductionInProgress) {
|
if (!performedEductionInProgress) {
|
||||||
performedEductionInProgress = true
|
performedEductionInProgress = true
|
||||||
// Show education views
|
// Show education views
|
||||||
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
Handler(Looper.getMainLooper()).post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,19 +644,24 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
|
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
|
||||||
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
||||||
{
|
{
|
||||||
onOptionsItemSelected(menu.findItem(R.id.menu_open_file_read_mode_key))
|
try {
|
||||||
|
menu.findItem(R.id.menu_open_file_read_mode_key)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to find read mode menu")
|
||||||
|
}
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!readOnlyEducationPerformed) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||||
val biometricCanAuthenticate = BiometricManager.from(this).canAuthenticate()
|
&& !readOnlyEducationPerformed) {
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
val biometricCanAuthenticate = BiometricUnlockDatabaseHelper.canAuthenticate(this)
|
||||||
&& PreferencesUtil.isBiometricUnlockEnable(applicationContext)
|
PreferencesUtil.isBiometricUnlockEnable(applicationContext)
|
||||||
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||||
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.unlockIconImageView != null
|
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.visibility == View.VISIBLE
|
||||||
|
&& advancedUnlockInfoView?.unlockIconImageView != null
|
||||||
&& passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!,
|
&& passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!,
|
||||||
{
|
{
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
@@ -767,7 +714,7 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -779,9 +726,12 @@ 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()
|
||||||
|
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
|
||||||
|
}
|
||||||
|
Activity.RESULT_CANCELED -> {
|
||||||
clearCredentialsViews()
|
clearCredentialsViews()
|
||||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -818,19 +768,33 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Share Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForSearchResult(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Keyboard Launch
|
* Keyboard Launch
|
||||||
@@ -838,13 +802,12 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launchForKeyboardResult(
|
fun launchForKeyboardResult(activity: Activity,
|
||||||
activity: Activity,
|
databaseFile: Uri,
|
||||||
databaseFile: Uri,
|
keyFile: Uri?,
|
||||||
keyFile: Uri?,
|
searchInfo: SearchInfo?) {
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
EntrySelectionHelper.startActivityForEntrySelectionResult(
|
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
@@ -859,22 +822,90 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launchForAutofillResult(
|
fun launchForAutofillResult(activity: Activity,
|
||||||
activity: Activity,
|
databaseFile: Uri,
|
||||||
databaseFile: Uri,
|
keyFile: Uri?,
|
||||||
keyFile: Uri?,
|
assistStructure: AssistStructure,
|
||||||
assistStructure: AssistStructure?,
|
searchInfo: SearchInfo?) {
|
||||||
searchInfo: SearchInfo?) {
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
if (assistStructure != null) {
|
AutofillHelper.startActivityForAutofillResult(
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
activity,
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
intent,
|
||||||
activity,
|
assistStructure,
|
||||||
intent,
|
searchInfo)
|
||||||
assistStructure,
|
}
|
||||||
searchInfo)
|
}
|
||||||
}
|
|
||||||
} else {
|
/*
|
||||||
launch(activity, databaseFile, keyFile, searchInfo)
|
* -------------------------
|
||||||
|
* Registration Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
fun launchForRegistration(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
registerInfo: RegisterInfo?) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
registerInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Global Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
fun launch(activity: Activity,
|
||||||
|
databaseUri: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||||
|
onCancelSpecialMode: () -> Unit,
|
||||||
|
onLaunchActivitySpecialMode: () -> Unit) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||||
|
{
|
||||||
|
PasswordActivity.launch(activity,
|
||||||
|
databaseUri, keyFile)
|
||||||
|
},
|
||||||
|
{ searchInfo -> // Search Action
|
||||||
|
PasswordActivity.launchForSearchResult(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{ // Save Action
|
||||||
|
// Not directly used, a search is performed before
|
||||||
|
},
|
||||||
|
{ searchInfo -> // Keyboard Selection Action
|
||||||
|
PasswordActivity.launchForKeyboardResult(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{ searchInfo, assistStructure -> // Autofill Selection Action
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
PasswordActivity.launchForAutofillResult(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
assistStructure,
|
||||||
|
searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
} else {
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ registerInfo -> // Registration Action
|
||||||
|
PasswordActivity.launchForRegistration(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
registerInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
fileNoFoundAction(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,16 +25,19 @@ 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.SpannableStringBuilder
|
||||||
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.ImageView
|
||||||
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.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
|
|
||||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||||
@@ -56,7 +59,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
private var mListener: AssignPasswordDialogListener? = null
|
private var mListener: AssignPasswordDialogListener? = null
|
||||||
|
|
||||||
private var mOpenFileHelper: OpenFileHelper? = null
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
|
||||||
|
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
||||||
|
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||||
|
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
|
||||||
|
|
||||||
private val passwordTextWatcher = object : TextWatcher {
|
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) {}
|
||||||
@@ -85,6 +92,17 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
mEmptyPasswordConfirmationDialog?.dismiss()
|
||||||
|
mEmptyPasswordConfirmationDialog = null
|
||||||
|
mNoKeyConfirmationDialog?.dismiss()
|
||||||
|
mNoKeyConfirmationDialog = null
|
||||||
|
mEmptyKeyFileConfirmationDialog?.dismiss()
|
||||||
|
mEmptyKeyFileConfirmationDialog = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
|
|
||||||
@@ -99,11 +117,15 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
rootView = inflater.inflate(R.layout.fragment_set_password, null)
|
rootView = inflater.inflate(R.layout.fragment_set_password, null)
|
||||||
builder.setView(rootView)
|
builder.setView(rootView)
|
||||||
.setTitle(R.string.assign_master_key)
|
|
||||||
// Add action buttons
|
// Add action buttons
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
|
||||||
|
val credentialsInfo: ImageView? = rootView?.findViewById(R.id.credentials_information)
|
||||||
|
credentialsInfo?.setOnClickListener {
|
||||||
|
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
|
||||||
|
}
|
||||||
|
|
||||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
||||||
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
||||||
passwordView = rootView?.findViewById(R.id.pass_password)
|
passwordView = rootView?.findViewById(R.id.pass_password)
|
||||||
@@ -113,10 +135,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()
|
||||||
@@ -129,7 +151,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
mMasterPassword = ""
|
mMasterPassword = ""
|
||||||
mKeyFile = null
|
mKeyFile = null
|
||||||
|
|
||||||
var error = verifyPassword() || verifyFile()
|
var error = verifyPassword() || verifyKeyFile()
|
||||||
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
||||||
error = true
|
error = true
|
||||||
if (allowNoMasterKey)
|
if (allowNoMasterKey)
|
||||||
@@ -199,7 +221,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyFile(): Boolean {
|
private fun verifyKeyFile(): Boolean {
|
||||||
var error = false
|
var error = false
|
||||||
if (keyFileCheckBox != null
|
if (keyFileCheckBox != null
|
||||||
&& keyFileCheckBox!!.isChecked) {
|
&& keyFileCheckBox!!.isChecked) {
|
||||||
@@ -219,7 +241,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
val builder = AlertDialog.Builder(it)
|
val builder = AlertDialog.Builder(it)
|
||||||
builder.setMessage(R.string.warning_empty_password)
|
builder.setMessage(R.string.warning_empty_password)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
if (!verifyFile()) {
|
if (!verifyKeyFile()) {
|
||||||
mListener?.onAssignKeyDialogPositiveClick(
|
mListener?.onAssignKeyDialogPositiveClick(
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
passwordCheckBox!!.isChecked, mMasterPassword,
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
keyFileCheckBox!!.isChecked, mKeyFile)
|
||||||
@@ -227,7 +249,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
builder.create().show()
|
mEmptyPasswordConfirmationDialog = builder.create()
|
||||||
|
mEmptyPasswordConfirmationDialog?.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,18 +265,44 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
this@AssignMasterKeyDialogFragment.dismiss()
|
this@AssignMasterKeyDialogFragment.dismiss()
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
builder.create().show()
|
mNoKeyConfirmationDialog = builder.create()
|
||||||
|
mNoKeyConfirmationDialog?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showEmptyKeyFileConfirmationDialog() {
|
||||||
|
activity?.let {
|
||||||
|
val builder = AlertDialog.Builder(it)
|
||||||
|
builder.setMessage(SpannableStringBuilder().apply {
|
||||||
|
append(getString(R.string.warning_empty_keyfile))
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_empty_keyfile_explanation))
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_sure_add_file))
|
||||||
|
})
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
keyFileCheckBox?.isChecked = false
|
||||||
|
keyFileSelectionView?.uri = null
|
||||||
|
}
|
||||||
|
mEmptyKeyFileConfirmationDialog = builder.create()
|
||||||
|
mEmptyKeyFileConfirmationDialog?.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
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
|
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||||
keyFileSelectionView?.uri = pathUri
|
keyFileSelectionView?.error = null
|
||||||
|
keyFileCheckBox?.isChecked = true
|
||||||
|
keyFileSelectionView?.uri = pathUri
|
||||||
|
if (lengthFile <= 0L) {
|
||||||
|
showEmptyKeyFileConfirmationDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ class DatePickerFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
// Create a new instance of DatePickerDialog and return it
|
// Create a new instance of DatePickerDialog and return it
|
||||||
return context?.let {
|
return context?.let {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
|||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
||||||
|
|
||||||
class DeleteNodesDialogFragment : DialogFragment() {
|
open class DeleteNodesDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
private var mNodesToDelete: List<Node> = ArrayList()
|
private var mNodesToDelete: List<Node> = ArrayList()
|
||||||
private var mListener: DeleteNodeListener? = null
|
private var mListener: DeleteNodeListener? = null
|
||||||
@@ -46,6 +46,15 @@ class DeleteNodesDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun retrieveMessage(): String {
|
||||||
|
return getString(R.string.warning_permanently_delete_nodes)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
@@ -63,11 +72,11 @@ class DeleteNodesDialogFragment : DialogFragment() {
|
|||||||
// Use the Builder class for convenient dialog construction
|
// Use the Builder class for convenient dialog construction
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
builder.setMessage(getString(R.string.warning_permanently_delete_nodes))
|
builder.setMessage(retrieveMessage())
|
||||||
builder.setPositiveButton(android.R.string.yes) { _, _ ->
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
mListener?.permanentlyDeleteNodes(mNodesToDelete)
|
mListener?.permanentlyDeleteNodes(mNodesToDelete)
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(android.R.string.no) { _, _ -> dismiss() }
|
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||||
// Create the AlertDialog object and return it
|
// Create the AlertDialog object and return it
|
||||||
return builder.create()
|
return builder.create()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||||
|
|
||||||
|
class EmptyRecycleBinDialogFragment : DeleteNodesDialogFragment() {
|
||||||
|
|
||||||
|
override fun retrieveMessage(): String {
|
||||||
|
return getString(R.string.warning_empty_recycle_bin)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getInstance(nodesToDelete: List<Node>): EmptyRecycleBinDialogFragment {
|
||||||
|
return EmptyRecycleBinDialogFragment().apply {
|
||||||
|
arguments = getBundleFromListNodes(nodesToDelete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
|
import com.kunzisoft.keepass.model.Field
|
||||||
|
|
||||||
|
|
||||||
|
class EntryCustomFieldDialogFragment: DialogFragment() {
|
||||||
|
|
||||||
|
private var oldField: Field? = null
|
||||||
|
|
||||||
|
private var entryCustomFieldListener: EntryCustomFieldListener? = null
|
||||||
|
|
||||||
|
private var customFieldLabelContainer: TextInputLayout? = null
|
||||||
|
private var customFieldLabel: TextView? = null
|
||||||
|
private var customFieldDeleteButton: ImageView? = null
|
||||||
|
private var customFieldProtectionButton: CompoundButton? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
try {
|
||||||
|
entryCustomFieldListener = context as EntryCustomFieldListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
// The activity doesn't implement the interface, throw exception
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + EntryCustomFieldListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
entryCustomFieldListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
val root = activity.layoutInflater.inflate(R.layout.fragment_entry_new_field, null)
|
||||||
|
customFieldLabelContainer = root?.findViewById(R.id.entry_custom_field_label_container)
|
||||||
|
customFieldLabel = root?.findViewById(R.id.entry_custom_field_label)
|
||||||
|
customFieldDeleteButton = root?.findViewById(R.id.entry_custom_field_delete)
|
||||||
|
customFieldProtectionButton = root?.findViewById(R.id.entry_custom_field_protection)
|
||||||
|
|
||||||
|
oldField = arguments?.getParcelable(KEY_FIELD)
|
||||||
|
oldField?.let { oldCustomField ->
|
||||||
|
customFieldLabel?.text = oldCustomField.name
|
||||||
|
customFieldProtectionButton?.isChecked = oldCustomField.protectedValue.isProtected
|
||||||
|
|
||||||
|
customFieldDeleteButton?.visibility = View.VISIBLE
|
||||||
|
customFieldDeleteButton?.setOnClickListener {
|
||||||
|
entryCustomFieldListener?.onDeleteCustomFieldApproved(oldCustomField)
|
||||||
|
(dialog as AlertDialog?)?.dismiss()
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
customFieldDeleteButton?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setView(root)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
val dialogCreated = builder.create()
|
||||||
|
|
||||||
|
customFieldLabel?.requestFocus()
|
||||||
|
customFieldLabel?.imeOptions = EditorInfo.IME_ACTION_DONE
|
||||||
|
customFieldLabel?.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||||
|
approveIfValid()
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogCreated.window?.setSoftInputMode(SOFT_INPUT_STATE_VISIBLE)
|
||||||
|
return dialogCreated
|
||||||
|
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// To prevent auto dismiss
|
||||||
|
val d = dialog as AlertDialog?
|
||||||
|
if (d != null) {
|
||||||
|
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||||
|
positiveButton.setOnClickListener {
|
||||||
|
approveIfValid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun approveIfValid() {
|
||||||
|
if (isValid()) {
|
||||||
|
oldField?.let {
|
||||||
|
// New property with old value
|
||||||
|
entryCustomFieldListener?.onEditCustomFieldApproved(it,
|
||||||
|
Field(customFieldLabel?.text?.toString() ?: "",
|
||||||
|
ProtectedString(customFieldProtectionButton?.isChecked == true,
|
||||||
|
it.protectedValue.stringValue))
|
||||||
|
)
|
||||||
|
} ?: run {
|
||||||
|
entryCustomFieldListener?.onNewCustomFieldApproved(
|
||||||
|
Field(customFieldLabel?.text?.toString() ?: "",
|
||||||
|
ProtectedString(customFieldProtectionButton?.isChecked == true))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
(dialog as AlertDialog?)?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValid(): Boolean {
|
||||||
|
return if (customFieldLabel?.text?.toString()?.isNotEmpty() != true) {
|
||||||
|
setError(R.string.error_string_key)
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
setError(null)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setError(@StringRes errorId: Int?) {
|
||||||
|
customFieldLabelContainer?.error = if (errorId == null) null else {
|
||||||
|
requireContext().getString(errorId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntryCustomFieldListener {
|
||||||
|
fun onNewCustomFieldApproved(newField: Field)
|
||||||
|
fun onEditCustomFieldApproved(oldField: Field, newField: Field)
|
||||||
|
fun onDeleteCustomFieldApproved(oldField: Field)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val KEY_FIELD = "KEY_FIELD"
|
||||||
|
|
||||||
|
fun getInstance(): EntryCustomFieldDialogFragment {
|
||||||
|
return EntryCustomFieldDialogFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInstance(field: Field): EntryCustomFieldDialogFragment {
|
||||||
|
return EntryCustomFieldDialogFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putParcelable(KEY_FIELD, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Dialog to confirm big file to upload
|
||||||
|
*/
|
||||||
|
class FileTooBigDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private var mActionChooseListener: ActionChooseListener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
// Verify that the host activity implements the callback interface
|
||||||
|
try {
|
||||||
|
mActionChooseListener = context as ActionChooseListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + ActionChooseListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mActionChooseListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
// Use the Builder class for convenient dialog construction
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setMessage(SpannableStringBuilder().apply {
|
||||||
|
append(getString(R.string.warning_file_too_big))
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_sure_add_file))
|
||||||
|
})
|
||||||
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mActionChooseListener?.onValidateUploadFileTooBig(
|
||||||
|
arguments?.getParcelable(KEY_FILE_URI),
|
||||||
|
arguments?.getString(KEY_FILE_NAME))
|
||||||
|
}
|
||||||
|
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
// Create the AlertDialog object and return it
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionChooseListener {
|
||||||
|
fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MAX_WARNING_BINARY_FILE = 5242880
|
||||||
|
|
||||||
|
private const val KEY_FILE_URI = "KEY_FILE_URI"
|
||||||
|
private const val KEY_FILE_NAME = "KEY_FILE_NAME"
|
||||||
|
|
||||||
|
fun build(attachmentToUploadUri: Uri,
|
||||||
|
fileName: String): FileTooBigDialogFragment {
|
||||||
|
val fragment = FileTooBigDialogFragment()
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
|
||||||
|
putString(KEY_FILE_NAME, fileName)
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,6 +64,11 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|||||||
@@ -73,7 +73,11 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
throw ClassCastException(context.toString()
|
throw ClassCastException(context.toString()
|
||||||
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
editGroupListener = null
|
||||||
|
super.onDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|||||||
@@ -31,10 +31,11 @@ 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.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.widget.ImageViewCompat
|
import androidx.core.widget.ImageViewCompat
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
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
|
||||||
@@ -56,6 +57,11 @@ class IconPickerDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
iconPickerListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
@@ -132,7 +138,7 @@ class IconPickerDialogFragment : DialogFragment() {
|
|||||||
return bundle.getParcelable(KEY_ICON_STANDARD)
|
return bundle.getParcelable(KEY_ICON_STANDARD)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launch(activity: StylishActivity) {
|
fun launch(activity: FragmentActivity) {
|
||||||
// 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")
|
||||||
|
|||||||
@@ -21,20 +21,51 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.DialogInterface
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
class PasswordEncodingDialogFragment : DialogFragment() {
|
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
var positiveButtonClickListener: DialogInterface.OnClickListener? = null
|
private var mListener: Listener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
try {
|
||||||
|
mListener = context as Listener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + Listener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
|
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
|
||||||
|
val masterPasswordChecked: Boolean = savedInstanceState?.getBoolean(MASTER_PASSWORD_CHECKED_KEY) ?: false
|
||||||
|
val masterPassword: String? = savedInstanceState?.getString(MASTER_PASSWORD_KEY)
|
||||||
|
val keyFileChecked: Boolean = savedInstanceState?.getBoolean(KEY_FILE_CHECKED_KEY) ?: false
|
||||||
|
val keyFile: Uri? = savedInstanceState?.getParcelable(KEY_FILE_URI_KEY)
|
||||||
|
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
|
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
|
||||||
builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener)
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mListener?.onPasswordEncodingValidateListener(
|
||||||
|
databaseUri,
|
||||||
|
masterPasswordChecked,
|
||||||
|
masterPassword,
|
||||||
|
keyFileChecked,
|
||||||
|
keyFile
|
||||||
|
)
|
||||||
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
@@ -42,5 +73,36 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
|||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
||||||
|
masterPasswordChecked: Boolean,
|
||||||
|
masterPassword: String?,
|
||||||
|
keyFileChecked: Boolean,
|
||||||
|
keyFile: Uri?)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
||||||
|
private const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
|
||||||
|
private const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
|
||||||
|
private const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
|
||||||
|
private const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY"
|
||||||
|
|
||||||
|
fun getInstance(databaseUri: Uri,
|
||||||
|
masterPasswordChecked: Boolean,
|
||||||
|
masterPassword: String?,
|
||||||
|
keyFileChecked: Boolean,
|
||||||
|
keyFile: Uri?): SortDialogFragment {
|
||||||
|
val fragment = SortDialogFragment()
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||||
|
putBoolean(MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
||||||
|
putString(MASTER_PASSWORD_KEY, masterPassword)
|
||||||
|
putBoolean(KEY_FILE_CHECKED_KEY, keyFileChecked)
|
||||||
|
putParcelable(KEY_FILE_URI_KEY, keyFile)
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Dialog to confirm big file to upload
|
||||||
|
*/
|
||||||
|
class ReplaceFileDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private var mActionChooseListener: ActionChooseListener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
// Verify that the host activity implements the callback interface
|
||||||
|
try {
|
||||||
|
mActionChooseListener = context as ActionChooseListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + ActionChooseListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mActionChooseListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
// Use the Builder class for convenient dialog construction
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setMessage(SpannableStringBuilder().apply {
|
||||||
|
append(getString(R.string.warning_replace_file))
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_sure_add_file))
|
||||||
|
})
|
||||||
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mActionChooseListener?.onValidateReplaceFile(
|
||||||
|
arguments?.getParcelable(KEY_FILE_URI),
|
||||||
|
arguments?.getParcelable(KEY_ENTRY_ATTACHMENT))
|
||||||
|
}
|
||||||
|
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
// Create the AlertDialog object and return it
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionChooseListener {
|
||||||
|
fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_FILE_URI = "KEY_FILE_URI"
|
||||||
|
private const val KEY_ENTRY_ATTACHMENT = "KEY_ENTRY_ATTACHMENT"
|
||||||
|
|
||||||
|
fun build(attachmentToUploadUri: Uri,
|
||||||
|
attachment: Attachment): ReplaceFileDialogFragment {
|
||||||
|
val fragment = ReplaceFileDialogFragment()
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
|
||||||
|
putParcelable(KEY_ENTRY_ATTACHMENT, attachment)
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import android.text.TextWatcher
|
|||||||
import android.view.MotionEvent
|
import android.view.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)
|
||||||
@@ -106,6 +107,11 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mCreateOTPElementListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
@@ -152,6 +158,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 +393,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) {
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ class SortDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ class TimePickerFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
// Create a new instance of DatePickerDialog and return it
|
// Create a new instance of DatePickerDialog and return it
|
||||||
return context?.let {
|
return context?.let {
|
||||||
|
|||||||
@@ -24,54 +24,186 @@ 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.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
object EntrySelectionHelper {
|
object EntrySelectionHelper {
|
||||||
|
|
||||||
private const val EXTRA_ENTRY_SELECTION_MODE = "com.kunzisoft.keepass.extra.ENTRY_SELECTION_MODE"
|
private const val KEY_SPECIAL_MODE = "com.kunzisoft.keepass.extra.SPECIAL_MODE"
|
||||||
private const val DEFAULT_ENTRY_SELECTION_MODE = false
|
private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
|
||||||
// Key to retrieve search in intent
|
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
|
||||||
const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
|
||||||
|
|
||||||
fun startActivityForEntrySelectionResult(context: Context,
|
fun startActivityForSearchModeResult(context: Context,
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
searchInfo: SearchInfo?) {
|
searchInfo: SearchInfo) {
|
||||||
addEntrySelectionModeExtraInIntent(intent)
|
addSpecialModeInIntent(intent, SpecialMode.SEARCH)
|
||||||
searchInfo?.let {
|
addSearchInfoInIntent(intent, searchInfo)
|
||||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
}
|
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addEntrySelectionModeExtraInIntent(intent: Intent) {
|
fun startActivityForSaveModeResult(context: Context,
|
||||||
intent.putExtra(EXTRA_ENTRY_SELECTION_MODE, true)
|
intent: Intent,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
addSpecialModeInIntent(intent, SpecialMode.SAVE)
|
||||||
|
addTypeModeInIntent(intent, TypeMode.DEFAULT)
|
||||||
|
addSearchInfoInIntent(intent, searchInfo)
|
||||||
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeEntrySelectionModeFromIntent(intent: Intent) {
|
fun startActivityForKeyboardSelectionModeResult(context: Context,
|
||||||
intent.removeExtra(EXTRA_ENTRY_SELECTION_MODE)
|
intent: Intent,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
|
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||||
|
addTypeModeInIntent(intent, TypeMode.MAGIKEYBOARD)
|
||||||
|
addSearchInfoInIntent(intent, searchInfo)
|
||||||
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retrieveEntrySelectionModeFromIntent(intent: Intent): Boolean {
|
fun startActivityForRegistrationModeResult(context: Context,
|
||||||
return intent.getBooleanExtra(EXTRA_ENTRY_SELECTION_MODE, DEFAULT_ENTRY_SELECTION_MODE)
|
intent: Intent,
|
||||||
|
registerInfo: RegisterInfo?) {
|
||||||
|
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
||||||
|
// At the moment, only autofill for registration
|
||||||
|
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
|
||||||
|
addRegisterInfoInIntent(intent, registerInfo)
|
||||||
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun doEntrySelectionAction(intent: Intent,
|
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
|
||||||
standardAction: () -> Unit,
|
searchInfo?.let {
|
||||||
keyboardAction: () -> Unit,
|
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||||
autofillAction: (assistStructure: AssistStructure) -> Unit) {
|
|
||||||
var assistStructureInit = false
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
AutofillHelper.retrieveAssistStructure(intent)?.let { assistStructure ->
|
|
||||||
autofillAction.invoke(assistStructure)
|
|
||||||
assistStructureInit = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!assistStructureInit) {
|
}
|
||||||
if (intent.getBooleanExtra(EXTRA_ENTRY_SELECTION_MODE, DEFAULT_ENTRY_SELECTION_MODE)) {
|
|
||||||
intent.removeExtra(EXTRA_ENTRY_SELECTION_MODE)
|
fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? {
|
||||||
keyboardAction.invoke()
|
return intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||||
} else {
|
}
|
||||||
standardAction.invoke()
|
|
||||||
|
fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
|
||||||
|
registerInfo?.let {
|
||||||
|
intent.putExtra(KEY_REGISTER_INFO, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? {
|
||||||
|
return intent.getParcelableExtra(KEY_REGISTER_INFO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeInfoFromIntent(intent: Intent) {
|
||||||
|
intent.removeExtra(KEY_SEARCH_INFO)
|
||||||
|
intent.removeExtra(KEY_REGISTER_INFO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
||||||
|
intent.putExtra(KEY_SPECIAL_MODE, specialMode as Serializable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
if (AutofillHelper.retrieveAssistStructure(intent) != null)
|
||||||
|
return SpecialMode.SELECTION
|
||||||
|
}
|
||||||
|
return intent.getSerializableExtra(KEY_SPECIAL_MODE) as SpecialMode?
|
||||||
|
?: SpecialMode.DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
||||||
|
intent.putExtra(KEY_TYPE_MODE, typeMode as Serializable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
if (AutofillHelper.retrieveAssistStructure(intent) != null)
|
||||||
|
return TypeMode.AUTOFILL
|
||||||
|
}
|
||||||
|
return intent.getSerializableExtra(KEY_TYPE_MODE) as TypeMode? ?: TypeMode.DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeModesFromIntent(intent: Intent) {
|
||||||
|
intent.removeExtra(KEY_SPECIAL_MODE)
|
||||||
|
intent.removeExtra(KEY_TYPE_MODE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doSpecialAction(intent: Intent,
|
||||||
|
defaultAction: () -> Unit,
|
||||||
|
searchAction: (searchInfo: SearchInfo) -> Unit,
|
||||||
|
saveAction: (searchInfo: SearchInfo) -> Unit,
|
||||||
|
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
||||||
|
autofillSelectionAction: (searchInfo: SearchInfo?,
|
||||||
|
assistStructure: AssistStructure) -> Unit,
|
||||||
|
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
|
||||||
|
|
||||||
|
when (retrieveSpecialModeFromIntent(intent)) {
|
||||||
|
SpecialMode.DEFAULT -> {
|
||||||
|
removeModesFromIntent(intent)
|
||||||
|
removeInfoFromIntent(intent)
|
||||||
|
defaultAction.invoke()
|
||||||
|
}
|
||||||
|
SpecialMode.SEARCH -> {
|
||||||
|
val searchInfo = retrieveSearchInfoFromIntent(intent)
|
||||||
|
removeModesFromIntent(intent)
|
||||||
|
removeInfoFromIntent(intent)
|
||||||
|
if (searchInfo != null)
|
||||||
|
searchAction.invoke(searchInfo)
|
||||||
|
else {
|
||||||
|
defaultAction.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpecialMode.SAVE -> {
|
||||||
|
val searchInfo = retrieveSearchInfoFromIntent(intent)
|
||||||
|
removeModesFromIntent(intent)
|
||||||
|
removeInfoFromIntent(intent)
|
||||||
|
if (searchInfo != null)
|
||||||
|
saveAction.invoke(searchInfo)
|
||||||
|
else {
|
||||||
|
defaultAction.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpecialMode.SELECTION -> {
|
||||||
|
val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent)
|
||||||
|
var assistStructureInit = false
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
AutofillHelper.retrieveAssistStructure(intent)?.let { assistStructure ->
|
||||||
|
autofillSelectionAction.invoke(searchInfo, assistStructure)
|
||||||
|
assistStructureInit = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!assistStructureInit) {
|
||||||
|
if (intent.getSerializableExtra(KEY_SPECIAL_MODE) != null) {
|
||||||
|
when (retrieveTypeModeFromIntent(intent)) {
|
||||||
|
TypeMode.DEFAULT -> {
|
||||||
|
removeModesFromIntent(intent)
|
||||||
|
if (searchInfo != null)
|
||||||
|
searchAction.invoke(searchInfo)
|
||||||
|
else
|
||||||
|
defaultAction.invoke()
|
||||||
|
}
|
||||||
|
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
|
||||||
|
else -> {
|
||||||
|
// In this case, error
|
||||||
|
removeModesFromIntent(intent)
|
||||||
|
removeInfoFromIntent(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (searchInfo != null)
|
||||||
|
searchAction.invoke(searchInfo)
|
||||||
|
else
|
||||||
|
defaultAction.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpecialMode.REGISTRATION -> {
|
||||||
|
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
|
||||||
|
removeModesFromIntent(intent)
|
||||||
|
removeInfoFromIntent(intent)
|
||||||
|
autofillRegistrationAction.invoke(registerInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.helpers
|
||||||
|
|
||||||
|
enum class SpecialMode {
|
||||||
|
DEFAULT,
|
||||||
|
SEARCH,
|
||||||
|
SAVE,
|
||||||
|
SELECTION,
|
||||||
|
REGISTRATION;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.helpers
|
||||||
|
|
||||||
|
enum class TypeMode {
|
||||||
|
DEFAULT, MAGIKEYBOARD, AUTOFILL
|
||||||
|
}
|
||||||
@@ -19,21 +19,25 @@
|
|||||||
*/
|
*/
|
||||||
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 android.widget.Toast
|
||||||
|
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.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
|
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.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() {
|
||||||
|
|
||||||
protected var mTimeoutEnable: Boolean = true
|
protected var mTimeoutEnable: Boolean = true
|
||||||
|
|
||||||
@@ -43,16 +47,15 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
// Force readOnly if Entry Selection mode
|
// Force readOnly if Entry Selection mode
|
||||||
protected var mReadOnly: Boolean
|
protected var mReadOnly: Boolean
|
||||||
get() {
|
get() {
|
||||||
return mReadOnlyToSave || mSelectionMode
|
return mReadOnlyToSave
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
mReadOnlyToSave = value
|
mReadOnlyToSave = value
|
||||||
}
|
}
|
||||||
private var mReadOnlyToSave: Boolean = false
|
private var mReadOnlyToSave: Boolean = false
|
||||||
protected var mSelectionMode: 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?) {
|
||||||
@@ -73,6 +76,7 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
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)
|
||||||
@@ -80,7 +84,7 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
|
|
||||||
mExitLock = false
|
mExitLock = false
|
||||||
|
|
||||||
mProgressDialogThread = ProgressDialogThread(this)
|
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
@@ -96,11 +100,19 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
mProgressDialogThread?.registerProgressTask()
|
// If in ave or registration mode, don't allow read only
|
||||||
|
if ((mSpecialMode == SpecialMode.SAVE
|
||||||
|
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||||
|
&& mReadOnly) {
|
||||||
|
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
|
||||||
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
|
||||||
// To refresh when back to normal workflow from selection workflow
|
// To refresh when back to normal workflow from selection workflow
|
||||||
mReadOnlyToSave = ReadOnlyHelper.retrieveReadOnlyFromIntent(intent)
|
mReadOnlyToSave = ReadOnlyHelper.retrieveReadOnlyFromIntent(intent)
|
||||||
mSelectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
|
|
||||||
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
|
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
|
||||||
|
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
@@ -132,7 +144,7 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
LOCKING_ACTIVITY_UI_VISIBLE = false
|
LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||||
|
|
||||||
mProgressDialogThread?.unregisterProgressTask()
|
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
@@ -154,11 +166,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.selection
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
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.helpers.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.TypeMode
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
|
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 mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
||||||
|
protected var mTypeMode: TypeMode = TypeMode.DEFAULT
|
||||||
|
|
||||||
|
private var mSpecialModeView: SpecialModeView? = null
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (mSpecialMode != SpecialMode.DEFAULT)
|
||||||
|
onCancelSpecialMode()
|
||||||
|
else
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To call the regular onBackPressed() method in special mode
|
||||||
|
*/
|
||||||
|
protected fun onRegularBackPressed() {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intent sender uses special retains data in callback
|
||||||
|
*/
|
||||||
|
private fun isIntentSender(): Boolean {
|
||||||
|
return (mSpecialMode == SpecialMode.SELECTION
|
||||||
|
&& mTypeMode == TypeMode.AUTOFILL)
|
||||||
|
/* TODO Registration callback #765
|
||||||
|
|| (mSpecialMode == SpecialMode.REGISTRATION
|
||||||
|
&& mTypeMode == TypeMode.AUTOFILL
|
||||||
|
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onLaunchActivitySpecialMode() {
|
||||||
|
if (!isIntentSender()) {
|
||||||
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
|
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onValidateSpecialMode() {
|
||||||
|
if (isIntentSender()) {
|
||||||
|
super.finish()
|
||||||
|
} else {
|
||||||
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
|
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||||
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
|
// To move the app in background
|
||||||
|
moveTaskToBack(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onCancelSpecialMode() {
|
||||||
|
if (isIntentSender()) {
|
||||||
|
// To get the app caller, only for IntentSender
|
||||||
|
super.onBackPressed()
|
||||||
|
} else {
|
||||||
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
|
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||||
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
|
// To move the app in background
|
||||||
|
moveTaskToBack(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun backToTheAppCaller() {
|
||||||
|
if (isIntentSender()) {
|
||||||
|
// To get the app caller, only for IntentSender
|
||||||
|
super.onBackPressed()
|
||||||
|
} else {
|
||||||
|
// To move the app in background
|
||||||
|
moveTaskToBack(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
||||||
|
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
||||||
|
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
||||||
|
val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)?.searchInfo
|
||||||
|
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||||
|
|
||||||
|
// To show the selection mode
|
||||||
|
mSpecialModeView = findViewById(R.id.special_mode_view)
|
||||||
|
mSpecialModeView?.apply {
|
||||||
|
// Populate title
|
||||||
|
val selectionModeStringId = when (mSpecialMode) {
|
||||||
|
SpecialMode.DEFAULT, // Not important because hidden
|
||||||
|
SpecialMode.SEARCH -> R.string.search_mode
|
||||||
|
SpecialMode.SAVE -> R.string.save_mode
|
||||||
|
SpecialMode.SELECTION -> R.string.selection_mode
|
||||||
|
SpecialMode.REGISTRATION -> R.string.registration_mode
|
||||||
|
}
|
||||||
|
val typeModeStringId = when (mTypeMode) {
|
||||||
|
TypeMode.DEFAULT, // Not important because hidden
|
||||||
|
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
|
||||||
|
TypeMode.AUTOFILL -> R.string.autofill
|
||||||
|
}
|
||||||
|
title = getString(selectionModeStringId)
|
||||||
|
if (mTypeMode != TypeMode.DEFAULT)
|
||||||
|
title = "$title (${getString(typeModeStringId)})"
|
||||||
|
// Populate subtitle
|
||||||
|
subtitle = searchInfo?.getName(resources)
|
||||||
|
|
||||||
|
// Show the toolbar or not
|
||||||
|
visible = when (mSpecialMode) {
|
||||||
|
SpecialMode.DEFAULT -> false
|
||||||
|
SpecialMode.SEARCH -> true
|
||||||
|
SpecialMode.SAVE -> true
|
||||||
|
SpecialMode.SELECTION -> true
|
||||||
|
SpecialMode.REGISTRATION -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add back listener
|
||||||
|
onCancelButtonClickListener = View.OnClickListener {
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create menu
|
||||||
|
menu.clear()
|
||||||
|
if (mTypeMode == TypeMode.AUTOFILL) {
|
||||||
|
menuInflater.inflate(R.menu.autofill, menu)
|
||||||
|
setOnMenuItemClickListener { menuItem ->
|
||||||
|
when (menuItem.itemId) {
|
||||||
|
R.id.menu_block_autofill -> {
|
||||||
|
blockAutofill(searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// To hide home button from the regular toolbar in special mode
|
||||||
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,148 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.TypedArray
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.text.format.Formatter
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
|
|
||||||
|
|
||||||
|
class EntryAttachmentsItemsAdapter(context: Context)
|
||||||
|
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
|
||||||
|
|
||||||
|
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
|
||||||
|
|
||||||
|
private var mTitleColor: Int
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Get the primary text color of the theme
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
context.theme.resolveAttribute(android.R.attr.textColorPrimary, typedValue, true)
|
||||||
|
val typedArray: TypedArray = context.obtainStyledAttributes(typedValue.data, intArrayOf(
|
||||||
|
android.R.attr.textColor))
|
||||||
|
mTitleColor = typedArray.getColor(0, -1)
|
||||||
|
typedArray.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder {
|
||||||
|
return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) {
|
||||||
|
val entryAttachmentState = itemsList[position]
|
||||||
|
|
||||||
|
holder.itemView.visibility = View.VISIBLE
|
||||||
|
holder.binaryFileBroken.apply {
|
||||||
|
setColorFilter(Color.RED)
|
||||||
|
visibility = if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder.binaryFileTitle.text = entryAttachmentState.attachment.name
|
||||||
|
if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
|
||||||
|
holder.binaryFileTitle.setTextColor(Color.RED)
|
||||||
|
} else {
|
||||||
|
holder.binaryFileTitle.setTextColor(mTitleColor)
|
||||||
|
}
|
||||||
|
holder.binaryFileSize.text = Formatter.formatFileSize(context,
|
||||||
|
entryAttachmentState.attachment.binaryAttachment.length())
|
||||||
|
holder.binaryFileCompression.apply {
|
||||||
|
if (entryAttachmentState.attachment.binaryAttachment.isCompressed) {
|
||||||
|
text = CompressionAlgorithm.GZip.getName(context.resources)
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
text = ""
|
||||||
|
visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when (entryAttachmentState.streamDirection) {
|
||||||
|
StreamDirection.UPLOAD -> {
|
||||||
|
holder.binaryFileProgressIcon.isActivated = true
|
||||||
|
when (entryAttachmentState.downloadState) {
|
||||||
|
AttachmentState.START,
|
||||||
|
AttachmentState.IN_PROGRESS -> {
|
||||||
|
holder.binaryFileProgressContainer.visibility = View.VISIBLE
|
||||||
|
holder.binaryFileProgress.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
progress = entryAttachmentState.downloadProgression
|
||||||
|
}
|
||||||
|
holder.binaryFileDeleteButton.apply {
|
||||||
|
visibility = View.GONE
|
||||||
|
setOnClickListener(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AttachmentState.NULL,
|
||||||
|
AttachmentState.ERROR,
|
||||||
|
AttachmentState.COMPLETE -> {
|
||||||
|
holder.binaryFileProgressContainer.visibility = View.GONE
|
||||||
|
holder.binaryFileProgress.visibility = View.GONE
|
||||||
|
holder.binaryFileDeleteButton.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
onBindDeleteButton(holder, this, entryAttachmentState, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder.itemView.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
StreamDirection.DOWNLOAD -> {
|
||||||
|
holder.binaryFileProgressIcon.isActivated = false
|
||||||
|
holder.binaryFileProgressContainer.visibility = View.VISIBLE
|
||||||
|
holder.binaryFileDeleteButton.visibility = View.GONE
|
||||||
|
holder.binaryFileProgress.apply {
|
||||||
|
visibility = when (entryAttachmentState.downloadState) {
|
||||||
|
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
|
||||||
|
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
|
||||||
|
}
|
||||||
|
progress = entryAttachmentState.downloadProgression
|
||||||
|
}
|
||||||
|
holder.itemView.setOnClickListener {
|
||||||
|
onItemClickListener?.invoke(entryAttachmentState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
var binaryFileBroken: ImageView = itemView.findViewById(R.id.item_attachment_broken)
|
||||||
|
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
|
||||||
|
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
|
||||||
|
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
|
||||||
|
var binaryFileProgressContainer: View = itemView.findViewById(R.id.item_attachment_progress_container)
|
||||||
|
var binaryFileProgressIcon: ImageView = itemView.findViewById(R.id.item_attachment_icon)
|
||||||
|
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
|
||||||
|
var binaryFileDeleteButton: View = itemView.findViewById(R.id.item_attachment_delete_button)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,31 +22,33 @@ package com.kunzisoft.keepass.adapters
|
|||||||
import android.content.Context
|
import android.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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,72 +52,72 @@ import java.util.*
|
|||||||
class NodeAdapter (private val context: Context)
|
class NodeAdapter (private val context: Context)
|
||||||
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
||||||
|
|
||||||
private var nodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
||||||
private val nodeSortedListCallback: NodeSortedListCallback
|
private val mNodeSortedListCallback: NodeSortedListCallback
|
||||||
private val nodeSortedList: SortedList<Node>
|
private val mNodeSortedList: SortedList<Node>
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
private val mInflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
|
||||||
private var calculateViewTypeTextSize = Array(2) { true} // number of view type
|
private var mCalculateViewTypeTextSize = Array(2) { true} // number of view type
|
||||||
private var textSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
||||||
private var prefSizeMultiplier: Float = 0F
|
private var mPrefSizeMultiplier: Float = 0F
|
||||||
private var subtextDefaultDimension: Float = 0F
|
private var mSubtextDefaultDimension: Float = 0F
|
||||||
private var infoTextDefaultDimension: Float = 0F
|
private var mInfoTextDefaultDimension: Float = 0F
|
||||||
private var numberChildrenTextDefaultDimension: Float = 0F
|
private var mNumberChildrenTextDefaultDimension: Float = 0F
|
||||||
private var iconDefaultDimension: Float = 0F
|
private var mIconDefaultDimension: Float = 0F
|
||||||
|
|
||||||
private var showUserNames: Boolean = true
|
private var mShowUserNames: Boolean = true
|
||||||
private var showNumberEntries: Boolean = true
|
private var mShowNumberEntries: Boolean = true
|
||||||
private var entryFilters = arrayOf<Group.ChildFilter>()
|
private var mEntryFilters = arrayOf<Group.ChildFilter>()
|
||||||
|
|
||||||
private var actionNodesList = LinkedList<Node>()
|
private var mActionNodesList = LinkedList<Node>()
|
||||||
private var nodeClickCallback: NodeClickCallback? = null
|
private var mNodeClickCallback: NodeClickCallback? = null
|
||||||
|
|
||||||
private val mDatabase: Database
|
private val mDatabase: Database
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private val contentSelectionColor: Int
|
private val mContentSelectionColor: Int
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private val iconGroupColor: Int
|
private val mIconGroupColor: Int
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private val iconEntryColor: Int
|
private val mIconEntryColor: Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the adapter contains or not any element
|
* Determine if the adapter contains or not any element
|
||||||
* @return true if the list is empty
|
* @return true if the list is empty
|
||||||
*/
|
*/
|
||||||
val isEmpty: Boolean
|
val isEmpty: Boolean
|
||||||
get() = nodeSortedList.size() <= 0
|
get() = mNodeSortedList.size() <= 0
|
||||||
|
|
||||||
init {
|
init {
|
||||||
this.infoTextDefaultDimension = context.resources.getDimension(R.dimen.list_medium_size_default)
|
this.mInfoTextDefaultDimension = context.resources.getDimension(R.dimen.list_medium_size_default)
|
||||||
this.subtextDefaultDimension = context.resources.getDimension(R.dimen.list_small_size_default)
|
this.mSubtextDefaultDimension = context.resources.getDimension(R.dimen.list_small_size_default)
|
||||||
this.numberChildrenTextDefaultDimension = context.resources.getDimension(R.dimen.list_tiny_size_default)
|
this.mNumberChildrenTextDefaultDimension = context.resources.getDimension(R.dimen.list_tiny_size_default)
|
||||||
this.iconDefaultDimension = context.resources.getDimension(R.dimen.list_icon_size_default)
|
this.mIconDefaultDimension = context.resources.getDimension(R.dimen.list_icon_size_default)
|
||||||
|
|
||||||
assignPreferences()
|
assignPreferences()
|
||||||
|
|
||||||
this.nodeSortedListCallback = NodeSortedListCallback()
|
this.mNodeSortedListCallback = NodeSortedListCallback()
|
||||||
this.nodeSortedList = SortedList(Node::class.java, nodeSortedListCallback)
|
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
this.mDatabase = Database.getInstance()
|
this.mDatabase = Database.getInstance()
|
||||||
|
|
||||||
// Color of content selection
|
// Color of content selection
|
||||||
val taContentSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
val taContentSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||||
this.contentSelectionColor = taContentSelectionColor.getColor(0, Color.WHITE)
|
this.mContentSelectionColor = taContentSelectionColor.getColor(0, Color.WHITE)
|
||||||
taContentSelectionColor.recycle()
|
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.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||||
taTextColorPrimary.recycle()
|
taTextColorPrimary.recycle()
|
||||||
// In two times to fix bug compilation
|
// In two times to fix bug compilation
|
||||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
this.iconEntryColor = taTextColor.getColor(0, Color.BLACK)
|
this.mIconEntryColor = taTextColor.getColor(0, Color.BLACK)
|
||||||
taTextColor.recycle()
|
taTextColor.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assignPreferences() {
|
fun assignPreferences() {
|
||||||
this.prefSizeMultiplier = PreferencesUtil.getListTextSize(context)
|
this.mPrefSizeMultiplier = PreferencesUtil.getListTextSize(context)
|
||||||
|
|
||||||
notifyChangeSort(
|
notifyChangeSort(
|
||||||
PreferencesUtil.getListSort(context),
|
PreferencesUtil.getListSort(context),
|
||||||
@@ -128,13 +128,13 @@ class NodeAdapter (private val context: Context)
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
||||||
this.showNumberEntries = PreferencesUtil.showNumberEntries(context)
|
this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||||
|
|
||||||
this.entryFilters = Group.ChildFilter.getDefaults(context)
|
this.mEntryFilters = Group.ChildFilter.getDefaults(context)
|
||||||
|
|
||||||
// Reinit textSize for all view type
|
// Reinit textSize for all view type
|
||||||
calculateViewTypeTextSize.forEachIndexed { index, _ -> calculateViewTypeTextSize[index] = true }
|
mCalculateViewTypeTextSize.forEachIndexed { index, _ -> mCalculateViewTypeTextSize[index] = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,12 +142,12 @@ class NodeAdapter (private val context: Context)
|
|||||||
*/
|
*/
|
||||||
fun rebuildList(group: Group) {
|
fun rebuildList(group: Group) {
|
||||||
assignPreferences()
|
assignPreferences()
|
||||||
nodeSortedList.replaceAll(group.getFilteredChildren(entryFilters))
|
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
|
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
|
||||||
override fun compare(item1: Node, item2: Node): Int {
|
override fun compare(item1: Node, item2: Node): Int {
|
||||||
return nodeComparator!!.compare(item1, item2)
|
return mNodeComparator!!.compare(item1, item2)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
||||||
@@ -162,7 +162,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun contains(node: Node): Boolean {
|
fun contains(node: Node): Boolean {
|
||||||
return nodeSortedList.indexOf(node) != SortedList.INVALID_POSITION
|
return mNodeSortedList.indexOf(node) != SortedList.INVALID_POSITION
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,7 +170,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
* @param node Node to add
|
* @param node Node to add
|
||||||
*/
|
*/
|
||||||
fun addNode(node: Node) {
|
fun addNode(node: Node) {
|
||||||
nodeSortedList.add(node)
|
mNodeSortedList.add(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -178,7 +178,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
* @param nodes Nodes to add
|
* @param nodes Nodes to add
|
||||||
*/
|
*/
|
||||||
fun addNodes(nodes: List<Node>) {
|
fun addNodes(nodes: List<Node>) {
|
||||||
nodeSortedList.addAll(nodes)
|
mNodeSortedList.addAll(nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -186,7 +186,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
* @param node Node to delete
|
* @param node Node to delete
|
||||||
*/
|
*/
|
||||||
fun removeNode(node: Node) {
|
fun removeNode(node: Node) {
|
||||||
nodeSortedList.remove(node)
|
mNodeSortedList.remove(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -195,7 +195,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
*/
|
*/
|
||||||
fun removeNodes(nodes: List<Node>) {
|
fun removeNodes(nodes: List<Node>) {
|
||||||
nodes.forEach { node ->
|
nodes.forEach { node ->
|
||||||
nodeSortedList.remove(node)
|
mNodeSortedList.remove(node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,9 +203,9 @@ class NodeAdapter (private val context: Context)
|
|||||||
* Remove a node at [position] in the list
|
* Remove a node at [position] in the list
|
||||||
*/
|
*/
|
||||||
fun removeNodeAt(position: Int) {
|
fun removeNodeAt(position: Int) {
|
||||||
nodeSortedList.removeItemAt(position)
|
mNodeSortedList.removeItemAt(position)
|
||||||
// Refresh all the next items
|
// Refresh all the next items
|
||||||
notifyItemRangeChanged(position, nodeSortedList.size() - position)
|
notifyItemRangeChanged(position, mNodeSortedList.size() - position)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -226,10 +226,10 @@ class NodeAdapter (private val context: Context)
|
|||||||
* @param newNode Node after the update
|
* @param newNode Node after the update
|
||||||
*/
|
*/
|
||||||
fun updateNode(oldNode: Node, newNode: Node) {
|
fun updateNode(oldNode: Node, newNode: Node) {
|
||||||
nodeSortedList.beginBatchedUpdates()
|
mNodeSortedList.beginBatchedUpdates()
|
||||||
nodeSortedList.remove(oldNode)
|
mNodeSortedList.remove(oldNode)
|
||||||
nodeSortedList.add(newNode)
|
mNodeSortedList.add(newNode)
|
||||||
nodeSortedList.endBatchedUpdates()
|
mNodeSortedList.endBatchedUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -238,30 +238,30 @@ class NodeAdapter (private val context: Context)
|
|||||||
* @param newNodes Node after the update
|
* @param newNodes Node after the update
|
||||||
*/
|
*/
|
||||||
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
|
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
|
||||||
nodeSortedList.beginBatchedUpdates()
|
mNodeSortedList.beginBatchedUpdates()
|
||||||
oldNodes.forEach { oldNode ->
|
oldNodes.forEach { oldNode ->
|
||||||
nodeSortedList.remove(oldNode)
|
mNodeSortedList.remove(oldNode)
|
||||||
}
|
}
|
||||||
nodeSortedList.addAll(newNodes)
|
mNodeSortedList.addAll(newNodes)
|
||||||
nodeSortedList.endBatchedUpdates()
|
mNodeSortedList.endBatchedUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun notifyNodeChanged(node: Node) {
|
fun notifyNodeChanged(node: Node) {
|
||||||
notifyItemChanged(nodeSortedList.indexOf(node))
|
notifyItemChanged(mNodeSortedList.indexOf(node))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setActionNodes(actionNodes: List<Node>) {
|
fun setActionNodes(actionNodes: List<Node>) {
|
||||||
this.actionNodesList.apply {
|
this.mActionNodesList.apply {
|
||||||
clear()
|
clear()
|
||||||
addAll(actionNodes)
|
addAll(actionNodes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unselectActionNodes() {
|
fun unselectActionNodes() {
|
||||||
actionNodesList.forEach {
|
mActionNodesList.forEach {
|
||||||
notifyItemChanged(nodeSortedList.indexOf(it))
|
notifyItemChanged(mNodeSortedList.indexOf(it))
|
||||||
}
|
}
|
||||||
this.actionNodesList.apply {
|
this.mActionNodesList.apply {
|
||||||
clear()
|
clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,49 +271,49 @@ class NodeAdapter (private val context: Context)
|
|||||||
*/
|
*/
|
||||||
fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
|
fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
|
||||||
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||||
this.nodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
|
this.mNodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
return nodeSortedList.get(position).type.ordinal
|
return mNodeSortedList.get(position).type.ordinal
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NodeViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NodeViewHolder {
|
||||||
val view: View = if (viewType == Type.GROUP.ordinal) {
|
val view: View = if (viewType == Type.GROUP.ordinal) {
|
||||||
inflater.inflate(R.layout.item_list_nodes_group, parent, false)
|
mInflater.inflate(R.layout.item_list_nodes_group, parent, false)
|
||||||
} else {
|
} else {
|
||||||
inflater.inflate(R.layout.item_list_nodes_entry, parent, false)
|
mInflater.inflate(R.layout.item_list_nodes_entry, parent, false)
|
||||||
}
|
}
|
||||||
return NodeViewHolder(view)
|
return NodeViewHolder(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: NodeViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: NodeViewHolder, position: Int) {
|
||||||
val subNode = nodeSortedList.get(position)
|
val subNode = mNodeSortedList.get(position)
|
||||||
|
|
||||||
// Node selection
|
// Node selection
|
||||||
holder.container.isSelected = actionNodesList.contains(subNode)
|
holder.container.isSelected = mActionNodesList.contains(subNode)
|
||||||
|
|
||||||
// Assign image
|
// Assign image
|
||||||
val iconColor = if (holder.container.isSelected)
|
val iconColor = if (holder.container.isSelected)
|
||||||
contentSelectionColor
|
mContentSelectionColor
|
||||||
else when (subNode.type) {
|
else when (subNode.type) {
|
||||||
Type.GROUP -> iconGroupColor
|
Type.GROUP -> mIconGroupColor
|
||||||
Type.ENTRY -> iconEntryColor
|
Type.ENTRY -> mIconEntryColor
|
||||||
}
|
}
|
||||||
holder.imageIdentifier?.setColorFilter(iconColor)
|
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
|
||||||
layoutParams?.apply {
|
layoutParams?.apply {
|
||||||
height = (iconDefaultDimension * prefSizeMultiplier).toInt()
|
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
||||||
width = (iconDefaultDimension * prefSizeMultiplier).toInt()
|
width = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign text
|
// Assign text
|
||||||
holder.text.apply {
|
holder.text.apply {
|
||||||
text = subNode.title
|
text = subNode.title
|
||||||
setTextSize(textSizeUnit, infoTextDefaultDimension, prefSizeMultiplier)
|
setTextSize(mTextSizeUnit, mInfoTextDefaultDimension, mPrefSizeMultiplier)
|
||||||
strikeOut(subNode.isCurrentlyExpires)
|
strikeOut(subNode.isCurrentlyExpires)
|
||||||
}
|
}
|
||||||
// Add subText with username
|
// Add subText with username
|
||||||
@@ -331,24 +331,27 @@ class NodeAdapter (private val context: Context)
|
|||||||
holder.text.text = entry.getVisualTitle()
|
holder.text.text = entry.getVisualTitle()
|
||||||
holder.subText.apply {
|
holder.subText.apply {
|
||||||
val username = entry.username
|
val username = entry.username
|
||||||
if (showUserNames && username.isNotEmpty()) {
|
if (mShowUserNames && username.isNotEmpty()) {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
text = username
|
text = username
|
||||||
setTextSize(textSizeUnit, subtextDefaultDimension, prefSizeMultiplier)
|
setTextSize(mTextSizeUnit, mSubtextDefaultDimension, mPrefSizeMultiplier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
holder.attachmentIcon?.visibility =
|
||||||
|
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
mDatabase.stopManageEntry(entry)
|
mDatabase.stopManageEntry(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add number of entries in groups
|
// Add number of entries in groups
|
||||||
if (subNode.type == Type.GROUP) {
|
if (subNode.type == Type.GROUP) {
|
||||||
if (showNumberEntries) {
|
if (mShowNumberEntries) {
|
||||||
holder.numberChildren?.apply {
|
holder.numberChildren?.apply {
|
||||||
text = (subNode as Group)
|
text = (subNode as Group)
|
||||||
.getNumberOfChildEntries(entryFilters)
|
.getNumberOfChildEntries(mEntryFilters)
|
||||||
.toString()
|
.toString()
|
||||||
setTextSize(textSizeUnit, numberChildrenTextDefaultDimension, prefSizeMultiplier)
|
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -358,22 +361,22 @@ class NodeAdapter (private val context: Context)
|
|||||||
|
|
||||||
// Assign click
|
// Assign click
|
||||||
holder.container.setOnClickListener {
|
holder.container.setOnClickListener {
|
||||||
nodeClickCallback?.onNodeClick(subNode)
|
mNodeClickCallback?.onNodeClick(subNode)
|
||||||
}
|
}
|
||||||
holder.container.setOnLongClickListener {
|
holder.container.setOnLongClickListener {
|
||||||
nodeClickCallback?.onNodeLongClick(subNode) ?: false
|
mNodeClickCallback?.onNodeLongClick(subNode) ?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return nodeSortedList.size()
|
return mNodeSortedList.size()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assign a listener when a node is clicked
|
* Assign a listener when a node is clicked
|
||||||
*/
|
*/
|
||||||
fun setOnNodeClickListener(nodeClickCallback: NodeClickCallback?) {
|
fun setOnNodeClickListener(nodeClickCallback: NodeClickCallback?) {
|
||||||
this.nodeClickCallback = nodeClickCallback
|
this.mNodeClickCallback = nodeClickCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -391,6 +394,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
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,7 +131,9 @@ 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))) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.app
|
|||||||
import androidx.multidex.MultiDexApplication
|
import androidx.multidex.MultiDexApplication
|
||||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class App : MultiDexApplication() {
|
class App : MultiDexApplication() {
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ class App : MultiDexApplication() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onTerminate() {
|
override fun onTerminate() {
|
||||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
|
||||||
super.onTerminate()
|
super.onTerminate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,45 @@ package com.kunzisoft.keepass.app.database
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
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 +73,124 @@ 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
|
||||||
|
try {
|
||||||
|
if (fileDatabaseHistoryRetrieve == null) {
|
||||||
|
databaseFileHistoryDao.add(fileDatabaseHistory)
|
||||||
|
} else {
|
||||||
|
databaseFileHistoryDao.update(fileDatabaseHistory)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to add or update database history", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +198,7 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAllKeyFiles() {
|
fun deleteAllKeyFiles() {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.deleteAllKeyFiles()
|
databaseFileHistoryDao.deleteAllKeyFiles()
|
||||||
}
|
}
|
||||||
@@ -129,12 +206,14 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAll() {
|
fun deleteAll() {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.deleteAll()
|
databaseFileHistoryDao.deleteAll()
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : SingletonHolderParameter<FileDatabaseHistoryAction, Context>(::FileDatabaseHistoryAction)
|
companion object : SingletonHolderParameter<FileDatabaseHistoryAction, Context>(::FileDatabaseHistoryAction) {
|
||||||
|
private val TAG = FileDatabaseHistoryAction::class.java.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +34,7 @@ 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.activities.helpers.SpecialMode
|
||||||
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.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
@@ -119,29 +119,33 @@ 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)) {
|
} else {
|
||||||
activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure ->
|
var setResultOk = false
|
||||||
StructureParser(structure).parse()?.let { result ->
|
activity.intent?.extras?.let { extras ->
|
||||||
// New Response
|
if (extras.containsKey(ASSIST_STRUCTURE)) {
|
||||||
val responseBuilder = FillResponse.Builder()
|
activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure ->
|
||||||
entriesInfo.forEach {
|
StructureParser(structure).parse()?.let { result ->
|
||||||
responseBuilder.addDataset(buildDataset(activity, it, 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)
|
||||||
}
|
}
|
||||||
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) {
|
||||||
if (!setResultOk) {
|
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
||||||
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
activity.setResult(Activity.RESULT_CANCELED)
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,11 +157,9 @@ object AutofillHelper {
|
|||||||
intent: Intent,
|
intent: Intent,
|
||||||
assistStructure: AssistStructure,
|
assistStructure: AssistStructure,
|
||||||
searchInfo: SearchInfo?) {
|
searchInfo: SearchInfo?) {
|
||||||
EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent)
|
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||||
intent.putExtra(ASSIST_STRUCTURE, assistStructure)
|
intent.putExtra(ASSIST_STRUCTURE, assistStructure)
|
||||||
searchInfo?.let {
|
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
||||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
|
||||||
}
|
|
||||||
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
|
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,57 +23,91 @@ import android.os.Build
|
|||||||
import android.os.CancellationSignal
|
import android.os.CancellationSignal
|
||||||
import android.service.autofill.*
|
import android.service.autofill.*
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.autofill.AutofillId
|
||||||
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.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.database.search.SearchHelper
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
@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
|
||||||
|
var askToSaveData: Boolean = false
|
||||||
|
private var mLock = AtomicBoolean()
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
|
||||||
|
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
|
||||||
|
askToSaveData = PreferencesUtil.askToSaveAutofillData(this) // TODO apply when changed
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFillRequest(request: FillRequest,
|
override fun onFillRequest(request: FillRequest,
|
||||||
cancellationSignal: CancellationSignal,
|
cancellationSignal: CancellationSignal,
|
||||||
callback: FillCallback) {
|
callback: FillCallback) {
|
||||||
val fillContexts = request.fillContexts
|
|
||||||
val latestStructure = fillContexts[fillContexts.size - 1].structure
|
|
||||||
|
|
||||||
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
||||||
|
|
||||||
// Check user's settings for authenticating Responses and Datasets.
|
// Lock
|
||||||
StructureParser(latestStructure).parse()?.let { parseResult ->
|
if (!mLock.get()) {
|
||||||
|
mLock.set(true)
|
||||||
|
// Check user's settings for authenticating Responses and Datasets.
|
||||||
|
val latestStructure = request.fillContexts.last().structure
|
||||||
|
StructureParser(latestStructure).parse()?.let { parseResult ->
|
||||||
|
|
||||||
val searchInfo = SearchInfo().apply {
|
// Build search info only if applicationId or webDomain are not blocked
|
||||||
applicationId = parseResult.applicationId
|
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
|
||||||
webDomain = parseResult.domain
|
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
|
||||||
}
|
val searchInfo = SearchInfo().apply {
|
||||||
|
applicationId = parseResult.applicationId
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
webDomain = parseResult.webDomain
|
||||||
Database.getInstance(),
|
webScheme = parseResult.webScheme
|
||||||
searchInfo,
|
|
||||||
{ items ->
|
|
||||||
val responseBuilder = FillResponse.Builder()
|
|
||||||
AutofillHelper.addHeader(responseBuilder, packageName,
|
|
||||||
parseResult.domain, parseResult.applicationId)
|
|
||||||
items.forEach {
|
|
||||||
responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult))
|
|
||||||
}
|
|
||||||
callback.onSuccess(responseBuilder.build())
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Show UI if no search result
|
|
||||||
showUIForEntrySelection(parseResult, searchInfo, callback)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Show UI if database not open
|
|
||||||
showUIForEntrySelection(parseResult, searchInfo, callback)
|
|
||||||
}
|
}
|
||||||
)
|
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
|
||||||
|
searchInfo.webDomain = webDomainWithoutSubDomain
|
||||||
|
launchSelection(searchInfo, parseResult, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun launchSelection(searchInfo: SearchInfo,
|
||||||
|
parseResult: StructureParser.Result,
|
||||||
|
callback: FillCallback) {
|
||||||
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
Database.getInstance(),
|
||||||
|
searchInfo,
|
||||||
|
{ items ->
|
||||||
|
val responseBuilder = FillResponse.Builder()
|
||||||
|
AutofillHelper.addHeader(responseBuilder, packageName,
|
||||||
|
parseResult.webDomain, parseResult.applicationId)
|
||||||
|
items.forEach {
|
||||||
|
responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult))
|
||||||
|
}
|
||||||
|
callback.onSuccess(responseBuilder.build())
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Show UI if no search result
|
||||||
|
showUIForEntrySelection(parseResult, searchInfo, callback)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Show UI if database not open
|
||||||
|
showUIForEntrySelection(parseResult, searchInfo, callback)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||||
searchInfo: SearchInfo,
|
searchInfo: SearchInfo,
|
||||||
callback: FillCallback) {
|
callback: FillCallback) {
|
||||||
@@ -81,12 +115,12 @@ class KeeAutofillService : AutofillService() {
|
|||||||
if (autofillIds.isNotEmpty()) {
|
if (autofillIds.isNotEmpty()) {
|
||||||
// If the entire Autofill Response is authenticated, AuthActivity is used
|
// If the entire Autofill Response is authenticated, AuthActivity is used
|
||||||
// to generate Response.
|
// to generate Response.
|
||||||
val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this,
|
val intentSender = AutofillLauncherActivity.getAuthIntentSenderForSelection(this,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
val responseBuilder = FillResponse.Builder()
|
val responseBuilder = FillResponse.Builder()
|
||||||
val remoteViewsUnlock: RemoteViews = if (!parseResult.domain.isNullOrEmpty()) {
|
val remoteViewsUnlock: RemoteViews = if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||||
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
|
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
|
||||||
setTextViewText(R.id.autofill_web_domain_text, parseResult.domain)
|
setTextViewText(R.id.autofill_web_domain_text, parseResult.webDomain)
|
||||||
}
|
}
|
||||||
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
||||||
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
|
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
|
||||||
@@ -95,15 +129,63 @@ class KeeAutofillService : AutofillService() {
|
|||||||
} else {
|
} else {
|
||||||
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
||||||
}
|
}
|
||||||
responseBuilder.setAuthentication(autofillIds, sender, remoteViewsUnlock)
|
|
||||||
|
// Tell to service the interest to save credentials
|
||||||
|
if (askToSaveData) {
|
||||||
|
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
|
||||||
|
val info = ArrayList<AutofillId>()
|
||||||
|
// Only if at least a password
|
||||||
|
parseResult.passwordId?.let { passwordInfo ->
|
||||||
|
parseResult.usernameId?.let { usernameInfo ->
|
||||||
|
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
|
||||||
|
info.add(usernameInfo)
|
||||||
|
}
|
||||||
|
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
|
||||||
|
info.add(passwordInfo)
|
||||||
|
}
|
||||||
|
if (info.isNotEmpty()) {
|
||||||
|
responseBuilder.setSaveInfo(
|
||||||
|
SaveInfo.Builder(types, info.toTypedArray()).build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Build response
|
||||||
|
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
||||||
callback.onSuccess(responseBuilder.build())
|
callback.onSuccess(responseBuilder.build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
||||||
// TODO Save autofill
|
if (askToSaveData) {
|
||||||
//callback.onFailure(getString(R.string.autofill_not_support_save));
|
val latestStructure = request.fillContexts.last().structure
|
||||||
|
StructureParser(latestStructure).parse(true)?.let { parseResult ->
|
||||||
|
|
||||||
|
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
|
||||||
|
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
|
||||||
|
Log.d(TAG, "autofill onSaveRequest password")
|
||||||
|
|
||||||
|
// Show UI to save data
|
||||||
|
val registerInfo = RegisterInfo(SearchInfo().apply {
|
||||||
|
applicationId = parseResult.applicationId
|
||||||
|
webDomain = parseResult.webDomain
|
||||||
|
webScheme = parseResult.webScheme
|
||||||
|
},
|
||||||
|
parseResult.usernameValue?.textValue?.toString(),
|
||||||
|
parseResult.passwordValue?.textValue?.toString())
|
||||||
|
// TODO Callback in each activity #765
|
||||||
|
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
|
||||||
|
// registerInfo))
|
||||||
|
//} else {
|
||||||
|
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
|
||||||
|
callback.onSuccess()
|
||||||
|
//}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback.onFailure("Saving form values is not allowed")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConnected() {
|
override fun onConnected() {
|
||||||
@@ -111,10 +193,24 @@ class KeeAutofillService : AutofillService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDisconnected() {
|
override fun onDisconnected() {
|
||||||
|
mLock.set(false)
|
||||||
Log.d(TAG, "onDisconnected")
|
Log.d(TAG, "onDisconnected")
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = KeeAutofillService::class.java.name
|
private val TAG = KeeAutofillService::class.java.name
|
||||||
|
|
||||||
|
fun autofillAllowedFor(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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import androidx.annotation.RequiresApi
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.autofill.AutofillId
|
import android.view.autofill.AutofillId
|
||||||
|
import android.view.autofill.AutofillValue
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
@@ -34,14 +35,19 @@ import java.util.*
|
|||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
internal class StructureParser(private val structure: AssistStructure) {
|
internal class StructureParser(private val structure: AssistStructure) {
|
||||||
private var result: Result? = null
|
private var result: Result? = null
|
||||||
private var usernameCandidate: AutofillId? = null
|
|
||||||
private var usernameNeeded = true
|
private var usernameNeeded = true
|
||||||
|
|
||||||
fun parse(): Result? {
|
private var usernameCandidate: AutofillId? = null
|
||||||
|
private var usernameValueCandidate: AutofillValue? = null
|
||||||
|
|
||||||
|
fun parse(saveValue: Boolean = false): Result? {
|
||||||
try {
|
try {
|
||||||
result = Result()
|
result = Result()
|
||||||
result?.apply {
|
result?.apply {
|
||||||
|
allowSaveValues = saveValue
|
||||||
usernameCandidate = null
|
usernameCandidate = null
|
||||||
|
usernameValueCandidate = null
|
||||||
mainLoop@ for (i in 0 until structure.windowNodeCount) {
|
mainLoop@ for (i in 0 until structure.windowNodeCount) {
|
||||||
val windowNode = structure.getWindowNodeAt(i)
|
val windowNode = structure.getWindowNodeAt(i)
|
||||||
applicationId = windowNode.title.toString().split("/")[0]
|
applicationId = windowNode.title.toString().split("/")[0]
|
||||||
@@ -51,8 +57,12 @@ internal class StructureParser(private val structure: AssistStructure) {
|
|||||||
break@mainLoop
|
break@mainLoop
|
||||||
}
|
}
|
||||||
// If not explicit username field found, add the field just before password field.
|
// If not explicit username field found, add the field just before password field.
|
||||||
if (usernameId == null && passwordId != null && usernameCandidate != null)
|
if (usernameId == null && passwordId != null && usernameCandidate != null) {
|
||||||
usernameId = usernameCandidate
|
usernameId = usernameCandidate
|
||||||
|
if (allowSaveValues) {
|
||||||
|
usernameValue = usernameValueCandidate
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the result only if password field is retrieved
|
// Return the result only if password field is retrieved
|
||||||
@@ -68,11 +78,23 @@ internal class StructureParser(private val structure: AssistStructure) {
|
|||||||
|
|
||||||
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
|
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
|
||||||
// Get the domain of a web app
|
// Get the domain of a web app
|
||||||
node.webDomain?.let {
|
node.webDomain?.let { webDomain ->
|
||||||
result?.domain = it
|
if (webDomain.isNotEmpty()) {
|
||||||
Log.d(TAG, "Autofill domain: $it")
|
result?.webDomain = webDomain
|
||||||
|
Log.d(TAG, "Autofill domain: $webDomain")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
node.webScheme?.let { webScheme ->
|
||||||
|
if (webScheme.isNotEmpty()) {
|
||||||
|
result?.webScheme = webScheme
|
||||||
|
Log.d(TAG, "Autofill scheme: $webScheme")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val domainNotEmpty = result?.webDomain?.isNotEmpty() == true
|
||||||
|
|
||||||
|
var returnValue = false
|
||||||
// Only parse visible nodes
|
// Only parse visible nodes
|
||||||
if (node.visibility == View.VISIBLE) {
|
if (node.visibility == View.VISIBLE) {
|
||||||
if (node.autofillId != null
|
if (node.autofillId != null
|
||||||
@@ -81,39 +103,41 @@ internal class StructureParser(private val structure: AssistStructure) {
|
|||||||
val hints = node.autofillHints
|
val hints = node.autofillHints
|
||||||
if (hints != null && hints.isNotEmpty()) {
|
if (hints != null && hints.isNotEmpty()) {
|
||||||
if (parseNodeByAutofillHint(node))
|
if (parseNodeByAutofillHint(node))
|
||||||
return true
|
returnValue = true
|
||||||
} else if (parseNodeByHtmlAttributes(node))
|
} else if (parseNodeByHtmlAttributes(node))
|
||||||
return true
|
returnValue = true
|
||||||
else if (parseNodeByAndroidInput(node))
|
else if (parseNodeByAndroidInput(node))
|
||||||
return true
|
returnValue = true
|
||||||
}
|
}
|
||||||
|
// Optimized return but only if domain not empty
|
||||||
|
if (domainNotEmpty && returnValue)
|
||||||
|
return true
|
||||||
// Recursive method to process each node
|
// Recursive method to process each node
|
||||||
for (i in 0 until node.childCount) {
|
for (i in 0 until node.childCount) {
|
||||||
if (parseViewNode(node.getChildAt(i)))
|
if (parseViewNode(node.getChildAt(i)))
|
||||||
|
returnValue = true
|
||||||
|
if (domainNotEmpty && returnValue)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return returnValue
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseNodeByAutofillHint(node: AssistStructure.ViewNode): Boolean {
|
private fun parseNodeByAutofillHint(node: AssistStructure.ViewNode): Boolean {
|
||||||
val autofillId = node.autofillId
|
val autofillId = node.autofillId
|
||||||
node.autofillHints?.forEach {
|
node.autofillHints?.forEach {
|
||||||
when {
|
when {
|
||||||
it.equals(View.AUTOFILL_HINT_USERNAME, true)
|
it.contains(View.AUTOFILL_HINT_USERNAME, true)
|
||||||
|| it.equals(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
||||||
|| it.equals("email", true)
|
|| it.contains("email", true)
|
||||||
|| it.equals(View.AUTOFILL_HINT_PHONE, true)
|
|| it.contains(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
|
||||||
|
result?.usernameValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username hint")
|
Log.d(TAG, "Autofill username hint")
|
||||||
}
|
}
|
||||||
it.equals(View.AUTOFILL_HINT_PASSWORD, true)
|
it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
|
||||||
|| it.contains("password", true) -> {
|
|
||||||
result?.passwordId = autofillId
|
result?.passwordId = autofillId
|
||||||
|
result?.passwordValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill password hint")
|
Log.d(TAG, "Autofill password hint")
|
||||||
// Username not needed in this case
|
// Username not needed in this case
|
||||||
usernameNeeded = false
|
usernameNeeded = false
|
||||||
@@ -143,14 +167,17 @@ internal class StructureParser(private val structure: AssistStructure) {
|
|||||||
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
|
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
|
||||||
"tel", "email" -> {
|
"tel", "email" -> {
|
||||||
result?.usernameId = autofillId
|
result?.usernameId = autofillId
|
||||||
|
result?.usernameValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||||
}
|
}
|
||||||
"text" -> {
|
"text" -> {
|
||||||
usernameCandidate = autofillId
|
usernameCandidate = autofillId
|
||||||
|
usernameValueCandidate = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||||
}
|
}
|
||||||
"password" -> {
|
"password" -> {
|
||||||
result?.passwordId = autofillId
|
result?.passwordId = autofillId
|
||||||
|
result?.passwordValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill password web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
Log.d(TAG, "Autofill password web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -163,44 +190,83 @@ 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
|
||||||
|
result?.usernameValue = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill username android text type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_NORMAL,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
|
||||||
|
usernameCandidate = autofillId
|
||||||
|
usernameValueCandidate = node.autofillValue
|
||||||
|
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
|
||||||
|
result?.passwordValue = node.autofillValue
|
||||||
|
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 ||
|
usernameValueCandidate = node.autofillValue
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD != 0 ||
|
Log.d(TAG, "Autofill usernale candidate android number type: ${showHexInputType(inputType)}")
|
||||||
inputType and InputType.TYPE_NUMBER_VARIATION_PASSWORD != 0 -> {
|
}
|
||||||
result?.passwordId = autofillId
|
inputIsVariationType(inputType,
|
||||||
Log.d(TAG, "Autofill password android type: $inputType")
|
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
|
||||||
// Username not needed in this case
|
result?.passwordId = autofillId
|
||||||
usernameNeeded = false
|
result?.passwordValue = node.autofillValue
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,7 +276,14 @@ internal class StructureParser(private val structure: AssistStructure) {
|
|||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
internal class Result {
|
internal class Result {
|
||||||
var applicationId: String? = null
|
var applicationId: String? = null
|
||||||
var domain: String? = null
|
|
||||||
|
var webDomain: String? = null
|
||||||
|
set(value) {
|
||||||
|
if (field == null)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var webScheme: String? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
if (field == null)
|
if (field == null)
|
||||||
field = value
|
field = value
|
||||||
@@ -238,6 +311,21 @@ internal class StructureParser(private val structure: AssistStructure) {
|
|||||||
}
|
}
|
||||||
return all.toTypedArray()
|
return all.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only in registration mode
|
||||||
|
var allowSaveValues = false
|
||||||
|
|
||||||
|
var usernameValue: AutofillValue? = null
|
||||||
|
set(value) {
|
||||||
|
if (allowSaveValues && field == null)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var passwordValue: AutofillValue? = null
|
||||||
|
set(value) {
|
||||||
|
if (allowSaveValues && field == null)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ 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
|
||||||
import androidx.biometric.BiometricConstants
|
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
@@ -50,14 +50,25 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
: BiometricUnlockDatabaseHelper.BiometricUnlockCallback {
|
: BiometricUnlockDatabaseHelper.BiometricUnlockCallback {
|
||||||
|
|
||||||
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
|
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
|
||||||
private var biometricMode: Mode = Mode.UNAVAILABLE
|
private var biometricMode: Mode = Mode.BIOMETRIC_UNAVAILABLE
|
||||||
|
|
||||||
|
// Only to fix multiple fingerprint menu #332
|
||||||
|
private var mAllowAdvancedUnlockMenu = false
|
||||||
|
private var mAddBiometricMenuInProgress = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage setting to auto open biometric prompt
|
||||||
|
*/
|
||||||
private var biometricPromptAutoOpenPreference = PreferencesUtil.isBiometricPromptAutoOpenEnable(context)
|
private var biometricPromptAutoOpenPreference = PreferencesUtil.isBiometricPromptAutoOpenEnable(context)
|
||||||
var isBiometricPromptAutoOpenEnable: Boolean = true
|
var isBiometricPromptAutoOpenEnable: Boolean = false
|
||||||
get() {
|
get() {
|
||||||
return field && biometricPromptAutoOpenPreference
|
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)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -76,12 +87,15 @@ 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 = BiometricUnlockDatabaseHelper.canAuthenticate(context)
|
||||||
|
allowOpenBiometricPrompt = true
|
||||||
|
|
||||||
if (!PreferencesUtil.isBiometricUnlockEnable(context)
|
if (!PreferencesUtil.isBiometricUnlockEnable(context)
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
|
||||||
toggleMode(Mode.UNAVAILABLE)
|
toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
|
||||||
|
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED){
|
||||||
|
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
|
||||||
} else {
|
} else {
|
||||||
// biometric is available but not configured, show icon but in disabled state with some information
|
// biometric is available but not configured, show icon but in disabled state with some information
|
||||||
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
||||||
@@ -105,7 +119,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
} else {
|
} else {
|
||||||
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
|
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
|
||||||
// biometric available but no stored password found yet for this DB so show info don't listen
|
// biometric available but no stored password found yet for this DB so show info don't listen
|
||||||
toggleMode( if (containsCipher) {
|
toggleMode(if (containsCipher) {
|
||||||
// listen for decryption
|
// listen for decryption
|
||||||
Mode.EXTRACT_CREDENTIAL
|
Mode.EXTRACT_CREDENTIAL
|
||||||
} else {
|
} else {
|
||||||
@@ -147,10 +161,16 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||||
context.runOnUiThread {
|
context.runOnUiThread {
|
||||||
when (biometricMode) {
|
when (biometricMode) {
|
||||||
Mode.UNAVAILABLE -> {}
|
Mode.BIOMETRIC_UNAVAILABLE -> {
|
||||||
Mode.BIOMETRIC_NOT_CONFIGURED -> {}
|
}
|
||||||
Mode.KEY_MANAGER_UNAVAILABLE -> {}
|
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> {
|
||||||
Mode.WAIT_CREDENTIAL -> {}
|
}
|
||||||
|
Mode.BIOMETRIC_NOT_CONFIGURED -> {
|
||||||
|
}
|
||||||
|
Mode.KEY_MANAGER_UNAVAILABLE -> {
|
||||||
|
}
|
||||||
|
Mode.WAIT_CREDENTIAL -> {
|
||||||
|
}
|
||||||
Mode.STORE_CREDENTIAL -> {
|
Mode.STORE_CREDENTIAL -> {
|
||||||
// newly store the entered password in encrypted way
|
// newly store the entered password in encrypted way
|
||||||
biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString())
|
biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString())
|
||||||
@@ -174,6 +194,15 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
advancedUnlockInfoView?.setIconViewClickListener(false, null)
|
advancedUnlockInfoView?.setIconViewClickListener(false, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun initSecurityUpdateRequired() {
|
||||||
|
showFingerPrintViews(true)
|
||||||
|
setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
|
||||||
|
|
||||||
|
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||||
|
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun initNotConfigured() {
|
private fun initNotConfigured() {
|
||||||
showFingerPrintViews(true)
|
showFingerPrintViews(true)
|
||||||
setAdvancedUnlockedTitleView(R.string.configure_biometric)
|
setAdvancedUnlockedTitleView(R.string.configure_biometric)
|
||||||
@@ -187,7 +216,6 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
private fun initKeyManagerNotAvailable() {
|
private fun initKeyManagerNotAvailable() {
|
||||||
showFingerPrintViews(true)
|
showFingerPrintViews(true)
|
||||||
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
|
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
|
||||||
setAdvancedUnlockedMessageView("")
|
|
||||||
|
|
||||||
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||||
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
||||||
@@ -200,17 +228,26 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
setAdvancedUnlockedMessageView("")
|
setAdvancedUnlockedMessageView("")
|
||||||
|
|
||||||
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||||
biometricAuthenticationCallback.onAuthenticationError(
|
biometricAuthenticationCallback.onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
|
||||||
BiometricConstants.ERROR_UNABLE_TO_PROCESS
|
context.getString(R.string.credential_before_click_biometric_button))
|
||||||
, context.getString(R.string.credential_before_click_biometric_button))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openBiometricPrompt(biometricPrompt: BiometricPrompt?,
|
private fun openBiometricPrompt(biometricPrompt: BiometricPrompt?,
|
||||||
cryptoObject: BiometricPrompt.CryptoObject,
|
cryptoObject: BiometricPrompt.CryptoObject?,
|
||||||
promptInfo: BiometricPrompt.PromptInfo) {
|
promptInfo: BiometricPrompt.PromptInfo) {
|
||||||
context.runOnUiThread {
|
context.runOnUiThread {
|
||||||
biometricPrompt?.authenticate(promptInfo, cryptoObject)
|
if (allowOpenBiometricPrompt) {
|
||||||
|
if (biometricPrompt != null) {
|
||||||
|
if (cryptoObject != null) {
|
||||||
|
biometricPrompt.authenticate(promptInfo, cryptoObject)
|
||||||
|
} else {
|
||||||
|
setAdvancedUnlockedTitleView(R.string.crypto_object_not_initialized)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setAdvancedUnlockedTitleView(R.string.biometric_prompt_not_initialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,14 +257,10 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
setAdvancedUnlockedMessageView("")
|
setAdvancedUnlockedMessageView("")
|
||||||
|
|
||||||
biometricUnlockDatabaseHelper?.initEncryptData { biometricPrompt, cryptoObject, promptInfo ->
|
biometricUnlockDatabaseHelper?.initEncryptData { biometricPrompt, cryptoObject, promptInfo ->
|
||||||
|
// Set listener to open the biometric dialog and save credential
|
||||||
cryptoObject?.let { crypto ->
|
advancedUnlockInfoView?.setIconViewClickListener { _ ->
|
||||||
// Set listener to open the biometric dialog and save credential
|
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
|
||||||
advancedUnlockInfoView?.setIconViewClickListener { _ ->
|
|
||||||
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,19 +275,16 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
it?.specParameters?.let { specs ->
|
it?.specParameters?.let { specs ->
|
||||||
biometricUnlockDatabaseHelper?.initDecryptData(specs) { biometricPrompt, cryptoObject, promptInfo ->
|
biometricUnlockDatabaseHelper?.initDecryptData(specs) { biometricPrompt, cryptoObject, promptInfo ->
|
||||||
|
|
||||||
cryptoObject?.let { crypto ->
|
// Set listener to open the biometric dialog and check credential
|
||||||
// Set listener to open the biometric dialog and check credential
|
advancedUnlockInfoView?.setIconViewClickListener { _ ->
|
||||||
advancedUnlockInfoView?.setIconViewClickListener { _ ->
|
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
|
||||||
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto open the biometric prompt
|
|
||||||
if (isBiometricPromptAutoOpenEnable) {
|
|
||||||
isBiometricPromptAutoOpenEnable = false
|
|
||||||
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto open the biometric prompt
|
||||||
|
if (isBiometricPromptAutoOpenEnable) {
|
||||||
|
isBiometricPromptAutoOpenEnable = false
|
||||||
|
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,45 +293,55 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun initBiometricMode() {
|
fun initBiometricMode() {
|
||||||
|
mAllowAdvancedUnlockMenu = false
|
||||||
when (biometricMode) {
|
when (biometricMode) {
|
||||||
Mode.UNAVAILABLE -> initNotAvailable()
|
Mode.BIOMETRIC_UNAVAILABLE -> initNotAvailable()
|
||||||
|
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired()
|
||||||
Mode.BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
|
Mode.BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
|
||||||
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
|
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
|
||||||
Mode.WAIT_CREDENTIAL -> initWaitData()
|
Mode.WAIT_CREDENTIAL -> initWaitData()
|
||||||
Mode.STORE_CREDENTIAL -> initEncryptData()
|
Mode.STORE_CREDENTIAL -> initEncryptData()
|
||||||
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
|
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invalidateBiometricMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invalidateBiometricMenu() {
|
||||||
// Show fingerprint key deletion
|
// Show fingerprint key deletion
|
||||||
context.invalidateOptionsMenu()
|
if (!mAddBiometricMenuInProgress) {
|
||||||
|
mAddBiometricMenuInProgress = true
|
||||||
|
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
|
||||||
|
mAllowAdvancedUnlockMenu = containsCipher
|
||||||
|
&& (biometricMode != Mode.BIOMETRIC_UNAVAILABLE
|
||||||
|
&& biometricMode != Mode.KEY_MANAGER_UNAVAILABLE)
|
||||||
|
mAddBiometricMenuInProgress = false
|
||||||
|
context.invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
// Close the biometric prompt
|
// Close the biometric prompt
|
||||||
|
allowOpenBiometricPrompt = false
|
||||||
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
|
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
|
||||||
// Restore the checked listener
|
// Restore the checked listener
|
||||||
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
|
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only to fix multiple fingerprint menu #332
|
|
||||||
private var addBiometricMenuInProgress = false
|
|
||||||
fun inflateOptionsMenu(menuInflater: MenuInflater, menu: Menu) {
|
fun inflateOptionsMenu(menuInflater: MenuInflater, menu: Menu) {
|
||||||
if (!addBiometricMenuInProgress) {
|
if (mAllowAdvancedUnlockMenu)
|
||||||
addBiometricMenuInProgress = true
|
menuInflater.inflate(R.menu.advanced_unlock, menu)
|
||||||
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) {
|
|
||||||
if ((biometricMode != Mode.UNAVAILABLE && biometricMode != Mode.BIOMETRIC_NOT_CONFIGURED)
|
|
||||||
&& it) {
|
|
||||||
menuInflater.inflate(R.menu.advanced_unlock, menu)
|
|
||||||
addBiometricMenuInProgress = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteEntryKey() {
|
fun deleteEntryKey() {
|
||||||
|
allowOpenBiometricPrompt = false
|
||||||
|
advancedUnlockInfoView?.setIconViewClickListener(false, null)
|
||||||
|
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
|
||||||
biometricUnlockDatabaseHelper?.deleteEntryKey()
|
biometricUnlockDatabaseHelper?.deleteEntryKey()
|
||||||
cipherDatabaseAction.deleteByDatabaseUri(databaseFileUri)
|
cipherDatabaseAction.deleteByDatabaseUri(databaseFileUri) {
|
||||||
biometricMode = Mode.BIOMETRIC_NOT_CONFIGURED
|
checkBiometricAvailability()
|
||||||
checkBiometricAvailability()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
|
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
|
||||||
@@ -324,7 +364,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) {
|
||||||
@@ -346,7 +388,13 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class Mode {
|
enum class Mode {
|
||||||
UNAVAILABLE, BIOMETRIC_NOT_CONFIGURED, KEY_MANAGER_UNAVAILABLE, WAIT_CREDENTIAL, STORE_CREDENTIAL, EXTRACT_CREDENTIAL
|
BIOMETRIC_UNAVAILABLE,
|
||||||
|
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
|
||||||
|
BIOMETRIC_NOT_CONFIGURED,
|
||||||
|
KEY_MANAGER_UNAVAILABLE,
|
||||||
|
WAIT_CREDENTIAL,
|
||||||
|
STORE_CREDENTIAL,
|
||||||
|
EXTRACT_CREDENTIAL
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import android.util.Base64
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
@@ -66,7 +68,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
setDeviceCredentialAllowed(true)
|
setDeviceCredentialAllowed(true)
|
||||||
else
|
else
|
||||||
*/
|
*/
|
||||||
setNegativeButtonText(context.getString(android.R.string.cancel))
|
setNegativeButtonText(context.getString(android.R.string.cancel))
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
private val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
private val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
||||||
@@ -78,8 +80,8 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
if (keyguardManager?.isDeviceSecure == true)
|
if (keyguardManager?.isDeviceSecure == true)
|
||||||
setDeviceCredentialAllowed(true)
|
setDeviceCredentialAllowed(true)
|
||||||
else
|
else
|
||||||
*/
|
*/
|
||||||
setNegativeButtonText(context.getString(android.R.string.cancel))
|
setNegativeButtonText(context.getString(android.R.string.cancel))
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val isKeyManagerInitialized: Boolean
|
val isKeyManagerInitialized: Boolean
|
||||||
@@ -91,12 +93,8 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (BiometricManager.from(context).canAuthenticate() != BiometricManager.BIOMETRIC_SUCCESS) {
|
if (allowInitKeyStore(context)) {
|
||||||
// really not much to do when no fingerprint support found
|
|
||||||
isKeyManagerInit = false
|
|
||||||
} else {
|
|
||||||
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
|
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)
|
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)
|
||||||
this.keyGenerator = KeyGenerator.getInstance(BIOMETRIC_KEY_ALGORITHM, BIOMETRIC_KEYSTORE)
|
this.keyGenerator = KeyGenerator.getInstance(BIOMETRIC_KEY_ALGORITHM, BIOMETRIC_KEYSTORE)
|
||||||
@@ -113,6 +111,9 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
isKeyManagerInit = false
|
isKeyManagerInit = false
|
||||||
biometricUnlockCallback?.onBiometricException(e)
|
biometricUnlockCallback?.onBiometricException(e)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// really not much to do when no fingerprint support found
|
||||||
|
isKeyManagerInit = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +159,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
fun initEncryptData(actionIfCypherInit
|
fun initEncryptData(actionIfCypherInit
|
||||||
: (biometricPrompt: BiometricPrompt?,
|
: (biometricPrompt: BiometricPrompt?,
|
||||||
cryptoObject: BiometricPrompt.CryptoObject?,
|
cryptoObject: BiometricPrompt.CryptoObject?,
|
||||||
promptInfo: BiometricPrompt.PromptInfo)->Unit) {
|
promptInfo: BiometricPrompt.PromptInfo) -> Unit) {
|
||||||
if (!isKeyManagerInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -203,9 +204,9 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
|
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
|
||||||
: (biometricPrompt: BiometricPrompt?,
|
: (biometricPrompt: BiometricPrompt?,
|
||||||
cryptoObject: BiometricPrompt.CryptoObject?,
|
cryptoObject: BiometricPrompt.CryptoObject?,
|
||||||
promptInfo: BiometricPrompt.PromptInfo)->Unit) {
|
promptInfo: BiometricPrompt.PromptInfo) -> Unit) {
|
||||||
if (!isKeyManagerInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -295,6 +296,37 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
private const val BIOMETRIC_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
private const val BIOMETRIC_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
||||||
private const val BIOMETRIC_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
private const val BIOMETRIC_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||||
|
|
||||||
|
fun canAuthenticate(context: Context): Int {
|
||||||
|
return try {
|
||||||
|
BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
|
||||||
|
try {
|
||||||
|
BiometricManager.from(context).canAuthenticate(BIOMETRIC_WEAK)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
|
||||||
|
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun allowInitKeyStore(context: Context): Boolean {
|
||||||
|
val biometricCanAuthenticate = canAuthenticate(context)
|
||||||
|
return ( biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unlockSupported(context: Context): Boolean {
|
||||||
|
val biometricCanAuthenticate = canAuthenticate(context)
|
||||||
|
return ( biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove entry key in keystore
|
* Remove entry key in keystore
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -19,17 +19,23 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
package com.kunzisoft.keepass.crypto.keyDerivation
|
||||||
|
|
||||||
import com.kunzisoft.keepass.stream.LittleEndianDataInputStream
|
|
||||||
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
|
|
||||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
||||||
import com.kunzisoft.keepass.stream.uuidTo16Bytes
|
import com.kunzisoft.keepass.stream.uuidTo16Bytes
|
||||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
import com.kunzisoft.keepass.utils.VariantDictionary
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class KdfParameters(val uuid: UUID) : VariantDictionary() {
|
class KdfParameters: VariantDictionary {
|
||||||
|
|
||||||
|
val uuid: UUID
|
||||||
|
|
||||||
|
constructor(uuid: UUID): super() {
|
||||||
|
this.uuid = uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(uuid: UUID, d: VariantDictionary): super(d) {
|
||||||
|
this.uuid = uuid
|
||||||
|
}
|
||||||
|
|
||||||
fun setParamUUID() {
|
fun setParamUUID() {
|
||||||
setByteArray(PARAM_UUID, uuidTo16Bytes(uuid))
|
setByteArray(PARAM_UUID, uuidTo16Bytes(uuid))
|
||||||
@@ -41,25 +47,17 @@ class KdfParameters(val uuid: UUID) : VariantDictionary() {
|
|||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun deserialize(data: ByteArray): KdfParameters? {
|
fun deserialize(data: ByteArray): KdfParameters? {
|
||||||
val inputStream = LittleEndianDataInputStream(ByteArrayInputStream(data))
|
val dictionary = VariantDictionary.deserialize(data)
|
||||||
val dictionary = deserialize(inputStream)
|
|
||||||
|
|
||||||
val uuidBytes = dictionary.getByteArray(PARAM_UUID) ?: return null
|
val uuidBytes = dictionary.getByteArray(PARAM_UUID) ?: return null
|
||||||
val uuid = bytes16ToUuid(uuidBytes)
|
val uuid = bytes16ToUuid(uuidBytes)
|
||||||
|
|
||||||
val kdfParameters = KdfParameters(uuid)
|
return KdfParameters(uuid, dictionary)
|
||||||
kdfParameters.copyTo(dictionary)
|
|
||||||
return kdfParameters
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun serialize(kdfParameters: KdfParameters): ByteArray {
|
fun serialize(kdfParameters: KdfParameters): ByteArray {
|
||||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
return VariantDictionary.serialize(kdfParameters)
|
||||||
val outputStream = LittleEndianDataOutputStream(byteArrayOutputStream)
|
|
||||||
|
|
||||||
serialize(kdfParameters, outputStream)
|
|
||||||
|
|
||||||
return byteArrayOutputStream.toByteArray()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import android.util.Log
|
|||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
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.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.utils.closeDatabase
|
||||||
|
|
||||||
class CreateDatabaseRunnable(context: Context,
|
class CreateDatabaseRunnable(context: Context,
|
||||||
private val mDatabase: Database,
|
private val mDatabase: Database,
|
||||||
@@ -34,7 +36,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 +45,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(UriUtil.getBinaryDir(context))
|
||||||
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,10 +27,11 @@ 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
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.utils.closeDatabase
|
||||||
|
|
||||||
class LoadDatabaseRunnable(private val context: Context,
|
class LoadDatabaseRunnable(private val context: Context,
|
||||||
private val mDatabase: Database,
|
private val mDatabase: Database,
|
||||||
@@ -39,17 +40,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(UriUtil.getBinaryDir(context))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionRun() {
|
override fun onActionRun() {
|
||||||
@@ -57,21 +55,17 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
mDatabase.loadData(mUri, mPass, mKey,
|
mDatabase.loadData(mUri, mPass, mKey,
|
||||||
mReadonly,
|
mReadonly,
|
||||||
context.contentResolver,
|
context.contentResolver,
|
||||||
cacheDirectory,
|
UriUtil.getBinaryDir(context),
|
||||||
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 +82,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(UriUtil.getBinaryDir(context))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFinishRun() {
|
||||||
|
mLoadDatabaseResult?.invoke(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,9 @@ import android.content.*
|
|||||||
import android.content.Context.BIND_ABOVE_CLIENT
|
import android.content.Context.BIND_ABOVE_CLIENT
|
||||||
import android.content.Context.BIND_NOT_FOREGROUND
|
import android.content.Context.BIND_NOT_FOREGROUND
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
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 +35,6 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.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
|
||||||
@@ -48,6 +45,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
|
|||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||||
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.ACTION_DATABASE_MOVE_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
|
||||||
@@ -68,18 +66,17 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
|
|||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.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,9 +87,6 @@ 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()
|
|
||||||
// Stop the opening notification
|
|
||||||
DatabaseOpenNotificationService.stop(activity)
|
|
||||||
startDialog(titleId, messageId, warningId)
|
startDialog(titleId, messageId, warningId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,21 +96,8 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +169,10 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
serviceConnection = null
|
serviceConnection = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isBinded(): Boolean {
|
||||||
|
return mBinder != null
|
||||||
|
}
|
||||||
|
|
||||||
fun registerProgressTask() {
|
fun registerProgressTask() {
|
||||||
stopDialog()
|
stopDialog()
|
||||||
|
|
||||||
@@ -237,12 +222,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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -261,7 +242,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)
|
||||||
}
|
}
|
||||||
@@ -275,7 +256,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)
|
||||||
@@ -294,7 +275,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)
|
||||||
}
|
}
|
||||||
@@ -486,6 +467,13 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
|
||||||
|
start(Bundle().apply {
|
||||||
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
|
}
|
||||||
|
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
||||||
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
|
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
|
||||||
newMaxHistoryItems: Int,
|
newMaxHistoryItems: Int,
|
||||||
save: Boolean) {
|
save: Boolean) {
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.database.action
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
|
||||||
|
class RemoveUnlinkedDataDatabaseRunnable (
|
||||||
|
context: Context,
|
||||||
|
database: Database,
|
||||||
|
saveDatabase: Boolean)
|
||||||
|
: SaveDatabaseRunnable(context, database, saveDatabase) {
|
||||||
|
|
||||||
|
override fun onActionRun() {
|
||||||
|
try {
|
||||||
|
database.removeUnlinkedAttachments()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
setError(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onActionRun()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ class DeleteEntryHistoryDatabaseRunnable (
|
|||||||
|
|
||||||
override fun onStartRun() {
|
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,13 +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 java.io.*
|
import java.io.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
@@ -71,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() {
|
||||||
@@ -151,6 +156,17 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun compressionForNewEntry(): Boolean {
|
||||||
|
if (mDatabaseKDB != null)
|
||||||
|
return false
|
||||||
|
// Default compression not necessary if stored in header
|
||||||
|
mDatabaseKDBX?.let {
|
||||||
|
return it.compressionAlgorithm == CompressionAlgorithm.GZip
|
||||||
|
&& it.kdbxVersion.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
|
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
|
||||||
newCompression: CompressionAlgorithm) {
|
newCompression: CompressionAlgorithm) {
|
||||||
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
|
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
|
||||||
@@ -262,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
|
||||||
@@ -287,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,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)
|
||||||
@@ -316,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?) {
|
||||||
|
|
||||||
@@ -378,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()
|
||||||
@@ -393,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
|
||||||
@@ -421,7 +435,39 @@ 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,
|
||||||
|
compressed: Boolean = false,
|
||||||
|
protected: Boolean = false): BinaryAttachment? {
|
||||||
|
return mDatabaseKDB?.buildNewBinary(cacheDirectory)
|
||||||
|
?: mDatabaseKDBX?.buildNewBinary(cacheDirectory, compressed, protected)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAttachmentIfNotUsed(attachment: Attachment) {
|
||||||
|
// No need in KDB database because unique attachment by entry
|
||||||
|
// Don't clear to fix upload multiple times
|
||||||
|
mDatabaseKDBX?.removeUnlinkedAttachment(attachment.binaryAttachment, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeUnlinkedAttachments() {
|
||||||
|
// No check in database KDB because unique attachment by entry
|
||||||
|
mDatabaseKDBX?.removeUnlinkedAttachments(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(DatabaseOutputException::class)
|
@Throws(DatabaseOutputException::class)
|
||||||
@@ -469,7 +515,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) }
|
||||||
@@ -714,7 +760,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)
|
||||||
@@ -725,7 +771,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)
|
||||||
@@ -777,18 +823,25 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startManageEntry(entry: Entry) {
|
fun startManageEntry(entry: Entry?) {
|
||||||
mDatabaseKDBX?.let {
|
mDatabaseKDBX?.let {
|
||||||
entry.startToManageFieldReferences(it)
|
entry?.startToManageFieldReferences(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopManageEntry(entry: Entry) {
|
fun stopManageEntry(entry: Entry?) {
|
||||||
mDatabaseKDBX?.let {
|
mDatabaseKDBX?.let {
|
||||||
entry.stopToManageFieldReferences()
|
entry?.stopToManageFieldReferences()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if database allows custom field
|
||||||
|
*/
|
||||||
|
fun allowEntryCustomFields(): Boolean {
|
||||||
|
return mDatabaseKDBX != null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove oldest history for each entry if more than max items or max memory
|
* Remove oldest history for each entry if more than max items or max memory
|
||||||
*/
|
*/
|
||||||
@@ -796,7 +849,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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -804,34 +857,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -840,11 +878,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
|
||||||
}
|
}
|
||||||
@@ -853,6 +890,22 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun removeOldestEntryHistory(entry: Entry) {
|
||||||
|
entry.removeOldestEntryFromHistory()?.let {
|
||||||
|
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
|
||||||
|
removeAttachmentIfNotUsed(attachmentToRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeEntryHistory(entry: Entry, entryHistoryPosition: Int) {
|
||||||
|
entry.removeEntryFromHistory(entryHistoryPosition)?.let {
|
||||||
|
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
|
||||||
|
removeAttachmentIfNotUsed(attachmentToRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object : SingletonHolder<Database>(::Database) {
|
companion object : SingletonHolder<Database>(::Database) {
|
||||||
|
|
||||||
private val TAG = Database::class.java.name
|
private val TAG = Database::class.java.name
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import android.content.res.Resources
|
|||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.core.os.ConfigurationCompat
|
import androidx.core.os.ConfigurationCompat
|
||||||
|
import org.joda.time.Duration
|
||||||
|
import org.joda.time.Instant
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -95,6 +97,7 @@ class DateInstant : Parcelable {
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val NEVER_EXPIRE = neverExpire
|
val NEVER_EXPIRE = neverExpire
|
||||||
|
val IN_ONE_MONTH = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
|
||||||
private val dateFormat = SimpleDateFormat.getDateTimeInstance()
|
private val dateFormat = SimpleDateFormat.getDateTimeInstance()
|
||||||
|
|
||||||
private val neverExpire: DateInstant
|
private val neverExpire: DateInstant
|
||||||
|
|||||||
@@ -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,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.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.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
|
||||||
@@ -285,66 +283,90 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve custom fields to show, key is the label, value is the value of field (protected or not)
|
* Retrieve extra fields to show, key is the label, value is the value of field (protected or not)
|
||||||
* @return Map of label/value
|
* @return Map of label/value
|
||||||
*/
|
*/
|
||||||
val customFields: HashMap<String, ProtectedString>
|
fun getExtraFields(): List<Field> {
|
||||||
get() = entryKDBX?.customFields ?: HashMap()
|
val extraFields = ArrayList<Field>()
|
||||||
|
entryKDBX?.let {
|
||||||
/**
|
for (field in it.customFields) {
|
||||||
* To redefine if version of entry allow custom field,
|
extraFields.add(Field(field.key, field.value))
|
||||||
* @return true if entry allows custom field
|
}
|
||||||
*/
|
}
|
||||||
fun allowCustomFields(): Boolean {
|
return extraFields
|
||||||
return entryKDBX?.allowCustomFields() ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeAllFields() {
|
|
||||||
entryKDBX?.removeAllFields()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update or add an extra field to the list (standard or custom)
|
* Update or add an extra field to the list (standard or custom)
|
||||||
* @param label Label of field, must be unique
|
|
||||||
* @param value Value of field
|
|
||||||
*/
|
*/
|
||||||
fun putExtraField(label: String, value: ProtectedString) {
|
fun putExtraField(field: Field) {
|
||||||
entryKDBX?.putExtraField(label, value)
|
entryKDBX?.putExtraField(field.name, field.protectedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOtpElement(): OtpElement? {
|
private fun addExtraFields(fields: List<Field>) {
|
||||||
return OtpEntryFields.parseFields { key ->
|
fields.forEach {
|
||||||
customFields[key]?.toString()
|
putExtraField(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startToManageFieldReferences(db: DatabaseKDBX) {
|
private fun removeAllFields() {
|
||||||
entryKDBX?.startToManageFieldReferences(db)
|
entryKDBX?.removeAllFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOtpElement(): OtpElement? {
|
||||||
|
entryKDBX?.let {
|
||||||
|
return OtpEntryFields.parseFields { key ->
|
||||||
|
it.customFields[key]?.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startToManageFieldReferences(database: DatabaseKDBX) {
|
||||||
|
entryKDBX?.startToManageFieldReferences(database)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopToManageFieldReferences() {
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addAttachments(binaryPool: BinaryPool, attachments: List<Attachment>) {
|
||||||
|
attachments.forEach {
|
||||||
|
putAttachment(it, binaryPool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeAttachment(attachment: Attachment) {
|
||||||
|
entryKDB?.removeAttachment(attachment)
|
||||||
|
entryKDBX?.removeAttachment(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeAllAttachments() {
|
||||||
|
entryKDB?.removeAttachment()
|
||||||
|
entryKDBX?.removeAttachments()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
|
||||||
|
entryKDB?.putAttachment(attachment)
|
||||||
|
entryKDBX?.putAttachment(attachment, binaryPool)
|
||||||
|
}
|
||||||
|
|
||||||
fun getHistory(): ArrayList<Entry> {
|
fun getHistory(): ArrayList<Entry> {
|
||||||
val history = ArrayList<Entry>()
|
val history = ArrayList<Entry>()
|
||||||
val entryKDBXHistory = entryKDBX?.history ?: ArrayList()
|
val entryKDBXHistory = entryKDBX?.history ?: ArrayList()
|
||||||
@@ -360,20 +382,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 {
|
||||||
@@ -396,26 +420,57 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
database?.stopManageEntry(this)
|
database?.stopManageEntry(this)
|
||||||
else
|
else
|
||||||
database?.startManageEntry(this)
|
database?.startManageEntry(this)
|
||||||
|
|
||||||
entryInfo.id = nodeId.toString()
|
entryInfo.id = nodeId.toString()
|
||||||
entryInfo.title = title
|
entryInfo.title = title
|
||||||
entryInfo.icon = icon
|
entryInfo.icon = icon
|
||||||
entryInfo.username = username
|
entryInfo.username = username
|
||||||
entryInfo.password = password
|
entryInfo.password = password
|
||||||
|
entryInfo.expires = expires
|
||||||
|
entryInfo.expiryTime = expiryTime
|
||||||
entryInfo.url = url
|
entryInfo.url = url
|
||||||
entryInfo.notes = notes
|
entryInfo.notes = notes
|
||||||
for (entry in customFields.entries) {
|
entryInfo.customFields = getExtraFields()
|
||||||
entryInfo.customFields.add(
|
|
||||||
Field(entry.key, entry.value))
|
|
||||||
}
|
|
||||||
// Add otpElement to generate token
|
// Add otpElement to generate token
|
||||||
entryInfo.otpModel = getOtpElement()?.otpModel
|
entryInfo.otpModel = getOtpElement()?.otpModel
|
||||||
// Replace parameter fields by generated OTP fields
|
if (!raw) {
|
||||||
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
// Replace parameter fields by generated OTP fields
|
||||||
|
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
||||||
|
}
|
||||||
|
database?.binaryPool?.let { binaryPool ->
|
||||||
|
entryInfo.attachments = getAttachments(binaryPool)
|
||||||
|
}
|
||||||
|
|
||||||
if (!raw)
|
if (!raw)
|
||||||
database?.stopManageEntry(this)
|
database?.stopManageEntry(this)
|
||||||
return entryInfo
|
return entryInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setEntryInfo(database: Database?, newEntryInfo: EntryInfo) {
|
||||||
|
database?.startManageEntry(this)
|
||||||
|
|
||||||
|
removeAllFields()
|
||||||
|
removeAllAttachments()
|
||||||
|
// NodeId stay as is
|
||||||
|
title = newEntryInfo.title
|
||||||
|
icon = newEntryInfo.icon
|
||||||
|
username = newEntryInfo.username
|
||||||
|
password = newEntryInfo.password
|
||||||
|
expires = newEntryInfo.expires
|
||||||
|
expiryTime = newEntryInfo.expiryTime
|
||||||
|
url = newEntryInfo.url
|
||||||
|
notes = newEntryInfo.notes
|
||||||
|
addExtraFields(newEntryInfo.customFields)
|
||||||
|
database?.binaryPool?.let { binaryPool ->
|
||||||
|
addAttachments(binaryPool, newEntryInfo.attachments)
|
||||||
|
}
|
||||||
|
// Update date time
|
||||||
|
lastAccessTime = DateInstant()
|
||||||
|
lastModificationTime = DateInstant()
|
||||||
|
|
||||||
|
database?.stopManageEntry(this)
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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,76 +28,90 @@ import java.util.zip.GZIPOutputStream
|
|||||||
|
|
||||||
class BinaryAttachment : Parcelable {
|
class BinaryAttachment : Parcelable {
|
||||||
|
|
||||||
var isCompressed: Boolean? = null
|
private var dataFile: File? = null
|
||||||
|
var isCompressed: Boolean = false
|
||||||
private set
|
private set
|
||||||
var isProtected: Boolean = false
|
var isProtected: Boolean = false
|
||||||
private set
|
private set
|
||||||
private var dataFile: File? = null
|
var isCorrupted: Boolean = false
|
||||||
|
|
||||||
fun length(): Long {
|
fun length(): Long {
|
||||||
if (dataFile != null)
|
return dataFile?.length() ?: 0
|
||||||
return dataFile!!.length()
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Empty protected binary
|
* Empty protected binary
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor()
|
||||||
this.isCompressed = null
|
|
||||||
this.isProtected = false
|
|
||||||
this.dataFile = null
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(dataFile: File, enableProtection: Boolean = false, compressed: Boolean? = null) {
|
constructor(dataFile: File, compressed: Boolean = false, protected: Boolean = false) {
|
||||||
this.isCompressed = compressed
|
|
||||||
this.isProtected = enableProtection
|
|
||||||
this.dataFile = dataFile
|
this.dataFile = dataFile
|
||||||
|
this.isCompressed = compressed
|
||||||
|
this.isProtected = protected
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(parcel: Parcel) {
|
private constructor(parcel: Parcel) {
|
||||||
val compressedByte = parcel.readByte().toInt()
|
|
||||||
isCompressed = if (compressedByte == 2) null else compressedByte != 0
|
|
||||||
isProtected = parcel.readByte().toInt() != 0
|
|
||||||
parcel.readString()?.let {
|
parcel.readString()?.let {
|
||||||
dataFile = File(it)
|
dataFile = File(it)
|
||||||
}
|
}
|
||||||
|
isCompressed = parcel.readByte().toInt() != 0
|
||||||
|
isProtected = parcel.readByte().toInt() != 0
|
||||||
|
isCorrupted = parcel.readByte().toInt() != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun getInputDataStream(): InputStream {
|
fun getInputDataStream(): InputStream {
|
||||||
return when {
|
return when {
|
||||||
dataFile != null -> FileInputStream(dataFile!!)
|
length() > 0 -> FileInputStream(dataFile!!)
|
||||||
else -> ByteArrayInputStream(ByteArray(0))
|
else -> ByteArrayInputStream(ByteArray(0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getUnGzipInputDataStream(): InputStream {
|
||||||
|
return if (isCompressed)
|
||||||
|
GZIPInputStream(getInputDataStream())
|
||||||
|
else
|
||||||
|
getInputDataStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getOutputDataStream(): OutputStream {
|
||||||
|
return when {
|
||||||
|
dataFile != null -> FileOutputStream(dataFile!!)
|
||||||
|
else -> throw IOException("Unable to write in an unknown file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getGzipOutputDataStream(): OutputStream {
|
||||||
|
return if (isCompressed) {
|
||||||
|
GZIPOutputStream(getOutputDataStream())
|
||||||
|
} else {
|
||||||
|
getOutputDataStream()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@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 +119,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) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,26 +159,33 @@ class BinaryAttachment : Parcelable {
|
|||||||
|
|
||||||
return isCompressed == other.isCompressed
|
return isCompressed == other.isCompressed
|
||||||
&& isProtected == other.isProtected
|
&& isProtected == other.isProtected
|
||||||
|
&& isCorrupted == other.isCorrupted
|
||||||
&& sameData
|
&& sameData
|
||||||
}
|
}
|
||||||
|
|
||||||
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 + if (isCorrupted) 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 (isProtected) 1 else 0).toByte())
|
|
||||||
dest.writeString(dataFile?.absolutePath)
|
dest.writeString(dataFile?.absolutePath)
|
||||||
|
dest.writeByte((if (isCompressed) 1 else 0).toByte())
|
||||||
|
dest.writeByte((if (isProtected) 1 else 0).toByte())
|
||||||
|
dest.writeByte((if (isCorrupted) 1 else 0).toByte())
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -19,52 +19,137 @@
|
|||||||
*/
|
*/
|
||||||
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
val stringBuffer = StringBuffer()
|
||||||
|
for ((key, value) in pool) {
|
||||||
|
if (stringBuffer.isNotEmpty())
|
||||||
|
stringBuffer.append(", {$key:$value}")
|
||||||
|
else
|
||||||
|
stringBuffer.append("{$key:$value}")
|
||||||
|
}
|
||||||
|
return stringBuffer.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility data class to order binaries
|
||||||
|
*/
|
||||||
|
data class KeyBinary(val key: Int, val binary: BinaryAttachment)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
|||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.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,10 +40,12 @@ 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()
|
||||||
|
|
||||||
|
private var binaryIncrement = 0
|
||||||
|
|
||||||
override val version: String
|
override val version: String
|
||||||
get() = "KeePass 1"
|
get() = "KeePass 1"
|
||||||
|
|
||||||
@@ -57,7 +61,12 @@ 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() {
|
||||||
|
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]
|
||||||
@@ -177,6 +186,13 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
override fun isInRecycleBin(group: GroupKDB): Boolean {
|
override fun isInRecycleBin(group: GroupKDB): Boolean {
|
||||||
var currentGroup: GroupKDB? = group
|
var currentGroup: GroupKDB? = group
|
||||||
|
|
||||||
|
// Init backup group variable
|
||||||
|
if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
|
||||||
|
findBackupGroupId()
|
||||||
|
|
||||||
|
if (backupGroup == null)
|
||||||
|
return false
|
||||||
|
|
||||||
if (currentGroup == backupGroup)
|
if (currentGroup == backupGroup)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
@@ -191,17 +207,21 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun findBackupGroupId() {
|
||||||
* Ensure that the recycle bin tree exists, if enabled and create it
|
|
||||||
* if it doesn't exist
|
|
||||||
*/
|
|
||||||
fun ensureRecycleBinExists() {
|
|
||||||
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)) {
|
||||||
backupGroupId = currentGroup.id
|
backupGroupId = currentGroup.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that the backup tree exists if enabled, and create it
|
||||||
|
* if it doesn't exist
|
||||||
|
*/
|
||||||
|
fun ensureBackupExists() {
|
||||||
|
findBackupGroupId()
|
||||||
|
|
||||||
if (backupGroup == null) {
|
if (backupGroup == null) {
|
||||||
// Create recycle bin
|
// Create recycle bin
|
||||||
@@ -219,21 +239,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 (backupGroup == null)
|
||||||
fun canRecycle(): Boolean {
|
ensureBackupExists()
|
||||||
|
if (node == backupGroup)
|
||||||
|
return false
|
||||||
|
backupGroup?.let {
|
||||||
|
if (node.isContainedIn(it))
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recycle(group: GroupKDB) {
|
fun recycle(group: GroupKDB) {
|
||||||
ensureRecycleBinExists()
|
|
||||||
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()
|
|
||||||
removeEntryFrom(entry, entry.parent)
|
removeEntryFrom(entry, entry.parent)
|
||||||
addEntryTo(entry, backupGroup)
|
addEntryTo(entry, backupGroup)
|
||||||
entry.afterAssignNewParent()
|
entry.afterAssignNewParent()
|
||||||
@@ -249,6 +273,13 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
addEntryTo(entry, origParent)
|
addEntryTo(entry, origParent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun buildNewBinary(cacheDirectory: File): BinaryAttachment {
|
||||||
|
// Generate an unique new file
|
||||||
|
val fileInCache = File(cacheDirectory, binaryIncrement.toString())
|
||||||
|
binaryIncrement++
|
||||||
|
return BinaryAttachment(fileInCache)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TYPE = DatabaseKDB::class.java
|
val TYPE = DatabaseKDB::class.java
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
|||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.crypto.keyDerivation.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.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
||||||
@@ -46,6 +47,7 @@ import com.kunzisoft.keepass.utils.UnsignedInt
|
|||||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
import 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
|
||||||
@@ -105,6 +107,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
val customData = HashMap<String, String>()
|
val customData = HashMap<String, String>()
|
||||||
|
|
||||||
var binaryPool = BinaryPool()
|
var binaryPool = BinaryPool()
|
||||||
|
private var binaryIncrement = 0 // Unique id (don't use current time because CPU too fast)
|
||||||
|
|
||||||
var localizedAppName = "KeePassDX"
|
var localizedAppName = "KeePassDX"
|
||||||
|
|
||||||
@@ -173,33 +176,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 +557,60 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
return publicCustomData.size() > 0
|
return publicCustomData.size() > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun buildNewBinary(cacheDirectory: File,
|
||||||
|
compression: Boolean,
|
||||||
|
protection: Boolean,
|
||||||
|
binaryPoolId: Int? = null): BinaryAttachment {
|
||||||
|
// New file with current time
|
||||||
|
val fileInCache = File(cacheDirectory, binaryIncrement.toString())
|
||||||
|
binaryIncrement++
|
||||||
|
val binaryAttachment = BinaryAttachment(fileInCache, compression, protection)
|
||||||
|
// add attachment to pool
|
||||||
|
binaryPool.put(binaryPoolId, binaryAttachment)
|
||||||
|
return binaryAttachment
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeUnlinkedAttachment(binary: BinaryAttachment, clear: Boolean) {
|
||||||
|
val listBinaries = ArrayList<BinaryAttachment>()
|
||||||
|
listBinaries.add(binary)
|
||||||
|
removeUnlinkedAttachments(listBinaries, clear)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeUnlinkedAttachments(clear: Boolean) {
|
||||||
|
removeUnlinkedAttachments(emptyList(), clear)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeUnlinkedAttachments(binaries: List<BinaryAttachment>, clear: Boolean) {
|
||||||
|
// Build binaries to remove with all binaries known
|
||||||
|
val binariesToRemove = ArrayList<BinaryAttachment>()
|
||||||
|
if (binaries.isEmpty()) {
|
||||||
|
binaryPool.doForEachBinary { binary ->
|
||||||
|
binariesToRemove.add(binary)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binariesToRemove.addAll(binaries)
|
||||||
|
}
|
||||||
|
// Remove binaries from the list
|
||||||
|
rootGroup?.doForEachChild(object : NodeHandler<EntryKDBX>() {
|
||||||
|
override fun operate(node: EntryKDBX): Boolean {
|
||||||
|
node.getAttachments(binaryPool, true).forEach {
|
||||||
|
binariesToRemove.remove(it.binaryAttachment)
|
||||||
|
}
|
||||||
|
return binariesToRemove.isNotEmpty()
|
||||||
|
}
|
||||||
|
}, null)
|
||||||
|
// Effective removing
|
||||||
|
binariesToRemove.forEach {
|
||||||
|
try {
|
||||||
|
binaryPool.remove(it)
|
||||||
|
if (clear)
|
||||||
|
it.clear()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Unable to clean binaries", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
override fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
||||||
if (password == null)
|
if (password == null)
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
|||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
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 java.io.*
|
import java.io.*
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
@@ -136,7 +135,6 @@ abstract class DatabaseVersioned<
|
|||||||
}
|
}
|
||||||
|
|
||||||
when (keyData.size.toLong()) {
|
when (keyData.size.toLong()) {
|
||||||
0L -> throw KeyFileEmptyDatabaseException()
|
|
||||||
32L -> return keyData
|
32L -> return keyData
|
||||||
64L -> try {
|
64L -> try {
|
||||||
return hexStringToByteArray(String(keyData))
|
return hexStringToByteArray(String(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? = null) {
|
||||||
|
if (attachment == null || 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,12 @@ 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.LinkedHashMap
|
||||||
|
|
||||||
class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
|
class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
|
||||||
|
|
||||||
@@ -58,9 +61,9 @@ 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>()
|
var customData = LinkedHashMap<String, String>()
|
||||||
var fields = HashMap<String, ProtectedString>()
|
var fields = LinkedHashMap<String, ProtectedString>()
|
||||||
var binaries = HashMap<String, BinaryAttachment>()
|
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
|
||||||
var foregroundColor = ""
|
var foregroundColor = ""
|
||||||
var backgroundColor = ""
|
var backgroundColor = ""
|
||||||
var overrideURL = ""
|
var overrideURL = ""
|
||||||
@@ -69,36 +72,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 +108,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 +122,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 +165,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,23 +259,17 @@ 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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return field
|
return field
|
||||||
}
|
}
|
||||||
|
|
||||||
fun allowCustomFields(): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeAllFields() {
|
fun removeAllFields() {
|
||||||
fields.clear()
|
fields.clear()
|
||||||
}
|
}
|
||||||
@@ -285,12 +278,47 @@ 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 sizeOfHistory(): Int {
|
fun containsAttachment(): Boolean {
|
||||||
return history.size
|
return binaries.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
|
||||||
|
binaries[attachment.name] = binaryPool.put(attachment.binaryAttachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAttachment(attachment: Attachment) {
|
||||||
|
binaries.remove(attachment.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAttachments() {
|
||||||
|
binaries.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAttachmentsSize(binaryPool: BinaryPool): Long {
|
||||||
|
var size = 0L
|
||||||
|
for ((label, poolId) in binaries) {
|
||||||
|
size += label.length.toLong()
|
||||||
|
size += binaryPool[poolId]?.length() ?: 0
|
||||||
|
}
|
||||||
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putCustomData(key: String, value: String) {
|
override fun putCustomData(key: String, value: String) {
|
||||||
@@ -305,15 +333,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 +350,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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ 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
|
||||||
|
|||||||
@@ -116,13 +116,6 @@ class InvalidCredentialsDatabaseException : LoadDatabaseException {
|
|||||||
constructor(exception: Throwable) : super(exception)
|
constructor(exception: Throwable) : super(exception)
|
||||||
}
|
}
|
||||||
|
|
||||||
class KeyFileEmptyDatabaseException : LoadDatabaseException {
|
|
||||||
@StringRes
|
|
||||||
override var errorId: Int = R.string.keyfile_is_empty
|
|
||||||
constructor() : super()
|
|
||||||
constructor(exception: Throwable) : super(exception)
|
|
||||||
}
|
|
||||||
|
|
||||||
class NoMemoryDatabaseException: LoadDatabaseException {
|
class NoMemoryDatabaseException: LoadDatabaseException {
|
||||||
@StringRes
|
@StringRes
|
||||||
override var errorId: Int = R.string.error_out_of_memory
|
override var errorId: Int = R.string.error_out_of_memory
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import com.kunzisoft.keepass.database.exception.VersionDatabaseException
|
|||||||
import com.kunzisoft.keepass.stream.*
|
import com.kunzisoft.keepass.stream.*
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
import com.kunzisoft.keepass.utils.UnsignedLong
|
||||||
|
import com.kunzisoft.keepass.utils.VariantDictionary
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -176,10 +177,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,38 +193,37 @@ 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)
|
||||||
|
|
||||||
PwDbHeaderV4Fields.PublicCustomData -> {
|
PwDbHeaderV4Fields.PublicCustomData -> databaseV4.publicCustomData = VariantDictionary.deserialize(fieldData)
|
||||||
databaseV4.publicCustomData = KdfParameters.deserialize(fieldData)!! // TODO verify
|
|
||||||
throw IOException("Invalid header type: $fieldID")
|
|
||||||
}
|
|
||||||
else -> throw IOException("Invalid header type: $fieldID")
|
else -> throw IOException("Invalid header type: $fieldID")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +261,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 +277,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 +292,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 +306,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)
|
||||||
|
|||||||
@@ -20,12 +20,14 @@
|
|||||||
package com.kunzisoft.keepass.database.file.input
|
package com.kunzisoft.keepass.database.file.input
|
||||||
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||||
import com.kunzisoft.keepass.crypto.StreamCipherFactory
|
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 +37,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 +51,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 +72,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 +133,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 +181,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 +229,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 +252,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.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
||||||
FileOutputStream(file).use { outputStream ->
|
val byteLength = size - 1
|
||||||
|
// No compression at this level
|
||||||
|
val protectedBinary = mDatabase.buildNewBinary(cacheDirectory, false, protectedFlag)
|
||||||
|
protectedBinary.getOutputDataStream().use { outputStream ->
|
||||||
dataInputStream.readBytes(byteLength, DatabaseKDBX.BUFFER_SIZE_BYTES) { buffer ->
|
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 +444,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 +486,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 +540,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)) {
|
||||||
@@ -749,8 +743,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
if (entryInHistory) {
|
if (entryInHistory) {
|
||||||
ctxEntry = ctxHistoryBase
|
ctxEntry = ctxHistoryBase
|
||||||
return KdbContext.EntryHistory
|
return KdbContext.EntryHistory
|
||||||
}
|
} else if (ctxEntry != null) {
|
||||||
else if (ctxEntry != null) {
|
|
||||||
// Add entry to the index only when close the XML element
|
// Add entry to the index only when close the XML element
|
||||||
mDatabase.addEntryIndex(ctxEntry!!)
|
mDatabase.addEntryIndex(ctxEntry!!)
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
@@ -885,9 +879,14 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
if (encoded.isEmpty()) {
|
if (encoded.isEmpty()) {
|
||||||
return DatabaseVersioned.UUID_ZERO
|
return DatabaseVersioned.UUID_ZERO
|
||||||
}
|
}
|
||||||
val buf = Base64.decode(encoded, BASE_64_FLAG)
|
|
||||||
|
|
||||||
return bytes16ToUuid(buf)
|
return try {
|
||||||
|
val buf = Base64.decode(encoded, BASE_64_FLAG)
|
||||||
|
bytes16ToUuid(buf)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to read base 64 UUID, create a random one", e)
|
||||||
|
UUID.randomUUID()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class, XmlPullParserException::class)
|
@Throws(IOException::class, XmlPullParserException::class)
|
||||||
@@ -947,50 +946,69 @@ 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 {
|
var binaryRetrieve = mDatabase.binaryPool[id]
|
||||||
var compressed = false
|
// Create empty binary if not retrieved in pool
|
||||||
var protected = false
|
if (binaryRetrieve == null) {
|
||||||
|
binaryRetrieve = mDatabase.buildNewBinary(cacheDirectory,
|
||||||
if (xpp.attributeCount > 0) {
|
compression = false, protection = true, binaryPoolId = id)
|
||||||
val compress = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrCompressed)
|
|
||||||
if (compress != null) {
|
|
||||||
compressed = compress.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
|
|
||||||
if (protect != null) {
|
|
||||||
protected = protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
|
|
||||||
}
|
}
|
||||||
|
return binaryRetrieve
|
||||||
}
|
}
|
||||||
|
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 = true
|
||||||
|
|
||||||
|
if (xpp.attributeCount > 0) {
|
||||||
|
val compress = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrCompressed)
|
||||||
|
if (compress != null) {
|
||||||
|
compressed = compress.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
|
||||||
|
if (protect != null) {
|
||||||
|
protected = protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val base64 = readString(xpp)
|
||||||
|
if (base64.isEmpty())
|
||||||
|
return null
|
||||||
|
|
||||||
|
// Build the new binary and compress
|
||||||
|
val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory, compressed, protected, binaryId)
|
||||||
|
try {
|
||||||
|
binaryAttachment.getOutputDataStream().use { outputStream ->
|
||||||
|
outputStream.write(Base64.decode(base64, BASE_64_FLAG))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to read base 64 attachment", e)
|
||||||
|
binaryAttachment.isCorrupted = true
|
||||||
|
binaryAttachment.getOutputDataStream().use { outputStream ->
|
||||||
|
outputStream.write(base64.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return binaryAttachment
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IOException::class, XmlPullParserException::class)
|
@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)
|
||||||
@@ -1045,6 +1063,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
private val TAG = DatabaseInputKDBX::class.java.name
|
||||||
|
|
||||||
private val DEFAULT_HISTORY_DAYS = UnsignedInt(365)
|
private val DEFAULT_HISTORY_DAYS = UnsignedInt(365)
|
||||||
|
|
||||||
@Throws(XmlPullParserException::class)
|
@Throws(XmlPullParserException::class)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BU
|
|||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||||
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
|
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
|
||||||
import com.kunzisoft.keepass.stream.readBytes
|
import com.kunzisoft.keepass.stream.readBytes
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import kotlin.experimental.or
|
import kotlin.experimental.or
|
||||||
@@ -36,33 +37,40 @@ class DatabaseInnerHeaderOutputKDBX(private val database: DatabaseKDBX,
|
|||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun output() {
|
fun output() {
|
||||||
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID.toInt())
|
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID)
|
||||||
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.writeUInt(header.innerRandomStream!!.id)
|
||||||
|
|
||||||
val streamKeySize = header.innerRandomStreamKey.size
|
val streamKeySize = header.innerRandomStreamKey.size
|
||||||
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey.toInt())
|
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey)
|
||||||
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
|
||||||
|
// Force decompression to add binary in header
|
||||||
|
protectedBinary.decompress()
|
||||||
|
// Write type binary
|
||||||
|
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
|
||||||
|
// Write size
|
||||||
|
dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length() + 1))
|
||||||
|
// Write protected flag
|
||||||
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
|
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
|
||||||
if (protectedBinary.isProtected) {
|
if (protectedBinary.isProtected) {
|
||||||
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
||||||
}
|
}
|
||||||
|
dataOutputStream.writeByte(flag)
|
||||||
|
|
||||||
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary.toInt())
|
protectedBinary.getInputDataStream().use { inputStream ->
|
||||||
dataOutputStream.writeInt(protectedBinary.length().toInt() + 1) // TODO verify
|
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||||
dataOutputStream.write(flag.toInt())
|
dataOutputStream.write(buffer)
|
||||||
|
}
|
||||||
protectedBinary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
|
||||||
dataOutputStream.write(buffer)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader.toInt())
|
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader)
|
||||||
dataOutputStream.writeInt(0)
|
dataOutputStream.writeInt(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,6 +359,9 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
|
|
||||||
writeFields(entry.fields)
|
writeFields(entry.fields)
|
||||||
writeEntryBinaries(entry.binaries)
|
writeEntryBinaries(entry.binaries)
|
||||||
|
if (entry.containsCustomData()) {
|
||||||
|
writeCustomData(entry.customData)
|
||||||
|
}
|
||||||
writeAutoType(entry.autoType)
|
writeAutoType(entry.autoType)
|
||||||
|
|
||||||
if (!isHistory) {
|
if (!isHistory) {
|
||||||
@@ -385,7 +387,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 +424,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 +434,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 +450,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 +481,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 +551,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 +620,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 ->
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import com.kunzisoft.keepass.model.SearchInfo
|
|||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
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
|
||||||
@@ -48,9 +48,14 @@ class SearchHelper(private val isOmitBackup: Boolean) {
|
|||||||
if (database.loaded && TimeoutHelper.checkTime(context)) {
|
if (database.loaded && TimeoutHelper.checkTime(context)) {
|
||||||
var searchWithoutUI = false
|
var searchWithoutUI = false
|
||||||
if (PreferencesUtil.isAutofillAutoSearchEnable(context)
|
if (PreferencesUtil.isAutofillAutoSearchEnable(context)
|
||||||
&& searchInfo != null) {
|
&& searchInfo != null
|
||||||
|
&& !searchInfo.containsOnlyNullValues()) {
|
||||||
// If search provide results
|
// If search provide results
|
||||||
database.createVirtualGroupFromSearch(searchInfo, SearchHelper.MAX_SEARCH_ENTRY)?.let { searchGroup ->
|
database.createVirtualGroupFromSearchInfo(
|
||||||
|
searchInfo.toString(),
|
||||||
|
PreferencesUtil.omitBackup(context),
|
||||||
|
MAX_SEARCH_ENTRY
|
||||||
|
)?.let { searchGroup ->
|
||||||
if (searchGroup.getNumberOfChildEntries() > 0) {
|
if (searchGroup.getNumberOfChildEntries() > 0) {
|
||||||
searchWithoutUI = true
|
searchWithoutUI = true
|
||||||
onItemsFound.invoke(
|
onItemsFound.invoke(
|
||||||
@@ -72,6 +77,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()
|
||||||
@@ -96,7 +102,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)
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
|||||||
removeEntryInfo()
|
removeEntryInfo()
|
||||||
assignKeyboardView()
|
assignKeyboardView()
|
||||||
}
|
}
|
||||||
|
lockReceiver?.backToPreviousKeyboardAction = {
|
||||||
|
switchToPreviousKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
registerLockReceiver(lockReceiver, true)
|
registerLockReceiver(lockReceiver, true)
|
||||||
}
|
}
|
||||||
@@ -105,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
|
||||||
@@ -172,7 +184,6 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun switchToPreviousKeyboard() {
|
private fun switchToPreviousKeyboard() {
|
||||||
var imeManager: InputMethodManager? = null
|
var imeManager: InputMethodManager? = null
|
||||||
try {
|
try {
|
||||||
@@ -262,8 +273,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) {
|
||||||
@@ -314,10 +329,13 @@ 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) {
|
||||||
@@ -327,6 +345,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
|||||||
fun addEntryAndLaunchNotificationIfAllowed(context: Context, entry: EntryInfo, toast: Boolean = false) {
|
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, toast)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user