mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
629 Commits
feature/Cr
...
3.0.0_beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11c1cc7c72 | ||
|
|
6b7acb7bd5 | ||
|
|
bdebf19d7b | ||
|
|
cb1973ffb5 | ||
|
|
2447599364 | ||
|
|
6a2cda74f1 | ||
|
|
8385d55d69 | ||
|
|
85e3464a15 | ||
|
|
6680039de7 | ||
|
|
9935826877 | ||
|
|
84c26b7c40 | ||
|
|
1cd7940a17 | ||
|
|
9514032f25 | ||
|
|
c7d6da2373 | ||
|
|
41b822fb6c | ||
|
|
69bf098c84 | ||
|
|
b4283ed4dc | ||
|
|
0fa0cac9e6 | ||
|
|
cf0f665b14 | ||
|
|
2034e3ab78 | ||
|
|
89cfeec1b3 | ||
|
|
d8ae212df0 | ||
|
|
09d79d52ae | ||
|
|
5c4b98d0e9 | ||
|
|
e5d6fc0604 | ||
|
|
5e656ebfba | ||
|
|
58fb75e55d | ||
|
|
e01621e658 | ||
|
|
b4aee17f53 | ||
|
|
dc70918648 | ||
|
|
69772edfa3 | ||
|
|
dd224cab05 | ||
|
|
b62873129e | ||
|
|
5052a1f564 | ||
|
|
2c36163e7a | ||
|
|
1bf912d6f0 | ||
|
|
d290259075 | ||
|
|
1689672faf | ||
|
|
8196e05679 | ||
|
|
fe0235da43 | ||
|
|
0895a73546 | ||
|
|
f06821e35b | ||
|
|
9cce5f645f | ||
|
|
c1a46408e9 | ||
|
|
21c3ccd637 | ||
|
|
3b9b034d80 | ||
|
|
348a5c3eb7 | ||
|
|
4c5be658c3 | ||
|
|
b6517a449b | ||
|
|
ae0b8db0b0 | ||
|
|
56f0f8a299 | ||
|
|
c9af786b79 | ||
|
|
f34f615b80 | ||
|
|
aef2ef8479 | ||
|
|
7afbc9f5a4 | ||
|
|
df51b62041 | ||
|
|
045abc54fb | ||
|
|
9b2d9683eb | ||
|
|
3b0dd4a36c | ||
|
|
5e15f82313 | ||
|
|
d841c25bd3 | ||
|
|
8d3f1fe179 | ||
|
|
130ec130cc | ||
|
|
5e7a95eac0 | ||
|
|
a8cb49d12d | ||
|
|
c179ac626a | ||
|
|
041583bf96 | ||
|
|
ed710335b3 | ||
|
|
b556581a87 | ||
|
|
77a1b7918c | ||
|
|
45149e1b28 | ||
|
|
932338a25a | ||
|
|
925509e5a0 | ||
|
|
25646fbad7 | ||
|
|
e1733512c4 | ||
|
|
8379ffe1ce | ||
|
|
c77537ecee | ||
|
|
2192d97c69 | ||
|
|
a0dc76bda8 | ||
|
|
7fe177edc6 | ||
|
|
1f5e6f1e17 | ||
|
|
bf0aa295b0 | ||
|
|
649dffc3e0 | ||
|
|
a0f5ed66e2 | ||
|
|
7df3b95c22 | ||
|
|
0756474d40 | ||
|
|
60747db945 | ||
|
|
afcfad162e | ||
|
|
63f15bdc9e | ||
|
|
3b826869e9 | ||
|
|
af0256add0 | ||
|
|
b8d8cba12c | ||
|
|
616e9a0ec2 | ||
|
|
366434cbd7 | ||
|
|
f6d4046af6 | ||
|
|
82932f002e | ||
|
|
7593a05953 | ||
|
|
3026a9e3e4 | ||
|
|
362939eab9 | ||
|
|
61d52731a5 | ||
|
|
6aecc6521c | ||
|
|
ef5829593e | ||
|
|
4a8f67093f | ||
|
|
9cbe0664f6 | ||
|
|
965d6e4e8e | ||
|
|
eefdeb0bb7 | ||
|
|
a904a51293 | ||
|
|
cce377d70d | ||
|
|
5721bca5a3 | ||
|
|
bcd5b024f0 | ||
|
|
571f257c17 | ||
|
|
3451135800 | ||
|
|
f426a78a94 | ||
|
|
3d65236e63 | ||
|
|
7b51b5005a | ||
|
|
3d9cf16960 | ||
|
|
35def53666 | ||
|
|
5c46a89ddc | ||
|
|
4e429025bf | ||
|
|
95fae11eee | ||
|
|
9a22a9fb8b | ||
|
|
f60e2e2ca6 | ||
|
|
deb9101335 | ||
|
|
407f93ac43 | ||
|
|
78c39edceb | ||
|
|
c8445fb711 | ||
|
|
7c0e7347c8 | ||
|
|
12f37d0931 | ||
|
|
9a5086d9ba | ||
|
|
3222c7e677 | ||
|
|
632e0648d4 | ||
|
|
e3198031e3 | ||
|
|
0ced9c8e26 | ||
|
|
65f4a708cd | ||
|
|
36e7b00d9a | ||
|
|
8b2c48f5ca | ||
|
|
9f7a0d4f17 | ||
|
|
fa5ae17621 | ||
|
|
7a2536c559 | ||
|
|
96d2edb641 | ||
|
|
8a2bd23c32 | ||
|
|
d3b935ea7f | ||
|
|
e53bc3b048 | ||
|
|
5f1cfc9dda | ||
|
|
43207b316f | ||
|
|
96ed4c419a | ||
|
|
840a2253e2 | ||
|
|
18db9b0a77 | ||
|
|
6c7a5292a4 | ||
|
|
bef1c74226 | ||
|
|
176ec8bace | ||
|
|
45b7800a68 | ||
|
|
fa761ac69b | ||
|
|
cc11e98aa6 | ||
|
|
8f1c71137a | ||
|
|
8fdf2dcb7a | ||
|
|
d4cd5b73bd | ||
|
|
77975aed2a | ||
|
|
a7700ce27e | ||
|
|
726ff1a126 | ||
|
|
e24269c452 | ||
|
|
686f4656ec | ||
|
|
55fe10d2dc | ||
|
|
422984ac41 | ||
|
|
706d117d80 | ||
|
|
13f8df4e0d | ||
|
|
263d433193 | ||
|
|
c15c11f3b1 | ||
|
|
524c8ccfc5 | ||
|
|
902392ea30 | ||
|
|
bef179187f | ||
|
|
ea7221c39a | ||
|
|
edaf9f6296 | ||
|
|
0d83725b77 | ||
|
|
6ce31305c6 | ||
|
|
90935c033d | ||
|
|
b4c3f831a7 | ||
|
|
f0e25e8198 | ||
|
|
d800082621 | ||
|
|
653d3da718 | ||
|
|
0f39409386 | ||
|
|
ccae0d1a57 | ||
|
|
257992d314 | ||
|
|
5eb843b63d | ||
|
|
3929b478a7 | ||
|
|
18734ed822 | ||
|
|
876e749b31 | ||
|
|
32b8c505d9 | ||
|
|
37a0dce7c5 | ||
|
|
2332f36b56 | ||
|
|
21cc9cc026 | ||
|
|
db4d76502e | ||
|
|
1578ea7590 | ||
|
|
7405de01fe | ||
|
|
77dc5943e5 | ||
|
|
f8a2748ede | ||
|
|
a99ca00bb3 | ||
|
|
6eb80eea2f | ||
|
|
82828f7f82 | ||
|
|
23c9a5963a | ||
|
|
7595f113ec | ||
|
|
e9e5a4ee0d | ||
|
|
1947fc3e83 | ||
|
|
8844689482 | ||
|
|
0d2ba54c10 | ||
|
|
a4bb5137ea | ||
|
|
b8aea1f97a | ||
|
|
120e1893bd | ||
|
|
836df52a50 | ||
|
|
00498aaeac | ||
|
|
f4f47cff75 | ||
|
|
40dc3d45fc | ||
|
|
b89d2a6da1 | ||
|
|
55a4af9f00 | ||
|
|
719d45e75e | ||
|
|
8703684740 | ||
|
|
e95e7218f6 | ||
|
|
e9d4711978 | ||
|
|
9309506e97 | ||
|
|
00d2a80e95 | ||
|
|
c1e62b7d90 | ||
|
|
84775d36dc | ||
|
|
fc4eb11fd8 | ||
|
|
ce70ce6c76 | ||
|
|
ffd404ec1b | ||
|
|
6f943db012 | ||
|
|
12342ac426 | ||
|
|
489ddc3f56 | ||
|
|
02a266cbea | ||
|
|
bdc9facd41 | ||
|
|
7d679aac0b | ||
|
|
ae1719e795 | ||
|
|
6cea05e9f4 | ||
|
|
cda84d4e64 | ||
|
|
b3f63a85d5 | ||
|
|
196903cb12 | ||
|
|
e6f082adfd | ||
|
|
bb2f641073 | ||
|
|
de6312d317 | ||
|
|
887b0f3119 | ||
|
|
f13c8d7884 | ||
|
|
a10fdb260d | ||
|
|
0f8b1790f3 | ||
|
|
531e345dd9 | ||
|
|
c1942759d4 | ||
|
|
eca9e573de | ||
|
|
61376f8d68 | ||
|
|
292e2c60e2 | ||
|
|
2ffaf81109 | ||
|
|
da37381678 | ||
|
|
3236bf6122 | ||
|
|
a352ae6922 | ||
|
|
f3631a6a09 | ||
|
|
0d376bd5b7 | ||
|
|
fd57bd0378 | ||
|
|
5a11882d1b | ||
|
|
0f041da548 | ||
|
|
00a23119c5 | ||
|
|
cdeb49473b | ||
|
|
b55b3731a5 | ||
|
|
4afbb688ba | ||
|
|
f289a921f1 | ||
|
|
f2165bc4c1 | ||
|
|
73b20bfe4a | ||
|
|
fbf2006e3f | ||
|
|
358b701396 | ||
|
|
d3caae3a2d | ||
|
|
300062d3ac | ||
|
|
bb0aaad383 | ||
|
|
e9aa8609f1 | ||
|
|
37cbb18626 | ||
|
|
3dad6daa2e | ||
|
|
f49485e161 | ||
|
|
f3f268742f | ||
|
|
99e76f2254 | ||
|
|
a7801e376b | ||
|
|
90000aa1fb | ||
|
|
a2a26cd058 | ||
|
|
e3cbefc5b6 | ||
|
|
c6ceb25ee8 | ||
|
|
022777888c | ||
|
|
ad5e644362 | ||
|
|
bf6cb04fe8 | ||
|
|
dbde23fb7b | ||
|
|
c9cb469d65 | ||
|
|
2e37c20e55 | ||
|
|
ade665d228 | ||
|
|
a62f0cfd3b | ||
|
|
8b867f78fe | ||
|
|
d6a43fd8e5 | ||
|
|
9887b8142d | ||
|
|
4d92d6dc2b | ||
|
|
d8cd84ed9e | ||
|
|
6bc740e881 | ||
|
|
db348cc368 | ||
|
|
6ebf59d7ff | ||
|
|
d35e31d128 | ||
|
|
b9b6d3d2cb | ||
|
|
728b111ac9 | ||
|
|
5afbfbfd43 | ||
|
|
9e5ce589ae | ||
|
|
f486150a0f | ||
|
|
075ee815f0 | ||
|
|
d321283b13 | ||
|
|
0ec72bb013 | ||
|
|
cdfbcd873c | ||
|
|
8be382fa7e | ||
|
|
82f5ab1446 | ||
|
|
c97f8c31ce | ||
|
|
92e5b5e9c3 | ||
|
|
a70fca493d | ||
|
|
42c8f0c345 | ||
|
|
d3d5a1745d | ||
|
|
8392d8b684 | ||
|
|
1abd13c6e0 | ||
|
|
e126b66e19 | ||
|
|
47449d93db | ||
|
|
13002f96f1 | ||
|
|
1985a035be | ||
|
|
cdfc7e4158 | ||
|
|
5050052710 | ||
|
|
48e39d2ffa | ||
|
|
a8d053e82a | ||
|
|
2154945c3c | ||
|
|
8d83a0a86a | ||
|
|
c196cdf405 | ||
|
|
b6a5f43176 | ||
|
|
008ded4a5c | ||
|
|
d476574d05 | ||
|
|
371b3813d4 | ||
|
|
e4c3c224d9 | ||
|
|
69bc697568 | ||
|
|
fe08d034bb | ||
|
|
18f4714410 | ||
|
|
1b6c416893 | ||
|
|
6153a28b4b | ||
|
|
9574cf16fb | ||
|
|
d309a67416 | ||
|
|
fb865af088 | ||
|
|
c1e7039357 | ||
|
|
0fd3b37641 | ||
|
|
cea91f7b2f | ||
|
|
3959896832 | ||
|
|
d55dccdeb1 | ||
|
|
c46c286b51 | ||
|
|
36b37b3b8f | ||
|
|
ce9931d8a3 | ||
|
|
66f5ff35d3 | ||
|
|
aa15d261f3 | ||
|
|
00a32463c7 | ||
|
|
dd60ff8b74 | ||
|
|
4588611cbf | ||
|
|
80c0152d46 | ||
|
|
fac727cd3d | ||
|
|
9c30183068 | ||
|
|
9c28d5c5c5 | ||
|
|
3c55e3a3f0 | ||
|
|
ba6e3b801d | ||
|
|
4064ab47ac | ||
|
|
b6005fcb56 | ||
|
|
71de526366 | ||
|
|
a3e5d8448b | ||
|
|
f65b6e5484 | ||
|
|
05d0d9e501 | ||
|
|
d107beadf2 | ||
|
|
9c2fd26579 | ||
|
|
d796ea6324 | ||
|
|
8386c9e729 | ||
|
|
00ca4524b5 | ||
|
|
f4d4853319 | ||
|
|
00678cc9ca | ||
|
|
2845486a3f | ||
|
|
a815e6447d | ||
|
|
5b71a30ee9 | ||
|
|
ac2d94420b | ||
|
|
1460c1364a | ||
|
|
37f38fe988 | ||
|
|
cf025b9135 | ||
|
|
283ff7a280 | ||
|
|
e668f016b4 | ||
|
|
256c2c955a | ||
|
|
d560c3e8de | ||
|
|
4f8e8e6669 | ||
|
|
7cdc2e0915 | ||
|
|
74b236b317 | ||
|
|
e344cafd9b | ||
|
|
4b0d16cad1 | ||
|
|
da4d8629bd | ||
|
|
68ae3b79ab | ||
|
|
78e0336c1b | ||
|
|
89ffeaf03b | ||
|
|
e439f4d643 | ||
|
|
8b779a0fca | ||
|
|
4b71dc8445 | ||
|
|
780875d5f2 | ||
|
|
7356d4b0e2 | ||
|
|
654dea6b7e | ||
|
|
1204374637 | ||
|
|
880fde2148 | ||
|
|
d776d76100 | ||
|
|
39d95105e1 | ||
|
|
2a7af826a8 | ||
|
|
9d2fd53073 | ||
|
|
4c99923467 | ||
|
|
edc5985a7e | ||
|
|
96fc79103b | ||
|
|
76879f3a73 | ||
|
|
38dac3803b | ||
|
|
c0d4ad2042 | ||
|
|
c669d5657a | ||
|
|
98c44fa578 | ||
|
|
328629fe88 | ||
|
|
9754535055 | ||
|
|
26f701d890 | ||
|
|
f3df8024e6 | ||
|
|
bc9fdfc7a4 | ||
|
|
e1e37989e4 | ||
|
|
dc67cb1807 | ||
|
|
2e8a2457bc | ||
|
|
6d4398c6fd | ||
|
|
9f67ad872d | ||
|
|
aba396f274 | ||
|
|
4315f34398 | ||
|
|
47b456e1ee | ||
|
|
529810b2dc | ||
|
|
6f67b8e788 | ||
|
|
a9c9d12444 | ||
|
|
f8428cec61 | ||
|
|
ac46bce807 | ||
|
|
23e899042d | ||
|
|
80c4a3c06d | ||
|
|
d2a31601ba | ||
|
|
7ddb4f3486 | ||
|
|
e56da87e0e | ||
|
|
2e4ebecf67 | ||
|
|
1b4ccaed91 | ||
|
|
1e2d41c7fb | ||
|
|
1b2ead054a | ||
|
|
468c1b95b7 | ||
|
|
60d8eff71f | ||
|
|
8baae8b801 | ||
|
|
839375fcf1 | ||
|
|
9f16f26347 | ||
|
|
69b4cacab4 | ||
|
|
b74b5040b1 | ||
|
|
a28decc854 | ||
|
|
ed82c36628 | ||
|
|
3536629dd9 | ||
|
|
c87696696e | ||
|
|
cb59cef1b8 | ||
|
|
b5ba03df4d | ||
|
|
d9b600466c | ||
|
|
0d37a59a5c | ||
|
|
4edf2f8cd1 | ||
|
|
c60dfdf0d7 | ||
|
|
006afc6841 | ||
|
|
f70879581d | ||
|
|
7a2d2b0376 | ||
|
|
b5368fa239 | ||
|
|
db1d71af9f | ||
|
|
6b03ef35a6 | ||
|
|
2afd02d86f | ||
|
|
b32c00f455 | ||
|
|
fbe9fb41ed | ||
|
|
cc0a7f7d76 | ||
|
|
db33fc60b9 | ||
|
|
6feaee4f86 | ||
|
|
1a27a31a32 | ||
|
|
5b611e71d5 | ||
|
|
6de88bfe11 | ||
|
|
6d7236249f | ||
|
|
69fbaba8a6 | ||
|
|
6d88737505 | ||
|
|
9869cfc736 | ||
|
|
8505326a68 | ||
|
|
3a4af88384 | ||
|
|
5b2e7d0f70 | ||
|
|
ddeea6bee3 | ||
|
|
0cfe3a7634 | ||
|
|
727463e4d1 | ||
|
|
d42abfdc56 | ||
|
|
e01ea1df4c | ||
|
|
078bfac5f5 | ||
|
|
111b07b9e6 | ||
|
|
dfbc89addc | ||
|
|
bf44da9a14 | ||
|
|
d75d13965b | ||
|
|
8aedebdc94 | ||
|
|
9388c4bb0d | ||
|
|
77d4f601af | ||
|
|
7fae590848 | ||
|
|
bc41558a26 | ||
|
|
f6651face4 | ||
|
|
345f00f7f2 | ||
|
|
e876d02118 | ||
|
|
5b7018f71b | ||
|
|
f45b3fc50a | ||
|
|
01196be30d | ||
|
|
0a2999bffb | ||
|
|
8f097096e7 | ||
|
|
cd97fc046a | ||
|
|
eeb10f31a6 | ||
|
|
9df5e116e8 | ||
|
|
1228a03d39 | ||
|
|
a5e1b3096e | ||
|
|
b41ae67128 | ||
|
|
ddfbe20125 | ||
|
|
0bfe9291dd | ||
|
|
622b2e1edc | ||
|
|
4a40719534 | ||
|
|
384993d363 | ||
|
|
01b7d28154 | ||
|
|
d7c4f5577f | ||
|
|
a69d23ca64 | ||
|
|
e2f8b7a6e3 | ||
|
|
171a0b012f | ||
|
|
5c04b15433 | ||
|
|
6397feffff | ||
|
|
e73b9b7f1c | ||
|
|
2d528db054 | ||
|
|
80c4ba6723 | ||
|
|
0d82e40c67 | ||
|
|
b75d6d02fa | ||
|
|
76d4542716 | ||
|
|
87955de849 | ||
|
|
6df60cf5da | ||
|
|
3c23a314f0 | ||
|
|
8fda6b04a4 | ||
|
|
9fa98e6b76 | ||
|
|
deb685f39b | ||
|
|
d7851d3a18 | ||
|
|
44946fc54a | ||
|
|
a033d10adc | ||
|
|
5a3e599fe0 | ||
|
|
a9f645f389 | ||
|
|
d662f0903a | ||
|
|
beaa947eb7 | ||
|
|
48006b64d6 | ||
|
|
8f195ba66f | ||
|
|
123288e745 | ||
|
|
5866e95d49 | ||
|
|
e79f395424 | ||
|
|
999ca87fec | ||
|
|
1217266d88 | ||
|
|
bb262198be | ||
|
|
11aae77caf | ||
|
|
8212cede6e | ||
|
|
a3c51884f4 | ||
|
|
b8890aca7f | ||
|
|
014b0cce14 | ||
|
|
6d860c5cb7 | ||
|
|
d8be832858 | ||
|
|
afcb9fcf41 | ||
|
|
3c7ae0aaf0 | ||
|
|
6b7f93dbfe | ||
|
|
c40b255022 | ||
|
|
1742d265f3 | ||
|
|
3240e0bcae | ||
|
|
ff185f6505 | ||
|
|
346b517c9d | ||
|
|
80f00aba0a | ||
|
|
949905f6e2 | ||
|
|
b9e26fecfd | ||
|
|
232682f4a8 | ||
|
|
de3b690d60 | ||
|
|
de69a78a98 | ||
|
|
1c341c34a3 | ||
|
|
33beb57e9d | ||
|
|
66eeadca0b | ||
|
|
a10d1c98a8 | ||
|
|
59ead4986f | ||
|
|
09f6c18189 | ||
|
|
a5cd6d5ac0 | ||
|
|
0f3ad7c8b1 | ||
|
|
0487dea7fc | ||
|
|
a6803bf0e3 | ||
|
|
8cac1ee284 | ||
|
|
196620e1bd | ||
|
|
43d6c76873 | ||
|
|
b864c39a0d | ||
|
|
818b975111 | ||
|
|
d5fbc8393f | ||
|
|
df9a71a63d | ||
|
|
7b5e9d2344 | ||
|
|
7fc2d95886 | ||
|
|
2ba8702787 | ||
|
|
78d3b369bb | ||
|
|
bb3620680b | ||
|
|
d4a45655ca | ||
|
|
c9c739fd52 | ||
|
|
2b359cc592 | ||
|
|
151b7a323d | ||
|
|
1063dc2b63 | ||
|
|
f9f59a6eb1 | ||
|
|
73156cc337 | ||
|
|
7d53607f49 | ||
|
|
7539945465 | ||
|
|
51df8e7bb1 | ||
|
|
17029ce67c | ||
|
|
8cedc313cf | ||
|
|
5afe3acac1 | ||
|
|
9887b58b71 | ||
|
|
ec8363ba6a | ||
|
|
fcfb71f13b | ||
|
|
3a12e431ff | ||
|
|
bc4ed8e123 | ||
|
|
445e9540a5 | ||
|
|
bbc2a2a9dd | ||
|
|
5117bc78b6 | ||
|
|
26d8b2fa22 | ||
|
|
2b7fe35305 | ||
|
|
d5819ea4d0 | ||
|
|
0cf136712a | ||
|
|
34a453873a | ||
|
|
0acac3b096 | ||
|
|
ede6070e43 | ||
|
|
a61b1d4337 | ||
|
|
0f8c71a9df | ||
|
|
cb0b6e010d | ||
|
|
23dc7be1ab | ||
|
|
4b14ad07d2 | ||
|
|
8a4bf7896f | ||
|
|
208ea29643 | ||
|
|
7c52ec731a | ||
|
|
c6ee38e435 | ||
|
|
65253cc5b9 | ||
|
|
d1a1a23cbc | ||
|
|
e8bb3a5ba7 | ||
|
|
22b8f82770 |
62
CHANGELOG
62
CHANGELOG
@@ -1,6 +1,66 @@
|
|||||||
|
KeePassDX(3.0.0)
|
||||||
|
* Add / Manage dynamic templates #191
|
||||||
|
* Manually select RecycleBin group and Templates group #191
|
||||||
|
* Setting to display OTP Token in list #655
|
||||||
|
* Fix timeout in dialogs #716
|
||||||
|
* Check URI permissions #626
|
||||||
|
* Improvements #1035 #1043 #942 #1021 #1027
|
||||||
|
|
||||||
|
KeePassDX(2.10.5)
|
||||||
|
* Increase the saving speed of database #1028
|
||||||
|
* Fix advanced unlocking by device credential #1029
|
||||||
|
|
||||||
|
KeePassDX(2.10.4)
|
||||||
|
* Hot fix to increase the opening speed of database #1028
|
||||||
|
|
||||||
|
KeePassDX(2.10.3)
|
||||||
|
* Improve Magikeyboard options description #1022 #1023 (Thx @djibux)
|
||||||
|
* Fix database opened without notification (database is now closed when screen is killed in background #1025)
|
||||||
|
* Fix biometric prompt #1018
|
||||||
|
|
||||||
|
KeePassDX(2.10.2)
|
||||||
|
* Fix search fields references #987
|
||||||
|
* Fix Auto-Types with same key #997
|
||||||
|
|
||||||
|
KeePassDX(2.10.1)
|
||||||
|
* Fix parcelable with custom data #986
|
||||||
|
|
||||||
|
KeePassDX(2.10.0)
|
||||||
|
* Manage new database format 4.1 #956
|
||||||
|
* Fix show button consistency #980
|
||||||
|
* Fix persistent notification #979
|
||||||
|
|
||||||
|
KeePassDX(2.9.20)
|
||||||
|
* Fix search with non-latin chars #971
|
||||||
|
* Fix action mode with search #972 (rollback ignore accents #945)
|
||||||
|
* Fix timeout with 0s #974
|
||||||
|
|
||||||
|
KeePassDX(2.9.19)
|
||||||
|
* Fix search slowdown #964
|
||||||
|
* Fix closing notification after lock request #965
|
||||||
|
* Better temp advanced unlocking code implementation #965
|
||||||
|
* Fix OTP token generation #967
|
||||||
|
|
||||||
|
KeePassDX(2.9.18)
|
||||||
|
* Move groups #658
|
||||||
|
* Improve autofill recognition #960
|
||||||
|
* Remove diacritical marks in search string #945
|
||||||
|
* Fix search in references #962
|
||||||
|
* Fix themes in Libre version
|
||||||
|
|
||||||
|
KeePassDX(2.9.17)
|
||||||
|
* Import / Export app properties #839
|
||||||
|
* Force twofish padding compatibility #955
|
||||||
|
* Better timeout preference #579
|
||||||
|
|
||||||
|
KeePassDX(2.9.16)
|
||||||
|
* Fix small bugs #948
|
||||||
|
|
||||||
KeePassDX(2.9.15)
|
KeePassDX(2.9.15)
|
||||||
* Fix themes #935
|
* Fix themes #935 #926
|
||||||
* Decrease default clipboard time #934
|
* Decrease default clipboard time #934
|
||||||
|
* Better opening performance #929 #933
|
||||||
|
* Fix memory usage setting #941
|
||||||
|
|
||||||
KeePassDX(2.9.14)
|
KeePassDX(2.9.14)
|
||||||
* Add custom icons #96
|
* Add custom icons #96
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 15
|
minSdkVersion 15
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode = 66
|
versionCode = 84
|
||||||
versionName = "2.9.15"
|
versionName = "3.0.0_beta01"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
@@ -109,7 +109,7 @@ dependencies {
|
|||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.biometric:biometric:1.1.0-rc01'
|
implementation 'androidx.biometric:biometric:1.1.0'
|
||||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||||
implementation "androidx.core:core-ktx:1.3.2"
|
implementation "androidx.core:core-ktx:1.3.2"
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.kunzisoft.keepass.tests.template
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateAttributeOption
|
||||||
|
import junit.framework.TestCase
|
||||||
|
import org.junit.Assert
|
||||||
|
|
||||||
|
class TemplateAttributeOptionTest: TestCase() {
|
||||||
|
|
||||||
|
fun testSerializeOptions() {
|
||||||
|
val options = TemplateAttributeOption().apply {
|
||||||
|
put("TestA", "TestB")
|
||||||
|
put("{D", "}C")
|
||||||
|
put("E,gyu", "15,jk")
|
||||||
|
put("ù*:**", "78:96?545")
|
||||||
|
}
|
||||||
|
|
||||||
|
val strings = TemplateAttributeOption.getStringFromOptions(options)
|
||||||
|
val optionsAfterSerialization = TemplateAttributeOption.getOptionsFromString(strings)
|
||||||
|
val otherString = TemplateAttributeOption.getStringFromOptions(optionsAfterSerialization)
|
||||||
|
|
||||||
|
Assert.assertEquals("Output not equal to input", strings, otherString)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.kunzisoft.keepass.tests.utils
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
|
import junit.framework.TestCase
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class UUIDTest: TestCase() {
|
||||||
|
|
||||||
|
fun testUUID() {
|
||||||
|
val randomUUID = UUID.randomUUID()
|
||||||
|
val hexStringUUID = UuidUtil.toHexString(randomUUID)
|
||||||
|
val retrievedUUID = UuidUtil.fromHexString(hexStringUUID)
|
||||||
|
assertEquals(randomUUID, retrievedUUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="stateHidden" >
|
android:windowSoftInputMode="stateHidden|stateAlwaysHidden" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
@@ -112,8 +112,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="adjustPan"
|
android:windowSoftInputMode="adjustPan">
|
||||||
android:launchMode="singleTask">
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.default_searchable"
|
android:name="android.app.default_searchable"
|
||||||
android:value="com.kunzisoft.keepass.search.SearchResults"
|
android:value="com.kunzisoft.keepass.search.SearchResults"
|
||||||
@@ -209,7 +208,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.magikeyboard.MagikIME"
|
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
|
||||||
android:label="@string/keyboard_label"
|
android:label="@string/keyboard_label"
|
||||||
android:permission="android.permission.BIND_INPUT_METHOD" >
|
android:permission="android.permission.BIND_INPUT_METHOD" >
|
||||||
<meta-data android:name="android.view.im"
|
<meta-data android:name="android.view.im"
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ package com.igreenwood.loupe
|
|||||||
import android.animation.Animator
|
import android.animation.Animator
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
@@ -108,6 +109,8 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
var viewDragFriction = DEFAULT_VIEW_DRAG_FRICTION
|
var viewDragFriction = DEFAULT_VIEW_DRAG_FRICTION
|
||||||
// drag distance threshold in dp for swipe to dismiss
|
// drag distance threshold in dp for swipe to dismiss
|
||||||
var dragDismissDistanceInDp = DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP
|
var dragDismissDistanceInDp = DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP
|
||||||
|
// on view touched
|
||||||
|
var onViewTouchedListener: View.OnTouchListener? = null
|
||||||
// on view translate listener
|
// on view translate listener
|
||||||
var onViewTranslateListener: OnViewTranslateListener? = null
|
var onViewTranslateListener: OnViewTranslateListener? = null
|
||||||
// on scale changed
|
// on scale changed
|
||||||
@@ -272,7 +275,10 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
private var imageViewRef: WeakReference<ImageView> = WeakReference(imageView)
|
private var imageViewRef: WeakReference<ImageView> = WeakReference(imageView)
|
||||||
private var containerRef: WeakReference<ViewGroup> = WeakReference(container)
|
private var containerRef: WeakReference<ViewGroup> = WeakReference(container)
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
|
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
|
||||||
|
onViewTouchedListener?.onTouch(view, event)
|
||||||
|
|
||||||
event ?: return false
|
event ?: return false
|
||||||
val imageView = imageViewRef.get() ?: return false
|
val imageView = imageViewRef.get() ?: return false
|
||||||
val container = containerRef.get() ?: return false
|
val container = containerRef.get() ?: return false
|
||||||
|
|||||||
@@ -25,14 +25,13 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
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.view.inputmethod.InlineSuggestionsRequest
|
import android.view.inputmethod.InlineSuggestionsRequest
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
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.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST
|
import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST
|
||||||
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
||||||
@@ -44,9 +43,18 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class AutofillLauncherActivity : AppCompatActivity() {
|
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun applyCustomStyle(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
// Retrieve selection mode
|
// Retrieve selection mode
|
||||||
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
||||||
@@ -60,7 +68,7 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
searchInfo.webDomain = concreteWebDomain
|
searchInfo.webDomain = concreteWebDomain
|
||||||
launchSelection(searchInfo)
|
launchSelection(database, searchInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpecialMode.REGISTRATION -> {
|
SpecialMode.REGISTRATION -> {
|
||||||
@@ -69,7 +77,7 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
||||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
searchInfo.webDomain = concreteWebDomain
|
searchInfo.webDomain = concreteWebDomain
|
||||||
launchRegistration(searchInfo, registerInfo)
|
launchRegistration(database, searchInfo, registerInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -79,11 +87,10 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchSelection(searchInfo: SearchInfo) {
|
private fun launchSelection(database: Database?,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||||
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
|
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
|
||||||
|
|
||||||
@@ -98,24 +105,22 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(Activity.RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
} else {
|
} 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,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ items ->
|
{ openedDatabase, items ->
|
||||||
// Items found
|
// Items found
|
||||||
AutofillHelper.buildResponseAndSetResult(this, items)
|
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
||||||
finish()
|
finish()
|
||||||
},
|
},
|
||||||
{
|
{ openedDatabase ->
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
GroupActivity.launchForAutofillResult(this,
|
GroupActivity.launchForAutofillResult(this,
|
||||||
readOnly,
|
openedDatabase,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
false)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// If database not open
|
// If database not open
|
||||||
@@ -127,7 +132,9 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchRegistration(searchInfo: SearchInfo, registerInfo: RegisterInfo?) {
|
private fun launchRegistration(database: Database?,
|
||||||
|
searchInfo: SearchInfo,
|
||||||
|
registerInfo: RegisterInfo?) {
|
||||||
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
||||||
PreferencesUtil.applicationIdBlocklist(this))
|
PreferencesUtil.applicationIdBlocklist(this))
|
||||||
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
||||||
@@ -135,25 +142,26 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
showBlockRestartMessage()
|
showBlockRestartMessage()
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(Activity.RESULT_CANCELED)
|
||||||
} else {
|
} else {
|
||||||
val database = Database.getInstance()
|
val readOnly = database?.isReadOnly != false
|
||||||
val readOnly = database.isReadOnly
|
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
database,
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ _ ->
|
{ openedDatabase, _ ->
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
GroupActivity.launchForRegistration(this,
|
GroupActivity.launchForRegistration(this,
|
||||||
registerInfo)
|
openedDatabase,
|
||||||
|
registerInfo)
|
||||||
} else {
|
} else {
|
||||||
showReadOnlySaveMessage()
|
showReadOnlySaveMessage()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{ openedDatabase ->
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
GroupActivity.launchForRegistration(this,
|
GroupActivity.launchForRegistration(this,
|
||||||
registerInfo)
|
openedDatabase,
|
||||||
|
registerInfo)
|
||||||
} else {
|
} else {
|
||||||
showReadOnlySaveMessage()
|
showReadOnlySaveMessage()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,68 +32,62 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.Toast
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
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.fragments.EntryFragment
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
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.icon.IconImage
|
||||||
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.magikeyboard.MagikIME
|
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
|
||||||
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.view.EntryContentsView
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.HashMap
|
import kotlin.collections.HashMap
|
||||||
|
|
||||||
class EntryActivity : LockingActivity() {
|
class EntryActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||||
private var titleIconView: ImageView? = null
|
private var titleIconView: ImageView? = null
|
||||||
private var historyView: View? = null
|
private var historyView: View? = null
|
||||||
private var entryContentsView: EntryContentsView? = null
|
|
||||||
private var entryProgress: ProgressBar? = null
|
private var entryProgress: ProgressBar? = null
|
||||||
private var lockView: View? = null
|
private var lockView: View? = null
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
|
private var loadingView: ProgressBar? = null
|
||||||
|
|
||||||
private var mDatabase: Database? = null
|
private val mEntryViewModel: EntryViewModel by viewModels()
|
||||||
|
|
||||||
private var mEntry: Entry? = null
|
private var mMainEntryId: NodeId<UUID>? = null
|
||||||
|
private var mHistoryPosition: Int = -1
|
||||||
private var mIsHistory: Boolean = false
|
private var mEntryIsHistory: Boolean = false
|
||||||
private var mEntryLastVersion: Entry? = null
|
private var mUrl: String? = null
|
||||||
private var mEntryHistoryPosition: Int = -1
|
|
||||||
|
|
||||||
private var mShowPassword: Boolean = false
|
|
||||||
|
|
||||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||||
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
|
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
|
||||||
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var clipboardHelper: ClipboardHelper? = null
|
private var mIcon: IconImage? = null
|
||||||
private var mFirstLaunchOfActivity: Boolean = false
|
private var mIconColor: Int = 0
|
||||||
|
|
||||||
private var iconColor: Int = 0
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -105,60 +99,167 @@ class EntryActivity : LockingActivity() {
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
mDatabase = Database.getInstance()
|
|
||||||
mReadOnly = mDatabase!!.isReadOnly || mReadOnly
|
|
||||||
|
|
||||||
mShowPassword = !PreferencesUtil.isPasswordMask(this)
|
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
|
||||||
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
|
||||||
iconColor = taIconColor.getColor(0, Color.BLACK)
|
|
||||||
taIconColor.recycle()
|
|
||||||
|
|
||||||
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
|
|
||||||
invalidateOptionsMenu()
|
|
||||||
|
|
||||||
// Get views
|
// Get views
|
||||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||||
titleIconView = findViewById(R.id.entry_icon)
|
titleIconView = findViewById(R.id.entry_icon)
|
||||||
historyView = findViewById(R.id.history_container)
|
historyView = findViewById(R.id.history_container)
|
||||||
entryContentsView = findViewById(R.id.entry_contents)
|
|
||||||
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
|
||||||
entryContentsView?.setAttachmentCipherKey(mDatabase)
|
|
||||||
entryProgress = findViewById(R.id.entry_progress)
|
entryProgress = findViewById(R.id.entry_progress)
|
||||||
lockView = findViewById(R.id.lock_button)
|
lockView = findViewById(R.id.lock_button)
|
||||||
|
loadingView = findViewById(R.id.loading)
|
||||||
|
|
||||||
|
// Empty title
|
||||||
|
collapsingToolbarLayout?.title = " "
|
||||||
|
toolbar?.title = " "
|
||||||
|
|
||||||
|
// Retrieve the textColor to tint the icon
|
||||||
|
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||||
|
mIconColor = taIconColor.getColor(0, Color.BLACK)
|
||||||
|
taIconColor.recycle()
|
||||||
|
|
||||||
|
// Get Entry from UUID
|
||||||
|
try {
|
||||||
|
intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { entryId ->
|
||||||
|
mMainEntryId = entryId
|
||||||
|
intent.removeExtra(KEY_ENTRY)
|
||||||
|
mHistoryPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
|
||||||
|
intent.removeExtra(KEY_ENTRY_HISTORY_POSITION)
|
||||||
|
}
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
Log.e(TAG, "Unable to retrieve the entry key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init SAF manager
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
// Init attachment service binder manager
|
||||||
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
|
|
||||||
lockView?.setOnClickListener {
|
lockView?.setOnClickListener {
|
||||||
lockAndExit()
|
lockAndExit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focus view to reinitialize timeout
|
mEntryViewModel.mainEntryId.observe(this) { mainEntryId ->
|
||||||
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
this.mMainEntryId = mainEntryId
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
|
||||||
// Init the clipboard helper
|
mEntryViewModel.historyPosition.observe(this) { historyPosition ->
|
||||||
clipboardHelper = ClipboardHelper(this)
|
this.mHistoryPosition = historyPosition
|
||||||
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
|
val entryIsHistory = historyPosition > -1
|
||||||
|
this.mEntryIsHistory = entryIsHistory
|
||||||
|
// Assign history dedicated view
|
||||||
|
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
||||||
|
if (entryIsHistory) {
|
||||||
|
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||||
|
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||||
|
taColorAccent.recycle()
|
||||||
|
}
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
|
||||||
// Init attachment service binder manager
|
mEntryViewModel.entryInfo.observe(this) { entryInfo ->
|
||||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
// Manage entry copy to start notification if allowed (at the first start)
|
||||||
|
if (savedInstanceState == null) {
|
||||||
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
// Manage entry to launch copying notification if allowed
|
||||||
when (actionTask) {
|
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
||||||
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||||
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
||||||
// Close the current activity after an history action
|
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
||||||
if (result.isSuccess)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
ACTION_DATABASE_RELOAD_TASK -> {
|
|
||||||
// Close the current activity
|
|
||||||
this.showActionErrorIfNeeded(result)
|
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
coordinatorLayout?.showActionErrorIfNeeded(result)
|
|
||||||
|
// Assign title icon
|
||||||
|
mIcon = entryInfo.icon
|
||||||
|
titleIconView?.let { iconView ->
|
||||||
|
mIconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, mIconColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign title text
|
||||||
|
val entryTitle = if (entryInfo.title.isNotEmpty()) entryInfo.title else entryInfo.id.toString()
|
||||||
|
collapsingToolbarLayout?.title = entryTitle
|
||||||
|
toolbar?.title = entryTitle
|
||||||
|
|
||||||
|
mUrl = entryInfo.url
|
||||||
|
|
||||||
|
// Refresh Menu
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
|
loadingView?.hideByFading()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
|
||||||
|
if (otpElement == null)
|
||||||
|
entryProgress?.visibility = View.GONE
|
||||||
|
when (otpElement?.type) {
|
||||||
|
// Only add token if HOTP
|
||||||
|
OtpType.HOTP -> {
|
||||||
|
entryProgress?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
// Refresh view if TOTP
|
||||||
|
OtpType.TOTP -> {
|
||||||
|
entryProgress?.apply {
|
||||||
|
max = otpElement.period
|
||||||
|
progress = otpElement.secondsRemaining
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
|
||||||
|
mExternalFileHelper?.createDocument(attachmentSelected.name)?.let { requestCode ->
|
||||||
|
mAttachmentsToDownload[requestCode] = attachmentSelected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
launch(
|
||||||
|
this,
|
||||||
|
database,
|
||||||
|
historySelected.nodeId,
|
||||||
|
historySelected.historyPosition
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun viewToInvalidateTimeout(): View? {
|
||||||
|
return coordinatorLayout
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
|
mEntryViewModel.loadEntry(mDatabase, mMainEntryId, mHistoryPosition)
|
||||||
|
|
||||||
|
// Assign title icon
|
||||||
|
mIcon?.let { icon ->
|
||||||
|
titleIconView?.let { iconView ->
|
||||||
|
mIconDrawableFactory?.assignDatabaseIcon(iconView, icon, mIconColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: Database,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
||||||
|
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
||||||
|
// Close the current activity after an history action
|
||||||
|
if (result.isSuccess)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
coordinatorLayout?.showActionErrorIfNeeded(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -171,63 +272,14 @@ class EntryActivity : LockingActivity() {
|
|||||||
View.GONE
|
View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Entry from UUID
|
|
||||||
try {
|
|
||||||
val keyEntry: NodeId<UUID>? = intent.getParcelableExtra(KEY_ENTRY)
|
|
||||||
if (keyEntry != null) {
|
|
||||||
mEntry = mDatabase?.getEntryById(keyEntry)
|
|
||||||
mEntryLastVersion = mEntry
|
|
||||||
}
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
Log.e(TAG, "Unable to retrieve the entry key")
|
|
||||||
}
|
|
||||||
|
|
||||||
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, mEntryHistoryPosition)
|
|
||||||
mEntryHistoryPosition = historyPosition
|
|
||||||
if (historyPosition >= 0) {
|
|
||||||
mIsHistory = true
|
|
||||||
mEntry = mEntry?.getHistory()?.get(historyPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mEntry == null) {
|
|
||||||
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update last access time.
|
|
||||||
mEntry?.touch(modified = false, touchParents = false)
|
|
||||||
|
|
||||||
mEntry?.let { entry ->
|
|
||||||
// Fill data in resume to update from EntryEditActivity
|
|
||||||
fillEntryDataInContentsView(entry)
|
|
||||||
// Refresh Menu
|
|
||||||
invalidateOptionsMenu()
|
|
||||||
|
|
||||||
val entryInfo = entry.getEntryInfo(mDatabase)
|
|
||||||
// Manage entry copy to start notification if allowed
|
|
||||||
if (mFirstLaunchOfActivity) {
|
|
||||||
// Manage entry to launch copying notification if allowed
|
|
||||||
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
|
||||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
|
||||||
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
|
||||||
MagikIME.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mAttachmentFileBinderManager?.apply {
|
mAttachmentFileBinderManager?.apply {
|
||||||
registerProgressTask()
|
registerProgressTask()
|
||||||
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||||
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||||
if (entryAttachmentState.streamDirection != StreamDirection.UPLOAD) {
|
mEntryViewModel.onAttachmentAction(entryAttachmentState)
|
||||||
entryContentsView?.putAttachment(entryAttachmentState)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mFirstLaunchOfActivity = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -236,151 +288,17 @@ class EntryActivity : LockingActivity() {
|
|||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fillEntryDataInContentsView(entry: Entry) {
|
|
||||||
|
|
||||||
val entryInfo = entry.getEntryInfo(mDatabase)
|
|
||||||
|
|
||||||
// Assign title icon
|
|
||||||
titleIconView?.let { iconView ->
|
|
||||||
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, iconColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign title text
|
|
||||||
val entryTitle = entryInfo.title
|
|
||||||
collapsingToolbarLayout?.title = entryTitle
|
|
||||||
toolbar?.title = entryTitle
|
|
||||||
|
|
||||||
// Assign basic fields
|
|
||||||
entryContentsView?.assignUserName(entryInfo.username) {
|
|
||||||
clipboardHelper?.timeoutCopyToClipboard(entryInfo.username,
|
|
||||||
getString(R.string.copy_field,
|
|
||||||
getString(R.string.entry_user_name)))
|
|
||||||
}
|
|
||||||
|
|
||||||
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
|
||||||
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
|
||||||
val allowCopyPasswordAndProtectedFields =
|
|
||||||
PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
|
|
||||||
|
|
||||||
val showWarningClipboardDialogOnClickListener = View.OnClickListener {
|
|
||||||
AlertDialog.Builder(this@EntryActivity)
|
|
||||||
.setMessage(getString(R.string.allow_copy_password_warning) +
|
|
||||||
"\n\n" +
|
|
||||||
getString(R.string.clipboard_warning))
|
|
||||||
.create().apply {
|
|
||||||
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
|
|
||||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
|
|
||||||
dialog.dismiss()
|
|
||||||
fillEntryDataInContentsView(entry)
|
|
||||||
}
|
|
||||||
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
|
|
||||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false)
|
|
||||||
dialog.dismiss()
|
|
||||||
fillEntryDataInContentsView(entry)
|
|
||||||
}
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
|
|
||||||
View.OnClickListener {
|
|
||||||
clipboardHelper?.timeoutCopyToClipboard(entryInfo.password,
|
|
||||||
getString(R.string.copy_field,
|
|
||||||
getString(R.string.entry_password)))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If dialog not already shown
|
|
||||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
|
||||||
showWarningClipboardDialogOnClickListener
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entryContentsView?.assignPassword(entryInfo.password,
|
|
||||||
allowCopyPasswordAndProtectedFields,
|
|
||||||
onPasswordCopyClickListener)
|
|
||||||
|
|
||||||
//Assign OTP field
|
|
||||||
entry.getOtpElement()?.let { otpElement ->
|
|
||||||
entryContentsView?.assignOtp(otpElement, entryProgress) {
|
|
||||||
clipboardHelper?.timeoutCopyToClipboard(
|
|
||||||
otpElement.token,
|
|
||||||
getString(R.string.copy_field, getString(R.string.entry_otp))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entryContentsView?.assignURL(entryInfo.url)
|
|
||||||
entryContentsView?.assignNotes(entryInfo.notes)
|
|
||||||
|
|
||||||
// Assign custom fields
|
|
||||||
if (mDatabase?.allowEntryCustomFields() == true) {
|
|
||||||
entryContentsView?.clearExtraFields()
|
|
||||||
entryInfo.customFields.forEach { field ->
|
|
||||||
val label = field.name
|
|
||||||
// OTP field is already managed in dedicated view
|
|
||||||
if (label != OtpEntryFields.OTP_TOKEN_FIELD) {
|
|
||||||
val value = field.protectedValue
|
|
||||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
|
||||||
if (allowCopyProtectedField) {
|
|
||||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
|
|
||||||
clipboardHelper?.timeoutCopyToClipboard(
|
|
||||||
value.toString(),
|
|
||||||
getString(R.string.copy_field, label)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If dialog not already shown
|
|
||||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
|
||||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
|
|
||||||
} else {
|
|
||||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
|
||||||
|
|
||||||
// Manage attachments
|
|
||||||
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
|
||||||
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
|
||||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign dates
|
|
||||||
entryContentsView?.assignCreationDate(entryInfo.creationTime)
|
|
||||||
entryContentsView?.assignModificationDate(entryInfo.lastModificationTime)
|
|
||||||
entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime)
|
|
||||||
|
|
||||||
// Manage history
|
|
||||||
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
|
|
||||||
if (mIsHistory) {
|
|
||||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
|
||||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
|
||||||
taColorAccent.recycle()
|
|
||||||
}
|
|
||||||
entryContentsView?.assignHistory(entry.getHistory()) { historyItem, position ->
|
|
||||||
launch(this, historyItem, mReadOnly, position)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign special data
|
|
||||||
entryContentsView?.assignUUID(entry.nodeId.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
when (requestCode) {
|
when (requestCode) {
|
||||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE ->
|
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
||||||
// Not directly get the entry from intent data but from database
|
// Reload the current id from database
|
||||||
mEntry?.let {
|
mEntryViewModel.updateEntry(mDatabase)
|
||||||
fillEntryDataInContentsView(it)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
||||||
if (createdFileUri != null) {
|
if (createdFileUri != null) {
|
||||||
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
||||||
mAttachmentFileBinderManager
|
mAttachmentFileBinderManager
|
||||||
@@ -395,12 +313,18 @@ class EntryActivity : LockingActivity() {
|
|||||||
|
|
||||||
val inflater = menuInflater
|
val inflater = menuInflater
|
||||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||||
|
|
||||||
inflater.inflate(R.menu.entry, menu)
|
inflater.inflate(R.menu.entry, menu)
|
||||||
inflater.inflate(R.menu.database, menu)
|
inflater.inflate(R.menu.database, menu)
|
||||||
if (mIsHistory && !mReadOnly) {
|
|
||||||
|
if (mUrl?.isEmpty() != false) {
|
||||||
|
menu.findItem(R.id.menu_goto_url)?.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mEntryIsHistory && !mDatabaseReadOnly) {
|
||||||
inflater.inflate(R.menu.entry_history, menu)
|
inflater.inflate(R.menu.entry_history, menu)
|
||||||
}
|
}
|
||||||
if (mIsHistory || mReadOnly) {
|
if (mEntryIsHistory || mDatabaseReadOnly) {
|
||||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
menu.findItem(R.id.menu_edit)?.isVisible = false
|
||||||
}
|
}
|
||||||
@@ -408,20 +332,6 @@ class EntryActivity : LockingActivity() {
|
|||||||
menu.findItem(R.id.menu_reload_database)?.isVisible = false
|
menu.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
|
||||||
gotoUrl?.apply {
|
|
||||||
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
|
||||||
// so mEntry may not be set
|
|
||||||
if (mEntry == null) {
|
|
||||||
isVisible = false
|
|
||||||
} else {
|
|
||||||
if (mEntry?.url?.isEmpty() != false) {
|
|
||||||
// disable button if url is not available
|
|
||||||
isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show education views
|
// Show education views
|
||||||
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
|
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
|
||||||
|
|
||||||
@@ -430,18 +340,18 @@ class EntryActivity : LockingActivity() {
|
|||||||
|
|
||||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||||
menu: Menu) {
|
menu: Menu) {
|
||||||
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
|
val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG)
|
||||||
|
as? EntryFragment?
|
||||||
|
val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView()
|
||||||
val entryCopyEducationPerformed = entryFieldCopyView != null
|
val entryCopyEducationPerformed = entryFieldCopyView != null
|
||||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||||
entryFieldCopyView,
|
entryFieldCopyView,
|
||||||
{
|
{
|
||||||
val appNameString = getString(R.string.app_name)
|
entryFragment.launchEntryCopyEducationAction()
|
||||||
clipboardHelper?.timeoutCopyToClipboard(appNameString,
|
},
|
||||||
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)
|
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
|
||||||
@@ -465,60 +375,53 @@ class EntryActivity : LockingActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_edit -> {
|
R.id.menu_edit -> {
|
||||||
mEntry?.let {
|
mDatabase?.let { database ->
|
||||||
EntryEditActivity.launch(this@EntryActivity, it)
|
mMainEntryId?.let { entryId ->
|
||||||
|
EntryEditActivity.launchToUpdate(
|
||||||
|
this,
|
||||||
|
database,
|
||||||
|
entryId
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_goto_url -> {
|
R.id.menu_goto_url -> {
|
||||||
var url: String = mEntry?.url ?: ""
|
mUrl?.let { url ->
|
||||||
|
UriUtil.gotoUrl(this, url)
|
||||||
// Default http:// if no protocol specified
|
|
||||||
if (!url.contains("://")) {
|
|
||||||
url = "http://$url"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UriUtil.gotoUrl(this, url)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_restore_entry_history -> {
|
R.id.menu_restore_entry_history -> {
|
||||||
mEntryLastVersion?.let { mainEntry ->
|
mMainEntryId?.let { mainEntryId ->
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory(
|
restoreEntryHistory(
|
||||||
mainEntry,
|
mainEntryId,
|
||||||
mEntryHistoryPosition,
|
mHistoryPosition)
|
||||||
!mReadOnly && mAutoSaveEnable)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.menu_delete_entry_history -> {
|
R.id.menu_delete_entry_history -> {
|
||||||
mEntryLastVersion?.let { mainEntry ->
|
mMainEntryId?.let { mainEntryId ->
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(
|
deleteEntryHistory(
|
||||||
mainEntry,
|
mainEntryId,
|
||||||
mEntryHistoryPosition,
|
mHistoryPosition)
|
||||||
!mReadOnly && mAutoSaveEnable)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.menu_save_database -> {
|
R.id.menu_save_database -> {
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
saveDatabase()
|
||||||
}
|
}
|
||||||
R.id.menu_reload_database -> {
|
R.id.menu_reload_database -> {
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseReload(false)
|
reloadDatabase()
|
||||||
}
|
}
|
||||||
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
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry)
|
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
||||||
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this)
|
setResult(EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE, this)
|
||||||
}
|
}
|
||||||
super.finish()
|
super.finish()
|
||||||
}
|
}
|
||||||
@@ -526,19 +429,46 @@ 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"
|
||||||
|
|
||||||
fun launch(activity: Activity, entry: Entry, readOnly: Boolean, historyPosition: Int? = null) {
|
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
|
||||||
val intent = Intent(activity, EntryActivity::class.java)
|
/**
|
||||||
intent.putExtra(KEY_ENTRY, entry.nodeId)
|
* Open standard Entry activity
|
||||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
*/
|
||||||
if (historyPosition != null)
|
fun launch(activity: Activity,
|
||||||
|
database: Database,
|
||||||
|
entryId: NodeId<UUID>) {
|
||||||
|
if (database.loaded) {
|
||||||
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
|
val intent = Intent(activity, EntryActivity::class.java)
|
||||||
|
intent.putExtra(KEY_ENTRY, entryId)
|
||||||
|
activity.startActivityForResult(
|
||||||
|
intent,
|
||||||
|
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open history Entry activity
|
||||||
|
*/
|
||||||
|
fun launch(activity: Activity,
|
||||||
|
database: Database,
|
||||||
|
entryId: NodeId<UUID>,
|
||||||
|
historyPosition: Int) {
|
||||||
|
if (database.loaded) {
|
||||||
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
|
val intent = Intent(activity, EntryActivity::class.java)
|
||||||
|
intent.putExtra(KEY_ENTRY, entryId)
|
||||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||||
activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
activity.startActivityForResult(
|
||||||
|
intent,
|
||||||
|
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -22,14 +22,13 @@ package com.kunzisoft.keepass.activities
|
|||||||
import android.app.Activity
|
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.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
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.legacy.DatabaseModeActivity
|
||||||
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.magikeyboard.MagikIME
|
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||||
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.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
@@ -39,10 +38,18 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
* Activity to search or select entry in database,
|
* Activity to search or select entry in database,
|
||||||
* Commonly used with Magikeyboard
|
* Commonly used with Magikeyboard
|
||||||
*/
|
*/
|
||||||
class EntrySelectionLauncherActivity : AppCompatActivity() {
|
class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun applyCustomStyle(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
var sharedWebDomain: String? = null
|
var sharedWebDomain: String? = null
|
||||||
var otpString: String? = null
|
var otpString: String? = null
|
||||||
|
|
||||||
@@ -68,39 +75,39 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
|
|||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Build domain search param
|
// Build domain search param
|
||||||
val searchInfo = SearchInfo().apply {
|
val searchInfo = SearchInfo().apply {
|
||||||
this.webDomain = sharedWebDomain
|
this.webDomain = sharedWebDomain
|
||||||
this.otpString = otpString
|
this.otpString = otpString
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
searchInfo.webDomain = concreteWebDomain
|
searchInfo.webDomain = concreteWebDomain
|
||||||
launch(searchInfo)
|
launch(database, searchInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launch(searchInfo: SearchInfo) {
|
private fun launch(database: Database?,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
|
||||||
if (!searchInfo.containsOnlyNullValues()) {
|
if (!searchInfo.containsOnlyNullValues()) {
|
||||||
// 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 != false
|
||||||
val readOnly = database.isReadOnly
|
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
database,
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ items ->
|
{ openedDatabase, items ->
|
||||||
// Items found
|
// Items found
|
||||||
if (searchInfo.otpString != null) {
|
if (searchInfo.otpString != null) {
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
GroupActivity.launchForSaveResult(this,
|
GroupActivity.launchForSaveResult(
|
||||||
searchInfo,
|
this,
|
||||||
false)
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(applicationContext,
|
Toast.makeText(applicationContext,
|
||||||
R.string.autofill_read_only_save,
|
R.string.autofill_read_only_save,
|
||||||
@@ -111,30 +118,32 @@ 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,
|
populateKeyboardAndMoveAppToBackground(
|
||||||
|
this,
|
||||||
entryPopulate,
|
entryPopulate,
|
||||||
intent)
|
intent)
|
||||||
} else {
|
} else {
|
||||||
// Select the one we want
|
// Select the one we want
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||||
readOnly,
|
openedDatabase,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
true)
|
true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
GroupActivity.launchForSearchResult(this,
|
GroupActivity.launchForSearchResult(this,
|
||||||
readOnly,
|
openedDatabase,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
true)
|
true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{ openedDatabase ->
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
if (searchInfo.otpString != null) {
|
if (searchInfo.otpString != null) {
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
GroupActivity.launchForSaveResult(this,
|
GroupActivity.launchForSaveResult(this,
|
||||||
searchInfo,
|
openedDatabase,
|
||||||
false)
|
searchInfo,
|
||||||
|
false)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(applicationContext,
|
Toast.makeText(applicationContext,
|
||||||
R.string.autofill_read_only_save,
|
R.string.autofill_read_only_save,
|
||||||
@@ -143,13 +152,14 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
} else if (readOnly || searchShareForMagikeyboard) {
|
} else if (readOnly || searchShareForMagikeyboard) {
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||||
readOnly,
|
openedDatabase,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
false)
|
||||||
} else {
|
} else {
|
||||||
GroupActivity.launchForSaveResult(this,
|
GroupActivity.launchForSaveResult(this,
|
||||||
searchInfo,
|
openedDatabase,
|
||||||
false)
|
searchInfo,
|
||||||
|
false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -183,7 +193,7 @@ fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
|||||||
intent: Intent,
|
intent: Intent,
|
||||||
toast: Boolean = true) {
|
toast: Boolean = true) {
|
||||||
// Populate Magikeyboard with entry
|
// Populate Magikeyboard with entry
|
||||||
MagikIME.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
||||||
// Consume the selection mode
|
// Consume the selection mode
|
||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
activity.moveTaskToBack(true)
|
activity.moveTaskToBack(true)
|
||||||
|
|||||||
@@ -42,14 +42,14 @@ 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.SelectFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
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.AutofillComponent
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
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.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
@@ -60,12 +60,13 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
|||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
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 com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
class FileDatabaseSelectActivity : SpecialModeActivity(),
|
class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
@@ -82,9 +83,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
|
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -103,14 +102,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
||||||
|
|
||||||
// Open database button
|
// Open database button
|
||||||
mSelectFileHelper = SelectFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
||||||
openDatabaseButtonView?.apply {
|
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
|
||||||
setOnClickListener(it)
|
|
||||||
setOnLongClickListener(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// History list
|
// History list
|
||||||
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
||||||
@@ -132,6 +126,12 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
}
|
}
|
||||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
||||||
// Remove from app database
|
// Remove from app database
|
||||||
|
fileDatabaseHistoryToDelete.databaseUri?.let { databaseUri ->
|
||||||
|
UriUtil.releaseUriPermission(
|
||||||
|
contentResolver,
|
||||||
|
databaseUri
|
||||||
|
)
|
||||||
|
}
|
||||||
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
|
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -162,29 +162,31 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
|
|
||||||
// Observe list of databases
|
// Observe list of databases
|
||||||
databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles ->
|
databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles ->
|
||||||
when (databaseFiles.databaseFileAction) {
|
try {
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
|
when (databaseFiles.databaseFileAction) {
|
||||||
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
|
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
|
||||||
}
|
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
|
|
||||||
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
|
||||||
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
|
||||||
}
|
}
|
||||||
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
|
||||||
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
|
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
||||||
}
|
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
}
|
||||||
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
}
|
||||||
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
|
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
||||||
}
|
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
||||||
}
|
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
|
}
|
||||||
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
|
}
|
||||||
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
|
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
|
||||||
|
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
|
||||||
|
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
databaseFilesViewModel.consumeAction()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to observe database action", e)
|
||||||
}
|
}
|
||||||
databaseFilesViewModel.consumeAction()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe default database
|
// Observe default database
|
||||||
@@ -192,37 +194,62 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
// Retrieve settings for default database
|
// Retrieve settings for default database
|
||||||
mAdapterDatabaseHistory?.setDefaultDatabase(it)
|
mAdapterDatabaseHistory?.setDefaultDatabase(it)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Attach the dialog thread to this activity
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
super.onDatabaseRetrieved(database)
|
||||||
onActionFinish = { actionTask, result ->
|
if (database != null) {
|
||||||
when (actionTask) {
|
launchGroupActivityIfLoaded(database)
|
||||||
ACTION_DATABASE_CREATE_TASK -> {
|
}
|
||||||
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
|
}
|
||||||
val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential()
|
|
||||||
databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri)
|
override fun onDatabaseActionFinished(
|
||||||
}
|
database: Database,
|
||||||
}
|
actionTask: String,
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
result: ActionRunnable.Result
|
||||||
val database = Database.getInstance()
|
) {
|
||||||
if (result.isSuccess
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
&& database.loaded) {
|
|
||||||
launchGroupActivity(database)
|
if (result.isSuccess) {
|
||||||
} else {
|
// Update list
|
||||||
var resultError = ""
|
when (actionTask) {
|
||||||
val resultMessage = result.message
|
ACTION_DATABASE_CREATE_TASK,
|
||||||
// Show error message
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
|
||||||
resultError = "$resultError $resultMessage"
|
val mainCredential =
|
||||||
}
|
result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY)
|
||||||
Log.e(TAG, resultError)
|
?: MainCredential()
|
||||||
Snackbar.make(coordinatorLayout,
|
databaseFilesViewModel.addDatabaseFile(
|
||||||
resultError,
|
databaseUri,
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
mainCredential.keyFileUri
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Launch activity
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_CREATE_TASK -> {
|
||||||
|
GroupActivity.launch(
|
||||||
|
this@FileDatabaseSelectActivity,
|
||||||
|
database,
|
||||||
|
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
|
launchGroupActivityIfLoaded(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(coordinatorLayout,
|
||||||
|
resultError,
|
||||||
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +257,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
* Create a new file by calling the content provider
|
* Create a new file by calling the content provider
|
||||||
*/
|
*/
|
||||||
private fun createNewFile() {
|
private fun createNewFile() {
|
||||||
createDocument(this, getString(R.string.database_file_name_default) +
|
mExternalFileHelper?.createDocument( 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,12 +278,14 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
{ onLaunchActivitySpecialMode() })
|
{ onLaunchActivitySpecialMode() })
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivity(database: Database) {
|
private fun launchGroupActivityIfLoaded(database: Database) {
|
||||||
GroupActivity.launch(this,
|
if (database.loaded) {
|
||||||
database.isReadOnly,
|
GroupActivity.launch(this,
|
||||||
|
database,
|
||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() })
|
{ onLaunchActivitySpecialMode() })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onValidateSpecialMode() {
|
override fun onValidateSpecialMode() {
|
||||||
@@ -282,7 +311,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
// Show open and create button or special mode
|
// Show open and create button or special mode
|
||||||
when (mSpecialMode) {
|
when (mSpecialMode) {
|
||||||
SpecialMode.DEFAULT -> {
|
SpecialMode.DEFAULT -> {
|
||||||
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
if (ExternalFileHelper.allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
||||||
// There is an activity which can handle this intent.
|
// There is an activity which can handle this intent.
|
||||||
createDatabaseButtonView?.visibility = View.VISIBLE
|
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||||
} else{
|
} else{
|
||||||
@@ -296,28 +325,16 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val database = Database.getInstance()
|
mDatabase?.let { database ->
|
||||||
if (database.loaded) {
|
launchGroupActivityIfLoaded(database)
|
||||||
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() {
|
// Show recent files if allowed
|
||||||
// Unregister progress task
|
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
|
||||||
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
databaseFilesViewModel.loadListOfDatabases()
|
||||||
|
} else {
|
||||||
super.onPause()
|
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
@@ -329,15 +346,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
|
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mDatabaseFileUri?.let { databaseUri ->
|
mDatabaseFileUri?.let { databaseUri ->
|
||||||
|
|
||||||
// Create the new database
|
// Create the new database
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseCreate(
|
createDatabase(databaseUri, mainCredential)
|
||||||
databaseUri,
|
|
||||||
mainCredential
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val error = getString(R.string.error_create_database_file)
|
val error = getString(R.string.error_create_database_file)
|
||||||
@@ -355,14 +367,14 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
launchPasswordActivityWithPath(uri)
|
launchPasswordActivityWithPath(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the created URI from the file manager
|
// Retrieve the created URI from the file manager
|
||||||
onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
|
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
|
||||||
mDatabaseFileUri = databaseFileCreatedUri
|
mDatabaseFileUri = databaseFileCreatedUri
|
||||||
if (mDatabaseFileUri != null) {
|
if (mDatabaseFileUri != null) {
|
||||||
AssignMasterKeyDialogFragment.getInstance(true)
|
AssignMasterKeyDialogFragment.getInstance(true)
|
||||||
@@ -408,9 +420,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
openDatabaseButtonView != null
|
openDatabaseButtonView != null
|
||||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
||||||
openDatabaseButtonView!!,
|
openDatabaseButtonView!!,
|
||||||
{tapTargetView ->
|
{ tapTargetView ->
|
||||||
tapTargetView?.let {
|
tapTargetView?.let {
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
mExternalFileHelper?.openDocument()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -34,9 +34,9 @@ import androidx.fragment.app.commit
|
|||||||
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.fragments.IconPickerFragment
|
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
@@ -49,7 +49,7 @@ import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
|||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
|
||||||
class IconPickerActivity : LockingActivity() {
|
class IconPickerActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
private lateinit var toolbar: Toolbar
|
private lateinit var toolbar: Toolbar
|
||||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
@@ -64,17 +64,13 @@ class IconPickerActivity : LockingActivity() {
|
|||||||
private var mCustomIconsSelectionMode = false
|
private var mCustomIconsSelectionMode = false
|
||||||
private var mIconsSelected: List<IconImageCustom> = ArrayList()
|
private var mIconsSelected: List<IconImageCustom> = ArrayList()
|
||||||
|
|
||||||
private var mDatabase: Database? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
setContentView(R.layout.activity_icon_picker)
|
setContentView(R.layout.activity_icon_picker)
|
||||||
|
|
||||||
mDatabase = Database.getInstance()
|
|
||||||
|
|
||||||
toolbar = findViewById(R.id.toolbar)
|
toolbar = findViewById(R.id.toolbar)
|
||||||
toolbar.title = " "
|
toolbar.title = " "
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
@@ -84,18 +80,9 @@ class IconPickerActivity : LockingActivity() {
|
|||||||
|
|
||||||
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
||||||
|
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
|
||||||
uploadButton = findViewById(R.id.icon_picker_upload)
|
uploadButton = findViewById(R.id.icon_picker_upload)
|
||||||
if (mDatabase?.allowCustomIcons == true) {
|
|
||||||
uploadButton.setOnClickListener {
|
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
|
||||||
}
|
|
||||||
uploadButton.setOnLongClickListener {
|
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.onLongClick(it)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
uploadButton.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
lockView = findViewById(R.id.lock_button)
|
lockView = findViewById(R.id.lock_button)
|
||||||
lockView?.setOnClickListener {
|
lockView?.setOnClickListener {
|
||||||
@@ -121,11 +108,6 @@ class IconPickerActivity : LockingActivity() {
|
|||||||
mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage
|
mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focus view to reinitialize timeout
|
|
||||||
findViewById<ViewGroup>(R.id.icon_picker_container)?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
|
||||||
|
|
||||||
mSelectFileHelper = SelectFileHelper(this)
|
|
||||||
|
|
||||||
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
|
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
|
||||||
mIconImage.standard = iconStandard
|
mIconImage.standard = iconStandard
|
||||||
// Remove the custom icon if a standard one is selected
|
// Remove the custom icon if a standard one is selected
|
||||||
@@ -159,6 +141,24 @@ class IconPickerActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun viewToInvalidateTimeout(): View? {
|
||||||
|
return findViewById<ViewGroup>(R.id.icon_picker_container)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
|
if (database?.allowCustomIcons == true) {
|
||||||
|
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
|
} else {
|
||||||
|
uploadButton.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateIconsSelectedViews() {
|
private fun updateIconsSelectedViews() {
|
||||||
if (mIconsSelected.isEmpty()) {
|
if (mIconsSelected.isEmpty()) {
|
||||||
mCustomIconsSelectionMode = false
|
mCustomIconsSelectionMode = false
|
||||||
@@ -192,13 +192,18 @@ class IconPickerActivity : LockingActivity() {
|
|||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
menuInflater.inflate(R.menu.icon, menu)
|
||||||
if (mCustomIconsSelectionMode) {
|
|
||||||
menuInflater.inflate(R.menu.icon, menu)
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
menu?.findItem(R.id.menu_delete)?.apply {
|
||||||
|
isEnabled = mCustomIconsSelectionMode
|
||||||
|
isVisible = isEnabled
|
||||||
|
}
|
||||||
|
return super.onPrepareOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
android.R.id.home -> {
|
android.R.id.home -> {
|
||||||
@@ -213,6 +218,9 @@ class IconPickerActivity : LockingActivity() {
|
|||||||
removeCustomIcon(iconToRemove)
|
removeCustomIcon(iconToRemove)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
R.id.menu_external_icon -> {
|
||||||
|
UriUtil.gotoUrl(this, R.string.external_icon_url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
@@ -281,7 +289,7 @@ class IconPickerActivity : LockingActivity() {
|
|||||||
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)
|
||||||
|
|
||||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||||
addCustomIcon(uri)
|
addCustomIcon(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -31,16 +32,19 @@ import android.widget.ImageView
|
|||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import com.igreenwood.loupe.Loupe
|
import com.igreenwood.loupe.Loupe
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
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.tasks.BinaryDatabaseManager
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class ImageViewerActivity : LockingActivity() {
|
class ImageViewerActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
private var mDatabase: Database? = null
|
private var imageContainerView: ViewGroup? = null
|
||||||
|
private lateinit var imageView: ImageView
|
||||||
|
private lateinit var progressView: View
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -50,49 +54,21 @@ class ImageViewerActivity : LockingActivity() {
|
|||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
toolbar.setOnTouchListener { _, _ ->
|
||||||
val imageContainerView: ViewGroup = findViewById(R.id.image_viewer_container)
|
resetAppTimeout()
|
||||||
val imageView: ImageView = findViewById(R.id.image_viewer_image)
|
false
|
||||||
val progressView: View = findViewById(R.id.image_viewer_progress)
|
|
||||||
|
|
||||||
// Approximately, to not OOM and allow a zoom
|
|
||||||
val mImagePreviewMaxWidth = max(
|
|
||||||
resources.displayMetrics.widthPixels * 2,
|
|
||||||
resources.displayMetrics.heightPixels * 2
|
|
||||||
)
|
|
||||||
|
|
||||||
mDatabase = Database.getInstance()
|
|
||||||
|
|
||||||
try {
|
|
||||||
progressView.visibility = View.VISIBLE
|
|
||||||
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
|
||||||
|
|
||||||
supportActionBar?.title = attachment.name
|
|
||||||
|
|
||||||
val size = attachment.binaryData.getSize()
|
|
||||||
supportActionBar?.subtitle = Formatter.formatFileSize(this, size)
|
|
||||||
|
|
||||||
mDatabase?.let { database ->
|
|
||||||
BinaryDatabaseManager.loadBitmap(
|
|
||||||
database,
|
|
||||||
attachment.binaryData,
|
|
||||||
mImagePreviewMaxWidth
|
|
||||||
) { bitmapLoaded ->
|
|
||||||
if (bitmapLoaded == null) {
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
progressView.visibility = View.GONE
|
|
||||||
imageView.setImageBitmap(bitmapLoaded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: finish()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to view the binary", e)
|
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Loupe.create(imageView, imageContainerView) {
|
imageContainerView = findViewById(R.id.image_viewer_container)
|
||||||
|
imageView = findViewById(R.id.image_viewer_image)
|
||||||
|
progressView = findViewById(R.id.image_viewer_progress)
|
||||||
|
|
||||||
|
Loupe.create(imageView, imageContainerView!!) {
|
||||||
|
onViewTouchedListener = View.OnTouchListener { _, _ ->
|
||||||
|
// to reset timeout when Loupe image view touched
|
||||||
|
resetAppTimeout()
|
||||||
|
false
|
||||||
|
}
|
||||||
onViewTranslateListener = object : Loupe.OnViewTranslateListener {
|
onViewTranslateListener = object : Loupe.OnViewTranslateListener {
|
||||||
|
|
||||||
override fun onStart(view: ImageView) {
|
override fun onStart(view: ImageView) {
|
||||||
@@ -115,6 +91,54 @@ class ImageViewerActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun viewToInvalidateTimeout(): View? {
|
||||||
|
// Null to manually manage events
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
|
try {
|
||||||
|
progressView.visibility = View.VISIBLE
|
||||||
|
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
||||||
|
|
||||||
|
supportActionBar?.title = attachment.name
|
||||||
|
|
||||||
|
val size = attachment.binaryData.getSize()
|
||||||
|
supportActionBar?.subtitle = Formatter.formatFileSize(this, size)
|
||||||
|
|
||||||
|
// Approximately, to not OOM and allow a zoom
|
||||||
|
val mImagePreviewMaxWidth = max(
|
||||||
|
resources.displayMetrics.widthPixels * 2,
|
||||||
|
resources.displayMetrics.heightPixels * 2
|
||||||
|
)
|
||||||
|
|
||||||
|
database?.let { database ->
|
||||||
|
BinaryDatabaseManager.loadBitmap(
|
||||||
|
database,
|
||||||
|
attachment.binaryData,
|
||||||
|
mImagePreviewMaxWidth
|
||||||
|
) { bitmapLoaded ->
|
||||||
|
if (bitmapLoaded == null) {
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
progressView.visibility = View.GONE
|
||||||
|
imageView.setImageBitmap(bitmapLoaded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: finish()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to view the binary", e)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
android.R.id.home -> finish()
|
android.R.id.home -> finish()
|
||||||
|
|||||||
@@ -19,36 +19,41 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.os.Bundle
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity to select entry in database and populate it in Magikeyboard
|
* Activity to select entry in database and populate it in Magikeyboard
|
||||||
*/
|
*/
|
||||||
class MagikeyboardLauncherActivity : AppCompatActivity() {
|
class MagikeyboardLauncherActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun applyCustomStyle(): Boolean {
|
||||||
val database = Database.getInstance()
|
return false
|
||||||
val readOnly = database.isReadOnly
|
}
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
database,
|
database,
|
||||||
null,
|
null,
|
||||||
{
|
{ _, _ ->
|
||||||
// Not called
|
// Not called
|
||||||
// if items found directly returns before calling this activity
|
// if items found directly returns before calling this activity
|
||||||
},
|
},
|
||||||
{
|
{ openedDatabase ->
|
||||||
// Select if not found
|
// Select if not found
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this, readOnly)
|
GroupActivity.launchForKeyboardSelectionResult(this, openedDatabase)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Pass extra to get entry
|
// Pass extra to get entry
|
||||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
|
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
finish()
|
finish()
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,11 @@ 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.KeyEvent.KEYCODE_ENTER
|
||||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import android.widget.TextView.OnEditorActionListener
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
@@ -42,17 +45,13 @@ import androidx.fragment.app.commit
|
|||||||
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.*
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
|
||||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
||||||
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.exception.FileNotFoundDatabaseException
|
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||||
@@ -66,6 +65,7 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
|||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
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
|
||||||
@@ -74,7 +74,8 @@ import com.kunzisoft.keepass.view.asError
|
|||||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
|
||||||
|
open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
@@ -95,14 +96,14 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
private var mDatabaseKeyFileUri: Uri? = null
|
private var mDatabaseKeyFileUri: Uri? = null
|
||||||
|
|
||||||
private var mRememberKeyFile: Boolean = false
|
private var mRememberKeyFile: Boolean = false
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mPermissionAsked = false
|
private var mPermissionAsked = false
|
||||||
private var readOnly: Boolean = false
|
private var mReadOnly: Boolean = false
|
||||||
private var mForceReadOnly: Boolean = false
|
private var mForceReadOnly: Boolean = false
|
||||||
set(value) {
|
set(value) {
|
||||||
infoContainerView?.visibility = if (value) {
|
infoContainerView?.visibility = if (value) {
|
||||||
readOnly = true
|
mReadOnly = true
|
||||||
View.VISIBLE
|
View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
View.GONE
|
View.GONE
|
||||||
@@ -110,8 +111,6 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
|
||||||
|
|
||||||
private var mAllowAutoOpenBiometricPrompt: Boolean = true
|
private var mAllowAutoOpenBiometricPrompt: Boolean = true
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -135,16 +134,15 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
|
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
|
||||||
|
|
||||||
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
||||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
|
||||||
|
savedInstanceState.getBoolean(KEY_READ_ONLY)
|
||||||
|
} else {
|
||||||
|
PreferencesUtil.enableReadOnlyDatabase(this)
|
||||||
|
}
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
|
|
||||||
mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
|
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
|
||||||
keyFileSelectionView?.apply {
|
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
|
||||||
setOnClickListener(it)
|
|
||||||
setOnLongClickListener(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
||||||
passwordView?.addTextChangedListener(object : TextWatcher {
|
passwordView?.addTextChangedListener(object : TextWatcher {
|
||||||
@@ -157,6 +155,15 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
checkboxPasswordView?.isChecked = true
|
checkboxPasswordView?.isChecked = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
passwordView?.setOnKeyListener { _, _, keyEvent ->
|
||||||
|
var handled = false
|
||||||
|
if (keyEvent.action == KeyEvent.ACTION_DOWN
|
||||||
|
&& keyEvent?.keyCode == KEYCODE_ENTER) {
|
||||||
|
verifyCheckboxesAndLoadDatabase()
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
handled
|
||||||
|
}
|
||||||
|
|
||||||
// If is a view intent
|
// If is a view intent
|
||||||
getUriFromIntent(intent)
|
getUriFromIntent(intent)
|
||||||
@@ -212,72 +219,114 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
|
|
||||||
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
|
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
override fun onResume() {
|
||||||
onActionFinish = { actionTask, result ->
|
super.onResume()
|
||||||
when (actionTask) {
|
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
|
||||||
// Recheck advanced unlock if error
|
|
||||||
advancedUnlockFragment?.initAdvancedUnlockMode()
|
|
||||||
|
|
||||||
if (result.isSuccess) {
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@PasswordActivity)
|
||||||
mDatabaseKeyFileUri = null
|
|
||||||
clearCredentialsViews(true)
|
|
||||||
launchGroupActivity()
|
|
||||||
} else {
|
|
||||||
var resultError = ""
|
|
||||||
val resultException = result.exception
|
|
||||||
val resultMessage = result.message
|
|
||||||
|
|
||||||
if (resultException != null) {
|
// Back to previous keyboard is setting activated
|
||||||
resultError = resultException.getLocalizedMessage(resources)
|
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@PasswordActivity)) {
|
||||||
|
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
||||||
|
}
|
||||||
|
|
||||||
when (resultException) {
|
// Don't allow auto open prompt if lock become when UI visible
|
||||||
is DuplicateUuidDatabaseException -> {
|
mAllowAutoOpenBiometricPrompt = if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
|
||||||
// Relaunch loading if we need to fix UUID
|
false
|
||||||
showLoadDatabaseDuplicateUuidMessage {
|
else
|
||||||
|
mAllowAutoOpenBiometricPrompt
|
||||||
|
mDatabaseFileUri?.let { databaseFileUri ->
|
||||||
|
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
var databaseUri: Uri? = null
|
checkPermission()
|
||||||
var mainCredential: MainCredential = MainCredential()
|
|
||||||
var readOnly = true
|
|
||||||
var cipherEntity: CipherDatabaseEntity? = null
|
|
||||||
|
|
||||||
result.data?.let { resultData ->
|
mDatabase?.let { database ->
|
||||||
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
launchGroupActivityIfLoaded(database)
|
||||||
mainCredential = resultData.getParcelable(MAIN_CREDENTIAL_KEY) ?: mainCredential
|
}
|
||||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
}
|
||||||
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
|
||||||
}
|
|
||||||
|
|
||||||
databaseUri?.let { databaseFileUri ->
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
showProgressDialogAndLoadDatabase(
|
super.onDatabaseRetrieved(database)
|
||||||
databaseFileUri,
|
if (database != null) {
|
||||||
mainCredential,
|
launchGroupActivityIfLoaded(database)
|
||||||
readOnly,
|
}
|
||||||
cipherEntity,
|
}
|
||||||
true)
|
|
||||||
}
|
override fun onDatabaseActionFinished(
|
||||||
}
|
database: Database,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
|
// Recheck advanced unlock if error
|
||||||
|
advancedUnlockFragment?.initAdvancedUnlockMode()
|
||||||
|
|
||||||
|
if (result.isSuccess) {
|
||||||
|
launchGroupActivityIfLoaded(database)
|
||||||
|
} else {
|
||||||
|
passwordView?.requestFocusFromTouch()
|
||||||
|
|
||||||
|
var resultError = ""
|
||||||
|
val resultException = result.exception
|
||||||
|
val resultMessage = result.message
|
||||||
|
|
||||||
|
if (resultException != null) {
|
||||||
|
resultError = resultException.getLocalizedMessage(resources)
|
||||||
|
|
||||||
|
when (resultException) {
|
||||||
|
is DuplicateUuidDatabaseException -> {
|
||||||
|
// Relaunch loading if we need to fix UUID
|
||||||
|
showLoadDatabaseDuplicateUuidMessage {
|
||||||
|
|
||||||
|
var databaseUri: Uri? = null
|
||||||
|
var mainCredential = MainCredential()
|
||||||
|
var readOnly = true
|
||||||
|
var cipherEntity: CipherDatabaseEntity? = null
|
||||||
|
|
||||||
|
result.data?.let { resultData ->
|
||||||
|
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||||
|
mainCredential =
|
||||||
|
resultData.getParcelable(MAIN_CREDENTIAL_KEY)
|
||||||
|
?: mainCredential
|
||||||
|
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||||
|
cipherEntity =
|
||||||
|
resultData.getParcelable(CIPHER_ENTITY_KEY)
|
||||||
}
|
}
|
||||||
is FileNotFoundDatabaseException -> {
|
|
||||||
// Remove this default database inaccessible
|
databaseUri?.let { databaseFileUri ->
|
||||||
if (mDefaultDatabase) {
|
showProgressDialogAndLoadDatabase(
|
||||||
databaseFileViewModel.removeDefaultDatabase()
|
databaseFileUri,
|
||||||
}
|
mainCredential,
|
||||||
|
readOnly,
|
||||||
|
cipherEntity,
|
||||||
|
true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is FileNotFoundDatabaseException -> {
|
||||||
// Show error message
|
// Remove this default database inaccessible
|
||||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
if (mDefaultDatabase) {
|
||||||
resultError = "$resultError $resultMessage"
|
databaseFileViewModel.removeDefaultDatabase()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Log.e(TAG, resultError)
|
|
||||||
Snackbar.make(coordinatorLayout,
|
|
||||||
resultError,
|
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||||
|
resultError = "$resultError $resultMessage"
|
||||||
|
}
|
||||||
|
Log.e(TAG, resultError)
|
||||||
|
Snackbar.make(
|
||||||
|
coordinatorLayout,
|
||||||
|
resultError,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).asError().show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,13 +353,17 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
getUriFromIntent(intent)
|
getUriFromIntent(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivity() {
|
private fun launchGroupActivityIfLoaded(database: Database) {
|
||||||
GroupActivity.launch(this,
|
// Check if database really loaded
|
||||||
readOnly,
|
if (database.loaded) {
|
||||||
|
clearCredentialsViews(true)
|
||||||
|
GroupActivity.launch(this,
|
||||||
|
database,
|
||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() }
|
{ onLaunchActivitySpecialMode() }
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onValidateSpecialMode() {
|
override fun onValidateSpecialMode() {
|
||||||
@@ -360,40 +413,6 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
if (Database.getInstance().loaded) {
|
|
||||||
launchGroupActivity()
|
|
||||||
} else {
|
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
|
||||||
|
|
||||||
// If the database isn't accessible make sure to clear the password field, if it
|
|
||||||
// was saved in the instance state
|
|
||||||
if (Database.getInstance().loaded) {
|
|
||||||
clearCredentialsViews()
|
|
||||||
}
|
|
||||||
|
|
||||||
mProgressDatabaseTaskProvider?.registerProgressTask()
|
|
||||||
|
|
||||||
// Back to previous keyboard is setting activated
|
|
||||||
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this)) {
|
|
||||||
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't allow auto open prompt if lock become when UI visible
|
|
||||||
mAllowAutoOpenBiometricPrompt = if (LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
|
|
||||||
false
|
|
||||||
else
|
|
||||||
mAllowAutoOpenBiometricPrompt
|
|
||||||
mDatabaseFileUri?.let { databaseFileUri ->
|
|
||||||
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
|
||||||
}
|
|
||||||
|
|
||||||
checkPermission()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||||
// Define Key File text
|
// Define Key File text
|
||||||
if (mRememberKeyFile) {
|
if (mRememberKeyFile) {
|
||||||
@@ -417,11 +436,17 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
} else {
|
} else {
|
||||||
// Init Biometric elements
|
// Init Biometric elements
|
||||||
advancedUnlockFragment?.loadDatabase(databaseFileUri,
|
advancedUnlockFragment?.loadDatabase(databaseFileUri,
|
||||||
mAllowAutoOpenBiometricPrompt
|
mAllowAutoOpenBiometricPrompt)
|
||||||
&& mProgressDatabaseTaskProvider?.isBinded() != true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enableOrNotTheConfirmationButton()
|
enableOrNotTheConfirmationButton()
|
||||||
|
|
||||||
|
// Auto select the password field and open keyboard
|
||||||
|
passwordView?.postDelayed({
|
||||||
|
passwordView?.requestFocusFromTouch()
|
||||||
|
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager?
|
||||||
|
inputMethodManager?.showSoftInput(passwordView, InputMethodManager.SHOW_IMPLICIT)
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun enableOrNotTheConfirmationButton() {
|
private fun enableOrNotTheConfirmationButton() {
|
||||||
@@ -439,6 +464,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
||||||
populatePasswordTextView(null)
|
populatePasswordTextView(null)
|
||||||
if (clearKeyFile) {
|
if (clearKeyFile) {
|
||||||
|
mDatabaseKeyFileUri = null
|
||||||
populateKeyFileTextView(null)
|
populateKeyFileTextView(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,10 +494,8 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
|
||||||
|
|
||||||
// Reinit locking activity UI variable
|
// Reinit locking activity UI variable
|
||||||
LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
||||||
mAllowAutoOpenBiometricPrompt = true
|
mAllowAutoOpenBiometricPrompt = true
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
@@ -482,7 +506,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
mDatabaseKeyFileUri?.let {
|
mDatabaseKeyFileUri?.let {
|
||||||
outState.putString(KEY_KEYFILE, it.toString())
|
outState.putString(KEY_KEYFILE, it.toString())
|
||||||
}
|
}
|
||||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
outState.putBoolean(KEY_READ_ONLY, mReadOnly)
|
||||||
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
|
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
@@ -520,7 +544,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
clearCredentialsViews()
|
clearCredentialsViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (readOnly && (
|
if (mReadOnly && (
|
||||||
mSpecialMode == SpecialMode.SAVE
|
mSpecialMode == SpecialMode.SAVE
|
||||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||||
) {
|
) {
|
||||||
@@ -534,7 +558,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
showProgressDialogAndLoadDatabase(
|
showProgressDialogAndLoadDatabase(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
MainCredential(password, keyFileUri),
|
MainCredential(password, keyFileUri),
|
||||||
readOnly,
|
mReadOnly,
|
||||||
cipherDatabaseEntity,
|
cipherDatabaseEntity,
|
||||||
false)
|
false)
|
||||||
}
|
}
|
||||||
@@ -546,7 +570,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
cipherDatabaseEntity: CipherDatabaseEntity?,
|
cipherDatabaseEntity: CipherDatabaseEntity?,
|
||||||
fixDuplicateUUID: Boolean) {
|
fixDuplicateUUID: Boolean) {
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseLoad(
|
loadDatabase(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
mainCredential,
|
mainCredential,
|
||||||
readOnly,
|
readOnly,
|
||||||
@@ -585,7 +609,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
// Check permission
|
// Check permission
|
||||||
private fun checkPermission() {
|
private fun checkPermission() {
|
||||||
if (Build.VERSION.SDK_INT in 23..28
|
if (Build.VERSION.SDK_INT in 23..28
|
||||||
&& !readOnly
|
&& !mReadOnly
|
||||||
&& !mPermissionAsked) {
|
&& !mPermissionAsked) {
|
||||||
mPermissionAsked = true
|
mPermissionAsked = true
|
||||||
// Check self permission to show or not the dialog
|
// Check self permission to show or not the dialog
|
||||||
@@ -662,7 +686,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
|
private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
|
||||||
if (readOnly) {
|
if (mReadOnly) {
|
||||||
togglePassword.setTitle(R.string.menu_file_selection_read_only)
|
togglePassword.setTitle(R.string.menu_file_selection_read_only)
|
||||||
togglePassword.setIcon(R.drawable.ic_read_only_white_24dp)
|
togglePassword.setIcon(R.drawable.ic_read_only_white_24dp)
|
||||||
} else {
|
} else {
|
||||||
@@ -676,7 +700,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
android.R.id.home -> finish()
|
android.R.id.home -> finish()
|
||||||
R.id.menu_open_file_read_mode_key -> {
|
R.id.menu_open_file_read_mode_key -> {
|
||||||
readOnly = !readOnly
|
mReadOnly = !mReadOnly
|
||||||
changeOpenFileReadIcon(item)
|
changeOpenFileReadIcon(item)
|
||||||
}
|
}
|
||||||
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||||
@@ -702,9 +726,8 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
}
|
}
|
||||||
|
|
||||||
var keyFileResult = false
|
var keyFileResult = false
|
||||||
mSelectFileHelper?.let {
|
mExternalFileHelper?.let {
|
||||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||||
) { uri ->
|
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
mDatabaseKeyFileUri = uri
|
mDatabaseKeyFileUri = uri
|
||||||
populateKeyFileTextView(uri)
|
populateKeyFileTextView(uri)
|
||||||
@@ -714,9 +737,9 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
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 -> {
|
DatabaseLockActivity.RESULT_EXIT_LOCK -> {
|
||||||
clearCredentialsViews()
|
clearCredentialsViews()
|
||||||
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
|
closeDatabase()
|
||||||
}
|
}
|
||||||
Activity.RESULT_CANCELED -> {
|
Activity.RESULT_CANCELED -> {
|
||||||
clearCredentialsViews()
|
clearCredentialsViews()
|
||||||
@@ -735,6 +758,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
private const val KEY_KEYFILE = "keyFile"
|
private const val KEY_KEYFILE = "keyFile"
|
||||||
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
||||||
|
|
||||||
|
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
|
||||||
private const val KEY_PASSWORD = "password"
|
private const val KEY_PASSWORD = "password"
|
||||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||||
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
|
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
|
||||||
|
|||||||
@@ -30,18 +30,17 @@ 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.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
|
|
||||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var mMasterPassword: String? = null
|
private var mMasterPassword: String? = null
|
||||||
private var mKeyFile: Uri? = null
|
private var mKeyFile: Uri? = null
|
||||||
@@ -60,7 +59,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
private var mListener: AssignPasswordDialogListener? = null
|
private var mListener: AssignPasswordDialogListener? = null
|
||||||
|
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
||||||
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||||
@@ -133,11 +132,8 @@ 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)
|
||||||
|
|
||||||
mSelectFileHelper = SelectFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
keyFileSelectionView?.apply {
|
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
|
||||||
setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
val dialog = builder.create()
|
val dialog = builder.create()
|
||||||
|
|
||||||
@@ -289,7 +285,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||||
uri?.let { pathUri ->
|
uri?.let { pathUri ->
|
||||||
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||||
keyFileSelectionView?.error = null
|
keyFileSelectionView?.error = null
|
||||||
|
|||||||
@@ -23,12 +23,11 @@ import android.app.Dialog
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
|
|
||||||
|
|
||||||
class DatabaseChangedDialogFragment : DialogFragment() {
|
class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
var actionDatabaseListener: ActionDatabaseChangedListener? = null
|
var actionDatabaseListener: ActionDatabaseChangedListener? = null
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
|
||||||
|
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
||||||
|
|
||||||
|
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
mDatabaseViewModel.database.observe(this) { database ->
|
||||||
|
this.mDatabase = database
|
||||||
|
resetAppTimeoutOnTouchOrFocus()
|
||||||
|
onDatabaseRetrieved(database)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.actionFinished.observe(this) { result ->
|
||||||
|
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
|
||||||
|
resetAppTimeoutOnTouchOrFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
// Can be overridden by a subclass
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: Database,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
// Can be overridden by a subclass
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetAppTimeout() {
|
||||||
|
context?.let {
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(it,
|
||||||
|
mDatabase?.loaded ?: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun overrideTimeoutTouchAndFocusEvents(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetAppTimeoutOnTouchOrFocus() {
|
||||||
|
if (!overrideTimeoutTouchAndFocusEvents()) {
|
||||||
|
context?.let {
|
||||||
|
dialog?.window?.decorView?.resetAppTimeoutWhenViewTouchedOrFocused(
|
||||||
|
it,
|
||||||
|
mDatabase?.loaded
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import android.content.Context
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
|
|
||||||
|
// Not as DatabaseDialogFragment because crash on KitKat
|
||||||
class DatePickerFragment : DialogFragment() {
|
class DatePickerFragment : DialogFragment() {
|
||||||
|
|
||||||
private var mDefaultYear: Int = 2000
|
private var mDefaultYear: Int = 2000
|
||||||
|
|||||||
@@ -20,61 +20,38 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.viewmodels.NodesViewModel
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
|
||||||
|
|
||||||
open class DeleteNodesDialogFragment : DialogFragment() {
|
class DeleteNodesDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var mNodesToDelete: List<Node> = ArrayList()
|
private var mNodesToDelete: List<Node> = listOf()
|
||||||
private var mListener: DeleteNodeListener? = null
|
private val mNodesViewModel: NodesViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
try {
|
|
||||||
mListener = context as DeleteNodeListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + DeleteNodeListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
|
mNodesViewModel.nodesToDelete.observe(this) { nodes ->
|
||||||
|
this.mNodesToDelete = nodes
|
||||||
|
}
|
||||||
|
var recycleBin = false
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
|
if (containsKey(RECYCLE_BIN_TAG)) {
|
||||||
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
|
recycleBin = this.getBoolean(RECYCLE_BIN_TAG)
|
||||||
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), this)
|
|
||||||
}
|
|
||||||
} ?: savedInstanceState?.apply {
|
|
||||||
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
|
|
||||||
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
|
|
||||||
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), savedInstanceState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
// 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(retrieveMessage())
|
builder.setMessage(if (recycleBin)
|
||||||
|
getString(R.string.warning_empty_recycle_bin)
|
||||||
|
else
|
||||||
|
getString(R.string.warning_permanently_delete_nodes))
|
||||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
mListener?.permanentlyDeleteNodes(mNodesToDelete)
|
mNodesViewModel.permanentlyDeleteNodes(mNodesToDelete)
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||||
// Create the AlertDialog object and return it
|
// Create the AlertDialog object and return it
|
||||||
@@ -83,19 +60,14 @@ open class DeleteNodesDialogFragment : DialogFragment() {
|
|||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
outState.putAll(getBundleFromListNodes(mNodesToDelete))
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteNodeListener {
|
|
||||||
fun permanentlyDeleteNodes(nodes: List<Node>)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getInstance(nodesToDelete: List<Node>): DeleteNodesDialogFragment {
|
private const val RECYCLE_BIN_TAG = "RECYCLE_BIN_TAG"
|
||||||
|
|
||||||
|
fun getInstance(recycleBin: Boolean): DeleteNodesDialogFragment {
|
||||||
return DeleteNodesDialogFragment().apply {
|
return DeleteNodesDialogFragment().apply {
|
||||||
arguments = getBundleFromListNodes(nodesToDelete)
|
arguments = Bundle().apply {
|
||||||
|
putBoolean(RECYCLE_BIN_TAG, recycleBin)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,14 +31,13 @@ import android.widget.ImageView
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.model.Field
|
|
||||||
|
|
||||||
|
|
||||||
class EntryCustomFieldDialogFragment: DialogFragment() {
|
class EntryCustomFieldDialogFragment: DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var oldField: Field? = null
|
private var oldField: Field? = null
|
||||||
|
|
||||||
|
|||||||
@@ -22,18 +22,18 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
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.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||||
|
|
||||||
class GeneratePasswordDialogFragment : DialogFragment() {
|
class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var mListener: GeneratePasswordListener? = null
|
private var mListener: GeneratePasswordListener? = null
|
||||||
|
|
||||||
@@ -42,6 +42,8 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
private var passwordInputLayoutView: TextInputLayout? = null
|
private var passwordInputLayoutView: TextInputLayout? = null
|
||||||
private var passwordView: EditText? = null
|
private var passwordView: EditText? = null
|
||||||
|
|
||||||
|
private var mPasswordField: Field? = null
|
||||||
|
|
||||||
private var uppercaseBox: CompoundButton? = null
|
private var uppercaseBox: CompoundButton? = null
|
||||||
private var lowercaseBox: CompoundButton? = null
|
private var lowercaseBox: CompoundButton? = null
|
||||||
private var digitsBox: CompoundButton? = null
|
private var digitsBox: CompoundButton? = null
|
||||||
@@ -77,7 +79,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
passwordView = root?.findViewById(R.id.password)
|
passwordView = root?.findViewById(R.id.password)
|
||||||
passwordView?.applyFontVisibility()
|
passwordView?.applyFontVisibility()
|
||||||
val passwordCopyView: ImageView? = root?.findViewById(R.id.password_copy_button)
|
val passwordCopyView: ImageView? = root?.findViewById(R.id.password_copy_button)
|
||||||
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyPasswordAndProtectedFields(activity))
|
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(activity))
|
||||||
View.VISIBLE else View.GONE
|
View.VISIBLE else View.GONE
|
||||||
val clipboardHelper = ClipboardHelper(activity)
|
val clipboardHelper = ClipboardHelper(activity)
|
||||||
passwordCopyView?.setOnClickListener {
|
passwordCopyView?.setOnClickListener {
|
||||||
@@ -98,6 +100,8 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
bracketsBox = root?.findViewById(R.id.cb_brackets)
|
bracketsBox = root?.findViewById(R.id.cb_brackets)
|
||||||
extendedBox = root?.findViewById(R.id.cb_extended)
|
extendedBox = root?.findViewById(R.id.cb_extended)
|
||||||
|
|
||||||
|
mPasswordField = arguments?.getParcelable(KEY_PASSWORD_FIELD)
|
||||||
|
|
||||||
assignDefaultCharacters()
|
assignDefaultCharacters()
|
||||||
|
|
||||||
val seekBar = root?.findViewById<SeekBar>(R.id.seekbar_length)
|
val seekBar = root?.findViewById<SeekBar>(R.id.seekbar_length)
|
||||||
@@ -120,16 +124,18 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
builder.setView(root)
|
builder.setView(root)
|
||||||
.setPositiveButton(R.string.accept) { _, _ ->
|
.setPositiveButton(R.string.accept) { _, _ ->
|
||||||
val bundle = Bundle()
|
mPasswordField?.let { passwordField ->
|
||||||
bundle.putString(KEY_PASSWORD_ID, passwordView!!.text.toString())
|
passwordView?.text?.toString()?.let { passwordValue ->
|
||||||
mListener?.acceptPassword(bundle)
|
passwordField.protectedValue.stringValue = passwordValue
|
||||||
|
}
|
||||||
|
mListener?.acceptPassword(passwordField)
|
||||||
|
}
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
val bundle = Bundle()
|
mPasswordField?.let { passwordField ->
|
||||||
mListener?.cancelPassword(bundle)
|
mListener?.cancelPassword(passwordField)
|
||||||
|
}
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,11 +206,19 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface GeneratePasswordListener {
|
interface GeneratePasswordListener {
|
||||||
fun acceptPassword(bundle: Bundle)
|
fun acceptPassword(passwordField: Field)
|
||||||
fun cancelPassword(bundle: Bundle)
|
fun cancelPassword(passwordField: Field)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val KEY_PASSWORD_ID = "KEY_PASSWORD_ID"
|
private const val KEY_PASSWORD_FIELD = "KEY_PASSWORD_FIELD"
|
||||||
|
|
||||||
|
fun getInstance(field: Field): GeneratePasswordDialogFragment {
|
||||||
|
return GeneratePasswordDialogFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putParcelable(KEY_PASSWORD_FIELD, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -28,35 +27,34 @@ import android.widget.Button
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.IconPickerActivity
|
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.*
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
|
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.model.GroupInfo
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
import com.kunzisoft.keepass.view.ExpirationView
|
import com.kunzisoft.keepass.view.DateTimeEditFieldView
|
||||||
|
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
class GroupEditDialogFragment : DialogFragment() {
|
class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var mDatabase: Database? = null
|
private val mGroupEditViewModel: GroupEditViewModel by activityViewModels()
|
||||||
|
|
||||||
private var mEditGroupListener: EditGroupListener? = null
|
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
||||||
|
private var mEditGroupDialogAction = NONE
|
||||||
private var mEditGroupDialogAction = EditGroupDialogAction.NONE
|
|
||||||
private var mGroupInfo = GroupInfo()
|
private var mGroupInfo = GroupInfo()
|
||||||
|
private var mGroupNamesNotAllowed: List<String>? = null
|
||||||
|
|
||||||
private lateinit var iconButtonView: ImageView
|
private lateinit var iconButtonView: ImageView
|
||||||
private var iconColor: Int = 0
|
private var mIconColor: Int = 0
|
||||||
private lateinit var nameTextLayoutView: TextInputLayout
|
private lateinit var nameTextLayoutView: TextInputLayout
|
||||||
private lateinit var nameTextView: TextView
|
private lateinit var nameTextView: TextView
|
||||||
private lateinit var notesTextLayoutView: TextInputLayout
|
private lateinit var notesTextLayoutView: TextInputLayout
|
||||||
private lateinit var notesTextView: TextView
|
private lateinit var notesTextView: TextView
|
||||||
private lateinit var expirationView: ExpirationView
|
private lateinit var expirationView: DateTimeEditFieldView
|
||||||
|
|
||||||
enum class EditGroupDialogAction {
|
enum class EditGroupDialogAction {
|
||||||
CREATION, UPDATE, NONE;
|
CREATION, UPDATE, NONE;
|
||||||
@@ -68,22 +66,51 @@ class GroupEditDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onAttach(context)
|
super.onCreate(savedInstanceState)
|
||||||
// Verify that the host activity implements the callback interface
|
|
||||||
try {
|
mGroupEditViewModel.onIconSelected.observe(this) { iconImage ->
|
||||||
// Instantiate the NoticeDialogListener so we can send events to the host
|
mGroupInfo.icon = iconImage
|
||||||
mEditGroupListener = context as EditGroupListener
|
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||||
} catch (e: ClassCastException) {
|
}
|
||||||
// The activity doesn't implement the interface, throw exception
|
|
||||||
throw ClassCastException(context.toString()
|
mGroupEditViewModel.onDateSelected.observe(this) { viewModelDate ->
|
||||||
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
// Save the date
|
||||||
|
mGroupInfo.expiryTime = DateInstant(
|
||||||
|
DateTime(mGroupInfo.expiryTime.date)
|
||||||
|
.withYear(viewModelDate.year)
|
||||||
|
.withMonthOfYear(viewModelDate.month + 1)
|
||||||
|
.withDayOfMonth(viewModelDate.day)
|
||||||
|
.toDate())
|
||||||
|
expirationView.dateTime = mGroupInfo.expiryTime
|
||||||
|
if (expirationView.dateTime.type == DateInstant.Type.DATE_TIME) {
|
||||||
|
val instantTime = DateInstant(mGroupInfo.expiryTime.date, DateInstant.Type.TIME)
|
||||||
|
// Trick to recall selection with time
|
||||||
|
mGroupEditViewModel.requestDateTimeSelection(instantTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mGroupEditViewModel.onTimeSelected.observe(this) { viewModelTime ->
|
||||||
|
// Save the time
|
||||||
|
mGroupInfo.expiryTime = DateInstant(
|
||||||
|
DateTime(mGroupInfo.expiryTime.date)
|
||||||
|
.withHourOfDay(viewModelTime.hours)
|
||||||
|
.withMinuteOfHour(viewModelTime.minutes)
|
||||||
|
.toDate(), mGroupInfo.expiryTime.type)
|
||||||
|
expirationView.dateTime = mGroupInfo.expiryTime
|
||||||
|
}
|
||||||
|
|
||||||
|
mGroupEditViewModel.groupNamesNotAllowed.observe(this) { namesNotAllowed ->
|
||||||
|
this.mGroupNamesNotAllowed = namesNotAllowed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetach() {
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
mEditGroupListener = null
|
super.onDatabaseRetrieved(database)
|
||||||
super.onDetach()
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
|
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
|
}
|
||||||
|
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
@@ -98,12 +125,9 @@ class GroupEditDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the textColor to tint the icon
|
||||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
iconColor = ta.getColor(0, Color.WHITE)
|
mIconColor = ta.getColor(0, Color.WHITE)
|
||||||
ta.recycle()
|
ta.recycle()
|
||||||
|
|
||||||
// Init elements
|
|
||||||
mDatabase = Database.getInstance()
|
|
||||||
|
|
||||||
if (savedInstanceState != null
|
if (savedInstanceState != null
|
||||||
&& savedInstanceState.containsKey(KEY_ACTION_ID)
|
&& savedInstanceState.containsKey(KEY_ACTION_ID)
|
||||||
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
||||||
@@ -120,32 +144,22 @@ class GroupEditDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// populate info in views
|
// populate info in views
|
||||||
populateInfoToViews()
|
populateInfoToViews(mGroupInfo)
|
||||||
expirationView.setOnDateClickListener = {
|
|
||||||
expirationView.expiryTime.date.let { expiresDate ->
|
iconButtonView.setOnClickListener { _ ->
|
||||||
val dateTime = DateTime(expiresDate)
|
mGroupEditViewModel.requestIconSelection(mGroupInfo.icon)
|
||||||
val defaultYear = dateTime.year
|
}
|
||||||
val defaultMonth = dateTime.monthOfYear-1
|
expirationView.setOnDateClickListener = { dateInstant ->
|
||||||
val defaultDay = dateTime.dayOfMonth
|
mGroupEditViewModel.requestDateTimeSelection(dateInstant)
|
||||||
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
|
|
||||||
.show(parentFragmentManager, "DatePickerFragment")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.setView(root)
|
builder.setView(root)
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
retrieveGroupInfoFromViews()
|
// Do nothing
|
||||||
mEditGroupListener?.cancelEditGroup(
|
|
||||||
mEditGroupDialogAction,
|
|
||||||
mGroupInfo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
iconButtonView.setOnClickListener { _ ->
|
|
||||||
IconPickerActivity.launch(activity, mGroupInfo.icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
}
|
}
|
||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
@@ -155,40 +169,34 @@ class GroupEditDialogFragment : DialogFragment() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
// To prevent auto dismiss
|
// To prevent auto dismiss
|
||||||
val d = dialog as AlertDialog?
|
val alertDialog = dialog as AlertDialog?
|
||||||
if (d != null) {
|
if (alertDialog != null) {
|
||||||
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
val positiveButton = alertDialog.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||||
positiveButton.setOnClickListener {
|
positiveButton.setOnClickListener {
|
||||||
retrieveGroupInfoFromViews()
|
retrieveGroupInfoFromViews()
|
||||||
if (isValid()) {
|
if (isValid()) {
|
||||||
mEditGroupListener?.approveEditGroup(
|
when (mEditGroupDialogAction) {
|
||||||
mEditGroupDialogAction,
|
CREATION ->
|
||||||
mGroupInfo)
|
mGroupEditViewModel.approveGroupCreation(mGroupInfo)
|
||||||
d.dismiss()
|
UPDATE ->
|
||||||
|
mGroupEditViewModel.approveGroupUpdate(mGroupInfo)
|
||||||
|
NONE -> {}
|
||||||
|
}
|
||||||
|
alertDialog.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getExpiryTime(): DateInstant {
|
private fun populateInfoToViews(groupInfo: GroupInfo) {
|
||||||
retrieveGroupInfoFromViews()
|
mGroupEditViewModel.selectIcon(groupInfo.icon)
|
||||||
return mGroupInfo.expiryTime
|
nameTextView.text = groupInfo.title
|
||||||
}
|
notesTextLayoutView.visibility = if (groupInfo.notes == null) View.GONE else View.VISIBLE
|
||||||
|
groupInfo.notes?.let {
|
||||||
fun setExpiryTime(expiryTime: DateInstant) {
|
|
||||||
mGroupInfo.expiryTime = expiryTime
|
|
||||||
populateInfoToViews()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun populateInfoToViews() {
|
|
||||||
assignIconView()
|
|
||||||
nameTextView.text = mGroupInfo.title
|
|
||||||
notesTextLayoutView.visibility = if (mGroupInfo.notes == null) View.GONE else View.VISIBLE
|
|
||||||
mGroupInfo.notes?.let {
|
|
||||||
notesTextView.text = it
|
notesTextView.text = it
|
||||||
}
|
}
|
||||||
expirationView.expires = mGroupInfo.expires
|
expirationView.activation = groupInfo.expires
|
||||||
expirationView.expiryTime = mGroupInfo.expiryTime
|
expirationView.dateTime = groupInfo.expiryTime
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun retrieveGroupInfoFromViews() {
|
private fun retrieveGroupInfoFromViews() {
|
||||||
@@ -198,17 +206,8 @@ class GroupEditDialogFragment : DialogFragment() {
|
|||||||
if (newNotes.isNotEmpty()) {
|
if (newNotes.isNotEmpty()) {
|
||||||
mGroupInfo.notes = newNotes
|
mGroupInfo.notes = newNotes
|
||||||
}
|
}
|
||||||
mGroupInfo.expires = expirationView.expires
|
mGroupInfo.expires = expirationView.activation
|
||||||
mGroupInfo.expiryTime = expirationView.expiryTime
|
mGroupInfo.expiryTime = expirationView.dateTime
|
||||||
}
|
|
||||||
|
|
||||||
private fun assignIconView() {
|
|
||||||
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconButtonView, mGroupInfo.icon, iconColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setIcon(icon: IconImage) {
|
|
||||||
mGroupInfo.icon = icon
|
|
||||||
assignIconView()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
@@ -219,19 +218,30 @@ class GroupEditDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isValid(): Boolean {
|
private fun isValid(): Boolean {
|
||||||
if (nameTextView.text.toString().isEmpty()) {
|
val name = nameTextView.text.toString()
|
||||||
nameTextLayoutView.error = getString(R.string.error_no_name)
|
val error = when {
|
||||||
return false
|
name.isEmpty() -> {
|
||||||
|
Error(true, R.string.error_no_name)
|
||||||
|
}
|
||||||
|
mGroupNamesNotAllowed == null -> {
|
||||||
|
Error(true, R.string.error_word_reserved)
|
||||||
|
}
|
||||||
|
mGroupNamesNotAllowed?.find { it.equals(name, ignoreCase = true) } != null -> {
|
||||||
|
Error(true, R.string.error_word_reserved)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Error(false, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
error.messageId?.let { messageId ->
|
||||||
|
nameTextLayoutView.error = getString(messageId)
|
||||||
|
} ?: kotlin.run {
|
||||||
|
nameTextLayoutView.error = null
|
||||||
|
}
|
||||||
|
return !error.isError
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditGroupListener {
|
data class Error(val isError: Boolean, val messageId: Int?)
|
||||||
fun approveEditGroup(action: EditGroupDialogAction,
|
|
||||||
groupInfo: GroupInfo)
|
|
||||||
fun cancelEditGroup(action: EditGroupDialogAction,
|
|
||||||
groupInfo: GroupInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,11 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
|
|||||||
@@ -25,14 +25,13 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Dialog to confirm big file to upload
|
* Custom Dialog to confirm big file to upload
|
||||||
*/
|
*/
|
||||||
class ReplaceFileDialogFragment : DialogFragment() {
|
class ReplaceFileDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var mActionChooseListener: ActionChooseListener? = null
|
private var mActionChooseListener: ActionChooseListener? = null
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import android.view.ViewGroup
|
|||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
@@ -49,7 +48,7 @@ import com.kunzisoft.keepass.otp.TokenCalculator
|
|||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class SetOTPDialogFragment : DialogFragment() {
|
class SetOTPDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var mCreateOTPElementListener: CreateOtpListener? = null
|
private var mCreateOTPElementListener: CreateOtpListener? = null
|
||||||
|
|
||||||
@@ -80,11 +79,15 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
|
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
|
||||||
if (!isFocus)
|
if (!isFocus)
|
||||||
mManualEvent = true
|
mManualEvent = true
|
||||||
|
else
|
||||||
|
resetAppTimeout()
|
||||||
}
|
}
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private var mOnTouchListener = View.OnTouchListener { _, event ->
|
private var mOnTouchListener = View.OnTouchListener { _, event ->
|
||||||
when (event.action) {
|
when (event.action) {
|
||||||
MotionEvent.ACTION_DOWN -> {
|
MotionEvent.ACTION_DOWN -> {
|
||||||
mManualEvent = true
|
mManualEvent = true
|
||||||
|
resetAppTimeout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
@@ -95,6 +98,10 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
private var mPeriodWellFormed = false
|
private var mPeriodWellFormed = false
|
||||||
private var mDigitsWellFormed = false
|
private var mDigitsWellFormed = false
|
||||||
|
|
||||||
|
override fun overrideTimeoutTouchAndFocusEvents(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
// Verify that the host activity implements the callback interface
|
// Verify that the host activity implements the callback interface
|
||||||
@@ -225,8 +232,11 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.apply {
|
builder.apply {
|
||||||
setView(root)
|
setView(root)
|
||||||
.setPositiveButton(android.R.string.ok) {_, _ -> }
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
resetAppTimeout()
|
||||||
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
resetAppTimeout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,16 +22,15 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.annotation.IdRes
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.RadioGroup
|
import android.widget.RadioGroup
|
||||||
|
import androidx.annotation.IdRes
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
|
|
||||||
class SortDialogFragment : DialogFragment() {
|
class SortDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var mListener: SortSelectionListener? = null
|
private var mListener: SortSelectionListener? = null
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.os.Bundle
|
|||||||
import android.text.format.DateFormat
|
import android.text.format.DateFormat
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
|
|
||||||
|
// Not as DatabaseDialogFragment because crash on KitKat
|
||||||
class TimePickerFragment : DialogFragment() {
|
class TimePickerFragment : DialogFragment() {
|
||||||
|
|
||||||
private var defaultHour: Int = 0
|
private var defaultHour: Int = 0
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
|
||||||
|
abstract class DatabaseFragment : StylishFragment(), DatabaseRetrieval {
|
||||||
|
|
||||||
|
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
|
protected var mDatabase: Database? = null
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
|
||||||
|
if (mDatabase == null || mDatabase != database) {
|
||||||
|
this.mDatabase = database
|
||||||
|
onDatabaseRetrieved(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result ->
|
||||||
|
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
|
||||||
|
context?.let {
|
||||||
|
view?.resetAppTimeoutWhenViewTouchedOrFocused(it, mDatabase?.loaded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: Database,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
// Can be overridden by a subclass
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun buildNewBinaryAttachment(): BinaryData? {
|
||||||
|
return mDatabase?.buildNewBinaryAttachment()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,431 +19,255 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.fragments
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
import androidx.core.view.isVisible
|
||||||
import android.widget.EditText
|
import androidx.fragment.app.activityViewModels
|
||||||
import android.widget.ImageView
|
|
||||||
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
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.EntryEditActivity
|
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
|
||||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
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.DateInstant
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
import com.kunzisoft.keepass.model.*
|
import com.kunzisoft.keepass.view.TemplateEditView
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.view.ExpirationView
|
|
||||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
|
||||||
import com.kunzisoft.keepass.view.collapse
|
import com.kunzisoft.keepass.view.collapse
|
||||||
import com.kunzisoft.keepass.view.expand
|
import com.kunzisoft.keepass.view.expand
|
||||||
|
import com.kunzisoft.keepass.view.showByFading
|
||||||
|
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||||
|
|
||||||
class EntryEditFragment: StylishFragment() {
|
class EntryEditFragment: DatabaseFragment() {
|
||||||
|
|
||||||
private lateinit var entryTitleLayoutView: TextInputLayout
|
private val mEntryEditViewModel: EntryEditViewModel by activityViewModels()
|
||||||
private lateinit var entryTitleView: EditText
|
|
||||||
private lateinit var entryIconView: ImageView
|
private lateinit var rootView: View
|
||||||
private lateinit var entryUserNameView: EditText
|
private lateinit var templateView: TemplateEditView
|
||||||
private lateinit var entryUrlView: EditText
|
private lateinit var attachmentsContainerView: ViewGroup
|
||||||
private lateinit var entryPasswordLayoutView: TextInputLayout
|
|
||||||
private lateinit var entryPasswordView: EditText
|
|
||||||
private lateinit var entryPasswordGeneratorView: View
|
|
||||||
private lateinit var entryExpirationView: ExpirationView
|
|
||||||
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 attachmentsListView: RecyclerView
|
||||||
|
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||||
|
|
||||||
private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter
|
private var mAllowMultipleAttachments: Boolean = false
|
||||||
|
|
||||||
private var fontInVisibility: Boolean = false
|
private var mIconColor: Int = 0
|
||||||
private var iconColor: Int = 0
|
|
||||||
|
|
||||||
var drawFactory: IconDrawableFactory? = null
|
override fun onCreateView(inflater: LayoutInflater,
|
||||||
var setOnDateClickListener: (() -> Unit)? = null
|
container: ViewGroup?,
|
||||||
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
|
savedInstanceState: Bundle?): View? {
|
||||||
var setOnIconViewClickListener: ((IconImage) -> Unit)? = 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)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
val rootView = inflater.cloneInContext(contextThemed)
|
// Retrieve the textColor to tint the icon
|
||||||
.inflate(R.layout.fragment_entry_edit_contents, container, false)
|
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
|
mIconColor = taIconColor?.getColor(0, Color.BLACK) ?: Color.BLACK
|
||||||
|
taIconColor?.recycle()
|
||||||
|
|
||||||
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
|
return inflater.cloneInContext(contextThemed)
|
||||||
|
.inflate(R.layout.fragment_entry_edit, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
entryTitleLayoutView = rootView.findViewById(R.id.entry_edit_container_title)
|
override fun onViewCreated(view: View,
|
||||||
entryTitleView = rootView.findViewById(R.id.entry_edit_title)
|
savedInstanceState: Bundle?) {
|
||||||
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
entryIconView.setOnClickListener {
|
|
||||||
setOnIconViewClickListener?.invoke(mEntryInfo.icon)
|
rootView = view
|
||||||
|
// Hide only the first time
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
view.isVisible = false
|
||||||
}
|
}
|
||||||
|
templateView = view.findViewById(R.id.template_view)
|
||||||
|
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
|
||||||
|
attachmentsListView = view.findViewById(R.id.entry_attachments_list)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
entryExpirationView = rootView.findViewById(R.id.entry_edit_expiration)
|
|
||||||
entryExpirationView.setOnDateClickListener = setOnDateClickListener
|
|
||||||
|
|
||||||
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 = EntryAttachmentsItemsAdapter(requireContext())
|
||||||
// TODO retrieve current database with its unique key
|
|
||||||
attachmentsAdapter.database = Database.getInstance()
|
|
||||||
//attachmentsAdapter.database = arguments?.getInt(KEY_DATABASE)
|
|
||||||
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
|
|
||||||
if (previousSize > 0 && newSize == 0) {
|
|
||||||
attachmentsContainerView.collapse(true)
|
|
||||||
} else if (previousSize == 0 && newSize == 1) {
|
|
||||||
attachmentsContainerView.expand(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
attachmentsListView.apply {
|
attachmentsListView.apply {
|
||||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||||
adapter = attachmentsAdapter
|
adapter = attachmentsAdapter
|
||||||
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
templateView.apply {
|
||||||
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
setOnIconClickListener {
|
||||||
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
|
mEntryEditViewModel.requestIconSelection(templateView.getIcon())
|
||||||
taIconColor?.recycle()
|
}
|
||||||
|
setOnCustomEditionActionClickListener { field ->
|
||||||
rootView?.resetAppTimeoutWhenViewFocusedOrChanged(requireContext())
|
mEntryEditViewModel.requestCustomFieldEdition(field)
|
||||||
|
}
|
||||||
// Retrieve the new entry after an orientation change
|
setOnPasswordGenerationActionClickListener { field ->
|
||||||
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
|
mEntryEditViewModel.requestPasswordSelection(field)
|
||||||
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
}
|
||||||
else if (savedInstanceState?.containsKey(KEY_TEMP_ENTRY_INFO) == true) {
|
setOnDateInstantClickListener { dateInstant ->
|
||||||
mEntryInfo = savedInstanceState.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
mEntryEditViewModel.requestDateTimeSelection(dateInstant)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) {
|
if (savedInstanceState != null) {
|
||||||
mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD) ?: mLastFocusedEditField
|
val attachments: List<Attachment> =
|
||||||
|
savedInstanceState.getParcelableArrayList(ATTACHMENTS_TAG) ?: listOf()
|
||||||
|
setAttachments(attachments)
|
||||||
}
|
}
|
||||||
|
|
||||||
populateViewsWithEntry()
|
mEntryEditViewModel.onTemplateChanged.observe(viewLifecycleOwner) { template ->
|
||||||
|
templateView.setTemplate(template)
|
||||||
|
}
|
||||||
|
|
||||||
return rootView
|
mEntryEditViewModel.templatesEntry.observe(viewLifecycleOwner) { templateEntry ->
|
||||||
}
|
templateView.setTemplate(templateEntry.defaultTemplate)
|
||||||
|
// Load entry info only the first time to keep change locally
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
assignEntryInfo(templateEntry.entryInfo)
|
||||||
|
}
|
||||||
|
// To prevent flickering
|
||||||
|
rootView.showByFading()
|
||||||
|
// Apply timeout reset
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDetach() {
|
mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) {
|
||||||
super.onDetach()
|
mEntryEditViewModel.saveEntryInfo(it.database, it.entry, it.parent, retrieveEntryInfo())
|
||||||
|
}
|
||||||
|
|
||||||
drawFactory = null
|
mEntryEditViewModel.onIconSelected.observe(viewLifecycleOwner) { iconImage ->
|
||||||
setOnDateClickListener = null
|
templateView.setIcon(iconImage)
|
||||||
setOnPasswordGeneratorClickListener = null
|
}
|
||||||
setOnIconViewClickListener = null
|
|
||||||
setOnRemoveAttachment = null
|
|
||||||
setOnEditCustomField = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getEntryInfo(): EntryInfo {
|
mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField ->
|
||||||
populateEntryWithViews()
|
templateView.setPasswordField(passwordField)
|
||||||
return mEntryInfo
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun generatePasswordEducationPerformed(entryEditActivityEducation: EntryEditActivityEducation): Boolean {
|
mEntryEditViewModel.onDateSelected.observe(viewLifecycleOwner) { viewModelDate ->
|
||||||
return entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
// Save the date
|
||||||
entryPasswordGeneratorView,
|
templateView.setCurrentDateTimeValue(viewModelDate)
|
||||||
{
|
}
|
||||||
GeneratePasswordDialogFragment().show(parentFragmentManager, "PasswordGeneratorFragment")
|
|
||||||
},
|
mEntryEditViewModel.onTimeSelected.observe(viewLifecycleOwner) { viewModelTime ->
|
||||||
{
|
// Save the time
|
||||||
try {
|
templateView.setCurrentTimeValue(viewModelTime)
|
||||||
(activity as? EntryEditActivity?)?.performedNextEducation(entryEditActivityEducation)
|
}
|
||||||
} catch (ignore: Exception) {}
|
|
||||||
|
mEntryEditViewModel.onCustomFieldEdited.observe(viewLifecycleOwner) { fieldAction ->
|
||||||
|
val oldField = fieldAction.oldField
|
||||||
|
val newField = fieldAction.newField
|
||||||
|
// Field to add
|
||||||
|
if (oldField == null) {
|
||||||
|
newField?.let {
|
||||||
|
if (!templateView.putCustomField(it)) {
|
||||||
|
mEntryEditViewModel.showCustomFieldEditionError()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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?.assignDatabaseIcon(entryIconView, 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()
|
|
||||||
}
|
}
|
||||||
}
|
// Field to replace
|
||||||
|
oldField?.let {
|
||||||
var expires: Boolean
|
newField?.let {
|
||||||
get() {
|
if (!templateView.replaceCustomField(oldField, newField)) {
|
||||||
return entryExpirationView.expires
|
mEntryEditViewModel.showCustomFieldEditionError()
|
||||||
}
|
}
|
||||||
set(value) {
|
}
|
||||||
entryExpirationView.expires = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var expiryTime: DateInstant
|
|
||||||
get() {
|
|
||||||
return entryExpirationView.expiryTime
|
|
||||||
}
|
|
||||||
set(value) {
|
|
||||||
entryExpirationView.expiryTime = value
|
|
||||||
}
|
|
||||||
|
|
||||||
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?.endIconMode = if (extraField.protectedValue.isProtected)
|
|
||||||
TextInputLayout.END_ICON_PASSWORD_TOGGLE else TextInputLayout.END_ICON_NONE
|
|
||||||
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())
|
// Field to remove
|
||||||
if (fontInVisibility)
|
if (newField == null) {
|
||||||
applyFontVisibility()
|
oldField?.let {
|
||||||
}
|
templateView.removeCustomField(it)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
mEntryEditViewModel.requestSetupOtp.observe(viewLifecycleOwner) {
|
||||||
* Remove all children and add new views for each field
|
// Retrieve the current otpElement if exists
|
||||||
*/
|
// and open the dialog to set up the OTP
|
||||||
fun assignExtraFields(fields: List<Field>,
|
SetOTPDialogFragment.build(templateView.getEntryInfo().otpModel)
|
||||||
onEditButtonClickListener: ((item: Field)->Unit)?) {
|
.show(parentFragmentManager, "addOTPDialog")
|
||||||
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 ->
|
mEntryEditViewModel.onOtpCreated.observe(viewLifecycleOwner) {
|
||||||
mExtraViewToRequestFocus?.apply {
|
// Update the otp field with otpauth:// url
|
||||||
requestFocus()
|
templateView.putOtpElement(it)
|
||||||
setSelection(focusField.cursorSelectionStart,
|
|
||||||
focusField.cursorSelectionEnd)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
mLastFocusedEditField = null
|
|
||||||
mOnEditButtonClickListener = onEditButtonClickListener
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
mEntryEditViewModel.onBuildNewAttachment.observe(viewLifecycleOwner) {
|
||||||
* Update an extra field or create a new one if doesn't exists, the old value is lost
|
val attachmentToUploadUri = it.attachmentToUploadUri
|
||||||
*/
|
val fileName = it.fileName
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
buildNewBinaryAttachment()?.let { binaryAttachment ->
|
||||||
* Update an extra field and keep the old value
|
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||||
*/
|
// Ask to replace the current attachment
|
||||||
fun replaceExtraField(oldExtraField: Field, newExtraField: Field) {
|
if ((!mAllowMultipleAttachments
|
||||||
extraFieldsContainerView.visibility = View.VISIBLE
|
&& containsAttachment()) ||
|
||||||
val index = mExtraFieldsList.indexOf(oldExtraField)
|
containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD))) {
|
||||||
val oldValueEditText: EditText = extraFieldsListView.getChildAt(index)
|
ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment)
|
||||||
.findViewWithTag("FIELD_VALUE_TAG")
|
.show(parentFragmentManager, "replacementFileFragment")
|
||||||
val oldValue = oldValueEditText.text.toString()
|
} else {
|
||||||
val newExtraFieldWithOldValue = Field(newExtraField).apply {
|
mEntryEditViewModel.startUploadAttachment(attachmentToUploadUri, entryAttachment)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->
|
||||||
|
when (entryAttachmentState?.downloadState) {
|
||||||
|
AttachmentState.START -> {
|
||||||
|
putAttachment(entryAttachmentState)
|
||||||
|
getAttachmentViewPosition(entryAttachmentState) { attachment, position ->
|
||||||
|
mEntryEditViewModel.binaryPreviewLoaded(attachment, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AttachmentState.IN_PROGRESS -> {
|
||||||
|
putAttachment(entryAttachmentState)
|
||||||
|
}
|
||||||
|
AttachmentState.COMPLETE -> {
|
||||||
|
putAttachment(entryAttachmentState) { entryAttachment ->
|
||||||
|
getAttachmentViewPosition(entryAttachment) { attachment, position ->
|
||||||
|
mEntryEditViewModel.binaryPreviewLoaded(attachment, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mEntryEditViewModel.onAttachmentAction(null)
|
||||||
|
}
|
||||||
|
AttachmentState.CANCELED,
|
||||||
|
AttachmentState.ERROR -> {
|
||||||
|
removeAttachment(entryAttachmentState)
|
||||||
|
mEntryEditViewModel.onAttachmentAction(null)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
|
||||||
|
templateView.populateIconMethod = { imageView, icon ->
|
||||||
|
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
mAllowMultipleAttachments = database?.allowMultipleAttachments == true
|
||||||
|
|
||||||
|
attachmentsAdapter?.database = database
|
||||||
|
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
|
||||||
|
if (previousSize > 0 && newSize == 0) {
|
||||||
|
attachmentsContainerView.collapse(true)
|
||||||
|
} else if (previousSize == 0 && newSize == 1) {
|
||||||
|
attachmentsContainerView.expand(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
||||||
|
// Populate entry views
|
||||||
|
templateView.setEntryInfo(entryInfo)
|
||||||
|
|
||||||
|
// Manage attachments
|
||||||
|
setAttachments(entryInfo?.attachments ?: listOf())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retrieveEntryInfo(): EntryInfo {
|
||||||
|
val entryInfo = templateView.getEntryInfo()
|
||||||
|
entryInfo.attachments = getAttachments().toMutableList()
|
||||||
|
return entryInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------
|
/* -------------
|
||||||
@@ -451,78 +275,84 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
* -------------
|
* -------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun getAttachments(): List<Attachment> {
|
private fun getAttachments(): List<Attachment> {
|
||||||
return attachmentsAdapter.itemsList.map { it.attachment }
|
return attachmentsAdapter?.itemsList?.map { it.attachment } ?: listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assignAttachments(attachments: List<Attachment>,
|
private fun setAttachments(attachments: List<Attachment>) {
|
||||||
streamDirection: StreamDirection,
|
|
||||||
onDeleteItem: (attachment: Attachment)->Unit) {
|
|
||||||
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||||
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
|
attachmentsAdapter?.assignItems(attachments.map {
|
||||||
attachmentsAdapter.onDeleteButtonClickListener = { item ->
|
EntryAttachmentState(it, StreamDirection.UPLOAD)
|
||||||
onDeleteItem.invoke(item.attachment)
|
})
|
||||||
|
attachmentsAdapter?.onDeleteButtonClickListener = { item ->
|
||||||
|
val attachment = item.attachment
|
||||||
|
removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD))
|
||||||
|
mEntryEditViewModel.deleteAttachment(attachment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun containsAttachment(): Boolean {
|
private fun containsAttachment(): Boolean {
|
||||||
return !attachmentsAdapter.isEmpty()
|
return attachmentsAdapter?.isEmpty() != true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun containsAttachment(attachment: EntryAttachmentState): Boolean {
|
private fun containsAttachment(attachment: EntryAttachmentState): Boolean {
|
||||||
return attachmentsAdapter.contains(attachment)
|
return attachmentsAdapter?.contains(attachment) ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putAttachment(attachment: EntryAttachmentState,
|
private fun putAttachment(attachment: EntryAttachmentState,
|
||||||
onPreviewLoaded: (()-> Unit)? = null) {
|
onPreviewLoaded: ((attachment: EntryAttachmentState) -> Unit)? = null) {
|
||||||
|
// When only one attachment is allowed
|
||||||
|
if (!mAllowMultipleAttachments
|
||||||
|
&& attachment.downloadState == AttachmentState.START) {
|
||||||
|
attachmentsAdapter?.clear()
|
||||||
|
}
|
||||||
attachmentsContainerView.visibility = View.VISIBLE
|
attachmentsContainerView.visibility = View.VISIBLE
|
||||||
attachmentsAdapter.putItem(attachment)
|
attachmentsAdapter?.putItem(attachment)
|
||||||
attachmentsAdapter.onBinaryPreviewLoaded = {
|
attachmentsAdapter?.onBinaryPreviewLoaded = {
|
||||||
onPreviewLoaded?.invoke()
|
onPreviewLoaded?.invoke(attachment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAttachment(attachment: EntryAttachmentState) {
|
private fun removeAttachment(attachment: EntryAttachmentState) {
|
||||||
attachmentsAdapter.removeItem(attachment)
|
attachmentsAdapter?.removeItem(attachment)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearAttachments() {
|
private fun getAttachmentViewPosition(attachment: EntryAttachmentState,
|
||||||
attachmentsAdapter.clear()
|
position: (attachment: EntryAttachmentState, Float) -> Unit) {
|
||||||
}
|
|
||||||
|
|
||||||
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
|
|
||||||
attachmentsListView.postDelayed({
|
attachmentsListView.postDelayed({
|
||||||
position.invoke(attachmentsContainerView.y
|
attachmentsAdapter?.indexOf(attachment)?.let { index ->
|
||||||
+ attachmentsListView.y
|
position.invoke(attachment,
|
||||||
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
|
attachmentsContainerView.y
|
||||||
?: 0F)
|
+ attachmentsListView.y
|
||||||
)
|
+ (attachmentsListView.getChildAt(index)?.y
|
||||||
|
?: 0F)
|
||||||
|
)
|
||||||
|
}
|
||||||
}, 250)
|
}, 250)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
populateEntryWithViews()
|
|
||||||
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
|
|
||||||
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
|
|
||||||
|
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putParcelableArrayList(ATTACHMENTS_TAG, ArrayList(getAttachments()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Education
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun getActionImageView(): View? {
|
||||||
|
return templateView.getActionImageView()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchGeneratePasswordEductionAction() {
|
||||||
|
mEntryEditViewModel.requestPasswordSelection(templateView.getPasswordField())
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
|
private val TAG = EntryEditFragment::class.java.name
|
||||||
const val KEY_DATABASE = "KEY_DATABASE"
|
|
||||||
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
|
|
||||||
|
|
||||||
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
|
private const val ATTACHMENTS_TAG = "ATTACHMENTS_TAG"
|
||||||
//database: Database?): EntryEditFragment {
|
|
||||||
return EntryEditFragment().apply {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
|
|
||||||
// TODO Unique database key database.key
|
|
||||||
putInt(KEY_DATABASE, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
|
import com.kunzisoft.keepass.view.TemplateView
|
||||||
|
import com.kunzisoft.keepass.view.showByFading
|
||||||
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class EntryFragment: DatabaseFragment() {
|
||||||
|
|
||||||
|
private lateinit var rootView: View
|
||||||
|
private lateinit var templateView: TemplateView
|
||||||
|
|
||||||
|
private lateinit var creationDateView: TextView
|
||||||
|
private lateinit var modificationDateView: TextView
|
||||||
|
|
||||||
|
private lateinit var attachmentsContainerView: View
|
||||||
|
private lateinit var attachmentsListView: RecyclerView
|
||||||
|
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||||
|
|
||||||
|
private lateinit var uuidContainerView: View
|
||||||
|
private lateinit var uuidView: TextView
|
||||||
|
private lateinit var uuidReferenceView: TextView
|
||||||
|
|
||||||
|
private var mClipboardHelper: ClipboardHelper? = null
|
||||||
|
|
||||||
|
private val mEntryViewModel: EntryViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View? {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
|
return inflater.cloneInContext(contextThemed)
|
||||||
|
.inflate(R.layout.fragment_entry, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View,
|
||||||
|
savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
context?.let { context ->
|
||||||
|
mClipboardHelper = ClipboardHelper(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootView = view
|
||||||
|
// Hide only the first time
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
view.isVisible = false
|
||||||
|
}
|
||||||
|
templateView = view.findViewById(R.id.entry_template)
|
||||||
|
loadTemplateSettings()
|
||||||
|
|
||||||
|
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
|
||||||
|
attachmentsListView = view.findViewById(R.id.entry_attachments_list)
|
||||||
|
attachmentsListView.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||||
|
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
}
|
||||||
|
|
||||||
|
creationDateView = view.findViewById(R.id.entry_created)
|
||||||
|
modificationDateView = view.findViewById(R.id.entry_modified)
|
||||||
|
|
||||||
|
uuidContainerView = view.findViewById(R.id.entry_UUID_container)
|
||||||
|
uuidContainerView.apply {
|
||||||
|
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
uuidView = view.findViewById(R.id.entry_UUID)
|
||||||
|
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
|
||||||
|
|
||||||
|
mEntryViewModel.template.observe(viewLifecycleOwner) { template ->
|
||||||
|
templateView.setTemplate(template)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.entryInfo.observe(viewLifecycleOwner) { entryInfo ->
|
||||||
|
assignEntryInfo(entryInfo)
|
||||||
|
// Smooth appearing
|
||||||
|
rootView.showByFading()
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->
|
||||||
|
entryAttachmentState?.let {
|
||||||
|
if (it.streamDirection != StreamDirection.UPLOAD) {
|
||||||
|
putAttachment(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
context?.let { context ->
|
||||||
|
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
||||||
|
attachmentsAdapter?.database = database
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentsListView.adapter = attachmentsAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadTemplateSettings() {
|
||||||
|
context?.let { context ->
|
||||||
|
templateView.setFirstTimeAskAllowCopyProtectedFields(PreferencesUtil.isFirstTimeAskAllowCopyProtectedFields(context))
|
||||||
|
templateView.setAllowCopyProtectedFields(PreferencesUtil.allowCopyProtectedFields(context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
||||||
|
// Set copy buttons
|
||||||
|
templateView.apply {
|
||||||
|
setOnAskCopySafeClickListener {
|
||||||
|
showClipboardDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnCopyActionClickListener { field ->
|
||||||
|
mClipboardHelper?.timeoutCopyToClipboard(
|
||||||
|
field.protectedValue.stringValue,
|
||||||
|
getString(
|
||||||
|
R.string.copy_field,
|
||||||
|
TemplateField.getLocalizedName(context, field.name)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate entry views
|
||||||
|
templateView.setEntryInfo(entryInfo)
|
||||||
|
|
||||||
|
// OTP timer updated
|
||||||
|
templateView.setOnOtpElementUpdated { otpElementUpdated ->
|
||||||
|
mEntryViewModel.onOtpElementUpdated(otpElementUpdated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manage attachments
|
||||||
|
assignAttachments(entryInfo?.attachments ?: listOf())
|
||||||
|
|
||||||
|
// Assign dates
|
||||||
|
assignCreationDate(entryInfo?.creationTime)
|
||||||
|
assignModificationDate(entryInfo?.lastModificationTime)
|
||||||
|
|
||||||
|
// Assign special data
|
||||||
|
assignUUID(entryInfo?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showClipboardDialog() {
|
||||||
|
context?.let {
|
||||||
|
AlertDialog.Builder(it)
|
||||||
|
.setMessage(
|
||||||
|
getString(R.string.allow_copy_password_warning) +
|
||||||
|
"\n\n" +
|
||||||
|
getString(R.string.clipboard_warning)
|
||||||
|
)
|
||||||
|
.create().apply {
|
||||||
|
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
|
||||||
|
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, true)
|
||||||
|
finishDialog(dialog)
|
||||||
|
}
|
||||||
|
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
|
||||||
|
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, false)
|
||||||
|
finishDialog(dialog)
|
||||||
|
}
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun finishDialog(dialog: DialogInterface) {
|
||||||
|
dialog.dismiss()
|
||||||
|
loadTemplateSettings()
|
||||||
|
templateView.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignCreationDate(date: DateInstant?) {
|
||||||
|
creationDateView.text = date?.getDateTimeString(resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignModificationDate(date: DateInstant?) {
|
||||||
|
modificationDateView.text = date?.getDateTimeString(resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignUUID(uuid: UUID?) {
|
||||||
|
uuidView.text = uuid?.toString()
|
||||||
|
uuidReferenceView.text = UuidUtil.toHexString(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Attachments
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
private fun assignAttachments(attachments: List<Attachment>) {
|
||||||
|
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
attachmentsAdapter?.assignItems(attachments.map {
|
||||||
|
EntryAttachmentState(it, StreamDirection.DOWNLOAD)
|
||||||
|
})
|
||||||
|
attachmentsAdapter?.onItemClickListener = { item ->
|
||||||
|
mEntryViewModel.onAttachmentSelected(item.attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putAttachment(attachmentToDownload: EntryAttachmentState) {
|
||||||
|
attachmentsAdapter?.putItem(attachmentToDownload)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Education
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun firstEntryFieldCopyView(): View? {
|
||||||
|
return try {
|
||||||
|
templateView.getActionImageView()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchEntryCopyEducationAction() {
|
||||||
|
val appNameString = getString(R.string.app_name)
|
||||||
|
mClipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||||
|
getString(R.string.copy_field, appNameString))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun getInstance(): EntryFragment {
|
||||||
|
return EntryFragment().apply {
|
||||||
|
arguments = Bundle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
|
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
|
|
||||||
|
class EntryHistoryFragment: StylishFragment() {
|
||||||
|
|
||||||
|
private lateinit var historyContainerView: View
|
||||||
|
private lateinit var historyListView: RecyclerView
|
||||||
|
private var historyAdapter: EntryHistoryAdapter? = null
|
||||||
|
|
||||||
|
private val mEntryViewModel: EntryViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
|
return inflater.cloneInContext(contextThemed)
|
||||||
|
.inflate(R.layout.fragment_entry_history, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
context?.let { context ->
|
||||||
|
historyAdapter = EntryHistoryAdapter(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
historyContainerView = view.findViewById(R.id.entry_history_container)
|
||||||
|
historyListView = view.findViewById(R.id.entry_history_list)
|
||||||
|
historyListView.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
|
||||||
|
adapter = historyAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.entryHistory.observe(viewLifecycleOwner) {
|
||||||
|
assignHistory(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* History
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
private fun assignHistory(history: List<EntryInfo>) {
|
||||||
|
historyAdapter?.clear()
|
||||||
|
historyAdapter?.entryHistoryList?.addAll(history)
|
||||||
|
historyAdapter?.onItemClickListener = { item, position ->
|
||||||
|
mEntryViewModel.onHistorySelected(item, position)
|
||||||
|
}
|
||||||
|
historyContainerView.visibility = if (historyAdapter?.entryHistoryList?.isEmpty() != false)
|
||||||
|
View.GONE
|
||||||
|
else
|
||||||
|
View.VISIBLE
|
||||||
|
historyAdapter?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,34 +25,40 @@ import android.os.Bundle
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.EntryEditActivity
|
import com.kunzisoft.keepass.activities.EntryEditActivity
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
|
||||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
import com.kunzisoft.keepass.adapters.NodeAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
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.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.viewmodels.GroupViewModel
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
|
class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListener {
|
||||||
|
|
||||||
private var nodeClickListener: NodeClickListener? = null
|
private var nodeClickListener: NodeClickListener? = null
|
||||||
private var onScrollListener: OnScrollListener? = null
|
private var onScrollListener: OnScrollListener? = null
|
||||||
|
|
||||||
private var mNodesRecyclerView: RecyclerView? = null
|
private var mNodesRecyclerView: RecyclerView? = null
|
||||||
var mainGroup: Group? = null
|
private var mLayoutManager: LinearLayoutManager? = null
|
||||||
private set
|
|
||||||
private var mAdapter: NodeAdapter? = null
|
private var mAdapter: NodeAdapter? = null
|
||||||
|
|
||||||
|
private val mGroupViewModel: GroupViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private var mCurrentGroup: Group? = null
|
||||||
|
|
||||||
var nodeActionSelectionMode = false
|
var nodeActionSelectionMode = false
|
||||||
private set
|
private set
|
||||||
var nodeActionPasteMode: PasteMode = PasteMode.UNDEFINED
|
var nodeActionPasteMode: PasteMode = PasteMode.UNDEFINED
|
||||||
@@ -63,13 +69,27 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
private var notFoundView: View? = null
|
private var notFoundView: View? = null
|
||||||
private var isASearchResult: Boolean = false
|
private var isASearchResult: Boolean = false
|
||||||
|
|
||||||
|
|
||||||
private var readOnly: Boolean = false
|
|
||||||
private var specialMode: SpecialMode = SpecialMode.DEFAULT
|
private var specialMode: SpecialMode = SpecialMode.DEFAULT
|
||||||
|
|
||||||
|
private var mRecycleBinEnable: Boolean = false
|
||||||
|
private var mRecycleBin: Group? = null
|
||||||
|
|
||||||
val isEmpty: Boolean
|
val isEmpty: Boolean
|
||||||
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
||||||
|
|
||||||
|
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
|
super.onScrollStateChanged(recyclerView, newState)
|
||||||
|
if (newState == SCROLL_STATE_IDLE) {
|
||||||
|
mGroupViewModel.assignPosition(getFirstVisiblePosition())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
onScrollListener?.onScrolled(dy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
try {
|
try {
|
||||||
@@ -100,128 +120,136 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
|
}
|
||||||
|
|
||||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments)
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
mRecycleBinEnable = database?.isRecycleBinEnabled == true
|
||||||
arguments?.let { args ->
|
mRecycleBin = database?.recycleBin
|
||||||
// Contains all the group in element
|
|
||||||
if (args.containsKey(GROUP_KEY)) {
|
|
||||||
mainGroup = args.getParcelable(GROUP_KEY)
|
|
||||||
}
|
|
||||||
if (args.containsKey(IS_SEARCH)) {
|
|
||||||
isASearchResult = args.getBoolean(IS_SEARCH)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contextThemed?.let { context ->
|
contextThemed?.let { context ->
|
||||||
mAdapter = NodeAdapter(context)
|
database?.let { database ->
|
||||||
mAdapter?.apply {
|
mAdapter = NodeAdapter(context, database).apply {
|
||||||
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
|
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
|
||||||
override fun onNodeClick(node: Node) {
|
override fun onNodeClick(database: Database, node: Node) {
|
||||||
if (nodeActionSelectionMode) {
|
if (nodeActionSelectionMode) {
|
||||||
if (listActionNodes.contains(node)) {
|
if (listActionNodes.contains(node)) {
|
||||||
// Remove selected item if already selected
|
// Remove selected item if already selected
|
||||||
listActionNodes.remove(node)
|
listActionNodes.remove(node)
|
||||||
|
} else {
|
||||||
|
// Add selected item if not already selected
|
||||||
|
listActionNodes.add(node)
|
||||||
|
}
|
||||||
|
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
||||||
|
setActionNodes(listActionNodes)
|
||||||
|
notifyNodeChanged(node)
|
||||||
} else {
|
} else {
|
||||||
// Add selected item if not already selected
|
nodeClickListener?.onNodeClick(database, node)
|
||||||
listActionNodes.add(node)
|
|
||||||
}
|
}
|
||||||
nodeClickListener?.onNodeSelected(listActionNodes)
|
|
||||||
setActionNodes(listActionNodes)
|
|
||||||
notifyNodeChanged(node)
|
|
||||||
} else {
|
|
||||||
nodeClickListener?.onNodeClick(node)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNodeLongClick(node: Node): Boolean {
|
override fun onNodeLongClick(database: Database, node: Node): Boolean {
|
||||||
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
||||||
// Select the first item after a long click
|
// Select the first item after a long click
|
||||||
if (!listActionNodes.contains(node))
|
if (!listActionNodes.contains(node))
|
||||||
listActionNodes.add(node)
|
listActionNodes.add(node)
|
||||||
|
|
||||||
nodeClickListener?.onNodeSelected(listActionNodes)
|
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
||||||
|
|
||||||
setActionNodes(listActionNodes)
|
setActionNodes(listActionNodes)
|
||||||
notifyNodeChanged(node)
|
notifyNodeChanged(node)
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return true
|
})
|
||||||
}
|
}
|
||||||
})
|
mNodesRecyclerView?.adapter = mAdapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onDatabaseActionFinished(
|
||||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
database: Database,
|
||||||
super.onSaveInstanceState(outState)
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
|
||||||
|
// Too many special cases to make specific additions or deletions,
|
||||||
|
// rebuilt the list works well.
|
||||||
|
if (result.isSuccess) {
|
||||||
|
rebuildList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
super.onCreateView(inflater, container, savedInstanceState)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
// To apply theme
|
// To apply theme
|
||||||
val rootView = inflater.cloneInContext(contextThemed)
|
return inflater.cloneInContext(contextThemed)
|
||||||
.inflate(R.layout.fragment_list_nodes, container, false)
|
.inflate(R.layout.fragment_group, container, false)
|
||||||
mNodesRecyclerView = rootView.findViewById(R.id.nodes_list)
|
}
|
||||||
notFoundView = rootView.findViewById(R.id.not_found_container)
|
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
mNodesRecyclerView = view.findViewById(R.id.nodes_list)
|
||||||
|
notFoundView = view.findViewById(R.id.not_found_container)
|
||||||
|
|
||||||
|
mLayoutManager = LinearLayoutManager(context)
|
||||||
mNodesRecyclerView?.apply {
|
mNodesRecyclerView?.apply {
|
||||||
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||||
layoutManager = LinearLayoutManager(context)
|
layoutManager = mLayoutManager
|
||||||
adapter = mAdapter
|
adapter = mAdapter
|
||||||
}
|
}
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||||
|
|
||||||
onScrollListener?.let { onScrollListener ->
|
mGroupViewModel.group.observe(viewLifecycleOwner) {
|
||||||
mNodesRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
mCurrentGroup = it.group
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
isASearchResult = it.group.isVirtual
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
rebuildList()
|
||||||
onScrollListener.onScrolled(dy)
|
it.showFromPosition?.let { position ->
|
||||||
}
|
mNodesRecyclerView?.scrollToPosition(position)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return rootView
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
|
||||||
activity?.intent?.let {
|
activity?.intent?.let {
|
||||||
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
|
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh data
|
rebuildList()
|
||||||
try {
|
}
|
||||||
rebuildList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to rebuild the list during resume")
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
|
override fun onPause() {
|
||||||
// To show the " no search entry found "
|
|
||||||
mNodesRecyclerView?.visibility = View.GONE
|
mNodesRecyclerView?.removeOnScrollListener(mRecycleViewScrollListener)
|
||||||
notFoundView?.visibility = View.VISIBLE
|
super.onPause()
|
||||||
} else {
|
}
|
||||||
mNodesRecyclerView?.visibility = View.VISIBLE
|
|
||||||
notFoundView?.visibility = View.GONE
|
fun getFirstVisiblePosition(): Int {
|
||||||
}
|
return mLayoutManager?.findFirstVisibleItemPosition() ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class)
|
@Throws(IllegalArgumentException::class)
|
||||||
fun rebuildList() {
|
private fun rebuildList() {
|
||||||
// Add elements to the list
|
try {
|
||||||
mainGroup?.let { mainGroup ->
|
// Add elements to the list
|
||||||
mAdapter?.apply {
|
mCurrentGroup?.let { mainGroup ->
|
||||||
// Thrown an exception when sort cannot be performed
|
// Thrown an exception when sort cannot be performed
|
||||||
rebuildList(mainGroup)
|
mAdapter?.rebuildList(mainGroup)
|
||||||
// To visually change the elements
|
|
||||||
if (PreferencesUtil.APPEARANCE_CHANGED) {
|
|
||||||
notifyDataSetChanged()
|
|
||||||
PreferencesUtil.APPEARANCE_CHANGED = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (e:Exception) {
|
||||||
|
Log.e(TAG, "Unable to rebuild the list", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isASearchResult && mAdapter != null && mAdapter!!.isEmpty) {
|
||||||
|
// To show the " no search entry found "
|
||||||
|
notFoundView?.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
notFoundView?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,8 +265,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
|
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
|
||||||
rebuildList()
|
rebuildList()
|
||||||
} catch (e:Exception) {
|
} catch (e:Exception) {
|
||||||
Log.e(TAG, "Unable to rebuild the list with the sort")
|
Log.e(TAG, "Unable to sort the list", e)
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +281,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
R.id.menu_sort -> {
|
R.id.menu_sort -> {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
val sortDialogFragment: SortDialogFragment =
|
val sortDialogFragment: SortDialogFragment =
|
||||||
if (Database.getInstance().isRecycleBinEnabled) {
|
if (mRecycleBinEnable) {
|
||||||
SortDialogFragment.getInstance(
|
SortDialogFragment.getInstance(
|
||||||
PreferencesUtil.getListSort(context),
|
PreferencesUtil.getListSort(context),
|
||||||
PreferencesUtil.getAscendingSort(context),
|
PreferencesUtil.getAscendingSort(context),
|
||||||
@@ -276,34 +303,32 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun actionNodesCallback(nodes: List<Node>,
|
fun actionNodesCallback(database: Database,
|
||||||
|
nodes: List<Node>,
|
||||||
menuListener: NodesActionMenuListener?,
|
menuListener: NodesActionMenuListener?,
|
||||||
actionModeCallback: ActionMode.Callback) : ActionMode.Callback {
|
onDestroyActionMode: (mode: ActionMode?) -> Unit) : 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 actionModeCallback.onCreateActionMode(mode, menu)
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||||
menu?.clear()
|
menu?.clear()
|
||||||
|
|
||||||
if (nodeActionPasteMode != PasteMode.UNDEFINED) {
|
if (nodeActionPasteMode != PasteMode.UNDEFINED) {
|
||||||
mode?.menuInflater?.inflate(R.menu.node_paste_menu, menu)
|
mode?.menuInflater?.inflate(R.menu.node_paste_menu, menu)
|
||||||
} else {
|
} else {
|
||||||
nodeActionSelectionMode = true
|
nodeActionSelectionMode = true
|
||||||
mode?.menuInflater?.inflate(R.menu.node_menu, menu)
|
mode?.menuInflater?.inflate(R.menu.node_menu, menu)
|
||||||
|
|
||||||
val database = Database.getInstance()
|
|
||||||
|
|
||||||
// Open and Edit for a single item
|
// Open and Edit for a single item
|
||||||
if (nodes.size == 1) {
|
if (nodes.size == 1) {
|
||||||
// Edition
|
// Edition
|
||||||
if (readOnly
|
if (database.isReadOnly
|
||||||
|| (database.isRecycleBinEnabled && nodes[0] == database.recycleBin)) {
|
|| (mRecycleBinEnable && nodes[0] == mRecycleBin)) {
|
||||||
menu?.removeItem(R.id.menu_edit)
|
menu?.removeItem(R.id.menu_edit)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -311,56 +336,59 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
menu?.removeItem(R.id.menu_edit)
|
menu?.removeItem(R.id.menu_edit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy and Move (not for groups)
|
// Move
|
||||||
if (readOnly
|
if (database.isReadOnly
|
||||||
|| isASearchResult
|
|| isASearchResult) {
|
||||||
|| nodes.any { it.type == Type.GROUP }) {
|
|
||||||
// TODO Copy For Group
|
|
||||||
menu?.removeItem(R.id.menu_copy)
|
|
||||||
menu?.removeItem(R.id.menu_move)
|
menu?.removeItem(R.id.menu_move)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy (not allowed for group)
|
||||||
|
if (database.isReadOnly
|
||||||
|
|| isASearchResult
|
||||||
|
|| nodes.any { it.type == Type.GROUP }) {
|
||||||
|
menu?.removeItem(R.id.menu_copy)
|
||||||
|
}
|
||||||
|
|
||||||
// Deletion
|
// Deletion
|
||||||
if (readOnly
|
if (database.isReadOnly
|
||||||
|| (database.isRecycleBinEnabled && nodes.any { it == database.recycleBin })) {
|
|| (mRecycleBinEnable && nodes.any { it == mRecycleBin })) {
|
||||||
menu?.removeItem(R.id.menu_delete)
|
menu?.removeItem(R.id.menu_delete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
if (menuListener == null)
|
if (menuListener == null)
|
||||||
return false
|
return false
|
||||||
return when (item?.itemId) {
|
return when (item?.itemId) {
|
||||||
R.id.menu_open -> menuListener.onOpenMenuClick(nodes[0])
|
R.id.menu_open -> menuListener.onOpenMenuClick(database, nodes[0])
|
||||||
R.id.menu_edit -> menuListener.onEditMenuClick(nodes[0])
|
R.id.menu_edit -> menuListener.onEditMenuClick(database, nodes[0])
|
||||||
R.id.menu_copy -> {
|
R.id.menu_copy -> {
|
||||||
nodeActionPasteMode = PasteMode.PASTE_FROM_COPY
|
nodeActionPasteMode = PasteMode.PASTE_FROM_COPY
|
||||||
mAdapter?.unselectActionNodes()
|
mAdapter?.unselectActionNodes()
|
||||||
val returnValue = menuListener.onCopyMenuClick(nodes)
|
val returnValue = menuListener.onCopyMenuClick(database, nodes)
|
||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
returnValue
|
returnValue
|
||||||
}
|
}
|
||||||
R.id.menu_move -> {
|
R.id.menu_move -> {
|
||||||
nodeActionPasteMode = PasteMode.PASTE_FROM_MOVE
|
nodeActionPasteMode = PasteMode.PASTE_FROM_MOVE
|
||||||
mAdapter?.unselectActionNodes()
|
mAdapter?.unselectActionNodes()
|
||||||
val returnValue = menuListener.onMoveMenuClick(nodes)
|
val returnValue = menuListener.onMoveMenuClick(database, nodes)
|
||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
returnValue
|
returnValue
|
||||||
}
|
}
|
||||||
R.id.menu_delete -> menuListener.onDeleteMenuClick(nodes)
|
R.id.menu_delete -> menuListener.onDeleteMenuClick(database, nodes)
|
||||||
R.id.menu_paste -> {
|
R.id.menu_paste -> {
|
||||||
val returnValue = menuListener.onPasteMenuClick(nodeActionPasteMode, nodes)
|
val returnValue = menuListener.onPasteMenuClick(database, nodeActionPasteMode, nodes)
|
||||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
returnValue
|
returnValue
|
||||||
}
|
}
|
||||||
else -> actionModeCallback.onActionItemClicked(mode, item)
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,7 +398,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
mAdapter?.unselectActionNodes()
|
mAdapter?.unselectActionNodes()
|
||||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
actionModeCallback.onDestroyActionMode(mode)
|
onDestroyActionMode(mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,73 +408,40 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
|
|
||||||
when (requestCode) {
|
when (requestCode) {
|
||||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
||||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|
if (resultCode == EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE) {
|
||||||
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
data?.getParcelableExtra<NodeId<UUID>>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let {
|
||||||
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { changedNode ->
|
// Simply refresh the list
|
||||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
|
rebuildList()
|
||||||
addNode(changedNode)
|
// Scroll to the new entry
|
||||||
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE)
|
mDatabase?.getEntryById(it)?.let { entry ->
|
||||||
mAdapter?.notifyDataSetChanged()
|
mAdapter?.indexOf(entry)?.let { position ->
|
||||||
|
mNodesRecyclerView?.scrollToPosition(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
|
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun contains(node: Node): Boolean {
|
|
||||||
return mAdapter?.contains(node) ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addNode(newNode: Node) {
|
|
||||||
mAdapter?.addNode(newNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addNodes(newNodes: List<Node>) {
|
|
||||||
mAdapter?.addNodes(newNodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateNode(oldNode: Node, newNode: Node? = null) {
|
|
||||||
mAdapter?.updateNode(oldNode, newNode ?: oldNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
|
|
||||||
mAdapter?.updateNodes(oldNodes, newNodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeNode(pwNode: Node) {
|
|
||||||
mAdapter?.removeNode(pwNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeNodes(nodes: List<Node>) {
|
|
||||||
mAdapter?.removeNodes(nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeNodeAt(position: Int) {
|
|
||||||
mAdapter?.removeNodeAt(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeNodesAt(positions: IntArray) {
|
|
||||||
mAdapter?.removeNodesAt(positions)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback listener to redefine to do an action when a node is click
|
* Callback listener to redefine to do an action when a node is click
|
||||||
*/
|
*/
|
||||||
interface NodeClickListener {
|
interface NodeClickListener {
|
||||||
fun onNodeClick(node: Node)
|
fun onNodeClick(database: Database, node: Node)
|
||||||
fun onNodeSelected(nodes: List<Node>): Boolean
|
fun onNodeSelected(database: Database, nodes: List<Node>): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Menu listener to redefine to do an action in menu
|
* Menu listener to redefine to do an action in menu
|
||||||
*/
|
*/
|
||||||
interface NodesActionMenuListener {
|
interface NodesActionMenuListener {
|
||||||
fun onOpenMenuClick(node: Node): Boolean
|
fun onOpenMenuClick(database: Database, node: Node): Boolean
|
||||||
fun onEditMenuClick(node: Node): Boolean
|
fun onEditMenuClick(database: Database, node: Node): Boolean
|
||||||
fun onCopyMenuClick(nodes: List<Node>): Boolean
|
fun onCopyMenuClick(database: Database, nodes: List<Node>): Boolean
|
||||||
fun onMoveMenuClick(nodes: List<Node>): Boolean
|
fun onMoveMenuClick(database: Database, nodes: List<Node>): Boolean
|
||||||
fun onDeleteMenuClick(nodes: List<Node>): Boolean
|
fun onDeleteMenuClick(database: Database, nodes: List<Node>): Boolean
|
||||||
fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List<Node>): Boolean
|
fun onPasteMenuClick(database: Database, pasteMode: PasteMode?, nodes: List<Node>): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class PasteMode {
|
enum class PasteMode {
|
||||||
@@ -465,22 +460,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val TAG = GroupFragment::class.java.name
|
||||||
private val TAG = ListNodesFragment::class.java.name
|
|
||||||
|
|
||||||
private const val GROUP_KEY = "GROUP_KEY"
|
|
||||||
private const val IS_SEARCH = "IS_SEARCH"
|
|
||||||
|
|
||||||
fun newInstance(group: Group?, readOnly: Boolean, isASearch: Boolean): ListNodesFragment {
|
|
||||||
val bundle = Bundle()
|
|
||||||
if (group != null) {
|
|
||||||
bundle.putParcelable(GROUP_KEY, group)
|
|
||||||
}
|
|
||||||
bundle.putBoolean(IS_SEARCH, isASearch)
|
|
||||||
ReadOnlyHelper.putReadOnlyInBundle(bundle, readOnly)
|
|
||||||
val listNodesFragment = ListNodesFragment()
|
|
||||||
listNodesFragment.arguments = bundle
|
|
||||||
return listNodesFragment
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.activities.fragments
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
|
||||||
|
|
||||||
@@ -31,8 +32,8 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
|
|||||||
return R.layout.fragment_icon_grid
|
return R.layout.fragment_icon_grid
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun defineIconList() {
|
override fun defineIconList(database: Database?) {
|
||||||
mDatabase?.doForEachCustomIcons { customIcon, _ ->
|
database?.doForEachCustomIcons { customIcon, _ ->
|
||||||
iconPickerAdapter.addIcon(customIcon, false)
|
iconPickerAdapter.addIcon(customIcon, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.fragments
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -28,7 +27,6 @@ import android.view.ViewGroup
|
|||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
|
||||||
import com.kunzisoft.keepass.adapters.IconPickerAdapter
|
import com.kunzisoft.keepass.adapters.IconPickerAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
|
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
|
||||||
@@ -38,39 +36,48 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
abstract class IconFragment<T: IconImageDraw> : StylishFragment(),
|
abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
|
||||||
IconPickerAdapter.IconPickerListener<T> {
|
IconPickerAdapter.IconPickerListener<T> {
|
||||||
|
|
||||||
protected lateinit var iconsGridView: RecyclerView
|
protected lateinit var iconsGridView: RecyclerView
|
||||||
protected lateinit var iconPickerAdapter: IconPickerAdapter<T>
|
protected lateinit var iconPickerAdapter: IconPickerAdapter<T>
|
||||||
protected var iconActionSelectionMode = false
|
protected var iconActionSelectionMode = false
|
||||||
|
|
||||||
protected var mDatabase: Database? = null
|
|
||||||
|
|
||||||
protected val iconPickerViewModel: IconPickerViewModel by activityViewModels()
|
protected val iconPickerViewModel: IconPickerViewModel by activityViewModels()
|
||||||
|
|
||||||
abstract fun retrieveMainLayoutId(): Int
|
abstract fun retrieveMainLayoutId(): Int
|
||||||
|
|
||||||
abstract fun defineIconList()
|
abstract fun defineIconList(database: Database?)
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onCreateView(inflater: LayoutInflater,
|
||||||
super.onAttach(context)
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View {
|
||||||
|
return inflater.inflate(retrieveMainLayoutId(), container, false)
|
||||||
|
}
|
||||||
|
|
||||||
mDatabase = Database.getInstance()
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the textColor to tint the icon
|
||||||
val ta = contextThemed?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val ta = contextThemed?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK
|
val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK
|
||||||
ta?.recycle()
|
ta?.recycle()
|
||||||
|
|
||||||
iconPickerAdapter = IconPickerAdapter<T>(context, tintColor).apply {
|
iconsGridView = view.findViewById(R.id.icons_grid_view)
|
||||||
iconDrawableFactory = mDatabase?.iconDrawableFactory
|
iconPickerAdapter = IconPickerAdapter(requireContext(), tintColor)
|
||||||
}
|
iconPickerAdapter.iconPickerListener = this
|
||||||
|
iconsGridView.adapter = iconPickerAdapter
|
||||||
|
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val populateList = launch {
|
val populateList = launch {
|
||||||
iconPickerAdapter.clear()
|
iconPickerAdapter.clear()
|
||||||
defineIconList()
|
defineIconList(database)
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
populateList.join()
|
populateList.join()
|
||||||
@@ -79,21 +86,6 @@ abstract class IconFragment<T: IconImageDraw> : StylishFragment(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?): View {
|
|
||||||
val root = inflater.inflate(retrieveMainLayoutId(), container, false)
|
|
||||||
iconsGridView = root.findViewById(R.id.icons_grid_view)
|
|
||||||
iconsGridView.adapter = iconPickerAdapter
|
|
||||||
return root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
iconPickerAdapter.iconPickerListener = this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onIconDeleteClicked() {
|
fun onIconDeleteClicked() {
|
||||||
iconActionSelectionMode = false
|
iconActionSelectionMode = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,20 +9,18 @@ import androidx.viewpager2.widget.ViewPager2
|
|||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
|
||||||
import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter
|
import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||||
|
|
||||||
class IconPickerFragment : StylishFragment() {
|
class IconPickerFragment : DatabaseFragment() {
|
||||||
|
|
||||||
private var iconPickerPagerAdapter: IconPickerPagerAdapter? = null
|
private var iconPickerPagerAdapter: IconPickerPagerAdapter? = null
|
||||||
private lateinit var viewPager: ViewPager2
|
private lateinit var viewPager: ViewPager2
|
||||||
|
private lateinit var tabLayout: TabLayout
|
||||||
|
|
||||||
private val iconPickerViewModel: IconPickerViewModel by activityViewModels()
|
private val iconPickerViewModel: IconPickerViewModel by activityViewModels()
|
||||||
|
|
||||||
private var mDatabase: Database? = null
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
@@ -32,19 +30,11 @@ class IconPickerFragment : StylishFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
mDatabase = Database.getInstance()
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
viewPager = view.findViewById(R.id.icon_picker_pager)
|
viewPager = view.findViewById(R.id.icon_picker_pager)
|
||||||
val tabLayout = view.findViewById<TabLayout>(R.id.icon_picker_tabs)
|
tabLayout = view.findViewById(R.id.icon_picker_tabs)
|
||||||
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||||
if (mDatabase?.allowCustomIcons == true) 2 else 1)
|
|
||||||
viewPager.adapter = iconPickerPagerAdapter
|
|
||||||
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
|
||||||
tab.text = when (position) {
|
|
||||||
1 -> getString(R.string.icon_section_custom)
|
|
||||||
else -> getString(R.string.icon_section_standard)
|
|
||||||
}
|
|
||||||
}.attach()
|
|
||||||
|
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
if (containsKey(ICON_TAB_ARG)) {
|
if (containsKey(ICON_TAB_ARG)) {
|
||||||
@@ -58,6 +48,18 @@ class IconPickerFragment : StylishFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
||||||
|
if (database?.allowCustomIcons == true) 2 else 1)
|
||||||
|
viewPager.adapter = iconPickerPagerAdapter
|
||||||
|
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
||||||
|
tab.text = when (position) {
|
||||||
|
1 -> getString(R.string.icon_section_custom)
|
||||||
|
else -> getString(R.string.icon_section_standard)
|
||||||
|
}
|
||||||
|
}.attach()
|
||||||
|
}
|
||||||
|
|
||||||
enum class IconTab {
|
enum class IconTab {
|
||||||
STANDARD, CUSTOM
|
STANDARD, CUSTOM
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
package com.kunzisoft.keepass.activities.fragments
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
|
|
||||||
|
|
||||||
@@ -29,8 +30,8 @@ class IconStandardFragment : IconFragment<IconImageStandard>() {
|
|||||||
return R.layout.fragment_icon_grid
|
return R.layout.fragment_icon_grid
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun defineIconList() {
|
override fun defineIconList(database: Database?) {
|
||||||
mDatabase?.doForEachStandardIcons { standardIcon ->
|
database?.doForEachStandardIcons { standardIcon ->
|
||||||
iconPickerAdapter.addIcon(standardIcon, false)
|
iconPickerAdapter.addIcon(standardIcon, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
/*
|
||||||
|
* 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.helpers
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
|
class ExternalFileHelper {
|
||||||
|
|
||||||
|
private var activity: FragmentActivity? = null
|
||||||
|
private var fragment: Fragment? = null
|
||||||
|
|
||||||
|
constructor(context: FragmentActivity) {
|
||||||
|
this.activity = context
|
||||||
|
this.fragment = null
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Fragment) {
|
||||||
|
this.activity = context.activity
|
||||||
|
this.fragment = context
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openDocument(getContent: Boolean = false,
|
||||||
|
typeString: String = "*/*") {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
try {
|
||||||
|
if (getContent) {
|
||||||
|
openActivityWithActionGetContent(typeString)
|
||||||
|
} else {
|
||||||
|
openActivityWithActionOpenDocument(typeString)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to open document", e)
|
||||||
|
showFileManagerDialogFragment()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showFileManagerDialogFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||||
|
private fun openActivityWithActionOpenDocument(typeString: String) {
|
||||||
|
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = typeString
|
||||||
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
if (fragment != null)
|
||||||
|
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||||
|
else
|
||||||
|
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||||
|
private fun openActivityWithActionGetContent(typeString: String) {
|
||||||
|
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = typeString
|
||||||
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
if (fragment != null)
|
||||||
|
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||||
|
else
|
||||||
|
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To use in onActivityResultCallback in Fragment or Activity
|
||||||
|
* @param onFileSelected Callback retrieve from data
|
||||||
|
* @return true if requestCode was captured, false elsewhere
|
||||||
|
*/
|
||||||
|
fun onOpenDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
|
||||||
|
onFileSelected: ((uri: Uri?) -> Unit)?): Boolean {
|
||||||
|
|
||||||
|
when (requestCode) {
|
||||||
|
FILE_BROWSE -> {
|
||||||
|
if (resultCode == RESULT_OK) {
|
||||||
|
val filename = data?.dataString
|
||||||
|
var keyUri: Uri? = null
|
||||||
|
if (filename != null) {
|
||||||
|
keyUri = UriUtil.parse(filename)
|
||||||
|
}
|
||||||
|
onFileSelected?.invoke(keyUri)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
GET_CONTENT, OPEN_DOC -> {
|
||||||
|
if (resultCode == RESULT_OK) {
|
||||||
|
if (data != null) {
|
||||||
|
val uri = data.data
|
||||||
|
if (uri != null) {
|
||||||
|
UriUtil.takeUriPermission(activity?.contentResolver, uri)
|
||||||
|
onFileSelected?.invoke(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show Browser dialog to select file picker app
|
||||||
|
*/
|
||||||
|
private fun showFileManagerDialogFragment() {
|
||||||
|
try {
|
||||||
|
if (fragment != null) {
|
||||||
|
fragment?.parentFragmentManager
|
||||||
|
} else {
|
||||||
|
activity?.supportFragmentManager
|
||||||
|
}?.let { fragmentManager ->
|
||||||
|
FileManagerDialogFragment().show(fragmentManager, "browserDialog")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Can't open BrowserDialog", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createDocument(titleString: String,
|
||||||
|
typeString: String = "application/octet-stream"): Int? {
|
||||||
|
val idCode = getUnusedCreateFileRequestCode()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
try {
|
||||||
|
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = typeString
|
||||||
|
putExtra(Intent.EXTRA_TITLE, titleString)
|
||||||
|
}
|
||||||
|
if (fragment != null)
|
||||||
|
fragment?.startActivityForResult(intent, idCode)
|
||||||
|
else
|
||||||
|
activity?.startActivityForResult(intent, idCode)
|
||||||
|
return idCode
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to create document", e)
|
||||||
|
showFileManagerDialogFragment()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showFileManagerDialogFragment()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To use in onActivityResultCallback in Fragment or Activity
|
||||||
|
* @param onFileCreated Callback retrieve from data
|
||||||
|
* @return true if requestCode was captured, false elsewhere
|
||||||
|
*/
|
||||||
|
fun onCreateDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
|
||||||
|
onFileCreated: (fileCreated: Uri?)->Unit) {
|
||||||
|
// Retrieve the created URI from the file manager
|
||||||
|
if (fileRequestCodes.contains(requestCode) && resultCode == RESULT_OK) {
|
||||||
|
onFileCreated.invoke(data?.data)
|
||||||
|
fileRequestCodes.remove(requestCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val TAG = "OpenFileHelper"
|
||||||
|
|
||||||
|
private const val GET_CONTENT = 25745
|
||||||
|
private const val OPEN_DOC = 25845
|
||||||
|
private const val FILE_BROWSE = 25645
|
||||||
|
|
||||||
|
private var CREATE_FILE_REQUEST_CODE_DEFAULT = 3853
|
||||||
|
private var fileRequestCodes = ArrayList<Int>()
|
||||||
|
|
||||||
|
private fun getUnusedCreateFileRequestCode(): Int {
|
||||||
|
val newCreateFileRequestCode = CREATE_FILE_REQUEST_CODE_DEFAULT++
|
||||||
|
fileRequestCodes.add(newCreateFileRequestCode)
|
||||||
|
return newCreateFileRequestCode
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager,
|
||||||
|
typeString: String = "application/octet-stream"): Boolean {
|
||||||
|
return when {
|
||||||
|
// To check if a custom file manager can manage the ACTION_CREATE_DOCUMENT
|
||||||
|
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT -> {
|
||||||
|
packageManager.queryIntentActivities(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = typeString
|
||||||
|
}, PackageManager.MATCH_DEFAULT_ONLY).isNotEmpty()
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) {
|
||||||
|
externalFileHelper?.let { fileHelper ->
|
||||||
|
setOnClickListener {
|
||||||
|
fileHelper.openDocument()
|
||||||
|
}
|
||||||
|
setOnLongClickListener {
|
||||||
|
fileHelper.openDocument(true)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} ?: kotlin.run {
|
||||||
|
setOnClickListener(null)
|
||||||
|
setOnLongClickListener(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,78 +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.activities.helpers
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
|
|
||||||
object ReadOnlyHelper {
|
|
||||||
|
|
||||||
private const val READ_ONLY_KEY = "READ_ONLY_KEY"
|
|
||||||
|
|
||||||
const val READ_ONLY_DEFAULT = false
|
|
||||||
|
|
||||||
fun retrieveReadOnlyFromIntent(intent: Intent): Boolean {
|
|
||||||
return intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun retrieveReadOnlyFromInstanceStateOrPreference(context: Context, savedInstanceState: Bundle?): Boolean {
|
|
||||||
return if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
|
|
||||||
savedInstanceState.getBoolean(READ_ONLY_KEY)
|
|
||||||
} else {
|
|
||||||
PreferencesUtil.enableReadOnlyDatabase(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState: Bundle?, arguments: Bundle?): Boolean {
|
|
||||||
var readOnly = READ_ONLY_DEFAULT
|
|
||||||
if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
|
|
||||||
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY)
|
|
||||||
} else if (arguments != null && arguments.containsKey(READ_ONLY_KEY)) {
|
|
||||||
readOnly = arguments.getBoolean(READ_ONLY_KEY)
|
|
||||||
}
|
|
||||||
return readOnly
|
|
||||||
}
|
|
||||||
|
|
||||||
fun retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState: Bundle?, intent: Intent?): Boolean {
|
|
||||||
var readOnly = READ_ONLY_DEFAULT
|
|
||||||
if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
|
|
||||||
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY)
|
|
||||||
} else {
|
|
||||||
if (intent != null)
|
|
||||||
readOnly = intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT)
|
|
||||||
}
|
|
||||||
return readOnly
|
|
||||||
}
|
|
||||||
|
|
||||||
fun putReadOnlyInIntent(intent: Intent, readOnly: Boolean) {
|
|
||||||
intent.putExtra(READ_ONLY_KEY, readOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun putReadOnlyInBundle(bundle: Bundle, readOnly: Boolean) {
|
|
||||||
bundle.putBoolean(READ_ONLY_KEY, readOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSaveInstanceState(outState: Bundle, readOnly: Boolean) {
|
|
||||||
outState.putBoolean(READ_ONLY_KEY, readOnly)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,244 +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.activities.helpers
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Activity.RESULT_OK
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
|
||||||
|
|
||||||
class SelectFileHelper {
|
|
||||||
|
|
||||||
private var activity: Activity? = null
|
|
||||||
private var fragment: Fragment? = null
|
|
||||||
|
|
||||||
val selectFileOnClickViewListener: SelectFileOnClickViewListener
|
|
||||||
get() = SelectFileOnClickViewListener()
|
|
||||||
|
|
||||||
constructor(context: Activity) {
|
|
||||||
this.activity = context
|
|
||||||
this.fragment = null
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Fragment) {
|
|
||||||
this.activity = context.activity
|
|
||||||
this.fragment = context
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class SelectFileOnClickViewListener :
|
|
||||||
View.OnClickListener,
|
|
||||||
View.OnLongClickListener,
|
|
||||||
MenuItem.OnMenuItemClickListener {
|
|
||||||
|
|
||||||
private fun onAbstractClick(longClick: Boolean = false) {
|
|
||||||
try {
|
|
||||||
if (longClick) {
|
|
||||||
try {
|
|
||||||
openActivityWithActionGetContent()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
openActivityWithActionOpenDocument()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
openActivityWithActionOpenDocument()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
openActivityWithActionGetContent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Enable to start the file picker activity", e)
|
|
||||||
// Open browser dialog
|
|
||||||
if (lookForOpenIntentsFilePicker())
|
|
||||||
showBrowserDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
|
||||||
onAbstractClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongClick(v: View?): Boolean {
|
|
||||||
onAbstractClick(true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
|
||||||
onAbstractClick()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
private fun openActivityWithActionOpenDocument() {
|
|
||||||
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "*/*"
|
|
||||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
if (fragment != null)
|
|
||||||
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
|
||||||
else
|
|
||||||
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
private fun openActivityWithActionGetContent() {
|
|
||||||
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "*/*"
|
|
||||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
if (fragment != null)
|
|
||||||
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
|
||||||
else
|
|
||||||
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun lookForOpenIntentsFilePicker(): Boolean {
|
|
||||||
var showBrowser = false
|
|
||||||
try {
|
|
||||||
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
|
|
||||||
val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
|
|
||||||
if (fragment != null)
|
|
||||||
fragment?.startActivityForResult(intent, FILE_BROWSE)
|
|
||||||
else
|
|
||||||
activity?.startActivityForResult(intent, FILE_BROWSE)
|
|
||||||
} else {
|
|
||||||
showBrowser = true
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "Enable to start OPEN_INTENTS_FILE_BROWSE", e)
|
|
||||||
showBrowser = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return showBrowser
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether the specified action can be used as an intent. This
|
|
||||||
* method queries the package manager for installed packages that can
|
|
||||||
* respond to an intent with the specified action. If no suitable package is
|
|
||||||
* found, this method returns false.
|
|
||||||
*
|
|
||||||
* @param context The application's environment.
|
|
||||||
* @param action The Intent action to check for availability.
|
|
||||||
*
|
|
||||||
* @return True if an Intent with the specified action can be sent and
|
|
||||||
* responded to, false otherwise.
|
|
||||||
*/
|
|
||||||
private fun isIntentAvailable(context: Context, action: String): Boolean {
|
|
||||||
val packageManager = context.packageManager
|
|
||||||
val intent = Intent(action)
|
|
||||||
val list = packageManager.queryIntentActivities(intent,
|
|
||||||
PackageManager.MATCH_DEFAULT_ONLY)
|
|
||||||
return list.size > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show Browser dialog to select file picker app
|
|
||||||
*/
|
|
||||||
private fun showBrowserDialog() {
|
|
||||||
try {
|
|
||||||
val fileManagerDialogFragment = FileManagerDialogFragment()
|
|
||||||
fragment?.let {
|
|
||||||
fileManagerDialogFragment.show(it.parentFragmentManager, "browserDialog")
|
|
||||||
} ?: fileManagerDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Can't open BrowserDialog", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To use in onActivityResultCallback in Fragment or Activity
|
|
||||||
* @param keyFileCallback Callback retrieve from data
|
|
||||||
* @return true if requestCode was captured, false elsechere
|
|
||||||
*/
|
|
||||||
fun onActivityResultCallback(
|
|
||||||
requestCode: Int,
|
|
||||||
resultCode: Int,
|
|
||||||
data: Intent?,
|
|
||||||
keyFileCallback: ((uri: Uri?) -> Unit)?): Boolean {
|
|
||||||
|
|
||||||
when (requestCode) {
|
|
||||||
FILE_BROWSE -> {
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
val filename = data?.dataString
|
|
||||||
var keyUri: Uri? = null
|
|
||||||
if (filename != null) {
|
|
||||||
keyUri = UriUtil.parse(filename)
|
|
||||||
}
|
|
||||||
keyFileCallback?.invoke(keyUri)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
GET_CONTENT, OPEN_DOC -> {
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
if (data != null) {
|
|
||||||
val uri = data.data
|
|
||||||
if (uri != null) {
|
|
||||||
try {
|
|
||||||
// try to persist read and write permissions
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
|
||||||
activity?.contentResolver?.apply {
|
|
||||||
takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// nop
|
|
||||||
}
|
|
||||||
keyFileCallback?.invoke(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val TAG = "OpenFileHelper"
|
|
||||||
|
|
||||||
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
|
|
||||||
|
|
||||||
private const val GET_CONTENT = 25745
|
|
||||||
private const val OPEN_DOC = 25845
|
|
||||||
private const val FILE_BROWSE = 25645
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.legacy
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
|
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||||
|
|
||||||
|
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
|
||||||
|
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||||
|
protected var mDatabase: Database? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
||||||
|
|
||||||
|
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||||
|
if (mDatabase == null || mDatabase != database) {
|
||||||
|
onDatabaseRetrieved(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mDatabaseTaskProvider?.onActionFinish = { database, actionTask, result ->
|
||||||
|
onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
mDatabase = database
|
||||||
|
mDatabaseViewModel.defineDatabase(database)
|
||||||
|
// optional method implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: Database,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.onActionFinished(database, actionTask, result)
|
||||||
|
// optional method implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createDatabase(databaseUri: Uri,
|
||||||
|
mainCredential: MainCredential) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadDatabase(databaseUri: Uri,
|
||||||
|
mainCredential: MainCredential,
|
||||||
|
readOnly: Boolean,
|
||||||
|
cipherEntity: CipherDatabaseEntity?,
|
||||||
|
fixDuplicateUuid: Boolean) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEntity, fixDuplicateUuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun closeDatabase() {
|
||||||
|
mDatabase?.clearAndClose(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reloadActivity() {
|
||||||
|
super.reloadActivity()
|
||||||
|
mDatabase?.wasReloaded = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
if (mDatabase?.wasReloaded == true) {
|
||||||
|
reloadActivity()
|
||||||
|
}
|
||||||
|
mDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
mDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,479 @@
|
|||||||
|
/*
|
||||||
|
* 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.legacy
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
import com.kunzisoft.keepass.utils.*
|
||||||
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
|
import com.kunzisoft.keepass.viewmodels.NodesViewModel
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||||
|
PasswordEncodingDialogFragment.Listener {
|
||||||
|
|
||||||
|
private val mNodesViewModel: NodesViewModel by viewModels()
|
||||||
|
|
||||||
|
protected var mTimeoutEnable: Boolean = true
|
||||||
|
|
||||||
|
private var mLockReceiver: LockReceiver? = null
|
||||||
|
private var mExitLock: Boolean = false
|
||||||
|
|
||||||
|
protected var mDatabaseReadOnly: Boolean = true
|
||||||
|
private var mAutoSaveEnable: Boolean = true
|
||||||
|
|
||||||
|
protected var mIconDrawableFactory: IconDrawableFactory? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
if (savedInstanceState != null
|
||||||
|
&& savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY)
|
||||||
|
) {
|
||||||
|
mTimeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
|
||||||
|
} else {
|
||||||
|
if (intent != null)
|
||||||
|
mTimeoutEnable =
|
||||||
|
intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
mNodesViewModel.nodesToPermanentlyDelete.observe(this) { nodes ->
|
||||||
|
deleteDatabaseNodes(nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveDatabase.observe(this) { save ->
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSave(save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
||||||
|
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveName.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveDescription.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveDescription(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveDefaultUsername.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveColor.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveColor(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveCompression.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveCompression(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.removeUnlinkData.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseRemoveUnlinkedData(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveRecycleBin.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveRecycleBin(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveTemplatesGroup.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveTemplatesGroup(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveMaxHistoryItems.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveMaxHistoryItems(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveMaxHistorySize.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveEncryption.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveEncryption(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveKeyDerivation.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveKeyDerivation(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveIterations.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveIterations(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveMemoryUsage.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveParallelism.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveParallelism(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mExitLock = false
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun finishActivityIfDatabaseNotLoaded(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
|
// End activity if database not loaded
|
||||||
|
if (finishActivityIfDatabaseNotLoaded() && (database == null || !database.loaded)) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus view to reinitialize timeout,
|
||||||
|
// view is not necessary loaded so retry later in resume
|
||||||
|
viewToInvalidateTimeout()
|
||||||
|
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database?.loaded)
|
||||||
|
|
||||||
|
database?.let {
|
||||||
|
// check timeout
|
||||||
|
if (mTimeoutEnable) {
|
||||||
|
if (mLockReceiver == null) {
|
||||||
|
mLockReceiver = LockReceiver {
|
||||||
|
mDatabase = null
|
||||||
|
closeDatabase(database)
|
||||||
|
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
||||||
|
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
||||||
|
// Add onActivityForResult response
|
||||||
|
setResult(RESULT_EXIT_LOCK)
|
||||||
|
closeOptionsMenu()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
registerLockReceiver(mLockReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
// After the first creation
|
||||||
|
// or If simply swipe with another application
|
||||||
|
// If the time is out -> close the Activity
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||||
|
// If onCreate already record time
|
||||||
|
if (!mExitLock)
|
||||||
|
TimeoutHelper.recordTime(this, database.loaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseReadOnly = database.isReadOnly
|
||||||
|
mIconDrawableFactory = database.iconDrawableFactory
|
||||||
|
|
||||||
|
checkRegister()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun viewToInvalidateTimeout(): View?
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: Database,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
when (actionTask) {
|
||||||
|
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
||||||
|
// Reload the current activity
|
||||||
|
if (result.isSuccess) {
|
||||||
|
reloadActivity()
|
||||||
|
} else {
|
||||||
|
this.showActionErrorIfNeeded(result)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
||||||
|
mainCredential: MainCredential) {
|
||||||
|
assignDatabasePassword(databaseUri, mainCredential)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignDatabasePassword(databaseUri: Uri?,
|
||||||
|
mainCredential: MainCredential) {
|
||||||
|
if (databaseUri != null) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseAssignPassword(databaseUri, mainCredential)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assignPassword(mainCredential: MainCredential) {
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
database.fileUri?.let { databaseUri ->
|
||||||
|
// Show the progress dialog now or after dialog confirmation
|
||||||
|
if (database.validatePasswordEncoding(mainCredential)) {
|
||||||
|
assignDatabasePassword(databaseUri, mainCredential)
|
||||||
|
} else {
|
||||||
|
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
|
||||||
|
.show(supportFragmentManager, "passwordEncodingTag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveDatabase() {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSave(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reloadDatabase() {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseReload(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createEntry(newEntry: Entry,
|
||||||
|
parent: Group) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseCreateEntry(newEntry, parent, mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateEntry(oldEntry: Entry,
|
||||||
|
entryToUpdate: Entry) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseUpdateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyNodes(nodesToCopy: List<Node>,
|
||||||
|
newParent: Group) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseCopyNodes(nodesToCopy, newParent, mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveNodes(nodesToMove: List<Node>,
|
||||||
|
newParent: Group) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun eachNodeRecyclable(database: Database, nodes: List<Node>): Boolean {
|
||||||
|
return nodes.find { node ->
|
||||||
|
var cannotRecycle = true
|
||||||
|
if (node is Entry) {
|
||||||
|
cannotRecycle = !database.canRecycle(node)
|
||||||
|
} else if (node is Group) {
|
||||||
|
cannotRecycle = !database.canRecycle(node)
|
||||||
|
}
|
||||||
|
cannotRecycle
|
||||||
|
} == null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) {
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
// If recycle bin enabled, ensure it exists
|
||||||
|
if (database.isRecycleBinEnabled) {
|
||||||
|
database.ensureRecycleBinExists(resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If recycle bin enabled and not in recycle bin, move in recycle bin
|
||||||
|
if (eachNodeRecyclable(database, nodes)) {
|
||||||
|
deleteDatabaseNodes(nodes)
|
||||||
|
}
|
||||||
|
// else open the dialog to confirm deletion
|
||||||
|
else {
|
||||||
|
DeleteNodesDialogFragment.getInstance(recycleBin)
|
||||||
|
.show(supportFragmentManager, "deleteNodesDialogFragment")
|
||||||
|
mNodesViewModel.deleteNodes(nodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteDatabaseNodes(nodes: List<Node>) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseDeleteNodes(nodes, mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createGroup(parent: Group,
|
||||||
|
groupInfo: GroupInfo?) {
|
||||||
|
// Build the group
|
||||||
|
mDatabase?.createGroup()?.let { newGroup ->
|
||||||
|
groupInfo?.let { info ->
|
||||||
|
newGroup.setGroupInfo(info)
|
||||||
|
}
|
||||||
|
// Not really needed here because added in runnable but safe
|
||||||
|
newGroup.parent = parent
|
||||||
|
mDatabaseTaskProvider?.startDatabaseCreateGroup(newGroup, parent, mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateGroup(oldGroup: Group,
|
||||||
|
groupInfo: GroupInfo) {
|
||||||
|
// If group updated save it in the database
|
||||||
|
val updateGroup = Group(oldGroup).let { updateGroup ->
|
||||||
|
updateGroup.apply {
|
||||||
|
// WARNING remove parent and children to keep memory
|
||||||
|
removeParent()
|
||||||
|
removeChildren()
|
||||||
|
this.setGroupInfo(groupInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mDatabaseTaskProvider?.startDatabaseUpdateGroup(oldGroup, updateGroup, mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreEntryHistory(mainEntryId: NodeId<UUID>,
|
||||||
|
entryHistoryPosition: Int) {
|
||||||
|
mDatabaseTaskProvider
|
||||||
|
?.startDatabaseRestoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteEntryHistory(mainEntryId: NodeId<UUID>,
|
||||||
|
entryHistoryPosition: Int) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
if (resultCode == RESULT_EXIT_LOCK) {
|
||||||
|
mExitLock = true
|
||||||
|
lockAndExit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkRegister() {
|
||||||
|
// If in ave or registration mode, don't allow read only
|
||||||
|
if ((mSpecialMode == SpecialMode.SAVE
|
||||||
|
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||||
|
&& mDatabaseReadOnly) {
|
||||||
|
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
|
||||||
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// To refresh when back to normal workflow from selection workflow
|
||||||
|
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
|
||||||
|
|
||||||
|
// Invalidate timeout by touch
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
viewToInvalidateTimeout()
|
||||||
|
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
|
LOCKING_ACTIVITY_UI_VISIBLE = true
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) {
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
|
||||||
|
mDatabase?.loaded == true,
|
||||||
|
action)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||||
|
|
||||||
|
super.onPause()
|
||||||
|
|
||||||
|
if (mTimeoutEnable) {
|
||||||
|
// If the time is out during our navigation in activity -> close the Activity
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
unregisterLockReceiver(mLockReceiver)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun lockAndExit() {
|
||||||
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetAppTimeout() {
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
|
||||||
|
mDatabase?.loaded ?: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (mTimeoutEnable) {
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
|
||||||
|
mDatabase?.loaded == true) {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val TAG = "LockingActivity"
|
||||||
|
|
||||||
|
const val RESULT_EXIT_LOCK = 1450
|
||||||
|
|
||||||
|
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||||
|
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||||
|
|
||||||
|
private var LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||||
|
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To reset the app timeout when a view is focused or changed
|
||||||
|
*/
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
fun View.resetAppTimeoutWhenViewTouchedOrFocused(context: Context, databaseLoaded: Boolean?) {
|
||||||
|
// Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
|
||||||
|
setOnTouchListener { _, event ->
|
||||||
|
when (event.action) {
|
||||||
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
// Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
|
||||||
|
databaseLoaded ?: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
setOnFocusChangeListener { _, _ ->
|
||||||
|
// Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout")
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
|
||||||
|
databaseLoaded ?: false)
|
||||||
|
}
|
||||||
|
if (this is ViewGroup) {
|
||||||
|
for (i in 0..childCount) {
|
||||||
|
getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.kunzisoft.keepass.activities.selection
|
package com.kunzisoft.keepass.activities.legacy
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -7,15 +7,14 @@ 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.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.helpers.TypeMode
|
import com.kunzisoft.keepass.activities.helpers.TypeMode
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
|
||||||
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.view.SpecialModeView
|
import com.kunzisoft.keepass.view.SpecialModeView
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity to manage special mode (ie: selection mode)
|
* Activity to manage database special mode (ie: selection mode)
|
||||||
*/
|
*/
|
||||||
abstract class SpecialModeActivity : StylishActivity() {
|
abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||||
|
|
||||||
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
||||||
private var mTypeMode: TypeMode = TypeMode.DEFAULT
|
private var mTypeMode: TypeMode = TypeMode.DEFAULT
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.legacy
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
|
||||||
|
interface DatabaseRetrieval {
|
||||||
|
fun onDatabaseRetrieved(database: Database?)
|
||||||
|
fun onDatabaseActionFinished(database: Database,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result)
|
||||||
|
}
|
||||||
@@ -1,215 +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.activities.lock
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
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.ReadOnlyHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
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.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
|
||||||
import com.kunzisoft.keepass.utils.*
|
|
||||||
|
|
||||||
abstract class LockingActivity : SpecialModeActivity() {
|
|
||||||
|
|
||||||
protected var mTimeoutEnable: Boolean = true
|
|
||||||
|
|
||||||
private var mLockReceiver: LockReceiver? = null
|
|
||||||
private var mExitLock: Boolean = false
|
|
||||||
|
|
||||||
// Force readOnly if Entry Selection mode
|
|
||||||
protected var mReadOnly: Boolean
|
|
||||||
get() {
|
|
||||||
return mReadOnlyToSave
|
|
||||||
}
|
|
||||||
set(value) {
|
|
||||||
mReadOnlyToSave = value
|
|
||||||
}
|
|
||||||
private var mReadOnlyToSave: Boolean = false
|
|
||||||
protected var mAutoSaveEnable: Boolean = true
|
|
||||||
|
|
||||||
var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
|
|
||||||
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
if (savedInstanceState != null
|
|
||||||
&& savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY)) {
|
|
||||||
mTimeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
|
|
||||||
} else {
|
|
||||||
if (intent != null)
|
|
||||||
mTimeoutEnable = intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mTimeoutEnable) {
|
|
||||||
mLockReceiver = LockReceiver {
|
|
||||||
closeDatabase()
|
|
||||||
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
|
||||||
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
|
||||||
// Add onActivityForResult response
|
|
||||||
setResult(RESULT_EXIT_LOCK)
|
|
||||||
closeOptionsMenu()
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
registerLockReceiver(mLockReceiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
mExitLock = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
if (resultCode == RESULT_EXIT_LOCK) {
|
|
||||||
mExitLock = true
|
|
||||||
if (Database.getInstance().loaded) {
|
|
||||||
lockAndExit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
// 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
|
|
||||||
mReadOnlyToSave = ReadOnlyHelper.retrieveReadOnlyFromIntent(intent)
|
|
||||||
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
|
|
||||||
|
|
||||||
invalidateOptionsMenu()
|
|
||||||
|
|
||||||
if (mTimeoutEnable) {
|
|
||||||
// End activity if database not loaded
|
|
||||||
if (!Database.getInstance().loaded) {
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// After the first creation
|
|
||||||
// or If simply swipe with another application
|
|
||||||
// If the time is out -> close the Activity
|
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
|
||||||
// If onCreate already record time
|
|
||||||
if (!mExitLock)
|
|
||||||
TimeoutHelper.recordTime(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
LOCKING_ACTIVITY_UI_VISIBLE = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
LOCKING_ACTIVITY_UI_VISIBLE = false
|
|
||||||
|
|
||||||
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
|
||||||
|
|
||||||
super.onPause()
|
|
||||||
|
|
||||||
if (mTimeoutEnable) {
|
|
||||||
// If the time is out during our navigation in activity -> close the Activity
|
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
unregisterLockReceiver(mLockReceiver)
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun lockAndExit() {
|
|
||||||
sendBroadcast(Intent(LOCK_ACTION))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
if (mTimeoutEnable) {
|
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
|
|
||||||
super.onBackPressed()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
super.onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val TAG = "LockingActivity"
|
|
||||||
|
|
||||||
const val RESULT_EXIT_LOCK = 1450
|
|
||||||
|
|
||||||
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
|
||||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
|
||||||
|
|
||||||
private var LOCKING_ACTIVITY_UI_VISIBLE = false
|
|
||||||
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To reset the app timeout when a view is focused or changed
|
|
||||||
*/
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
fun View.resetAppTimeoutWhenViewFocusedOrChanged(context: Context) {
|
|
||||||
setOnTouchListener { _, event ->
|
|
||||||
when (event.action) {
|
|
||||||
MotionEvent.ACTION_DOWN -> {
|
|
||||||
//Log.d(LockingActivity.TAG, "View touched, try to reset app timeout")
|
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
setOnFocusChangeListener { _, _ ->
|
|
||||||
//Log.d(LockingActivity.TAG, "View focused, try to reset app timeout")
|
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
|
|
||||||
}
|
|
||||||
if (this is ViewGroup) {
|
|
||||||
for (i in 0..childCount) {
|
|
||||||
getChildAt(i)?.resetAppTimeoutWhenViewFocusedOrChanged(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,12 +37,12 @@ object Stylish {
|
|||||||
* Initialize the class with a theme preference
|
* Initialize the class with a theme preference
|
||||||
* @param context Context to retrieve the theme preference
|
* @param context Context to retrieve the theme preference
|
||||||
*/
|
*/
|
||||||
fun init(context: Context) {
|
fun load(context: Context) {
|
||||||
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
||||||
themeString = PreferencesUtil.getStyle(context)
|
themeString = PreferencesUtil.getStyle(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
|
fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
|
||||||
val systemNightMode = when (PreferencesUtil.getStyleBrightness(context)) {
|
val systemNightMode = when (PreferencesUtil.getStyleBrightness(context)) {
|
||||||
context.getString(R.string.list_style_brightness_light) -> false
|
context.getString(R.string.list_style_brightness_light) -> false
|
||||||
context.getString(R.string.list_style_brightness_night) -> true
|
context.getString(R.string.list_style_brightness_night) -> true
|
||||||
@@ -84,12 +84,16 @@ object Stylish {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun defaultStyle(context: Context): String {
|
||||||
|
return context.getString(R.string.list_style_name_light)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assign the style to the class attribute
|
* Assign the style to the class attribute
|
||||||
* @param styleString Style id String
|
* @param styleString Style id String
|
||||||
*/
|
*/
|
||||||
fun assignStyle(context: Context, styleString: String) {
|
fun assignStyle(context: Context, styleString: String) {
|
||||||
themeString = retrieveEquivalentSystemStyle(context, styleString)
|
PreferencesUtil.setStyle(context, styleString)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -22,10 +22,13 @@ package com.kunzisoft.keepass.activities.stylish
|
|||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.annotation.StyleRes
|
import android.os.Handler
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import androidx.annotation.StyleRes
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_APPEARANCE_PREFERENCE_CHANGED
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
|
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
|
||||||
@@ -35,6 +38,7 @@ abstract class StylishActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
@StyleRes
|
@StyleRes
|
||||||
private var themeId: Int = 0
|
private var themeId: Int = 0
|
||||||
|
private var customStyle = true
|
||||||
|
|
||||||
/* (non-Javadoc) Workaround for HTC Linkify issues
|
/* (non-Javadoc) Workaround for HTC Linkify issues
|
||||||
* @see android.app.Activity#startActivity(android.content.Intent)
|
* @see android.app.Activity#startActivity(android.content.Intent)
|
||||||
@@ -52,10 +56,30 @@ abstract class StylishActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open fun applyCustomStyle(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun reloadActivity() {
|
||||||
|
if (!finishActivityIfReloadRequested()) {
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
finish()
|
||||||
|
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
this.themeId = Stylish.getThemeId(this)
|
|
||||||
setTheme(themeId)
|
customStyle = applyCustomStyle()
|
||||||
|
if (customStyle) {
|
||||||
|
this.themeId = Stylish.getThemeId(this)
|
||||||
|
setTheme(themeId)
|
||||||
|
}
|
||||||
|
|
||||||
// Several gingerbread devices have problems with FLAG_SECURE
|
// Several gingerbread devices have problems with FLAG_SECURE
|
||||||
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
@@ -63,9 +87,17 @@ abstract class StylishActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
if (Stylish.getThemeId(this) != this.themeId) {
|
|
||||||
|
if ((customStyle && Stylish.getThemeId(this) != this.themeId)
|
||||||
|
|| DATABASE_APPEARANCE_PREFERENCE_CHANGED) {
|
||||||
|
DATABASE_APPEARANCE_PREFERENCE_CHANGED = false
|
||||||
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
||||||
this.recreate()
|
recreateActivity()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun recreateActivity() {
|
||||||
|
// To prevent KitKat bugs
|
||||||
|
Handler(Looper.getMainLooper()).post { recreate() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ import android.view.ViewGroup
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
|
||||||
class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() {
|
class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() {
|
||||||
|
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
var entryHistoryList: MutableList<Entry> = ArrayList()
|
var entryHistoryList: MutableList<EntryInfo> = ArrayList()
|
||||||
var onItemClickListener: ((item: Entry, position: Int)->Unit)? = null
|
var onItemClickListener: ((item: EntryInfo, position: Int)->Unit)? = null
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryHistoryViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryHistoryViewHolder {
|
||||||
return EntryHistoryViewHolder(inflater.inflate(R.layout.item_list_entry_history, parent, false))
|
return EntryHistoryViewHolder(inflater.inflate(R.layout.item_list_entry_history, parent, false))
|
||||||
@@ -44,7 +44,6 @@ class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHist
|
|||||||
holder.lastModifiedView.text = entryHistory.lastModificationTime.getDateTimeString(context.resources)
|
holder.lastModifiedView.text = entryHistory.lastModificationTime.getDateTimeString(context.resources)
|
||||||
holder.titleView.text = entryHistory.title
|
holder.titleView.text = entryHistory.title
|
||||||
holder.usernameView.text = entryHistory.username
|
holder.usernameView.text = entryHistory.username
|
||||||
holder.urlView.text = entryHistory.url
|
|
||||||
|
|
||||||
holder.itemView.setOnClickListener {
|
holder.itemView.setOnClickListener {
|
||||||
onItemClickListener?.invoke(entryHistory, position)
|
onItemClickListener?.invoke(entryHistory, position)
|
||||||
@@ -64,6 +63,5 @@ class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHist
|
|||||||
var lastModifiedView: TextView = itemView.findViewById(R.id.entry_history_last_modified)
|
var lastModifiedView: TextView = itemView.findViewById(R.id.entry_history_last_modified)
|
||||||
var titleView: TextView = itemView.findViewById(R.id.entry_history_title)
|
var titleView: TextView = itemView.findViewById(R.id.entry_history_title)
|
||||||
var usernameView: TextView = itemView.findViewById(R.id.entry_history_username)
|
var usernameView: TextView = itemView.findViewById(R.id.entry_history_username)
|
||||||
var urlView: TextView = itemView.findViewById(R.id.entry_history_url)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ import android.view.ViewGroup
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.model.Field
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
|
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import android.view.ViewGroup
|
|||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.SortedList
|
||||||
|
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.model.DatabaseFile
|
import com.kunzisoft.keepass.model.DatabaseFile
|
||||||
import com.kunzisoft.keepass.view.collapse
|
import com.kunzisoft.keepass.view.collapse
|
||||||
@@ -44,11 +46,43 @@ class FileDatabaseHistoryAdapter(context: Context)
|
|||||||
private var fileSelectClearListener: ((DatabaseFile)->Boolean)? = null
|
private var fileSelectClearListener: ((DatabaseFile)->Boolean)? = null
|
||||||
private var saveAliasListener: ((DatabaseFile)->Unit)? = null
|
private var saveAliasListener: ((DatabaseFile)->Unit)? = null
|
||||||
|
|
||||||
private val listDatabaseFiles = ArrayList<DatabaseFile>()
|
private var mDefaultDatabase: DatabaseFile? = null
|
||||||
|
private var mExpandedDatabaseFile: SuperDatabaseFile? = null
|
||||||
|
private var mPreviousExpandedDatabaseFile: SuperDatabaseFile? = null
|
||||||
|
|
||||||
private var mDefaultDatabaseFile: DatabaseFile? = null
|
private val mListPosition = mutableListOf<SuperDatabaseFile>()
|
||||||
private var mExpandedDatabaseFile: DatabaseFile? = null
|
private val mSortedListDatabaseFiles = SortedList(SuperDatabaseFile::class.java,
|
||||||
private var mPreviousExpandedDatabaseFile: DatabaseFile? = null
|
object: SortedListAdapterCallback<SuperDatabaseFile>(this) {
|
||||||
|
override fun compare(item1: SuperDatabaseFile, item2: SuperDatabaseFile): Int {
|
||||||
|
val indexItem1 = mListPosition.indexOf(item1)
|
||||||
|
val indexItem2 = mListPosition.indexOf(item2)
|
||||||
|
return if (indexItem1 == -1 && indexItem2 == -1)
|
||||||
|
-1
|
||||||
|
else if (indexItem1 < indexItem2)
|
||||||
|
-1
|
||||||
|
else if (indexItem1 > indexItem2)
|
||||||
|
1
|
||||||
|
else
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: SuperDatabaseFile, newItem: SuperDatabaseFile): Boolean {
|
||||||
|
val oldDatabaseFile = oldItem.databaseFile
|
||||||
|
val newDatabaseFile = newItem.databaseFile
|
||||||
|
return oldDatabaseFile.databaseUri == newDatabaseFile.databaseUri
|
||||||
|
&& oldDatabaseFile.databaseDecodedPath == newDatabaseFile.databaseDecodedPath
|
||||||
|
&& oldDatabaseFile.databaseAlias == newDatabaseFile.databaseAlias
|
||||||
|
&& oldDatabaseFile.databaseFileExists == newDatabaseFile.databaseFileExists
|
||||||
|
&& oldDatabaseFile.databaseLastModified == newDatabaseFile.databaseLastModified
|
||||||
|
&& oldDatabaseFile.databaseSize == newDatabaseFile.databaseSize
|
||||||
|
&& oldItem.default == newItem.default
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areItemsTheSame(item1: SuperDatabaseFile, item2: SuperDatabaseFile): Boolean {
|
||||||
|
return item1.databaseFile == item2.databaseFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private val defaultColor: Int
|
private val defaultColor: Int
|
||||||
@@ -71,7 +105,8 @@ class FileDatabaseHistoryAdapter(context: Context)
|
|||||||
|
|
||||||
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
|
||||||
// Get info from position
|
// Get info from position
|
||||||
val databaseFile = listDatabaseFiles[position]
|
val superDatabaseFile = mSortedListDatabaseFiles[position]
|
||||||
|
val databaseFile = superDatabaseFile.databaseFile
|
||||||
|
|
||||||
// Click item to open file
|
// Click item to open file
|
||||||
holder.fileContainer.setOnClickListener {
|
holder.fileContainer.setOnClickListener {
|
||||||
@@ -80,7 +115,7 @@ class FileDatabaseHistoryAdapter(context: Context)
|
|||||||
|
|
||||||
// Default database
|
// Default database
|
||||||
holder.defaultFileButton.apply {
|
holder.defaultFileButton.apply {
|
||||||
this.isChecked = mDefaultDatabaseFile == databaseFile
|
this.isChecked = superDatabaseFile.default
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
defaultDatabaseListener?.invoke(if (isChecked) databaseFile else null)
|
defaultDatabaseListener?.invoke(if (isChecked) databaseFile else null)
|
||||||
}
|
}
|
||||||
@@ -115,7 +150,7 @@ class FileDatabaseHistoryAdapter(context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Click on information
|
// Click on information
|
||||||
val isExpanded = databaseFile == mExpandedDatabaseFile
|
val isExpanded = superDatabaseFile == mExpandedDatabaseFile
|
||||||
// Hides or shows info
|
// Hides or shows info
|
||||||
holder.fileExpandContainer.apply {
|
holder.fileExpandContainer.apply {
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
@@ -151,16 +186,16 @@ class FileDatabaseHistoryAdapter(context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
mPreviousExpandedDatabaseFile = databaseFile
|
mPreviousExpandedDatabaseFile = superDatabaseFile
|
||||||
}
|
}
|
||||||
holder.fileInformationButton.apply {
|
holder.fileInformationButton.apply {
|
||||||
animate().rotation(if (isExpanded) 180F else 0F).start()
|
animate().rotation(if (isExpanded) 180F else 0F).start()
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
mExpandedDatabaseFile = if (isExpanded) null else databaseFile
|
mExpandedDatabaseFile = if (isExpanded) null else superDatabaseFile
|
||||||
// Notify change
|
// Notify change
|
||||||
val previousExpandedPosition = listDatabaseFiles.indexOf(mPreviousExpandedDatabaseFile)
|
val previousExpandedPosition = mListPosition.indexOf(mPreviousExpandedDatabaseFile)
|
||||||
notifyItemChanged(previousExpandedPosition)
|
notifyItemChanged(previousExpandedPosition)
|
||||||
val expandedPosition = listDatabaseFiles.indexOf(mExpandedDatabaseFile)
|
val expandedPosition = mListPosition.indexOf(mExpandedDatabaseFile)
|
||||||
notifyItemChanged(expandedPosition)
|
notifyItemChanged(expandedPosition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,50 +207,67 @@ class FileDatabaseHistoryAdapter(context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return listDatabaseFiles.size
|
return mSortedListDatabaseFiles.size()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearDatabaseFileHistoryList() {
|
fun clearDatabaseFileHistoryList() {
|
||||||
listDatabaseFiles.clear()
|
mListPosition.clear()
|
||||||
|
mSortedListDatabaseFiles.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addDatabaseFileHistory(fileDatabaseHistoryToAdd: DatabaseFile) {
|
fun addDatabaseFileHistory(fileDatabaseHistoryToAdd: DatabaseFile) {
|
||||||
listDatabaseFiles.add(0, fileDatabaseHistoryToAdd)
|
val superToAdd = SuperDatabaseFile(fileDatabaseHistoryToAdd)
|
||||||
notifyItemInserted(0)
|
mListPosition.add(0, superToAdd)
|
||||||
|
mSortedListDatabaseFiles.add(superToAdd)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateDatabaseFileHistory(fileDatabaseHistoryToUpdate: DatabaseFile) {
|
fun updateDatabaseFileHistory(fileDatabaseHistoryToUpdate: DatabaseFile) {
|
||||||
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToUpdate)
|
val superToUpdate = SuperDatabaseFile(fileDatabaseHistoryToUpdate)
|
||||||
if (listDatabaseFiles.remove(fileDatabaseHistoryToUpdate)) {
|
val index = mListPosition.indexOf(superToUpdate)
|
||||||
listDatabaseFiles.add(index, fileDatabaseHistoryToUpdate)
|
if (mListPosition.remove(superToUpdate)) {
|
||||||
notifyItemChanged(index)
|
mListPosition.add(index, superToUpdate)
|
||||||
}
|
}
|
||||||
|
mSortedListDatabaseFiles.updateItemAt(index, superToUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: DatabaseFile) {
|
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: DatabaseFile) {
|
||||||
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToDelete)
|
val superToDelete = SuperDatabaseFile(fileDatabaseHistoryToDelete)
|
||||||
if (listDatabaseFiles.remove(fileDatabaseHistoryToDelete)) {
|
val index = mListPosition.indexOf(superToDelete)
|
||||||
notifyItemRemoved(index)
|
mListPosition.remove(superToDelete)
|
||||||
}
|
mSortedListDatabaseFiles.removeItemAt(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun replaceAllDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<DatabaseFile>) {
|
fun replaceAllDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<DatabaseFile>) {
|
||||||
if (listDatabaseFiles.isEmpty()) {
|
val superMapToReplace = listFileDatabaseHistoryToAdd.map {
|
||||||
listFileDatabaseHistoryToAdd.forEach {
|
SuperDatabaseFile(it)
|
||||||
listDatabaseFiles.add(it)
|
|
||||||
notifyItemInserted(listDatabaseFiles.size)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
listDatabaseFiles.clear()
|
|
||||||
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
}
|
||||||
|
mListPosition.clear()
|
||||||
|
mListPosition.addAll(superMapToReplace)
|
||||||
|
mSortedListDatabaseFiles.replaceAll(superMapToReplace)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setDefaultDatabase(databaseUri: Uri?) {
|
fun setDefaultDatabase(databaseUri: Uri?) {
|
||||||
val defaultDatabaseFile = listDatabaseFiles.firstOrNull { it.databaseUri == databaseUri }
|
// Remove default from last item
|
||||||
mDefaultDatabaseFile = defaultDatabaseFile
|
val oldDefaultDatabasePosition = mListPosition.indexOfFirst {
|
||||||
notifyDataSetChanged()
|
it.default
|
||||||
|
}
|
||||||
|
if (oldDefaultDatabasePosition >= 0) {
|
||||||
|
val oldDefaultDatabase = mListPosition[oldDefaultDatabasePosition].apply {
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
mSortedListDatabaseFiles.updateItemAt(oldDefaultDatabasePosition, oldDefaultDatabase)
|
||||||
|
}
|
||||||
|
// Add default to new item
|
||||||
|
val newDefaultDatabaseFilePosition = mListPosition.indexOfFirst {
|
||||||
|
it.databaseFile.databaseUri == databaseUri
|
||||||
|
}
|
||||||
|
if (newDefaultDatabaseFilePosition >= 0) {
|
||||||
|
val newDefaultDatabase = mListPosition[newDefaultDatabaseFilePosition].apply {
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
mDefaultDatabase = newDefaultDatabase.databaseFile
|
||||||
|
mSortedListDatabaseFiles.updateItemAt(newDefaultDatabaseFilePosition, newDefaultDatabase)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnDefaultDatabaseListener(listener: ((DatabaseFile?) -> Unit)?) {
|
fun setOnDefaultDatabaseListener(listener: ((DatabaseFile?) -> Unit)?) {
|
||||||
@@ -234,6 +286,30 @@ class FileDatabaseHistoryAdapter(context: Context)
|
|||||||
this.saveAliasListener = listener
|
this.saveAliasListener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class SuperDatabaseFile(
|
||||||
|
var databaseFile: DatabaseFile,
|
||||||
|
var default: Boolean = false
|
||||||
|
) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (mDefaultDatabase == databaseFile)
|
||||||
|
this.default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is SuperDatabaseFile) return false
|
||||||
|
|
||||||
|
if (databaseFile != other.databaseFile) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return databaseFile.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
|
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
|
||||||
@@ -95,6 +96,12 @@ class IconPickerAdapter<I: IconImageDraw>(val context: Context, private val tint
|
|||||||
override fun onBindViewHolder(holder: CustomIconViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: CustomIconViewHolder, position: Int) {
|
||||||
val icon = iconList[position]
|
val icon = iconList[position]
|
||||||
iconDrawableFactory?.assignDatabaseIcon(holder.iconImageView, icon, tintIcon)
|
iconDrawableFactory?.assignDatabaseIcon(holder.iconImageView, icon, tintIcon)
|
||||||
|
icon.getIconImageToDraw().custom.name.let { iconName ->
|
||||||
|
holder.iconTextView.apply {
|
||||||
|
text = iconName
|
||||||
|
visibility = if (iconName.isNotEmpty()) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
holder.iconContainerView.isSelected = icon.selected
|
holder.iconContainerView.isSelected = icon.selected
|
||||||
holder.itemView.setOnClickListener {
|
holder.itemView.setOnClickListener {
|
||||||
iconPickerListener?.onIconClickListener(icon)
|
iconPickerListener?.onIconClickListener(icon)
|
||||||
@@ -117,5 +124,6 @@ class IconPickerAdapter<I: IconImageDraw>(val context: Context, private val tint
|
|||||||
inner class CustomIconViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
inner class CustomIconViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
var iconContainerView: ViewGroup = itemView.findViewById(R.id.icon_container)
|
var iconContainerView: ViewGroup = itemView.findViewById(R.id.icon_container)
|
||||||
var iconImageView: ImageView = itemView.findViewById(R.id.icon_image)
|
var iconImageView: ImageView = itemView.findViewById(R.id.icon_image)
|
||||||
|
var iconTextView: TextView = itemView.findViewById(R.id.icon_name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@@ -40,6 +41,8 @@ import com.kunzisoft.keepass.database.element.SortNodeEnum
|
|||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.setTextSize
|
import com.kunzisoft.keepass.view.setTextSize
|
||||||
import com.kunzisoft.keepass.view.strikeOut
|
import com.kunzisoft.keepass.view.strikeOut
|
||||||
@@ -49,7 +52,8 @@ import java.util.*
|
|||||||
* Create node list adapter with contextMenu or not
|
* Create node list adapter with contextMenu or not
|
||||||
* @param context Context to use
|
* @param context Context to use
|
||||||
*/
|
*/
|
||||||
class NodeAdapter (private val context: Context)
|
class NodeAdapter (private val context: Context,
|
||||||
|
private val database: Database)
|
||||||
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
||||||
|
|
||||||
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
||||||
@@ -67,13 +71,13 @@ class NodeAdapter (private val context: Context)
|
|||||||
|
|
||||||
private var mShowUserNames: Boolean = true
|
private var mShowUserNames: Boolean = true
|
||||||
private var mShowNumberEntries: Boolean = true
|
private var mShowNumberEntries: Boolean = true
|
||||||
|
private var mShowOTP: Boolean = false
|
||||||
|
private var mShowUUID: Boolean = false
|
||||||
private var mEntryFilters = arrayOf<Group.ChildFilter>()
|
private var mEntryFilters = arrayOf<Group.ChildFilter>()
|
||||||
|
|
||||||
private var mActionNodesList = LinkedList<Node>()
|
private var mActionNodesList = LinkedList<Node>()
|
||||||
private var mNodeClickCallback: NodeClickCallback? = null
|
private var mNodeClickCallback: NodeClickCallback? = null
|
||||||
|
|
||||||
private val mDatabase: Database
|
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private val mContentSelectionColor: Int
|
private val mContentSelectionColor: Int
|
||||||
@ColorInt
|
@ColorInt
|
||||||
@@ -96,9 +100,6 @@ class NodeAdapter (private val context: Context)
|
|||||||
this.mNodeSortedListCallback = NodeSortedListCallback()
|
this.mNodeSortedListCallback = NodeSortedListCallback()
|
||||||
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
|
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
|
||||||
|
|
||||||
// Database
|
|
||||||
this.mDatabase = Database.getInstance()
|
|
||||||
|
|
||||||
// Color of content selection
|
// Color of content selection
|
||||||
this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
|
this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
|
||||||
// Retrieve the color to tint the icon
|
// Retrieve the color to tint the icon
|
||||||
@@ -111,7 +112,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
taTextColor.recycle()
|
taTextColor.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assignPreferences() {
|
private fun assignPreferences() {
|
||||||
this.mPrefSizeMultiplier = PreferencesUtil.getListTextSize(context)
|
this.mPrefSizeMultiplier = PreferencesUtil.getListTextSize(context)
|
||||||
|
|
||||||
notifyChangeSort(
|
notifyChangeSort(
|
||||||
@@ -125,6 +126,8 @@ class NodeAdapter (private val context: Context)
|
|||||||
|
|
||||||
this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
||||||
this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
|
this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||||
|
this.mShowOTP = PreferencesUtil.showOTPToken(context)
|
||||||
|
this.mShowUUID = PreferencesUtil.showUUID(context)
|
||||||
|
|
||||||
this.mEntryFilters = Group.ChildFilter.getDefaults(context)
|
this.mEntryFilters = Group.ChildFilter.getDefaults(context)
|
||||||
|
|
||||||
@@ -146,9 +149,21 @@ class NodeAdapter (private val context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
||||||
return oldItem.type == newItem.type
|
var typeContentTheSame = true
|
||||||
|
if (oldItem is Entry && newItem is Entry) {
|
||||||
|
typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle()
|
||||||
|
&& oldItem.username == newItem.username
|
||||||
|
&& oldItem.getOtpElement() == newItem.getOtpElement()
|
||||||
|
&& oldItem.containsAttachment() == newItem.containsAttachment()
|
||||||
|
} else if (oldItem is Group && newItem is Group) {
|
||||||
|
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
|
||||||
|
}
|
||||||
|
return typeContentTheSame
|
||||||
|
&& oldItem.nodeId == newItem.nodeId
|
||||||
|
&& oldItem.type == newItem.type
|
||||||
&& oldItem.title == newItem.title
|
&& oldItem.title == newItem.title
|
||||||
&& oldItem.icon == newItem.icon
|
&& oldItem.icon == newItem.icon
|
||||||
|
&& oldItem.isCurrentlyExpires == newItem.isCurrentlyExpires
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
|
override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
|
||||||
@@ -241,6 +256,10 @@ class NodeAdapter (private val context: Context)
|
|||||||
mNodeSortedList.endBatchedUpdates()
|
mNodeSortedList.endBatchedUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun indexOf(node: Node): Int {
|
||||||
|
return mNodeSortedList.indexOf(node)
|
||||||
|
}
|
||||||
|
|
||||||
fun notifyNodeChanged(node: Node) {
|
fun notifyNodeChanged(node: Node) {
|
||||||
notifyItemChanged(mNodeSortedList.indexOf(node))
|
notifyItemChanged(mNodeSortedList.indexOf(node))
|
||||||
}
|
}
|
||||||
@@ -266,7 +285,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
*/
|
*/
|
||||||
fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
|
fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
|
||||||
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||||
this.mNodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
|
this.mNodeComparator = sortNodeEnum.getNodeComparator(database, sortNodeParameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
@@ -303,7 +322,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
}
|
}
|
||||||
holder.imageIdentifier?.setColorFilter(iconColor)
|
holder.imageIdentifier?.setColorFilter(iconColor)
|
||||||
holder.icon.apply {
|
holder.icon.apply {
|
||||||
mDatabase.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
|
database.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
|
||||||
// Relative size of the icon
|
// Relative size of the icon
|
||||||
layoutParams?.apply {
|
layoutParams?.apply {
|
||||||
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
||||||
@@ -323,11 +342,16 @@ class NodeAdapter (private val context: Context)
|
|||||||
strikeOut(subNode.isCurrentlyExpires)
|
strikeOut(subNode.isCurrentlyExpires)
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
// Add meta text to show UUID
|
||||||
|
holder.meta.apply {
|
||||||
|
text = subNode.nodeId.toString()
|
||||||
|
visibility = if (mShowUUID) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
// Specific elements for entry
|
// Specific elements for entry
|
||||||
if (subNode.type == Type.ENTRY) {
|
if (subNode.type == Type.ENTRY) {
|
||||||
val entry = subNode as Entry
|
val entry = subNode as Entry
|
||||||
mDatabase.startManageEntry(entry)
|
database.startManageEntry(entry)
|
||||||
|
|
||||||
holder.text.text = entry.getVisualTitle()
|
holder.text.text = entry.getVisualTitle()
|
||||||
holder.subText.apply {
|
holder.subText.apply {
|
||||||
@@ -339,10 +363,29 @@ class NodeAdapter (private val context: Context)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val otpElement = entry.getOtpElement()
|
||||||
|
holder.otpContainer?.removeCallbacks(holder.otpRunnable)
|
||||||
|
if (otpElement != null
|
||||||
|
&& mShowOTP
|
||||||
|
&& otpElement.token.isNotEmpty()) {
|
||||||
|
|
||||||
|
// Execute runnable to show progress
|
||||||
|
holder.otpRunnable.action = {
|
||||||
|
populateOtpView(holder, otpElement)
|
||||||
|
}
|
||||||
|
if (otpElement.type == OtpType.TOTP) {
|
||||||
|
holder.otpRunnable.postDelayed()
|
||||||
|
}
|
||||||
|
populateOtpView(holder, otpElement)
|
||||||
|
|
||||||
|
holder.otpContainer?.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
holder.otpContainer?.visibility = View.GONE
|
||||||
|
}
|
||||||
holder.attachmentIcon?.visibility =
|
holder.attachmentIcon?.visibility =
|
||||||
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
mDatabase.stopManageEntry(entry)
|
database.stopManageEntry(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add number of entries in groups
|
// Add number of entries in groups
|
||||||
@@ -350,7 +393,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
if (mShowNumberEntries) {
|
if (mShowNumberEntries) {
|
||||||
holder.numberChildren?.apply {
|
holder.numberChildren?.apply {
|
||||||
text = (subNode as Group)
|
text = (subNode as Group)
|
||||||
.getNumberOfChildEntries(mEntryFilters)
|
.numberOfChildEntries
|
||||||
.toString()
|
.toString()
|
||||||
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
|
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
@@ -362,13 +405,45 @@ class NodeAdapter (private val context: Context)
|
|||||||
|
|
||||||
// Assign click
|
// Assign click
|
||||||
holder.container.setOnClickListener {
|
holder.container.setOnClickListener {
|
||||||
mNodeClickCallback?.onNodeClick(subNode)
|
mNodeClickCallback?.onNodeClick(database, subNode)
|
||||||
}
|
}
|
||||||
holder.container.setOnLongClickListener {
|
holder.container.setOnLongClickListener {
|
||||||
mNodeClickCallback?.onNodeLongClick(subNode) ?: false
|
mNodeClickCallback?.onNodeLongClick(database, subNode) ?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun populateOtpView(holder: NodeViewHolder?, otpElement: OtpElement?) {
|
||||||
|
when (otpElement?.type) {
|
||||||
|
OtpType.HOTP -> {
|
||||||
|
holder?.otpProgress?.apply {
|
||||||
|
max = 100
|
||||||
|
progress = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OtpType.TOTP -> {
|
||||||
|
holder?.otpProgress?.apply {
|
||||||
|
max = otpElement.period
|
||||||
|
progress = otpElement.secondsRemaining
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder?.otpToken?.text = otpElement?.token
|
||||||
|
}
|
||||||
|
|
||||||
|
class OtpRunnable(val view: View?): Runnable {
|
||||||
|
|
||||||
|
var action: (() -> Unit)? = null
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
action?.invoke()
|
||||||
|
postDelayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun postDelayed() {
|
||||||
|
view?.postDelayed(this, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return mNodeSortedList.size()
|
return mNodeSortedList.size()
|
||||||
}
|
}
|
||||||
@@ -384,8 +459,8 @@ class NodeAdapter (private val context: Context)
|
|||||||
* Callback listener to redefine to do an action when a node is click
|
* Callback listener to redefine to do an action when a node is click
|
||||||
*/
|
*/
|
||||||
interface NodeClickCallback {
|
interface NodeClickCallback {
|
||||||
fun onNodeClick(node: Node)
|
fun onNodeClick(database: Database, node: Node)
|
||||||
fun onNodeLongClick(node: Node): Boolean
|
fun onNodeLongClick(database: Database, node: Node): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
@@ -394,6 +469,11 @@ class NodeAdapter (private val context: Context)
|
|||||||
var icon: ImageView = itemView.findViewById(R.id.node_icon)
|
var icon: ImageView = itemView.findViewById(R.id.node_icon)
|
||||||
var text: TextView = itemView.findViewById(R.id.node_text)
|
var text: TextView = itemView.findViewById(R.id.node_text)
|
||||||
var subText: TextView = itemView.findViewById(R.id.node_subtext)
|
var subText: TextView = itemView.findViewById(R.id.node_subtext)
|
||||||
|
var meta: TextView = itemView.findViewById(R.id.node_meta)
|
||||||
|
var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container)
|
||||||
|
var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress)
|
||||||
|
var otpToken: TextView? = itemView.findViewById(R.id.node_otp_token)
|
||||||
|
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
|
||||||
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)
|
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,6 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
|
|
||||||
private fun getEntryFrom(cursor: Cursor): Entry? {
|
private fun getEntryFrom(cursor: Cursor): Entry? {
|
||||||
return database.createEntry()?.apply {
|
return database.createEntry()?.apply {
|
||||||
database.startManageEntry(this)
|
|
||||||
entryKDB?.let { entryKDB ->
|
entryKDB?.let { entryKDB ->
|
||||||
(cursor as EntryCursorKDB).populateEntry(entryKDB,
|
(cursor as EntryCursorKDB).populateEntry(entryKDB,
|
||||||
{ standardIconId ->
|
{ standardIconId ->
|
||||||
@@ -127,7 +126,6 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
database.stopManageEntry(this)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,12 +148,14 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
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))) {
|
||||||
|
database.startManageEntry(entry)
|
||||||
entry.entryKDB?.let {
|
entry.entryKDB?.let {
|
||||||
cursorKDB?.addEntry(it)
|
cursorKDB?.addEntry(it)
|
||||||
}
|
}
|
||||||
entry.entryKDBX?.let {
|
entry.entryKDBX?.let {
|
||||||
cursorKDBX?.addEntry(it)
|
cursorKDBX?.addEntry(it)
|
||||||
}
|
}
|
||||||
|
database.stopManageEntry(entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.BaseAdapter
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
|
|
||||||
|
|
||||||
|
class TemplatesSelectorAdapter(private val context: Context,
|
||||||
|
private val iconDrawableFactory: IconDrawableFactory?,
|
||||||
|
private var templates: List<Template>): BaseAdapter() {
|
||||||
|
|
||||||
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
private var mIconColor = Color.BLACK
|
||||||
|
|
||||||
|
init {
|
||||||
|
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
|
mIconColor = taIconColor.getColor(0, Color.BLACK)
|
||||||
|
taIconColor.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
|
val template: Template = getItem(position)
|
||||||
|
|
||||||
|
val holder: TemplateSelectorViewHolder
|
||||||
|
var templateView = convertView
|
||||||
|
if (templateView == null) {
|
||||||
|
holder = TemplateSelectorViewHolder()
|
||||||
|
templateView = inflater.inflate(R.layout.item_template, parent, false)
|
||||||
|
holder.icon = templateView?.findViewById(R.id.template_image)
|
||||||
|
holder.name = templateView?.findViewById(R.id.template_name)
|
||||||
|
templateView?.tag = holder
|
||||||
|
} else {
|
||||||
|
holder = templateView.tag as TemplateSelectorViewHolder
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.icon?.let { icon ->
|
||||||
|
iconDrawableFactory?.assignDatabaseIcon(icon, template.icon, mIconColor)
|
||||||
|
}
|
||||||
|
holder.name?.text = TemplateField.getLocalizedName(context, template.title)
|
||||||
|
|
||||||
|
return templateView!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCount(): Int {
|
||||||
|
return templates.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItem(position: Int): Template {
|
||||||
|
return templates[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemId(position: Int): Long {
|
||||||
|
return position.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class TemplateSelectorViewHolder {
|
||||||
|
var icon: ImageView? = null
|
||||||
|
var name: TextView? = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,20 +21,13 @@ 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.utils.UriUtil
|
|
||||||
|
|
||||||
class App : MultiDexApplication() {
|
class App : MultiDexApplication() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
Stylish.init(this)
|
Stylish.load(this)
|
||||||
PRNGFixes.apply()
|
PRNGFixes.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTerminate() {
|
|
||||||
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
|
|
||||||
super.onTerminate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.app.database
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.*
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.ServiceConnection
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||||
@@ -41,62 +39,95 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
// Temp DAO to easily remove content if object no longer in memory
|
// Temp DAO to easily remove content if object no longer in memory
|
||||||
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
||||||
|
|
||||||
private val mIntentAdvancedUnlockService = Intent(applicationContext,
|
|
||||||
AdvancedUnlockNotificationService::class.java)
|
|
||||||
private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null
|
private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null
|
||||||
private var mServiceConnection: ServiceConnection? = null
|
private var mServiceConnection: ServiceConnection? = null
|
||||||
|
|
||||||
private var mDatabaseListeners = LinkedList<DatabaseListener>()
|
private var mDatabaseListeners = LinkedList<CipherDatabaseListener>()
|
||||||
|
private var mAdvancedUnlockBroadcastReceiver = AdvancedUnlockNotificationService.AdvancedUnlockReceiver {
|
||||||
|
deleteAll()
|
||||||
|
removeAllDataAndDetach()
|
||||||
|
}
|
||||||
|
|
||||||
fun reloadPreferences() {
|
fun reloadPreferences() {
|
||||||
useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun attachService(performedAction: () -> Unit) {
|
private fun serviceActionTask(startService: Boolean = false, performedAction: () -> Unit) {
|
||||||
// Check if a service is currently running else do nothing
|
// Check if a service is currently running else call action without info
|
||||||
if (mBinder != null) {
|
if (startService && mServiceConnection == null) {
|
||||||
|
attachService(performedAction)
|
||||||
|
} else {
|
||||||
performedAction.invoke()
|
performedAction.invoke()
|
||||||
} else if (mServiceConnection == null) {
|
|
||||||
mServiceConnection = object : ServiceConnection {
|
|
||||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
|
||||||
mBinder = (serviceBinder as AdvancedUnlockNotificationService.AdvancedUnlockBinder)
|
|
||||||
performedAction.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
|
||||||
mBinder = null
|
|
||||||
mServiceConnection = null
|
|
||||||
mDatabaseListeners.forEach {
|
|
||||||
it.onDatabaseCleared()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
applicationContext.bindService(mIntentAdvancedUnlockService,
|
|
||||||
mServiceConnection!!,
|
|
||||||
Context.BIND_ABOVE_CLIENT)
|
|
||||||
if (mBinder == null) {
|
|
||||||
applicationContext.startService(mIntentAdvancedUnlockService)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun registerDatabaseListener(listener: DatabaseListener) {
|
@Synchronized
|
||||||
mDatabaseListeners.add(listener)
|
private fun attachService(performedAction: () -> Unit) {
|
||||||
|
applicationContext.registerReceiver(mAdvancedUnlockBroadcastReceiver, IntentFilter().apply {
|
||||||
|
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION)
|
||||||
|
})
|
||||||
|
|
||||||
|
mServiceConnection = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||||
|
mBinder = (serviceBinder as AdvancedUnlockNotificationService.AdvancedUnlockBinder)
|
||||||
|
performedAction.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
onClear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
AdvancedUnlockNotificationService.bindService(applicationContext,
|
||||||
|
mServiceConnection!!,
|
||||||
|
Context.BIND_AUTO_CREATE)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to start cipher action", e)
|
||||||
|
performedAction.invoke()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregisterDatabaseListener(listener: DatabaseListener) {
|
@Synchronized
|
||||||
mDatabaseListeners.remove(listener)
|
private fun detachService() {
|
||||||
|
try {
|
||||||
|
applicationContext.unregisterReceiver(mAdvancedUnlockBroadcastReceiver)
|
||||||
|
} catch (e: Exception) {}
|
||||||
|
|
||||||
|
mServiceConnection?.let {
|
||||||
|
AdvancedUnlockNotificationService.unbindService(applicationContext, it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DatabaseListener {
|
private fun removeAllDataAndDetach() {
|
||||||
fun onDatabaseCleared()
|
detachService()
|
||||||
|
onClear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerDatabaseListener(listenerCipher: CipherDatabaseListener) {
|
||||||
|
mDatabaseListeners.add(listenerCipher)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregisterDatabaseListener(listenerCipher: CipherDatabaseListener) {
|
||||||
|
mDatabaseListeners.remove(listenerCipher)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onClear() {
|
||||||
|
mBinder = null
|
||||||
|
mServiceConnection = null
|
||||||
|
mDatabaseListeners.forEach {
|
||||||
|
it.onCipherDatabaseCleared()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CipherDatabaseListener {
|
||||||
|
fun onCipherDatabaseCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCipherDatabase(databaseUri: Uri,
|
fun getCipherDatabase(databaseUri: Uri,
|
||||||
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
|
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
|
||||||
if (useTempDao) {
|
if (useTempDao) {
|
||||||
attachService {
|
serviceActionTask {
|
||||||
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri))
|
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -121,7 +152,8 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
||||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||||
if (useTempDao) {
|
if (useTempDao) {
|
||||||
attachService {
|
// The only case to create service (not needed to get an info)
|
||||||
|
serviceActionTask(true) {
|
||||||
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
|
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
|
||||||
cipherDatabaseResultListener?.invoke()
|
cipherDatabaseResultListener?.invoke()
|
||||||
}
|
}
|
||||||
@@ -146,7 +178,7 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
fun deleteByDatabaseUri(databaseUri: Uri,
|
fun deleteByDatabaseUri(databaseUri: Uri,
|
||||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||||
if (useTempDao) {
|
if (useTempDao) {
|
||||||
attachService {
|
serviceActionTask {
|
||||||
mBinder?.deleteByDatabaseUri(databaseUri)
|
mBinder?.deleteByDatabaseUri(databaseUri)
|
||||||
cipherDatabaseResultListener?.invoke()
|
cipherDatabaseResultListener?.invoke()
|
||||||
}
|
}
|
||||||
@@ -163,15 +195,22 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAll() {
|
fun deleteAll() {
|
||||||
attachService {
|
if (useTempDao) {
|
||||||
mBinder?.deleteAll()
|
serviceActionTask {
|
||||||
|
mBinder?.deleteAll()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// To erase the residues
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
cipherDatabaseDao.deleteAll()
|
cipherDatabaseDao.deleteAll()
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
|
// Unbind
|
||||||
|
removeAllDataAndDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction)
|
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {
|
||||||
|
private val TAG = CipherDatabaseAction::class.java.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -48,8 +48,11 @@ 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.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
@@ -85,13 +88,14 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun newRemoteViews(context: Context,
|
private fun newRemoteViews(context: Context,
|
||||||
|
database: Database,
|
||||||
remoteViewsText: String,
|
remoteViewsText: String,
|
||||||
remoteViewsIcon: IconImage? = null): RemoteViews {
|
remoteViewsIcon: IconImage? = null): RemoteViews {
|
||||||
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
|
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
|
||||||
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
|
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
|
||||||
if (remoteViewsIcon != null) {
|
if (remoteViewsIcon != null) {
|
||||||
try {
|
try {
|
||||||
Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context,
|
database.iconDrawableFactory.getBitmapFromIcon(context,
|
||||||
remoteViewsIcon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
remoteViewsIcon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
||||||
presentation.setImageViewBitmap(R.id.autofill_entry_icon, bitmap)
|
presentation.setImageViewBitmap(R.id.autofill_entry_icon, bitmap)
|
||||||
}
|
}
|
||||||
@@ -103,19 +107,94 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildDataset(context: Context,
|
private fun buildDataset(context: Context,
|
||||||
entryInfo: EntryInfo,
|
database: Database,
|
||||||
struct: StructureParser.Result,
|
entryInfo: EntryInfo,
|
||||||
inlinePresentation: InlinePresentation?): Dataset? {
|
struct: StructureParser.Result,
|
||||||
|
inlinePresentation: InlinePresentation?): Dataset? {
|
||||||
val title = makeEntryTitle(entryInfo)
|
val title = makeEntryTitle(entryInfo)
|
||||||
val views = newRemoteViews(context, title, entryInfo.icon)
|
val views = newRemoteViews(context, database, title, entryInfo.icon)
|
||||||
val builder = Dataset.Builder(views)
|
val builder = Dataset.Builder(views)
|
||||||
builder.setId(entryInfo.id)
|
builder.setId(entryInfo.id.toString())
|
||||||
|
|
||||||
struct.usernameId?.let { usernameId ->
|
struct.usernameId?.let { usernameId ->
|
||||||
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
|
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
|
||||||
}
|
}
|
||||||
struct.passwordId?.let { password ->
|
struct.passwordId?.let { passwordId ->
|
||||||
builder.setValue(password, AutofillValue.forText(entryInfo.password))
|
builder.setValue(passwordId, AutofillValue.forText(entryInfo.password))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entryInfo.expires) {
|
||||||
|
val year = entryInfo.expiryTime.getYearInt()
|
||||||
|
val month = entryInfo.expiryTime.getMonthInt()
|
||||||
|
val day = entryInfo.expiryTime.getDay()
|
||||||
|
|
||||||
|
struct.creditCardExpirationDateId?.let {
|
||||||
|
if (struct.isWebView) {
|
||||||
|
// set date string as defined in https://html.spec.whatwg.org
|
||||||
|
builder.setValue(it, AutofillValue.forText("$year\u002D$month"))
|
||||||
|
} else {
|
||||||
|
builder.setValue(it, AutofillValue.forDate(entryInfo.expiryTime.date.time))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct.creditCardExpirationYearId?.let {
|
||||||
|
var autofillValue: AutofillValue? = null
|
||||||
|
|
||||||
|
struct.creditCardExpirationYearOptions?.let { options ->
|
||||||
|
var yearIndex = options.indexOf(year.toString().substring(0, 2))
|
||||||
|
|
||||||
|
if (yearIndex == -1) {
|
||||||
|
yearIndex = options.indexOf(year.toString())
|
||||||
|
}
|
||||||
|
if (yearIndex != -1) {
|
||||||
|
autofillValue = AutofillValue.forList(yearIndex)
|
||||||
|
builder.setValue(it, autofillValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autofillValue == null) {
|
||||||
|
builder.setValue(it, AutofillValue.forText(year.toString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct.creditCardExpirationMonthId?.let {
|
||||||
|
if (struct.isWebView) {
|
||||||
|
builder.setValue(it, AutofillValue.forText(month.toString()))
|
||||||
|
} else {
|
||||||
|
if (struct.creditCardExpirationMonthOptions != null) {
|
||||||
|
// index starts at 0
|
||||||
|
builder.setValue(it, AutofillValue.forList(month - 1))
|
||||||
|
} else {
|
||||||
|
builder.setValue(it, AutofillValue.forText(month.toString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct.creditCardExpirationDayId?.let {
|
||||||
|
if (struct.isWebView) {
|
||||||
|
builder.setValue(it, AutofillValue.forText(day.toString()))
|
||||||
|
} else {
|
||||||
|
if (struct.creditCardExpirationDayOptions != null) {
|
||||||
|
builder.setValue(it, AutofillValue.forList(day - 1))
|
||||||
|
} else {
|
||||||
|
builder.setValue(it, AutofillValue.forText(day.toString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (field in entryInfo.customFields) {
|
||||||
|
if (field.name == TemplateField.LABEL_HOLDER) {
|
||||||
|
struct.creditCardHolderId?.let { ccNameId ->
|
||||||
|
builder.setValue(ccNameId, AutofillValue.forText(field.protectedValue.stringValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (field.name == TemplateField.LABEL_NUMBER) {
|
||||||
|
struct.creditCardNumberId?.let { ccnId ->
|
||||||
|
builder.setValue(ccnId, AutofillValue.forText(field.protectedValue.stringValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (field.name == TemplateField.LABEL_CVV) {
|
||||||
|
struct.cardVerificationValueId?.let { cvvId ->
|
||||||
|
builder.setValue(cvvId, AutofillValue.forText(field.protectedValue.stringValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
@@ -126,8 +205,8 @@ object AutofillHelper {
|
|||||||
|
|
||||||
return try {
|
return try {
|
||||||
builder.build()
|
builder.build()
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: Exception) {
|
||||||
// if not value be set
|
// at least one value must be set
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,9 +214,11 @@ object AutofillHelper {
|
|||||||
/**
|
/**
|
||||||
* Method to assign a drawable to a new icon from a database icon
|
* Method to assign a drawable to a new icon from a database icon
|
||||||
*/
|
*/
|
||||||
private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? {
|
private fun buildIconFromEntry(context: Context,
|
||||||
|
database: Database,
|
||||||
|
entryInfo: EntryInfo): Icon? {
|
||||||
try {
|
try {
|
||||||
Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context,
|
database.iconDrawableFactory.getBitmapFromIcon(context,
|
||||||
entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
||||||
return Icon.createWithBitmap(bitmap)
|
return Icon.createWithBitmap(bitmap)
|
||||||
}
|
}
|
||||||
@@ -150,13 +231,14 @@ object AutofillHelper {
|
|||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
private fun buildInlinePresentationForEntry(context: Context,
|
private fun buildInlinePresentationForEntry(context: Context,
|
||||||
|
database: Database,
|
||||||
inlineSuggestionsRequest: InlineSuggestionsRequest,
|
inlineSuggestionsRequest: InlineSuggestionsRequest,
|
||||||
positionItem: Int,
|
positionItem: Int,
|
||||||
entryInfo: EntryInfo): InlinePresentation? {
|
entryInfo: EntryInfo): InlinePresentation? {
|
||||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||||
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||||
|
|
||||||
if (positionItem <= maxSuggestion-1
|
if (positionItem <= maxSuggestion - 1
|
||||||
&& inlinePresentationSpecs.size > positionItem) {
|
&& inlinePresentationSpecs.size > positionItem) {
|
||||||
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
|
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
|
||||||
|
|
||||||
@@ -178,7 +260,7 @@ object AutofillHelper {
|
|||||||
setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
||||||
setTintBlendMode(BlendMode.DST)
|
setTintBlendMode(BlendMode.DST)
|
||||||
})
|
})
|
||||||
buildIconFromEntry(context, entryInfo)?.let { icon ->
|
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
|
||||||
setEndIcon(icon.apply {
|
setEndIcon(icon.apply {
|
||||||
setTintBlendMode(BlendMode.DST)
|
setTintBlendMode(BlendMode.DST)
|
||||||
})
|
})
|
||||||
@@ -189,9 +271,10 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun buildResponse(context: Context,
|
fun buildResponse(context: Context,
|
||||||
|
database: Database,
|
||||||
entriesInfo: List<EntryInfo>,
|
entriesInfo: List<EntryInfo>,
|
||||||
parseResult: StructureParser.Result,
|
parseResult: StructureParser.Result,
|
||||||
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse {
|
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse? {
|
||||||
val responseBuilder = FillResponse.Builder()
|
val responseBuilder = FillResponse.Builder()
|
||||||
// Add Header
|
// Add Header
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
@@ -208,31 +291,44 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add inline suggestion for new IME and dataset
|
// Add inline suggestion for new IME and dataset
|
||||||
entriesInfo.forEachIndexed { index, entryInfo ->
|
entriesInfo.forEachIndexed { index, entryInfo ->
|
||||||
val inlinePresentation = inlineSuggestionsRequest?.let {
|
val inlinePresentation = inlineSuggestionsRequest?.let {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
buildInlinePresentationForEntry(context, inlineSuggestionsRequest, index, entryInfo)
|
buildInlinePresentationForEntry(context, database, inlineSuggestionsRequest, index, entryInfo)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
responseBuilder.addDataset(buildDataset(context, entryInfo, parseResult, inlinePresentation))
|
val dataSet = buildDataset(context, database, entryInfo, parseResult, inlinePresentation)
|
||||||
|
dataSet?.let {
|
||||||
|
responseBuilder.addDataset(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
responseBuilder.build()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
return responseBuilder.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the Autofill response for one entry
|
* Build the Autofill response for one entry
|
||||||
*/
|
*/
|
||||||
fun buildResponseAndSetResult(activity: Activity, entryInfo: EntryInfo) {
|
fun buildResponseAndSetResult(activity: Activity,
|
||||||
buildResponseAndSetResult(activity, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
database: Database,
|
||||||
|
entryInfo: EntryInfo) {
|
||||||
|
buildResponseAndSetResult(activity, database, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the Autofill response for many entry
|
* Build the Autofill response for many entry
|
||||||
*/
|
*/
|
||||||
fun buildResponseAndSetResult(activity: Activity, entriesInfo: List<EntryInfo>) {
|
fun buildResponseAndSetResult(activity: Activity,
|
||||||
|
database: Database,
|
||||||
|
entriesInfo: List<EntryInfo>) {
|
||||||
if (entriesInfo.isEmpty()) {
|
if (entriesInfo.isEmpty()) {
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
activity.setResult(Activity.RESULT_CANCELED)
|
||||||
} else {
|
} else {
|
||||||
@@ -245,9 +341,9 @@ object AutofillHelper {
|
|||||||
if (inlineSuggestionsRequest != null) {
|
if (inlineSuggestionsRequest != null) {
|
||||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
buildResponse(activity, entriesInfo, result, inlineSuggestionsRequest)
|
buildResponse(activity, database, entriesInfo, result, inlineSuggestionsRequest)
|
||||||
} else {
|
} else {
|
||||||
buildResponse(activity, entriesInfo, result, null)
|
buildResponse(activity, database, entriesInfo, result, null)
|
||||||
}
|
}
|
||||||
val mReplyIntent = Intent()
|
val mReplyIntent = Intent()
|
||||||
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
||||||
|
|||||||
@@ -36,29 +36,47 @@ import androidx.autofill.inline.UiVersions
|
|||||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||||
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.action.DatabaseTaskProvider
|
||||||
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.CreditCard
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import org.joda.time.DateTime
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
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
|
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||||
var webDomainBlocklist: Set<String>? = null
|
private var mDatabase: Database? = null
|
||||||
var askToSaveData: Boolean = false
|
private var applicationIdBlocklist: Set<String>? = null
|
||||||
var autofillInlineSuggestionsEnabled: Boolean = false
|
private var webDomainBlocklist: Set<String>? = null
|
||||||
|
private var askToSaveData: Boolean = false
|
||||||
|
private var autofillInlineSuggestionsEnabled: Boolean = false
|
||||||
private var mLock = AtomicBoolean()
|
private var mLock = AtomicBoolean()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
||||||
|
mDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||||
|
this.mDatabase = database
|
||||||
|
}
|
||||||
|
|
||||||
getPreferences()
|
getPreferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
mDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
private fun getPreferences() {
|
private fun getPreferences() {
|
||||||
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
|
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
|
||||||
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
|
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
|
||||||
@@ -95,7 +113,8 @@ class KeeAutofillService : AutofillService() {
|
|||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
launchSelection(searchInfo,
|
launchSelection(mDatabase,
|
||||||
|
searchInfo,
|
||||||
parseResult,
|
parseResult,
|
||||||
inlineSuggestionsRequest,
|
inlineSuggestionsRequest,
|
||||||
callback)
|
callback)
|
||||||
@@ -105,16 +124,17 @@ class KeeAutofillService : AutofillService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchSelection(searchInfo: SearchInfo,
|
private fun launchSelection(database: Database?,
|
||||||
|
searchInfo: SearchInfo,
|
||||||
parseResult: StructureParser.Result,
|
parseResult: StructureParser.Result,
|
||||||
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
||||||
callback: FillCallback) {
|
callback: FillCallback) {
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
Database.getInstance(),
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ items ->
|
{ openedDatabase, items ->
|
||||||
callback.onSuccess(
|
callback.onSuccess(
|
||||||
AutofillHelper.buildResponse(this,
|
AutofillHelper.buildResponse(this, openedDatabase,
|
||||||
items, parseResult, inlineSuggestionsRequest)
|
items, parseResult, inlineSuggestionsRequest)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -155,23 +175,40 @@ class KeeAutofillService : AutofillService() {
|
|||||||
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tell to service the interest to save credentials
|
// Tell the autofill framework the interest to save credentials
|
||||||
if (askToSaveData) {
|
if (askToSaveData) {
|
||||||
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
|
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
|
||||||
val info = ArrayList<AutofillId>()
|
val requiredIds = ArrayList<AutofillId>()
|
||||||
|
val optionalIds = ArrayList<AutofillId>()
|
||||||
|
|
||||||
// Only if at least a password
|
// Only if at least a password
|
||||||
parseResult.passwordId?.let { passwordInfo ->
|
parseResult.passwordId?.let { passwordInfo ->
|
||||||
parseResult.usernameId?.let { usernameInfo ->
|
parseResult.usernameId?.let { usernameInfo ->
|
||||||
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
|
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
|
||||||
info.add(usernameInfo)
|
requiredIds.add(usernameInfo)
|
||||||
}
|
}
|
||||||
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
|
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
|
||||||
info.add(passwordInfo)
|
requiredIds.add(passwordInfo)
|
||||||
}
|
}
|
||||||
if (info.isNotEmpty()) {
|
// or a credit card form
|
||||||
responseBuilder.setSaveInfo(
|
if (requiredIds.isEmpty()) {
|
||||||
SaveInfo.Builder(types, info.toTypedArray()).build()
|
parseResult.creditCardNumberId?.let { numberId ->
|
||||||
)
|
types = types or SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD
|
||||||
|
requiredIds.add(numberId)
|
||||||
|
Log.d(TAG, "Asking to save credit card number")
|
||||||
|
}
|
||||||
|
parseResult.creditCardExpirationDateId?.let { id -> optionalIds.add(id) }
|
||||||
|
parseResult.creditCardExpirationYearId?.let { id -> optionalIds.add(id) }
|
||||||
|
parseResult.creditCardExpirationMonthId?.let { id -> optionalIds.add(id) }
|
||||||
|
parseResult.creditCardHolderId?.let { id -> optionalIds.add(id) }
|
||||||
|
parseResult.cardVerificationValueId?.let { id -> optionalIds.add(id) }
|
||||||
|
}
|
||||||
|
if (requiredIds.isNotEmpty()) {
|
||||||
|
val builder = SaveInfo.Builder(types, requiredIds.toTypedArray())
|
||||||
|
if (optionalIds.isNotEmpty()) {
|
||||||
|
builder.setOptionalIds(optionalIds.toTypedArray())
|
||||||
|
}
|
||||||
|
responseBuilder.setSaveInfo(builder.build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,14 +260,35 @@ class KeeAutofillService : AutofillService() {
|
|||||||
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
|
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
|
||||||
Log.d(TAG, "autofill onSaveRequest password")
|
Log.d(TAG, "autofill onSaveRequest password")
|
||||||
|
|
||||||
|
// Build expiration from date or from year and month
|
||||||
|
var expiration: DateTime? = parseResult.creditCardExpirationValue
|
||||||
|
if (parseResult.creditCardExpirationValue == null
|
||||||
|
&& parseResult.creditCardExpirationYearValue != 0
|
||||||
|
&& parseResult.creditCardExpirationMonthValue != 0) {
|
||||||
|
expiration = DateTime()
|
||||||
|
.withYear(parseResult.creditCardExpirationYearValue)
|
||||||
|
.withMonthOfYear(parseResult.creditCardExpirationMonthValue)
|
||||||
|
if (parseResult.creditCardExpirationDayValue != 0) {
|
||||||
|
expiration = expiration.withDayOfMonth(parseResult.creditCardExpirationDayValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show UI to save data
|
// Show UI to save data
|
||||||
val registerInfo = RegisterInfo(SearchInfo().apply {
|
val registerInfo = RegisterInfo(
|
||||||
applicationId = parseResult.applicationId
|
SearchInfo().apply {
|
||||||
webDomain = parseResult.webDomain
|
applicationId = parseResult.applicationId
|
||||||
webScheme = parseResult.webScheme
|
webDomain = parseResult.webDomain
|
||||||
},
|
webScheme = parseResult.webScheme
|
||||||
|
},
|
||||||
parseResult.usernameValue?.textValue?.toString(),
|
parseResult.usernameValue?.textValue?.toString(),
|
||||||
parseResult.passwordValue?.textValue?.toString())
|
parseResult.passwordValue?.textValue?.toString(),
|
||||||
|
CreditCard(
|
||||||
|
parseResult.creditCardHolder,
|
||||||
|
parseResult.creditCardNumber,
|
||||||
|
expiration,
|
||||||
|
parseResult.cardVerificationValue
|
||||||
|
))
|
||||||
|
|
||||||
// TODO Callback in each activity #765
|
// TODO Callback in each activity #765
|
||||||
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
|
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
|
||||||
|
|||||||
@@ -21,12 +21,14 @@ package com.kunzisoft.keepass.autofill
|
|||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
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 android.view.autofill.AutofillValue
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import org.joda.time.DateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,10 +37,8 @@ import java.util.*
|
|||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class StructureParser(private val structure: AssistStructure) {
|
class StructureParser(private val structure: AssistStructure) {
|
||||||
private var result: Result? = null
|
private var result: Result? = null
|
||||||
|
|
||||||
private var usernameNeeded = true
|
private var usernameNeeded = true
|
||||||
|
private var usernameIdCandidate: AutofillId? = null
|
||||||
private var usernameCandidate: AutofillId? = null
|
|
||||||
private var usernameValueCandidate: AutofillValue? = null
|
private var usernameValueCandidate: AutofillValue? = null
|
||||||
|
|
||||||
fun parse(saveValue: Boolean = false): Result? {
|
fun parse(saveValue: Boolean = false): Result? {
|
||||||
@@ -46,7 +46,7 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
result = Result()
|
result = Result()
|
||||||
result?.apply {
|
result?.apply {
|
||||||
allowSaveValues = saveValue
|
allowSaveValues = saveValue
|
||||||
usernameCandidate = null
|
usernameIdCandidate = null
|
||||||
usernameValueCandidate = 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)
|
||||||
@@ -57,26 +57,29 @@ 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 && usernameIdCandidate != null) {
|
||||||
usernameId = usernameCandidate
|
usernameId = usernameIdCandidate
|
||||||
if (allowSaveValues) {
|
if (allowSaveValues) {
|
||||||
usernameValue = usernameValueCandidate
|
usernameValue = usernameValueCandidate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the result only if password field is retrieved
|
return if (result?.passwordId != null || result?.creditCardNumberId != null)
|
||||||
return if ((!usernameNeeded || result?.usernameId != null)
|
result
|
||||||
&& result?.passwordId != null)
|
else
|
||||||
result
|
null
|
||||||
else
|
|
||||||
null
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
|
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
|
||||||
|
// remember this
|
||||||
|
if (node.className == "android.webkit.WebView") {
|
||||||
|
result?.isWebView = true
|
||||||
|
}
|
||||||
|
|
||||||
// Get the domain of a web app
|
// Get the domain of a web app
|
||||||
node.webDomain?.let { webDomain ->
|
node.webDomain?.let { webDomain ->
|
||||||
if (webDomain.isNotEmpty()) {
|
if (webDomain.isNotEmpty()) {
|
||||||
@@ -97,8 +100,7 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
var returnValue = false
|
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) {
|
||||||
&& node.autofillType == View.AUTOFILL_TYPE_TEXT) {
|
|
||||||
// Parse methods
|
// Parse methods
|
||||||
val hints = node.autofillHints
|
val hints = node.autofillHints
|
||||||
if (hints != null && hints.isNotEmpty()) {
|
if (hints != null && hints.isNotEmpty()) {
|
||||||
@@ -130,7 +132,7 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
it.contains(View.AUTOFILL_HINT_USERNAME, true)
|
it.contains(View.AUTOFILL_HINT_USERNAME, true)
|
||||||
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
||||||
|| it.contains("email", true)
|
|| it.contains("email", true)
|
||||||
|| it.contains(View.AUTOFILL_HINT_PHONE, true)-> {
|
|| it.contains(View.AUTOFILL_HINT_PHONE, true) -> {
|
||||||
result?.usernameId = autofillId
|
result?.usernameId = autofillId
|
||||||
result?.usernameValue = node.autofillValue
|
result?.usernameValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username hint")
|
Log.d(TAG, "Autofill username hint")
|
||||||
@@ -139,14 +141,123 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
result?.passwordId = autofillId
|
result?.passwordId = autofillId
|
||||||
result?.passwordValue = node.autofillValue
|
result?.passwordValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill password hint")
|
Log.d(TAG, "Autofill password hint")
|
||||||
// Username not needed in this case
|
|
||||||
usernameNeeded = false
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
it.contains("cc-name", true) -> {
|
||||||
|
Log.d(TAG, "Autofill credit card name hint")
|
||||||
|
result?.creditCardHolderId = autofillId
|
||||||
|
result?.creditCardHolder = node.autofillValue?.textValue?.toString()
|
||||||
|
}
|
||||||
|
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_NUMBER, true)
|
||||||
|
|| it.contains("cc-number", true) -> {
|
||||||
|
Log.d(TAG, "Autofill credit card number hint")
|
||||||
|
result?.creditCardNumberId = autofillId
|
||||||
|
result?.creditCardNumber = node.autofillValue?.textValue?.toString()
|
||||||
|
}
|
||||||
|
// expect date string as defined in https://html.spec.whatwg.org, e.g. 2014-12
|
||||||
|
it.contains("cc-exp", true) -> {
|
||||||
|
Log.d(TAG, "Autofill credit card expiration date hint")
|
||||||
|
result?.creditCardExpirationDateId = autofillId
|
||||||
|
node.autofillValue?.let { value ->
|
||||||
|
if (value.isText && value.textValue.length == 7) {
|
||||||
|
value.textValue.let { date ->
|
||||||
|
try {
|
||||||
|
result?.creditCardExpirationValue = DateTime()
|
||||||
|
.withYear(date.substring(2, 4).toInt())
|
||||||
|
.withMonthOfYear(date.substring(5, 7).toInt())
|
||||||
|
} catch(e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to retrieve expiration", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE, true) -> {
|
||||||
|
Log.d(TAG, "Autofill credit card expiration date hint")
|
||||||
|
result?.creditCardExpirationDateId = autofillId
|
||||||
|
node.autofillValue?.let { value ->
|
||||||
|
if (value.isDate) {
|
||||||
|
result?.creditCardExpirationValue = DateTime(value.dateValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR, true)
|
||||||
|
|| it.contains("cc-exp-year", true) -> {
|
||||||
|
Log.d(TAG, "Autofill credit card expiration year hint")
|
||||||
|
result?.creditCardExpirationYearId = autofillId
|
||||||
|
if (node.autofillOptions != null) {
|
||||||
|
result?.creditCardExpirationYearOptions = node.autofillOptions
|
||||||
|
}
|
||||||
|
node.autofillValue?.let { value ->
|
||||||
|
var year = 0
|
||||||
|
try {
|
||||||
|
if (value.isText) {
|
||||||
|
year = value.textValue.toString().toInt()
|
||||||
|
}
|
||||||
|
if (value.isList) {
|
||||||
|
year = node.autofillOptions?.get(value.listValue).toString().toInt()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to retrieve expiration year", e)
|
||||||
|
}
|
||||||
|
result?.creditCardExpirationYearValue = year % 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH, true)
|
||||||
|
|| it.contains("cc-exp-month", true) -> {
|
||||||
|
Log.d(TAG, "Autofill credit card expiration month hint")
|
||||||
|
result?.creditCardExpirationMonthId = autofillId
|
||||||
|
if (node.autofillOptions != null) {
|
||||||
|
result?.creditCardExpirationMonthOptions = node.autofillOptions
|
||||||
|
}
|
||||||
|
node.autofillValue?.let { value ->
|
||||||
|
var month = 0
|
||||||
|
try {
|
||||||
|
if (value.isText) {
|
||||||
|
month = value.textValue.toString().toInt()
|
||||||
|
}
|
||||||
|
if (value.isList) {
|
||||||
|
// assume list starts with January (index 0)
|
||||||
|
month = value.listValue + 1
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to retrieve expiration month", e)
|
||||||
|
}
|
||||||
|
result?.creditCardExpirationMonthValue = month
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY, true)
|
||||||
|
|| it.contains("cc-exp-day", true) -> {
|
||||||
|
Log.d(TAG, "Autofill credit card expiration day hint")
|
||||||
|
result?.creditCardExpirationDayId = autofillId
|
||||||
|
if (node.autofillOptions != null) {
|
||||||
|
result?.creditCardExpirationDayOptions = node.autofillOptions
|
||||||
|
}
|
||||||
|
node.autofillValue?.let { value ->
|
||||||
|
var day = 0
|
||||||
|
try {
|
||||||
|
if (value.isText) {
|
||||||
|
day = value.textValue.toString().toInt()
|
||||||
|
}
|
||||||
|
if (value.isList) {
|
||||||
|
day = node.autofillOptions?.get(value.listValue).toString().toInt()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to retrieve expiration day", e)
|
||||||
|
}
|
||||||
|
result?.creditCardExpirationDayValue = day
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE, true)
|
||||||
|
|| it.contains("cc-csc", true) -> {
|
||||||
|
Log.d(TAG, "Autofill card security code hint")
|
||||||
|
result?.cardVerificationValueId = autofillId
|
||||||
|
result?.cardVerificationValue = node.autofillValue?.textValue?.toString()
|
||||||
|
}
|
||||||
// Ignore autocomplete="off"
|
// Ignore autocomplete="off"
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
|
// https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
|
||||||
it.equals("off", true) ||
|
it.equals("off", true) ||
|
||||||
it.equals("on", true) -> {
|
it.equals("on", true) -> {
|
||||||
Log.d(TAG, "Autofill web hint")
|
Log.d(TAG, "Autofill web hint")
|
||||||
return parseNodeByHtmlAttributes(node)
|
return parseNodeByHtmlAttributes(node)
|
||||||
}
|
}
|
||||||
@@ -171,7 +282,7 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
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
|
usernameIdCandidate = autofillId
|
||||||
usernameValueCandidate = node.autofillValue
|
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}")
|
||||||
}
|
}
|
||||||
@@ -219,18 +330,30 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
InputType.TYPE_TEXT_VARIATION_NORMAL,
|
InputType.TYPE_TEXT_VARIATION_NORMAL,
|
||||||
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
|
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
|
||||||
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
|
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
|
||||||
usernameCandidate = autofillId
|
usernameIdCandidate = autofillId
|
||||||
usernameValueCandidate = node.autofillValue
|
usernameValueCandidate = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
||||||
}
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
|
||||||
|
// Some forms used visible password as username
|
||||||
|
if (usernameIdCandidate == null && usernameValueCandidate == null) {
|
||||||
|
usernameIdCandidate = autofillId
|
||||||
|
usernameValueCandidate = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill visible password android text type (as username): ${showHexInputType(inputType)}")
|
||||||
|
} else if (result?.passwordId == null && result?.passwordValue == null) {
|
||||||
|
result?.passwordId = autofillId
|
||||||
|
result?.passwordValue = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill visible password android text type (as password): ${showHexInputType(inputType)}")
|
||||||
|
usernameNeeded = false
|
||||||
|
}
|
||||||
|
}
|
||||||
inputIsVariationType(inputType,
|
inputIsVariationType(inputType,
|
||||||
InputType.TYPE_TEXT_VARIATION_PASSWORD,
|
InputType.TYPE_TEXT_VARIATION_PASSWORD,
|
||||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
|
|
||||||
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
|
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
|
||||||
result?.passwordId = autofillId
|
result?.passwordId = autofillId
|
||||||
result?.passwordValue = node.autofillValue
|
result?.passwordValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
|
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
|
||||||
usernameNeeded = false
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
inputIsVariationType(inputType,
|
inputIsVariationType(inputType,
|
||||||
@@ -252,16 +375,15 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
when {
|
when {
|
||||||
inputIsVariationType(inputType,
|
inputIsVariationType(inputType,
|
||||||
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
|
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
|
||||||
usernameCandidate = autofillId
|
usernameIdCandidate = autofillId
|
||||||
usernameValueCandidate = node.autofillValue
|
usernameValueCandidate = node.autofillValue
|
||||||
Log.d(TAG, "Autofill usernale candidate android number type: ${showHexInputType(inputType)}")
|
Log.d(TAG, "Autofill username candidate android number type: ${showHexInputType(inputType)}")
|
||||||
}
|
}
|
||||||
inputIsVariationType(inputType,
|
inputIsVariationType(inputType,
|
||||||
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
|
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
|
||||||
result?.passwordId = autofillId
|
result?.passwordId = autofillId
|
||||||
result?.passwordValue = node.autofillValue
|
result?.passwordValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
|
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
|
||||||
usernameNeeded = false
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -275,6 +397,7 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class Result {
|
class Result {
|
||||||
|
var isWebView: Boolean = false
|
||||||
var applicationId: String? = null
|
var applicationId: String? = null
|
||||||
|
|
||||||
var webDomain: String? = null
|
var webDomain: String? = null
|
||||||
@@ -289,6 +412,12 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the user selects the credit card expiration date from a list of options
|
||||||
|
// all options are stored here
|
||||||
|
var creditCardExpirationYearOptions: Array<CharSequence>? = null
|
||||||
|
var creditCardExpirationMonthOptions: Array<CharSequence>? = null
|
||||||
|
var creditCardExpirationDayOptions: Array<CharSequence>? = null
|
||||||
|
|
||||||
var usernameId: AutofillId? = null
|
var usernameId: AutofillId? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
if (field == null)
|
if (field == null)
|
||||||
@@ -301,6 +430,48 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var creditCardHolderId: AutofillId? = null
|
||||||
|
set(value) {
|
||||||
|
if (field == null)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var creditCardNumberId: AutofillId? = null
|
||||||
|
set(value) {
|
||||||
|
if (field == null)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var creditCardExpirationDateId: AutofillId? = null
|
||||||
|
set(value) {
|
||||||
|
if (field == null)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var creditCardExpirationYearId: AutofillId? = null
|
||||||
|
set(value) {
|
||||||
|
if (field == null)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var creditCardExpirationMonthId: AutofillId? = null
|
||||||
|
set(value) {
|
||||||
|
if (field == null)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var creditCardExpirationDayId: AutofillId? = null
|
||||||
|
set(value) {
|
||||||
|
if (field == null)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var cardVerificationValueId: AutofillId? = null
|
||||||
|
set(value) {
|
||||||
|
if (field == null)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
fun allAutofillIds(): Array<AutofillId> {
|
fun allAutofillIds(): Array<AutofillId> {
|
||||||
val all = ArrayList<AutofillId>()
|
val all = ArrayList<AutofillId>()
|
||||||
usernameId?.let {
|
usernameId?.let {
|
||||||
@@ -309,6 +480,15 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
passwordId?.let {
|
passwordId?.let {
|
||||||
all.add(it)
|
all.add(it)
|
||||||
}
|
}
|
||||||
|
creditCardHolderId?.let {
|
||||||
|
all.add(it)
|
||||||
|
}
|
||||||
|
creditCardNumberId?.let {
|
||||||
|
all.add(it)
|
||||||
|
}
|
||||||
|
cardVerificationValueId?.let {
|
||||||
|
all.add(it)
|
||||||
|
}
|
||||||
return all.toTypedArray()
|
return all.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,6 +506,52 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
if (allowSaveValues && field == null)
|
if (allowSaveValues && field == null)
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var creditCardHolder: String? = null
|
||||||
|
set(value) {
|
||||||
|
if (allowSaveValues)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var creditCardNumber: String? = null
|
||||||
|
set(value) {
|
||||||
|
if (allowSaveValues)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// format MMYY
|
||||||
|
var creditCardExpirationValue: DateTime? = null
|
||||||
|
set(value) {
|
||||||
|
if (allowSaveValues)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// for year of CC expiration date: YY
|
||||||
|
var creditCardExpirationYearValue = 0
|
||||||
|
set(value) {
|
||||||
|
if (allowSaveValues)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// for month of CC expiration date: MM
|
||||||
|
var creditCardExpirationMonthValue = 0
|
||||||
|
set(value) {
|
||||||
|
if (allowSaveValues)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var creditCardExpirationDayValue = 0
|
||||||
|
set(value) {
|
||||||
|
if (allowSaveValues)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// the security code for the credit card (also called CVV)
|
||||||
|
var cardVerificationValue: String? = null
|
||||||
|
set(value) {
|
||||||
|
if (allowSaveValues)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -30,15 +30,17 @@ import android.view.*
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.getkeepsafe.taptargetview.TapTargetView
|
import com.getkeepsafe.taptargetview.TapTargetView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||||
import com.kunzisoft.keepass.database.exception.IODatabaseException
|
import com.kunzisoft.keepass.database.exception.IODatabaseException
|
||||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedUnlockCallback {
|
class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedUnlockCallback {
|
||||||
|
|
||||||
@@ -68,7 +70,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
|
|
||||||
private lateinit var cipherDatabaseAction : CipherDatabaseAction
|
private lateinit var cipherDatabaseAction : CipherDatabaseAction
|
||||||
|
|
||||||
private var cipherDatabaseListener: CipherDatabaseAction.DatabaseListener? = null
|
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
|
||||||
|
|
||||||
// Only to fix multiple fingerprint menu #332
|
// Only to fix multiple fingerprint menu #332
|
||||||
private var mAllowAdvancedUnlockMenu = false
|
private var mAllowAdvancedUnlockMenu = false
|
||||||
@@ -125,8 +127,10 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(requireContext())
|
context?.let {
|
||||||
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext())
|
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(it)
|
||||||
|
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(it)
|
||||||
|
}
|
||||||
keepConnection = false
|
keepConnection = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,34 +180,36 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
* Check unlock availability and change the current mode depending of device's state
|
* Check unlock availability and change the current mode depending of device's state
|
||||||
*/
|
*/
|
||||||
fun checkUnlockAvailability() {
|
fun checkUnlockAvailability() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
context?.let { context ->
|
||||||
allowOpenBiometricPrompt = true
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
if (PreferencesUtil.isBiometricUnlockEnable(requireContext())) {
|
allowOpenBiometricPrompt = true
|
||||||
mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
|
if (PreferencesUtil.isBiometricUnlockEnable(context)) {
|
||||||
|
mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
|
||||||
|
|
||||||
// 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 = AdvancedUnlockManager.canAuthenticate(requireContext())
|
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(context)
|
||||||
if (!PreferencesUtil.isAdvancedUnlockEnable(requireContext())
|
if (!PreferencesUtil.isAdvancedUnlockEnable(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.BIOMETRIC_UNAVAILABLE)
|
toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
|
||||||
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
|
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
|
||||||
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
|
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
|
||||||
} else {
|
|
||||||
// biometric is available but not configured, show icon but in disabled state with some information
|
|
||||||
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
|
||||||
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
|
||||||
} else {
|
} else {
|
||||||
selectMode()
|
// biometric is available but not configured, show icon but in disabled state with some information
|
||||||
|
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
||||||
|
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
||||||
|
} else {
|
||||||
|
selectMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
||||||
|
mAdvancedUnlockInfoView?.setIconResource(R.drawable.bolt)
|
||||||
|
if (AdvancedUnlockManager.isDeviceSecure(context)) {
|
||||||
|
selectMode()
|
||||||
|
} else {
|
||||||
|
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(requireContext())) {
|
|
||||||
mAdvancedUnlockInfoView?.setIconResource(R.drawable.bolt)
|
|
||||||
if (AdvancedUnlockManager.isDeviceSecure(requireContext())) {
|
|
||||||
selectMode()
|
|
||||||
} else {
|
|
||||||
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,7 +267,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
private fun openBiometricSetting() {
|
private fun openBiometricSetting() {
|
||||||
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
|
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||||
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
|
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
|
||||||
requireContext().startActivity(Intent(Settings.ACTION_SETTINGS))
|
context?.startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,15 +302,17 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
|
setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
|
||||||
setAdvancedUnlockedMessageView("")
|
setAdvancedUnlockedMessageView("")
|
||||||
|
|
||||||
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
|
context?.let { context ->
|
||||||
onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
|
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||||
requireContext().getString(R.string.credential_before_click_advanced_unlock_button))
|
onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
|
||||||
|
context.getString(R.string.credential_before_click_advanced_unlock_button))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
|
private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
|
||||||
activity?.runOnUiThread {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
if (allowOpenBiometricPrompt) {
|
if (allowOpenBiometricPrompt) {
|
||||||
if (cryptoPrompt.isDeviceCredentialOperation)
|
if (cryptoPrompt.isDeviceCredentialOperation)
|
||||||
keepConnection = true
|
keepConnection = true
|
||||||
@@ -402,9 +410,10 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
fun connect(databaseUri: Uri) {
|
fun connect(databaseUri: Uri) {
|
||||||
showViews(true)
|
showViews(true)
|
||||||
this.databaseFileUri = databaseUri
|
this.databaseFileUri = databaseUri
|
||||||
cipherDatabaseListener = object: CipherDatabaseAction.DatabaseListener {
|
cipherDatabaseListener = object: CipherDatabaseAction.CipherDatabaseListener {
|
||||||
override fun onDatabaseCleared() {
|
override fun onCipherDatabaseCleared() {
|
||||||
deleteEncryptedDatabaseKey()
|
advancedUnlockManager?.closeBiometricPrompt()
|
||||||
|
checkUnlockAvailability()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cipherDatabaseAction.apply {
|
cipherDatabaseAction.apply {
|
||||||
@@ -435,18 +444,16 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
fun deleteEncryptedDatabaseKey() {
|
fun deleteEncryptedDatabaseKey() {
|
||||||
allowOpenBiometricPrompt = false
|
|
||||||
mAdvancedUnlockInfoView?.setIconViewClickListener(false, null)
|
|
||||||
advancedUnlockManager?.closeBiometricPrompt()
|
advancedUnlockManager?.closeBiometricPrompt()
|
||||||
databaseFileUri?.let { databaseUri ->
|
databaseFileUri?.let { databaseUri ->
|
||||||
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
|
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
|
||||||
checkUnlockAvailability()
|
checkUnlockAvailability()
|
||||||
}
|
}
|
||||||
}
|
} ?: checkUnlockAvailability()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
activity?.runOnUiThread {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
|
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
|
||||||
setAdvancedUnlockedMessageView(errString.toString())
|
setAdvancedUnlockedMessageView(errString.toString())
|
||||||
}
|
}
|
||||||
@@ -454,7 +461,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
override fun onAuthenticationFailed() {
|
override fun onAuthenticationFailed() {
|
||||||
activity?.runOnUiThread {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
|
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
|
||||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
|
setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
|
||||||
}
|
}
|
||||||
@@ -462,7 +469,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
override fun onAuthenticationSucceeded() {
|
override fun onAuthenticationSucceeded() {
|
||||||
activity?.runOnUiThread {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
when (biometricMode) {
|
when (biometricMode) {
|
||||||
Mode.BIOMETRIC_UNAVAILABLE -> {
|
Mode.BIOMETRIC_UNAVAILABLE -> {
|
||||||
}
|
}
|
||||||
@@ -479,7 +486,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
mBuilderListener?.retrieveCredentialForEncryption()?.let { credential ->
|
mBuilderListener?.retrieveCredentialForEncryption()?.let { credential ->
|
||||||
advancedUnlockManager?.encryptData(credential)
|
advancedUnlockManager?.encryptData(credential)
|
||||||
}
|
}
|
||||||
AdvancedUnlockNotificationService.startServiceForTimeout(requireContext())
|
|
||||||
}
|
}
|
||||||
Mode.EXTRACT_CREDENTIAL -> {
|
Mode.EXTRACT_CREDENTIAL -> {
|
||||||
// retrieve the encrypted value from preferences
|
// retrieve the encrypted value from preferences
|
||||||
@@ -521,7 +527,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showViews(show: Boolean) {
|
private fun showViews(show: Boolean) {
|
||||||
activity?.runOnUiThread {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
mAdvancedUnlockInfoView?.visibility = if (show)
|
mAdvancedUnlockInfoView?.visibility = if (show)
|
||||||
View.VISIBLE
|
View.VISIBLE
|
||||||
else {
|
else {
|
||||||
@@ -532,20 +538,20 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
private fun setAdvancedUnlockedTitleView(textId: Int) {
|
private fun setAdvancedUnlockedTitleView(textId: Int) {
|
||||||
activity?.runOnUiThread {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
mAdvancedUnlockInfoView?.setTitle(textId)
|
mAdvancedUnlockInfoView?.setTitle(textId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
private fun setAdvancedUnlockedMessageView(textId: Int) {
|
private fun setAdvancedUnlockedMessageView(textId: Int) {
|
||||||
activity?.runOnUiThread {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
mAdvancedUnlockInfoView?.setMessage(textId)
|
mAdvancedUnlockInfoView?.setMessage(textId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
|
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
|
||||||
activity?.runOnUiThread {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
mAdvancedUnlockInfoView?.message = text
|
mAdvancedUnlockInfoView?.message = text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ 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.model.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
|
||||||
|
|
||||||
class CreateDatabaseRunnable(context: Context,
|
class CreateDatabaseRunnable(context: Context,
|
||||||
private val mDatabase: Database,
|
private val mDatabase: Database,
|
||||||
databaseUri: Uri,
|
databaseUri: Uri,
|
||||||
private val databaseName: String,
|
private val databaseName: String,
|
||||||
private val rootName: String,
|
private val rootName: String,
|
||||||
|
private val templateGroupName: String?,
|
||||||
mainCredential: MainCredential,
|
mainCredential: MainCredential,
|
||||||
private val createDatabaseResult: ((Result) -> Unit)?)
|
private val createDatabaseResult: ((Result) -> Unit)?)
|
||||||
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
|
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
|
||||||
@@ -41,10 +41,10 @@ class CreateDatabaseRunnable(context: Context,
|
|||||||
try {
|
try {
|
||||||
// Create new database record
|
// Create new database record
|
||||||
mDatabase.apply {
|
mDatabase.apply {
|
||||||
createData(mDatabaseUri, databaseName, rootName)
|
createData(mDatabaseUri, databaseName, rootName, templateGroupName)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
|
mDatabase.clearAndClose(context)
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,18 +19,23 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.action
|
package com.kunzisoft.keepass.database.action
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
import android.content.*
|
import android.content.*
|
||||||
import android.content.Context.BIND_ABOVE_CLIENT
|
import android.content.Context.*
|
||||||
import android.content.Context.BIND_NOT_FOREGROUND
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
@@ -67,21 +72,35 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
|||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||||
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.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 kotlinx.coroutines.launch
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
/**
|
||||||
|
* Utility class to connect an activity or a service to the DatabaseTaskNotificationService,
|
||||||
|
* Useful to retrieve a database instance and sending tasks commands
|
||||||
|
*/
|
||||||
|
class DatabaseTaskProvider {
|
||||||
|
|
||||||
var onActionFinish: ((actionTask: String,
|
private var activity: FragmentActivity? = null
|
||||||
|
private var service: Service? = null
|
||||||
|
private var context: Context
|
||||||
|
|
||||||
|
var onDatabaseRetrieved: ((database: Database?) -> Unit)? = null
|
||||||
|
|
||||||
|
var onActionFinish: ((database: Database,
|
||||||
|
actionTask: String,
|
||||||
result: ActionRunnable.Result) -> Unit)? = null
|
result: ActionRunnable.Result) -> Unit)? = null
|
||||||
|
|
||||||
private var intentDatabaseTask = Intent(activity.applicationContext, DatabaseTaskNotificationService::class.java)
|
private var intentDatabaseTask: Intent
|
||||||
|
|
||||||
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
|
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
|
||||||
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
|
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
|
||||||
@@ -91,17 +110,31 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
|
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
|
||||||
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
|
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
|
||||||
|
|
||||||
|
constructor(activity: FragmentActivity) {
|
||||||
|
this.activity = activity
|
||||||
|
this.context = activity
|
||||||
|
this.intentDatabaseTask = Intent(activity.applicationContext,
|
||||||
|
DatabaseTaskNotificationService::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(service: Service) {
|
||||||
|
this.service = service
|
||||||
|
this.context = service
|
||||||
|
this.intentDatabaseTask = Intent(service.applicationContext,
|
||||||
|
DatabaseTaskNotificationService::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
||||||
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
override fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||||
startDialog(titleId, messageId, warningId)
|
startDialog(titleId, messageId, warningId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
override fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||||
updateDialog(titleId, messageId, warningId)
|
updateDialog(titleId, messageId, warningId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStopAction(actionTask: String, result: ActionRunnable.Result) {
|
override fun onStopAction(database: Database, actionTask: String, result: ActionRunnable.Result) {
|
||||||
onActionFinish?.invoke(actionTask, result)
|
onActionFinish?.invoke(database, actionTask, result)
|
||||||
// Remove the progress task
|
// Remove the progress task
|
||||||
stopDialog()
|
stopDialog()
|
||||||
}
|
}
|
||||||
@@ -116,31 +149,56 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
private var databaseInfoListener = object: DatabaseTaskNotificationService.DatabaseInfoListener {
|
private var databaseInfoListener = object: DatabaseTaskNotificationService.DatabaseInfoListener {
|
||||||
override fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo,
|
override fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
newDatabaseInfo: SnapFileDatabaseInfo) {
|
newDatabaseInfo: SnapFileDatabaseInfo) {
|
||||||
if (databaseChangedDialogFragment == null) {
|
activity?.let { activity ->
|
||||||
databaseChangedDialogFragment = activity.supportFragmentManager
|
activity.lifecycleScope.launch {
|
||||||
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
|
if (databaseChangedDialogFragment == null) {
|
||||||
databaseChangedDialogFragment?.actionDatabaseListener = mActionDatabaseListener
|
databaseChangedDialogFragment = activity.supportFragmentManager
|
||||||
}
|
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
|
||||||
if (progressTaskDialogFragment == null) {
|
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||||
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(previousDatabaseInfo, newDatabaseInfo)
|
mActionDatabaseListener
|
||||||
databaseChangedDialogFragment?.actionDatabaseListener = mActionDatabaseListener
|
}
|
||||||
databaseChangedDialogFragment?.show(activity.supportFragmentManager, DATABASE_CHANGED_DIALOG_TAG)
|
if (progressTaskDialogFragment == null) {
|
||||||
|
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
|
||||||
|
previousDatabaseInfo,
|
||||||
|
newDatabaseInfo
|
||||||
|
)
|
||||||
|
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||||
|
mActionDatabaseListener
|
||||||
|
databaseChangedDialogFragment?.show(
|
||||||
|
activity.supportFragmentManager,
|
||||||
|
DATABASE_CHANGED_DIALOG_TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var databaseListener = object: DatabaseTaskNotificationService.DatabaseListener {
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
onDatabaseRetrieved?.invoke(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun startDialog(titleId: Int? = null,
|
private fun startDialog(titleId: Int? = null,
|
||||||
messageId: Int? = null,
|
messageId: Int? = null,
|
||||||
warningId: Int? = null) {
|
warningId: Int? = null) {
|
||||||
if (progressTaskDialogFragment == null) {
|
activity?.let { activity ->
|
||||||
progressTaskDialogFragment = activity.supportFragmentManager
|
activity.lifecycleScope.launch {
|
||||||
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
|
if (progressTaskDialogFragment == null) {
|
||||||
|
progressTaskDialogFragment = activity.supportFragmentManager
|
||||||
|
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
|
||||||
|
}
|
||||||
|
if (progressTaskDialogFragment == null) {
|
||||||
|
progressTaskDialogFragment = ProgressTaskDialogFragment()
|
||||||
|
progressTaskDialogFragment?.show(
|
||||||
|
activity.supportFragmentManager,
|
||||||
|
PROGRESS_TASK_DIALOG_TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
updateDialog(titleId, messageId, warningId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (progressTaskDialogFragment == null) {
|
|
||||||
progressTaskDialogFragment = ProgressTaskDialogFragment()
|
|
||||||
progressTaskDialogFragment?.show(activity.supportFragmentManager, PROGRESS_TASK_DIALOG_TAG)
|
|
||||||
}
|
|
||||||
updateDialog(titleId, messageId, warningId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateDialog(titleId: Int?, messageId: Int?, warningId: Int?) {
|
private fun updateDialog(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||||
@@ -167,16 +225,19 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
serviceConnection = object : ServiceConnection {
|
serviceConnection = object : ServiceConnection {
|
||||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||||
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
||||||
addActionTaskListener(actionTaskListener)
|
addDatabaseListener(databaseListener)
|
||||||
addDatabaseFileInfoListener(databaseInfoListener)
|
addDatabaseFileInfoListener(databaseInfoListener)
|
||||||
getService().checkAction()
|
addActionTaskListener(actionTaskListener)
|
||||||
|
getService().checkDatabase()
|
||||||
getService().checkDatabaseInfo()
|
getService().checkDatabaseInfo()
|
||||||
|
getService().checkAction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
|
||||||
mBinder?.removeActionTaskListener(actionTaskListener)
|
mBinder?.removeActionTaskListener(actionTaskListener)
|
||||||
|
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||||
|
mBinder?.removeDatabaseListener(databaseListener)
|
||||||
mBinder = null
|
mBinder = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,7 +247,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
private fun bindService() {
|
private fun bindService() {
|
||||||
initServiceConnection()
|
initServiceConnection()
|
||||||
serviceConnection?.let {
|
serviceConnection?.let {
|
||||||
activity.bindService(intentDatabaseTask, it, BIND_NOT_FOREGROUND or BIND_ABOVE_CLIENT)
|
context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_NOT_FOREGROUND or BIND_ABOVE_CLIENT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +256,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
*/
|
*/
|
||||||
private fun unBindService() {
|
private fun unBindService() {
|
||||||
serviceConnection?.let {
|
serviceConnection?.let {
|
||||||
activity.unbindService(it)
|
context.unbindService(it)
|
||||||
}
|
}
|
||||||
serviceConnection = null
|
serviceConnection = null
|
||||||
}
|
}
|
||||||
@@ -223,7 +284,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
activity.registerReceiver(databaseTaskBroadcastReceiver,
|
context.registerReceiver(databaseTaskBroadcastReceiver,
|
||||||
IntentFilter().apply {
|
IntentFilter().apply {
|
||||||
addAction(DATABASE_START_TASK_ACTION)
|
addAction(DATABASE_START_TASK_ACTION)
|
||||||
addAction(DATABASE_STOP_TASK_ACTION)
|
addAction(DATABASE_STOP_TASK_ACTION)
|
||||||
@@ -237,25 +298,30 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
fun unregisterProgressTask() {
|
fun unregisterProgressTask() {
|
||||||
stopDialog()
|
stopDialog()
|
||||||
|
|
||||||
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
|
||||||
mBinder?.removeActionTaskListener(actionTaskListener)
|
mBinder?.removeActionTaskListener(actionTaskListener)
|
||||||
|
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||||
|
mBinder?.removeDatabaseListener(databaseListener)
|
||||||
mBinder = null
|
mBinder = null
|
||||||
|
|
||||||
unBindService()
|
unBindService()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
activity.unregisterReceiver(databaseTaskBroadcastReceiver)
|
context.unregisterReceiver(databaseTaskBroadcastReceiver)
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
// If receiver not register, do nothing
|
// If receiver not register, do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun start(bundle: Bundle? = null, actionTask: String) {
|
private fun start(bundle: Bundle? = null, actionTask: String) {
|
||||||
activity.stopService(intentDatabaseTask)
|
try {
|
||||||
if (bundle != null)
|
if (bundle != null)
|
||||||
intentDatabaseTask.putExtras(bundle)
|
intentDatabaseTask.putExtras(bundle)
|
||||||
intentDatabaseTask.action = actionTask
|
intentDatabaseTask.action = actionTask
|
||||||
activity.startService(intentDatabaseTask)
|
context.startService(intentDatabaseTask)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to perform database action", e)
|
||||||
|
Toast.makeText(activity, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -364,9 +430,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
nodesPaste.forEach { nodeVersioned ->
|
nodesPaste.forEach { nodeVersioned ->
|
||||||
when (nodeVersioned.type) {
|
when (nodeVersioned.type) {
|
||||||
Type.GROUP -> {
|
Type.GROUP -> {
|
||||||
(nodeVersioned as Group).nodeId?.let { groupId ->
|
groupsIdToCopy.add((nodeVersioned as Group).nodeId)
|
||||||
groupsIdToCopy.add(groupId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Type.ENTRY -> {
|
Type.ENTRY -> {
|
||||||
entriesIdToCopy.add((nodeVersioned as Entry).nodeId)
|
entriesIdToCopy.add((nodeVersioned as Entry).nodeId)
|
||||||
@@ -409,22 +473,22 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
-----------------
|
-----------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun startDatabaseRestoreEntryHistory(mainEntry: Entry,
|
fun startDatabaseRestoreEntryHistory(mainEntryId: NodeId<UUID>,
|
||||||
entryHistoryPosition: Int,
|
entryHistoryPosition: Int,
|
||||||
save: Boolean) {
|
save: Boolean) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntry.nodeId)
|
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
|
||||||
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
|
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseDeleteEntryHistory(mainEntry: Entry,
|
fun startDatabaseDeleteEntryHistory(mainEntryId: NodeId<UUID>,
|
||||||
entryHistoryPosition: Int,
|
entryHistoryPosition: Int,
|
||||||
save: Boolean) {
|
save: Boolean) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntry.nodeId)
|
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
|
||||||
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
@@ -499,6 +563,28 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startDatabaseSaveRecycleBin(oldRecycleBin: Group?,
|
||||||
|
newRecycleBin: Group?,
|
||||||
|
save: Boolean) {
|
||||||
|
start(Bundle().apply {
|
||||||
|
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldRecycleBin)
|
||||||
|
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newRecycleBin)
|
||||||
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
|
}
|
||||||
|
, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startDatabaseSaveTemplatesGroup(oldTemplatesGroup: Group?,
|
||||||
|
newTemplatesGroup: Group?,
|
||||||
|
save: Boolean) {
|
||||||
|
start(Bundle().apply {
|
||||||
|
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldTemplatesGroup)
|
||||||
|
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newTemplatesGroup)
|
||||||
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
|
}
|
||||||
|
, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
|
||||||
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
|
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
|
||||||
newMaxHistoryItems: Int,
|
newMaxHistoryItems: Int,
|
||||||
save: Boolean) {
|
save: Boolean) {
|
||||||
@@ -591,4 +677,8 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
, ACTION_DATABASE_SAVE)
|
, ACTION_DATABASE_SAVE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = DatabaseTaskProvider::class.java.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -25,8 +25,8 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
|||||||
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.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
@@ -47,7 +47,7 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
// Clear before we load
|
// Clear before we load
|
||||||
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
|
mDatabase.clearAndClose(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionRun() {
|
override fun onActionRun() {
|
||||||
@@ -85,7 +85,7 @@ 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)
|
||||||
} else {
|
} else {
|
||||||
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
|
mDatabase.clearAndClose(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ package com.kunzisoft.keepass.database.action
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
@@ -62,7 +62,7 @@ class ReloadDatabaseRunnable(private val context: Context,
|
|||||||
PreferencesUtil.saveCurrentTime(context)
|
PreferencesUtil.saveCurrentTime(context)
|
||||||
} else {
|
} else {
|
||||||
tempCipherKey = null
|
tempCipherKey = null
|
||||||
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
|
mDatabase.clearAndClose(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,41 +31,47 @@ class DeleteNodesRunnable(context: Context,
|
|||||||
afterActionNodesFinish: AfterActionNodesFinish)
|
afterActionNodesFinish: AfterActionNodesFinish)
|
||||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||||
|
|
||||||
private var mParent: Group? = null
|
private var mOldParent: Group? = null
|
||||||
private var mCanRecycle: Boolean = false
|
private var mCanRecycle: Boolean = false
|
||||||
|
|
||||||
private var mNodesToDeleteBackup = ArrayList<Node>()
|
private var mNodesToDeleteBackup = ArrayList<Node>()
|
||||||
|
|
||||||
override fun nodeAction() {
|
override fun nodeAction() {
|
||||||
|
|
||||||
foreachNode@ for(currentNode in mNodesToDelete) {
|
foreachNode@ for(nodeToDelete in mNodesToDelete) {
|
||||||
mParent = currentNode.parent
|
mOldParent = nodeToDelete.parent
|
||||||
mParent?.touch(modified = false, touchParents = true)
|
mOldParent?.touch(modified = false, touchParents = true)
|
||||||
|
|
||||||
when (currentNode.type) {
|
when (nodeToDelete.type) {
|
||||||
Type.GROUP -> {
|
Type.GROUP -> {
|
||||||
|
val groupToDelete = nodeToDelete as Group
|
||||||
// Create a copy to keep the old ref and remove it visually
|
// Create a copy to keep the old ref and remove it visually
|
||||||
mNodesToDeleteBackup.add(Group(currentNode as Group))
|
mNodesToDeleteBackup.add(Group(groupToDelete))
|
||||||
// Remove Node from parent
|
// Remove Node from parent
|
||||||
mCanRecycle = database.canRecycle(currentNode)
|
mCanRecycle = database.canRecycle(groupToDelete)
|
||||||
if (mCanRecycle) {
|
if (mCanRecycle) {
|
||||||
database.recycle(currentNode, context.resources)
|
groupToDelete.touch(modified = false, touchParents = true)
|
||||||
|
database.recycle(groupToDelete, context.resources)
|
||||||
|
groupToDelete.setPreviousParentGroup(mOldParent)
|
||||||
} else {
|
} else {
|
||||||
database.deleteGroup(currentNode)
|
database.deleteGroup(groupToDelete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Type.ENTRY -> {
|
Type.ENTRY -> {
|
||||||
|
val entryToDelete = nodeToDelete as Entry
|
||||||
// Create a copy to keep the old ref and remove it visually
|
// Create a copy to keep the old ref and remove it visually
|
||||||
mNodesToDeleteBackup.add(Entry(currentNode as Entry))
|
mNodesToDeleteBackup.add(Entry(entryToDelete))
|
||||||
// Remove Node from parent
|
// Remove Node from parent
|
||||||
mCanRecycle = database.canRecycle(currentNode)
|
mCanRecycle = database.canRecycle(entryToDelete)
|
||||||
if (mCanRecycle) {
|
if (mCanRecycle) {
|
||||||
database.recycle(currentNode, context.resources)
|
entryToDelete.touch(modified = false, touchParents = true)
|
||||||
|
database.recycle(entryToDelete, context.resources)
|
||||||
|
entryToDelete.setPreviousParentGroup(mOldParent)
|
||||||
} else {
|
} else {
|
||||||
database.deleteEntry(currentNode)
|
database.deleteEntry(entryToDelete)
|
||||||
}
|
}
|
||||||
// Remove the oldest attachments
|
// Remove the oldest attachments
|
||||||
currentNode.getAttachments(database.attachmentPool).forEach {
|
entryToDelete.getAttachments(database.attachmentPool).forEach {
|
||||||
database.removeAttachmentIfNotUsed(it)
|
database.removeAttachmentIfNotUsed(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,7 +82,7 @@ class DeleteNodesRunnable(context: Context,
|
|||||||
override fun nodeFinish(): ActionNodesValues {
|
override fun nodeFinish(): ActionNodesValues {
|
||||||
if (!result.isSuccess) {
|
if (!result.isSuccess) {
|
||||||
if (mCanRecycle) {
|
if (mCanRecycle) {
|
||||||
mParent?.let {
|
mOldParent?.let {
|
||||||
mNodesToDeleteBackup.forEach { backupNode ->
|
mNodesToDeleteBackup.forEach { backupNode ->
|
||||||
when (backupNode.type) {
|
when (backupNode.type) {
|
||||||
Type.GROUP -> {
|
Type.GROUP -> {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import android.util.Log
|
|||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.*
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.exception.EntryDatabaseException
|
import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException
|
||||||
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
|
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
|
||||||
|
|
||||||
class MoveNodesRunnable constructor(
|
class MoveNodesRunnable constructor(
|
||||||
@@ -47,11 +47,14 @@ class MoveNodesRunnable constructor(
|
|||||||
when (nodeToMove.type) {
|
when (nodeToMove.type) {
|
||||||
Type.GROUP -> {
|
Type.GROUP -> {
|
||||||
val groupToMove = nodeToMove as Group
|
val groupToMove = nodeToMove as Group
|
||||||
// Move group in new parent if not in the current group
|
// Move group if the parent change
|
||||||
if (groupToMove != mNewParent
|
if (mOldParent != mNewParent
|
||||||
|
// and if not in the current group
|
||||||
|
&& groupToMove != mNewParent
|
||||||
&& !mNewParent.isContainedIn(groupToMove)) {
|
&& !mNewParent.isContainedIn(groupToMove)) {
|
||||||
nodeToMove.touch(modified = true, touchParents = true)
|
groupToMove.touch(modified = true, touchParents = true)
|
||||||
database.moveGroupTo(groupToMove, mNewParent)
|
database.moveGroupTo(groupToMove, mNewParent)
|
||||||
|
groupToMove.setPreviousParentGroup(mOldParent)
|
||||||
} else {
|
} else {
|
||||||
// Only finish thread
|
// Only finish thread
|
||||||
setError(MoveGroupDatabaseException())
|
setError(MoveGroupDatabaseException())
|
||||||
@@ -64,11 +67,12 @@ class MoveNodesRunnable constructor(
|
|||||||
if (mOldParent != mNewParent
|
if (mOldParent != mNewParent
|
||||||
// and root can contains entry
|
// and root can contains entry
|
||||||
&& (mNewParent != database.rootGroup || database.rootCanContainsEntry())) {
|
&& (mNewParent != database.rootGroup || database.rootCanContainsEntry())) {
|
||||||
nodeToMove.touch(modified = true, touchParents = true)
|
entryToMove.touch(modified = true, touchParents = true)
|
||||||
database.moveEntryTo(entryToMove, mNewParent)
|
database.moveEntryTo(entryToMove, mNewParent)
|
||||||
|
entryToMove.setPreviousParentGroup(mOldParent)
|
||||||
} else {
|
} else {
|
||||||
// Only finish thread
|
// Only finish thread
|
||||||
setError(EntryDatabaseException())
|
setError(MoveEntryDatabaseException())
|
||||||
break@foreachNode
|
break@foreachNode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,54 +34,52 @@ class UpdateEntryRunnable constructor(
|
|||||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||||
|
|
||||||
// Keep backup of original values in case save fails
|
|
||||||
private var mBackupEntryHistory: Entry = Entry(mOldEntry)
|
|
||||||
|
|
||||||
override fun nodeAction() {
|
override fun nodeAction() {
|
||||||
// WARNING : Re attribute parent removed in entry edit activity to save memory
|
if (mOldEntry.nodeId == mNewEntry.nodeId) {
|
||||||
mNewEntry.addParentFrom(mOldEntry)
|
// WARNING : Re attribute parent removed in entry edit activity to save memory
|
||||||
|
mNewEntry.addParentFrom(mOldEntry)
|
||||||
|
|
||||||
// Build oldest attachments
|
// Build oldest attachments
|
||||||
val oldEntryAttachments = mOldEntry.getAttachments(database.attachmentPool, true)
|
val oldEntryAttachments = mOldEntry.getAttachments(database.attachmentPool, true)
|
||||||
val newEntryAttachments = mNewEntry.getAttachments(database.attachmentPool, true)
|
val newEntryAttachments = mNewEntry.getAttachments(database.attachmentPool, true)
|
||||||
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
|
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
|
||||||
// Not use equals because only check name
|
// Not use equals because only check name
|
||||||
newEntryAttachments.forEach { newAttachment ->
|
newEntryAttachments.forEach { newAttachment ->
|
||||||
oldEntryAttachments.forEach { oldAttachment ->
|
oldEntryAttachments.forEach { oldAttachment ->
|
||||||
if (oldAttachment.name == newAttachment.name
|
if (oldAttachment.name == newAttachment.name
|
||||||
&& oldAttachment.binaryData == newAttachment.binaryData)
|
&& oldAttachment.binaryData == newAttachment.binaryData
|
||||||
attachmentsToRemove.remove(oldAttachment)
|
)
|
||||||
|
attachmentsToRemove.remove(oldAttachment)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Update entry with new values
|
// Update entry with new values
|
||||||
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))
|
mNewEntry.addEntryToHistory(Entry(mOldEntry, copyHistory = false))
|
||||||
database.removeOldestEntryHistory(mOldEntry, database.attachmentPool)
|
database.removeOldestEntryHistory(mNewEntry, database.attachmentPool)
|
||||||
|
|
||||||
// Only change data in index
|
// Only change data in index
|
||||||
database.updateEntry(mOldEntry)
|
database.updateEntry(mNewEntry)
|
||||||
|
|
||||||
// Remove oldest attachments
|
// Remove oldest attachments
|
||||||
attachmentsToRemove.forEach {
|
attachmentsToRemove.forEach {
|
||||||
database.removeAttachmentIfNotUsed(it)
|
database.removeAttachmentIfNotUsed(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nodeFinish(): ActionNodesValues {
|
override fun nodeFinish(): ActionNodesValues {
|
||||||
if (!result.isSuccess) {
|
if (!result.isSuccess) {
|
||||||
mOldEntry.updateWith(mBackupEntryHistory)
|
|
||||||
// If we fail to save, back out changes to global structure
|
// If we fail to save, back out changes to global structure
|
||||||
database.updateEntry(mOldEntry)
|
database.updateEntry(mOldEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
val oldNodesReturn = ArrayList<Node>()
|
val oldNodesReturn = ArrayList<Node>()
|
||||||
oldNodesReturn.add(mBackupEntryHistory)
|
oldNodesReturn.add(mOldEntry)
|
||||||
val newNodesReturn = ArrayList<Node>()
|
val newNodesReturn = ArrayList<Node>()
|
||||||
newNodesReturn.add(mOldEntry)
|
newNodesReturn.add(mNewEntry)
|
||||||
return ActionNodesValues(oldNodesReturn, newNodesReturn)
|
return ActionNodesValues(oldNodesReturn, newNodesReturn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,33 +33,30 @@ class UpdateGroupRunnable constructor(
|
|||||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||||
|
|
||||||
// Keep backup of original values in case save fails
|
|
||||||
private val mBackupGroup: Group = Group(mOldGroup)
|
|
||||||
|
|
||||||
override fun nodeAction() {
|
override fun nodeAction() {
|
||||||
// WARNING : Re attribute parent and children removed in group activity to save memory
|
if (mOldGroup.nodeId == mNewGroup.nodeId) {
|
||||||
mNewGroup.addParentFrom(mOldGroup)
|
// WARNING : Re attribute parent and children removed in group activity to save memory
|
||||||
mNewGroup.addChildrenFrom(mOldGroup)
|
mNewGroup.addParentFrom(mOldGroup)
|
||||||
|
mNewGroup.addChildrenFrom(mOldGroup)
|
||||||
|
|
||||||
// Update group with new values
|
// Update group with new values
|
||||||
mOldGroup.updateWith(mNewGroup)
|
mNewGroup.touch(modified = true, touchParents = true)
|
||||||
mOldGroup.touch(modified = true, touchParents = true)
|
|
||||||
|
|
||||||
// Only change data in index
|
// Only change data in index
|
||||||
database.updateGroup(mOldGroup)
|
database.updateGroup(mNewGroup)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nodeFinish(): ActionNodesValues {
|
override fun nodeFinish(): ActionNodesValues {
|
||||||
if (!result.isSuccess) {
|
if (!result.isSuccess) {
|
||||||
// If we fail to save, back out changes to global structure
|
// If we fail to save, back out changes to global structure
|
||||||
mOldGroup.updateWith(mBackupGroup)
|
|
||||||
database.updateGroup(mOldGroup)
|
database.updateGroup(mOldGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
val oldNodesReturn = ArrayList<Node>()
|
val oldNodesReturn = ArrayList<Node>()
|
||||||
oldNodesReturn.add(mBackupGroup)
|
oldNodesReturn.add(mOldGroup)
|
||||||
val newNodesReturn = ArrayList<Node>()
|
val newNodesReturn = ArrayList<Node>()
|
||||||
newNodesReturn.add(mOldGroup)
|
newNodesReturn.add(mNewGroup)
|
||||||
return ActionNodesValues(oldNodesReturn, newNodesReturn)
|
return ActionNodesValues(oldNodesReturn, newNodesReturn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ abstract class CipherEngine {
|
|||||||
return 16
|
return 16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used only with padding workaround
|
||||||
|
var forcePaddingCompatibility = false
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||||
abstract fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher
|
abstract fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class TwofishEngine : CipherEngine() {
|
|||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||||
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
|
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
|
||||||
return CipherFactory.getTwofish(opmode, key, IV)
|
return CipherFactory.getTwofish(opmode, key, IV, forcePaddingCompatibility)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getEncryptionAlgorithm(): EncryptionAlgorithm {
|
override fun getEncryptionAlgorithm(): EncryptionAlgorithm {
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
|
|||||||
entry.expires
|
entry.expires
|
||||||
))
|
))
|
||||||
|
|
||||||
for (element in entry.customFields.entries) {
|
entry.doForEachDecodedCustomField { field ->
|
||||||
extraFieldCursor.addExtraField(entryId, element.key, element.value)
|
extraFieldCursor.addExtraField(entryId, field)
|
||||||
}
|
}
|
||||||
|
|
||||||
entryId++
|
entryId++
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.database.cursor
|
|||||||
|
|
||||||
import android.database.MatrixCursor
|
import android.database.MatrixCursor
|
||||||
import android.provider.BaseColumns
|
import android.provider.BaseColumns
|
||||||
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
@@ -36,13 +37,17 @@ class ExtraFieldCursor : MatrixCursor(arrayOf(
|
|||||||
private var fieldId: Long = 0
|
private var fieldId: Long = 0
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun addExtraField(entryId: Long, label: String, value: ProtectedString) {
|
fun addExtraField(entryId: Long, field: Field) {
|
||||||
addRow(arrayOf(fieldId, entryId, label, if (value.isProtected) 1 else 0, value.toString()))
|
addRow(arrayOf(fieldId,
|
||||||
|
entryId,
|
||||||
|
field.name,
|
||||||
|
if (field.protectedValue.isProtected) 1 else 0,
|
||||||
|
field.protectedValue.toString()))
|
||||||
fieldId++
|
fieldId++
|
||||||
}
|
}
|
||||||
|
|
||||||
fun populateExtraFieldInEntry(pwEntry: EntryKDBX) {
|
fun populateExtraFieldInEntry(pwEntry: EntryKDBX) {
|
||||||
pwEntry.putExtraField(getString(getColumnIndex(COLUMN_LABEL)),
|
pwEntry.putField(getString(getColumnIndex(COLUMN_LABEL)),
|
||||||
ProtectedString(getInt(getColumnIndex(COLUMN_PROTECTION)) > 0,
|
ProtectedString(getInt(getColumnIndex(COLUMN_PROTECTION)) > 0,
|
||||||
getString(getColumnIndex(COLUMN_VALUE))))
|
getString(getColumnIndex(COLUMN_VALUE))))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.kunzisoft.keepass.database.element
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.utils.ParcelableUtil
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class CustomData : Parcelable {
|
||||||
|
|
||||||
|
private val mCustomDataItems = HashMap<String, CustomDataItem>()
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
|
||||||
|
constructor(toCopy: CustomData) {
|
||||||
|
mCustomDataItems.clear()
|
||||||
|
mCustomDataItems.putAll(toCopy.mCustomDataItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) {
|
||||||
|
ParcelableUtil.readStringParcelableMap(parcel, CustomDataItem::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(key: String): CustomDataItem? {
|
||||||
|
return mCustomDataItems[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun put(customDataItem: CustomDataItem) {
|
||||||
|
mCustomDataItems[customDataItem.key] = customDataItem
|
||||||
|
}
|
||||||
|
|
||||||
|
fun containsItemWithValue(value: String): Boolean {
|
||||||
|
return mCustomDataItems.any { mapEntry -> mapEntry.value.value.equals(value, true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun containsItemWithLastModificationTime(): Boolean {
|
||||||
|
return mCustomDataItems.any { mapEntry -> mapEntry.value.lastModificationTime != null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isNotEmpty(): Boolean {
|
||||||
|
return mCustomDataItems.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doForEachItems(action: (CustomDataItem) -> Unit) {
|
||||||
|
for ((_, value) in mCustomDataItems) {
|
||||||
|
action.invoke(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
ParcelableUtil.writeStringParcelableMap(parcel, flags, mCustomDataItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<CustomData> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): CustomData {
|
||||||
|
return CustomData(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<CustomData?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.kunzisoft.keepass.database.element
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
class CustomDataItem : Parcelable {
|
||||||
|
|
||||||
|
val key: String
|
||||||
|
var value: String
|
||||||
|
var lastModificationTime: DateInstant? = null
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) {
|
||||||
|
key = parcel.readString() ?: ""
|
||||||
|
value = parcel.readString() ?: ""
|
||||||
|
lastModificationTime = parcel.readParcelable(DateInstant::class.java.classLoader)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(key: String, value: String, lastModificationTime: DateInstant? = null) {
|
||||||
|
this.key = key
|
||||||
|
this.value = value
|
||||||
|
this.lastModificationTime = lastModificationTime
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeString(key)
|
||||||
|
parcel.writeString(value)
|
||||||
|
parcel.writeParcelable(lastModificationTime, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<CustomDataItem> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): CustomDataItem {
|
||||||
|
return CustomDataItem(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<CustomDataItem?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,10 +20,12 @@
|
|||||||
package com.kunzisoft.keepass.database.element
|
package com.kunzisoft.keepass.database.element
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.utils.readBytes4ToUInt
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
@@ -40,10 +42,12 @@ import com.kunzisoft.keepass.database.element.icon.IconsManager
|
|||||||
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
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateEngine
|
||||||
import com.kunzisoft.keepass.database.exception.*
|
import com.kunzisoft.keepass.database.exception.*
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
|
||||||
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
|
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
|
||||||
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX
|
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX
|
||||||
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
|
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
|
||||||
@@ -55,6 +59,7 @@ import com.kunzisoft.keepass.model.MainCredential
|
|||||||
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 com.kunzisoft.keepass.utils.readBytes4ToUInt
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
@@ -105,10 +110,6 @@ class Database {
|
|||||||
return mDatabaseKDB?.binaryCache ?: mDatabaseKDBX?.binaryCache ?: BinaryCache()
|
return mDatabaseKDB?.binaryCache ?: mDatabaseKDBX?.binaryCache ?: BinaryCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCacheDirectory(cacheDirectory: File) {
|
|
||||||
binaryCache.cacheDirectory = cacheDirectory
|
|
||||||
}
|
|
||||||
|
|
||||||
private val iconsManager: IconsManager
|
private val iconsManager: IconsManager
|
||||||
get() {
|
get() {
|
||||||
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager(binaryCache)
|
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager(binaryCache)
|
||||||
@@ -146,6 +147,57 @@ class Database {
|
|||||||
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
|
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getTemplates(templateCreation: Boolean): List<Template> {
|
||||||
|
return mDatabaseKDBX?.getTemplates(templateCreation) ?: listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTemplate(entry: Entry): Template? {
|
||||||
|
if (entryIsTemplate(entry))
|
||||||
|
return TemplateEngine.CREATION
|
||||||
|
entry.entryKDBX?.let { entryKDBX ->
|
||||||
|
return mDatabaseKDBX?.getTemplate(entryKDBX)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun entryIsTemplate(entry: Entry?): Boolean {
|
||||||
|
// Define is current entry is a template (in direct template group)
|
||||||
|
if (entry == null || templatesGroup == null)
|
||||||
|
return false
|
||||||
|
return templatesGroup == entry.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not the same as decode, here remove in all cases the template link in the entry data
|
||||||
|
fun removeTemplateConfiguration(entry: Entry): Entry {
|
||||||
|
entry.entryKDBX?.let {
|
||||||
|
mDatabaseKDBX?.decodeEntryWithTemplateConfiguration(it, false)?.let { decode ->
|
||||||
|
return Entry(decode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the template link in the entry data if it's a basic entry
|
||||||
|
// or compress the template fields (as pseudo language) if it's a template entry
|
||||||
|
fun decodeEntryWithTemplateConfiguration(entry: Entry, lastEntryVersion: Entry? = null): Entry {
|
||||||
|
entry.entryKDBX?.let {
|
||||||
|
val lastEntry = lastEntryVersion ?: entry
|
||||||
|
mDatabaseKDBX?.decodeEntryWithTemplateConfiguration(it, entryIsTemplate(lastEntry))?.let { decode ->
|
||||||
|
return Entry(decode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encodeEntryWithTemplateConfiguration(entry: Entry, template: Template): Entry {
|
||||||
|
entry.entryKDBX?.let {
|
||||||
|
mDatabaseKDBX?.encodeEntryWithTemplateConfiguration(it, entryIsTemplate(entry), template)?.let { encode ->
|
||||||
|
return Entry(encode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
val allowName: Boolean
|
val allowName: Boolean
|
||||||
get() = mDatabaseKDBX != null
|
get() = mDatabaseKDBX != null
|
||||||
|
|
||||||
@@ -226,7 +278,7 @@ class Database {
|
|||||||
// Default compression not necessary if stored in header
|
// Default compression not necessary if stored in header
|
||||||
mDatabaseKDBX?.let {
|
mDatabaseKDBX?.let {
|
||||||
return it.compressionAlgorithm == CompressionAlgorithm.GZip
|
return it.compressionAlgorithm == CompressionAlgorithm.GZip
|
||||||
&& it.kdbxVersion.isBefore(FILE_VERSION_32_4)
|
&& it.kdbxVersion.isBefore(FILE_VERSION_40)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -319,6 +371,15 @@ class Database {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not modify groups here, used for read only
|
||||||
|
*/
|
||||||
|
fun getAllGroupsWithoutRoot(): List<Group> {
|
||||||
|
return mDatabaseKDB?.getAllGroupsWithoutRoot()?.map { Group(it) }
|
||||||
|
?: mDatabaseKDBX?.getAllGroupsWithoutRoot()?.map { Group(it) }
|
||||||
|
?: listOf()
|
||||||
|
}
|
||||||
|
|
||||||
val manageHistory: Boolean
|
val manageHistory: Boolean
|
||||||
get() = mDatabaseKDBX != null
|
get() = mDatabaseKDBX != null
|
||||||
|
|
||||||
@@ -345,12 +406,18 @@ class Database {
|
|||||||
val allowConfigurableRecycleBin: Boolean
|
val allowConfigurableRecycleBin: Boolean
|
||||||
get() = mDatabaseKDBX != null
|
get() = mDatabaseKDBX != null
|
||||||
|
|
||||||
var isRecycleBinEnabled: Boolean
|
val isRecycleBinEnabled: Boolean
|
||||||
// Backup is always enabled in KDB database
|
// Backup is always enabled in KDB database
|
||||||
get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false
|
get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false
|
||||||
set(value) {
|
|
||||||
mDatabaseKDBX?.isRecycleBinEnabled = value
|
fun enableRecycleBin(enable: Boolean, resources: Resources) {
|
||||||
|
mDatabaseKDBX?.isRecycleBinEnabled = enable
|
||||||
|
if (enable) {
|
||||||
|
ensureRecycleBinExists(resources)
|
||||||
|
} else {
|
||||||
|
mDatabaseKDBX?.removeRecycleBin()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val recycleBin: Group?
|
val recycleBin: Group?
|
||||||
get() {
|
get() {
|
||||||
@@ -363,16 +430,52 @@ class Database {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ensureRecycleBinExists(resources: Resources) {
|
fun setRecycleBin(group: Group?) {
|
||||||
mDatabaseKDB?.ensureBackupExists()
|
// Only the kdbx recycle bin can be changed
|
||||||
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
if (group != null) {
|
||||||
|
mDatabaseKDBX?.recycleBinUUID = group.nodeIdKDBX.id
|
||||||
|
} else {
|
||||||
|
mDatabaseKDBX?.removeTemplatesGroup()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeRecycleBin() {
|
/**
|
||||||
// Don't allow remove backup in KDB
|
* Determine if a configurable templates group is available or not for this version of database
|
||||||
mDatabaseKDBX?.removeRecycleBin()
|
* @return true if a configurable templates group available
|
||||||
|
*/
|
||||||
|
val allowConfigurableTemplatesGroup: Boolean
|
||||||
|
get() = mDatabaseKDBX != null
|
||||||
|
|
||||||
|
// Maybe another templates method with KDBX5
|
||||||
|
val isTemplatesEnabled: Boolean
|
||||||
|
get() = mDatabaseKDBX?.isTemplatesGroupEnabled() ?: false
|
||||||
|
|
||||||
|
fun enableTemplates(enable: Boolean, templatesGroupName: String) {
|
||||||
|
mDatabaseKDBX?.enableTemplatesGroup(enable, templatesGroupName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val templatesGroup: Group?
|
||||||
|
get() {
|
||||||
|
mDatabaseKDBX?.getTemplatesGroup()?.let {
|
||||||
|
return Group(it)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTemplatesGroup(group: Group?) {
|
||||||
|
// Only the kdbx templates group can be changed
|
||||||
|
if (group != null) {
|
||||||
|
mDatabaseKDBX?.entryTemplatesGroup = group.nodeIdKDBX.id
|
||||||
|
} else {
|
||||||
|
mDatabaseKDBX?.entryTemplatesGroup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val groupNamesNotAllowed: List<String>
|
||||||
|
get() {
|
||||||
|
return mDatabaseKDB?.groupNamesNotAllowed ?: ArrayList()
|
||||||
|
}
|
||||||
|
|
||||||
private fun setDatabaseKDB(databaseKDB: DatabaseKDB) {
|
private fun setDatabaseKDB(databaseKDB: DatabaseKDB) {
|
||||||
this.mDatabaseKDB = databaseKDB
|
this.mDatabaseKDB = databaseKDB
|
||||||
this.mDatabaseKDBX = null
|
this.mDatabaseKDBX = null
|
||||||
@@ -383,8 +486,11 @@ class Database {
|
|||||||
this.mDatabaseKDBX = databaseKDBX
|
this.mDatabaseKDBX = databaseKDBX
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
|
fun createData(databaseUri: Uri,
|
||||||
val newDatabase = DatabaseKDBX(databaseName, rootName)
|
databaseName: String,
|
||||||
|
rootName: String,
|
||||||
|
templateGroupName: String?) {
|
||||||
|
val newDatabase = DatabaseKDBX(databaseName, rootName, templateGroupName)
|
||||||
setDatabaseKDBX(newDatabase)
|
setDatabaseKDBX(newDatabase)
|
||||||
this.fileUri = databaseUri
|
this.fileUri = databaseUri
|
||||||
// Set Database state
|
// Set Database state
|
||||||
@@ -546,25 +652,28 @@ class Database {
|
|||||||
omitBackup: Boolean,
|
omitBackup: Boolean,
|
||||||
max: Int = Integer.MAX_VALUE): Group? {
|
max: Int = Integer.MAX_VALUE): Group? {
|
||||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
||||||
searchQuery, SearchParameters(), omitBackup, max)
|
SearchParameters().apply {
|
||||||
|
this.searchQuery = searchQuery
|
||||||
|
}, omitBackup, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createVirtualGroupFromSearchInfo(searchInfoString: String,
|
fun createVirtualGroupFromSearchInfo(searchInfoString: String,
|
||||||
omitBackup: Boolean,
|
omitBackup: Boolean,
|
||||||
max: Int = Integer.MAX_VALUE): Group? {
|
max: Int = Integer.MAX_VALUE): Group? {
|
||||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
||||||
searchInfoString, SearchParameters().apply {
|
SearchParameters().apply {
|
||||||
searchInTitles = true
|
searchQuery = searchInfoString
|
||||||
searchInUserNames = false
|
searchInTitles = true
|
||||||
searchInPasswords = false
|
searchInUserNames = false
|
||||||
searchInUrls = true
|
searchInPasswords = false
|
||||||
searchInNotes = true
|
searchInUrls = true
|
||||||
searchInOTP = false
|
searchInNotes = true
|
||||||
searchInOther = true
|
searchInOTP = false
|
||||||
searchInUUIDs = false
|
searchInOther = true
|
||||||
searchInTags = false
|
searchInUUIDs = false
|
||||||
ignoreCase = true
|
searchInTags = false
|
||||||
}, omitBackup, max)
|
searchInTemplates = false
|
||||||
|
}, omitBackup, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
val attachmentPool: AttachmentPool
|
val attachmentPool: AttachmentPool
|
||||||
@@ -581,10 +690,11 @@ class Database {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildNewBinaryAttachment(compressed: Boolean = false,
|
fun buildNewBinaryAttachment(): BinaryData? {
|
||||||
protected: Boolean = false): BinaryData? {
|
|
||||||
return mDatabaseKDB?.buildNewAttachment()
|
return mDatabaseKDB?.buildNewAttachment()
|
||||||
?: mDatabaseKDBX?.buildNewAttachment( false, compressed, protected)
|
?: mDatabaseKDBX?.buildNewAttachment( false,
|
||||||
|
compressionForNewEntry(),
|
||||||
|
false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAttachmentIfNotUsed(attachment: Attachment) {
|
fun removeAttachmentIfNotUsed(attachment: Attachment) {
|
||||||
@@ -675,8 +785,8 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearAndClose(filesDirectory: File? = null) {
|
fun clearAndClose(context: Context? = null) {
|
||||||
clear(filesDirectory)
|
clear(context?.let { UriUtil.getBinaryDir(context) })
|
||||||
this.mDatabaseKDB = null
|
this.mDatabaseKDB = null
|
||||||
this.mDatabaseKDBX = null
|
this.mDatabaseKDBX = null
|
||||||
this.fileUri = null
|
this.fileUri = null
|
||||||
@@ -794,11 +904,11 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addGroupTo(group: Group, parent: Group) {
|
fun addGroupTo(group: Group, parent: Group) {
|
||||||
group.groupKDB?.let { entryKDB ->
|
group.groupKDB?.let { groupKDB ->
|
||||||
mDatabaseKDB?.addGroupTo(entryKDB, parent.groupKDB)
|
mDatabaseKDB?.addGroupTo(groupKDB, parent.groupKDB)
|
||||||
}
|
}
|
||||||
group.groupKDBX?.let { entryKDBX ->
|
group.groupKDBX?.let { groupKDBX ->
|
||||||
mDatabaseKDBX?.addGroupTo(entryKDBX, parent.groupKDBX)
|
mDatabaseKDBX?.addGroupTo(groupKDBX, parent.groupKDBX)
|
||||||
}
|
}
|
||||||
group.afterAssignNewParent()
|
group.afterAssignNewParent()
|
||||||
}
|
}
|
||||||
@@ -813,11 +923,11 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun removeGroupFrom(group: Group, parent: Group) {
|
fun removeGroupFrom(group: Group, parent: Group) {
|
||||||
group.groupKDB?.let { entryKDB ->
|
group.groupKDB?.let { groupKDB ->
|
||||||
mDatabaseKDB?.removeGroupFrom(entryKDB, parent.groupKDB)
|
mDatabaseKDB?.removeGroupFrom(groupKDB, parent.groupKDB)
|
||||||
}
|
}
|
||||||
group.groupKDBX?.let { entryKDBX ->
|
group.groupKDBX?.let { groupKDBX ->
|
||||||
mDatabaseKDBX?.removeGroupFrom(entryKDBX, parent.groupKDBX)
|
mDatabaseKDBX?.removeGroupFrom(groupKDBX, parent.groupKDBX)
|
||||||
}
|
}
|
||||||
group.afterAssignNewParent()
|
group.afterAssignNewParent()
|
||||||
}
|
}
|
||||||
@@ -892,6 +1002,11 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ensureRecycleBinExists(resources: Resources) {
|
||||||
|
mDatabaseKDB?.ensureBackupExists()
|
||||||
|
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
||||||
|
}
|
||||||
|
|
||||||
fun canRecycle(entry: Entry): Boolean {
|
fun canRecycle(entry: Entry): Boolean {
|
||||||
var canRecycle: Boolean? = null
|
var canRecycle: Boolean? = null
|
||||||
entry.entryKDB?.let {
|
entry.entryKDB?.let {
|
||||||
|
|||||||
@@ -23,95 +23,209 @@ 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 com.kunzisoft.keepass.utils.readEnum
|
||||||
import org.joda.time.Instant
|
import com.kunzisoft.keepass.utils.writeEnum
|
||||||
|
import org.joda.time.*
|
||||||
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class DateInstant : Parcelable {
|
class DateInstant : Parcelable {
|
||||||
|
|
||||||
private var jDate: Date = Date()
|
private var jDate: Date = Date()
|
||||||
|
private var mType: Type = Type.DATE_TIME
|
||||||
|
|
||||||
val date: Date
|
val date: Date
|
||||||
get() = jDate
|
get() = jDate
|
||||||
|
|
||||||
|
var type: Type
|
||||||
|
get() = mType
|
||||||
|
set(value) {
|
||||||
|
mType = value
|
||||||
|
}
|
||||||
|
|
||||||
constructor(source: DateInstant) {
|
constructor(source: DateInstant) {
|
||||||
this.jDate = Date(source.jDate.time)
|
this.jDate = Date(source.jDate.time)
|
||||||
|
this.mType = source.mType
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(date: Date) {
|
constructor(date: Date, type: Type = Type.DATE_TIME) {
|
||||||
jDate = Date(date.time)
|
jDate = Date(date.time)
|
||||||
|
mType = type
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(millis: Long) {
|
constructor(millis: Long, type: Type = Type.DATE_TIME) {
|
||||||
jDate = Date(millis)
|
jDate = Date(millis)
|
||||||
|
mType = type
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(string: String) {
|
private fun parse(value: String, type: Type): Date {
|
||||||
jDate = dateFormat.parse(string) ?: jDate
|
return when (type) {
|
||||||
|
Type.DATE -> dateFormat.parse(value) ?: jDate
|
||||||
|
Type.TIME -> timeFormat.parse(value) ?: jDate
|
||||||
|
else -> dateTimeFormat.parse(value) ?: jDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(string: String, type: Type = Type.DATE_TIME) {
|
||||||
|
try {
|
||||||
|
jDate = parse(string, type)
|
||||||
|
mType = type
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Retry with second format
|
||||||
|
try {
|
||||||
|
when (type) {
|
||||||
|
Type.TIME -> {
|
||||||
|
jDate = parse(string, Type.DATE)
|
||||||
|
mType = Type.DATE
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
jDate = parse(string, Type.TIME)
|
||||||
|
mType = Type.TIME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Retry with third format
|
||||||
|
when (type) {
|
||||||
|
Type.DATE, Type.TIME -> {
|
||||||
|
jDate = parse(string, Type.DATE_TIME)
|
||||||
|
mType = Type.DATE_TIME
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
jDate = parse(string, Type.DATE)
|
||||||
|
mType = Type.DATE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(type: Type) {
|
||||||
|
mType = type
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
jDate = Date()
|
jDate = Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected constructor(parcel: Parcel) {
|
constructor(parcel: Parcel) {
|
||||||
jDate = parcel.readSerializable() as Date
|
jDate = parcel.readSerializable() as? Date? ?: jDate
|
||||||
|
mType = parcel.readEnum<Type>() ?: mType
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDateTimeString(resources: Resources): String {
|
|
||||||
return Companion.getDateTimeString(resources, this.date)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
dest.writeSerializable(date)
|
dest.writeSerializable(jDate)
|
||||||
|
dest.writeEnum(mType)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
fun getDateTimeString(resources: Resources): String {
|
||||||
if (this === other) {
|
return when (mType) {
|
||||||
return true
|
Type.DATE -> DateFormat.getDateInstance(
|
||||||
|
DateFormat.MEDIUM,
|
||||||
|
ConfigurationCompat.getLocales(resources.configuration)[0])
|
||||||
|
.format(jDate)
|
||||||
|
Type.TIME -> DateFormat.getTimeInstance(
|
||||||
|
DateFormat.SHORT,
|
||||||
|
ConfigurationCompat.getLocales(resources.configuration)[0])
|
||||||
|
.format(jDate)
|
||||||
|
else -> DateFormat.getDateTimeInstance(
|
||||||
|
DateFormat.MEDIUM,
|
||||||
|
DateFormat.SHORT,
|
||||||
|
ConfigurationCompat.getLocales(resources.configuration)[0])
|
||||||
|
.format(jDate)
|
||||||
}
|
}
|
||||||
if (other == null) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (javaClass != other.javaClass) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val date = other as DateInstant
|
|
||||||
return isSameDate(jDate, date.jDate)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
fun getYearInt(): Int {
|
||||||
return jDate.hashCode()
|
val dateFormat = SimpleDateFormat("yyyy", Locale.ENGLISH)
|
||||||
|
return dateFormat.format(date).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMonthInt(): Int {
|
||||||
|
val dateFormat = SimpleDateFormat("MM", Locale.ENGLISH)
|
||||||
|
return dateFormat.format(date).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDay(): Int {
|
||||||
|
val dateFormat = SimpleDateFormat("dd", Locale.ENGLISH)
|
||||||
|
return dateFormat.format(date).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If expireDate is before NEVER_EXPIRE date less 1 month (to be sure)
|
||||||
|
// it is not expires
|
||||||
|
fun isNeverExpires(): Boolean {
|
||||||
|
return LocalDateTime(jDate)
|
||||||
|
.isBefore(
|
||||||
|
LocalDateTime.fromDateFields(NEVER_EXPIRES.date)
|
||||||
|
.minusMonths(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isCurrentlyExpire(): Boolean {
|
||||||
|
return when (type) {
|
||||||
|
Type.DATE -> LocalDate.fromDateFields(jDate).isBefore(LocalDate.now())
|
||||||
|
Type.TIME -> LocalTime.fromDateFields(jDate).isBefore(LocalTime.now())
|
||||||
|
else -> LocalDateTime.fromDateFields(jDate).isBefore(LocalDateTime.now())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return dateFormat.format(jDate)
|
return when (type) {
|
||||||
|
Type.DATE -> dateFormat.format(jDate)
|
||||||
|
Type.TIME -> timeFormat.format(jDate)
|
||||||
|
else -> dateTimeFormat.format(jDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is DateInstant) return false
|
||||||
|
|
||||||
|
if (jDate != other.jDate) return false
|
||||||
|
if (mType != other.mType) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = jDate.hashCode()
|
||||||
|
result = 31 * result + mType.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Type {
|
||||||
|
DATE_TIME, DATE, TIME
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val NEVER_EXPIRE = neverExpire
|
val NEVER_EXPIRES = DateInstant(Calendar.getInstance().apply {
|
||||||
val IN_ONE_MONTH = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
|
set(Calendar.YEAR, 2999)
|
||||||
private val dateFormat = SimpleDateFormat.getDateTimeInstance()
|
set(Calendar.MONTH, 11)
|
||||||
|
set(Calendar.DAY_OF_MONTH, 28)
|
||||||
|
set(Calendar.HOUR, 23)
|
||||||
|
set(Calendar.MINUTE, 59)
|
||||||
|
set(Calendar.SECOND, 59)
|
||||||
|
}.time)
|
||||||
|
val IN_ONE_MONTH_DATE_TIME = DateInstant(
|
||||||
|
Instant.now().plus(Duration.standardDays(30)).toDate(), Type.DATE_TIME)
|
||||||
|
val IN_ONE_MONTH_DATE = DateInstant(
|
||||||
|
Instant.now().plus(Duration.standardDays(30)).toDate(), Type.DATE)
|
||||||
|
val IN_ONE_HOUR_TIME = DateInstant(
|
||||||
|
Instant.now().plus(Duration.standardHours(1)).toDate(), Type.TIME)
|
||||||
|
|
||||||
private val neverExpire: DateInstant
|
private val dateTimeFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.ROOT).apply {
|
||||||
get() {
|
timeZone = TimeZone.getTimeZone("UTC")
|
||||||
val cal = Calendar.getInstance()
|
}
|
||||||
cal.set(Calendar.YEAR, 2999)
|
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'Z'", Locale.ROOT).apply {
|
||||||
cal.set(Calendar.MONTH, 11)
|
timeZone = TimeZone.getTimeZone("UTC")
|
||||||
cal.set(Calendar.DAY_OF_MONTH, 28)
|
}
|
||||||
cal.set(Calendar.HOUR, 23)
|
private val timeFormat = SimpleDateFormat("HH:mm'Z'", Locale.ROOT).apply {
|
||||||
cal.set(Calendar.MINUTE, 59)
|
timeZone = TimeZone.getTimeZone("UTC")
|
||||||
cal.set(Calendar.SECOND, 59)
|
}
|
||||||
|
|
||||||
return DateInstant(cal.time)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val CREATOR: Parcelable.Creator<DateInstant> = object : Parcelable.Creator<DateInstant> {
|
val CREATOR: Parcelable.Creator<DateInstant> = object : Parcelable.Creator<DateInstant> {
|
||||||
@@ -123,31 +237,5 @@ class DateInstant : Parcelable {
|
|||||||
return arrayOfNulls(size)
|
return arrayOfNulls(size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isSameDate(d1: Date, d2: Date): Boolean {
|
|
||||||
val cal1 = Calendar.getInstance()
|
|
||||||
cal1.time = d1
|
|
||||||
cal1.set(Calendar.MILLISECOND, 0)
|
|
||||||
|
|
||||||
val cal2 = Calendar.getInstance()
|
|
||||||
cal2.time = d2
|
|
||||||
cal2.set(Calendar.MILLISECOND, 0)
|
|
||||||
|
|
||||||
return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
|
|
||||||
cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH) &&
|
|
||||||
cal1.get(Calendar.DAY_OF_MONTH) == cal2.get(Calendar.DAY_OF_MONTH) &&
|
|
||||||
cal1.get(Calendar.HOUR) == cal2.get(Calendar.HOUR) &&
|
|
||||||
cal1.get(Calendar.MINUTE) == cal2.get(Calendar.MINUTE) &&
|
|
||||||
cal1.get(Calendar.SECOND) == cal2.get(Calendar.SECOND)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDateTimeString(resources: Resources, date: Date): String {
|
|
||||||
return java.text.DateFormat.getDateTimeInstance(
|
|
||||||
java.text.DateFormat.MEDIUM,
|
|
||||||
java.text.DateFormat.SHORT,
|
|
||||||
ConfigurationCompat.getLocales(resources.configuration)[0])
|
|
||||||
.format(date)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,30 +19,37 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element
|
package com.kunzisoft.keepass.database.element
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.ParcelUuid
|
||||||
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||||
import java.util.Date
|
import java.util.*
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
class DeletedObject {
|
class DeletedObject : Parcelable {
|
||||||
|
|
||||||
var uuid: UUID = DatabaseVersioned.UUID_ZERO
|
var uuid: UUID = DatabaseVersioned.UUID_ZERO
|
||||||
private var mDeletionTime: Date? = null
|
private var mDeletionTime: DateInstant? = null
|
||||||
|
|
||||||
fun getDeletionTime(): Date {
|
constructor()
|
||||||
|
|
||||||
|
constructor(uuid: UUID, deletionTime: DateInstant = DateInstant()) {
|
||||||
|
this.uuid = uuid
|
||||||
|
this.mDeletionTime = deletionTime
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) {
|
||||||
|
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
|
||||||
|
mDeletionTime = parcel.readParcelable(DateInstant::class.java.classLoader)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDeletionTime(): DateInstant {
|
||||||
if (mDeletionTime == null) {
|
if (mDeletionTime == null) {
|
||||||
mDeletionTime = Date(System.currentTimeMillis())
|
mDeletionTime = DateInstant(System.currentTimeMillis())
|
||||||
}
|
}
|
||||||
return mDeletionTime!!
|
return mDeletionTime!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setDeletionTime(deletionTime: Date) {
|
fun setDeletionTime(deletionTime: DateInstant) {
|
||||||
this.mDeletionTime = deletionTime
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor()
|
|
||||||
|
|
||||||
constructor(uuid: UUID, deletionTime: Date = Date()) {
|
|
||||||
this.uuid = uuid
|
|
||||||
this.mDeletionTime = deletionTime
|
this.mDeletionTime = deletionTime
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,4 +66,23 @@ class DeletedObject {
|
|||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return uuid.hashCode()
|
return uuid.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeParcelable(ParcelUuid(uuid), flags)
|
||||||
|
parcel.writeParcelable(mDeletionTime, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<DeletedObject> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): DeletedObject {
|
||||||
|
return DeletedObject(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<DeletedObject?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import android.os.Parcel
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
|
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||||
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
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
|
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
|
||||||
@@ -32,7 +33,6 @@ 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.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.Field
|
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -45,15 +45,6 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
var entryKDBX: EntryKDBX? = null
|
var entryKDBX: EntryKDBX? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
fun updateWith(entry: Entry, copyHistory: Boolean = true) {
|
|
||||||
entry.entryKDB?.let {
|
|
||||||
this.entryKDB?.updateWith(it)
|
|
||||||
}
|
|
||||||
entry.entryKDBX?.let {
|
|
||||||
this.entryKDBX?.updateWith(it, copyHistory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use this constructor to copy an Entry with exact same values
|
* Use this constructor to copy an Entry with exact same values
|
||||||
*/
|
*/
|
||||||
@@ -64,7 +55,12 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
if (entry.entryKDBX != null) {
|
if (entry.entryKDBX != null) {
|
||||||
this.entryKDBX = EntryKDBX()
|
this.entryKDBX = EntryKDBX()
|
||||||
}
|
}
|
||||||
updateWith(entry, copyHistory)
|
entry.entryKDB?.let {
|
||||||
|
this.entryKDB?.updateWith(it)
|
||||||
|
}
|
||||||
|
entry.entryKDBX?.let {
|
||||||
|
this.entryKDBX?.updateWith(it, copyHistory)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(entry: EntryKDB) {
|
constructor(entry: EntryKDB) {
|
||||||
@@ -114,6 +110,20 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
entryKDBX?.icon = value
|
entryKDBX?.icon = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var tags: Tags
|
||||||
|
get() = entryKDBX?.tags ?: Tags()
|
||||||
|
set(value) {
|
||||||
|
entryKDBX?.tags = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO
|
||||||
|
get() = entryKDBX?.previousParentGroup ?: DatabaseVersioned.UUID_ZERO
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun setPreviousParentGroup(previousParent: Group?) {
|
||||||
|
entryKDBX?.previousParentGroup = previousParent?.groupKDBX?.id ?: DatabaseVersioned.UUID_ZERO
|
||||||
|
}
|
||||||
|
|
||||||
override val type: Type
|
override val type: Type
|
||||||
get() = Type.ENTRY
|
get() = Type.ENTRY
|
||||||
|
|
||||||
@@ -268,8 +278,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
fun getExtraFields(): List<Field> {
|
fun getExtraFields(): List<Field> {
|
||||||
val extraFields = ArrayList<Field>()
|
val extraFields = ArrayList<Field>()
|
||||||
entryKDBX?.let {
|
entryKDBX?.let {
|
||||||
for (field in it.customFields) {
|
it.doForEachDecodedCustomField { field ->
|
||||||
extraFields.add(Field(field.key, field.value))
|
extraFields.add(field)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return extraFields
|
return extraFields
|
||||||
@@ -279,7 +289,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
* Update or add an extra field to the list (standard or custom)
|
* Update or add an extra field to the list (standard or custom)
|
||||||
*/
|
*/
|
||||||
fun putExtraField(field: Field) {
|
fun putExtraField(field: Field) {
|
||||||
entryKDBX?.putExtraField(field.name, field.protectedValue)
|
entryKDBX?.putField(field)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addExtraFields(fields: List<Field>) {
|
private fun addExtraFields(fields: List<Field>) {
|
||||||
@@ -295,7 +305,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
fun getOtpElement(): OtpElement? {
|
fun getOtpElement(): OtpElement? {
|
||||||
entryKDBX?.let {
|
entryKDBX?.let {
|
||||||
return OtpEntryFields.parseFields { key ->
|
return OtpEntryFields.parseFields { key ->
|
||||||
it.customFields[key]?.toString()
|
it.getFieldValue(key)?.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@@ -373,10 +383,6 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
return entryKDBX?.getSize(attachmentPool) ?: 0L
|
return entryKDBX?.getSize(attachmentPool) ?: 0L
|
||||||
}
|
}
|
||||||
|
|
||||||
fun containsCustomData(): Boolean {
|
|
||||||
return entryKDBX?.containsCustomData() ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
------------
|
------------
|
||||||
Converter
|
Converter
|
||||||
@@ -387,37 +393,46 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
* Retrieve generated entry info.
|
* Retrieve generated entry info.
|
||||||
* If are not [raw] data, remove parameter fields and add auto generated elements in auto custom fields
|
* If are not [raw] data, remove parameter fields and add auto generated elements in auto custom fields
|
||||||
*/
|
*/
|
||||||
fun getEntryInfo(database: Database?, raw: Boolean = false): EntryInfo {
|
fun getEntryInfo(database: Database?,
|
||||||
|
raw: Boolean = false,
|
||||||
|
removeTemplateConfiguration: Boolean = true): EntryInfo {
|
||||||
val entryInfo = EntryInfo()
|
val entryInfo = EntryInfo()
|
||||||
if (raw)
|
// Remove unwanted template fields
|
||||||
database?.stopManageEntry(this)
|
val baseInfo = if (removeTemplateConfiguration)
|
||||||
|
database?.removeTemplateConfiguration(this) ?: this
|
||||||
else
|
else
|
||||||
database?.startManageEntry(this)
|
this
|
||||||
|
baseInfo.apply {
|
||||||
|
if (raw)
|
||||||
|
database?.stopManageEntry(this)
|
||||||
|
else
|
||||||
|
database?.startManageEntry(this)
|
||||||
|
|
||||||
entryInfo.id = nodeId.toString()
|
entryInfo.id = nodeId.id
|
||||||
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.creationTime = creationTime
|
entryInfo.creationTime = creationTime
|
||||||
entryInfo.lastModificationTime = lastModificationTime
|
entryInfo.lastModificationTime = lastModificationTime
|
||||||
entryInfo.expires = expires
|
entryInfo.expires = expires
|
||||||
entryInfo.expiryTime = expiryTime
|
entryInfo.expiryTime = expiryTime
|
||||||
entryInfo.url = url
|
entryInfo.url = url
|
||||||
entryInfo.notes = notes
|
entryInfo.notes = notes
|
||||||
entryInfo.customFields = getExtraFields()
|
entryInfo.customFields = getExtraFields().toMutableList()
|
||||||
// Add otpElement to generate token
|
// Add otpElement to generate token
|
||||||
entryInfo.otpModel = getOtpElement()?.otpModel
|
entryInfo.otpModel = getOtpElement()?.otpModel
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
// Replace parameter fields by generated OTP fields
|
// Replace parameter fields by generated OTP fields
|
||||||
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
||||||
}
|
}
|
||||||
database?.attachmentPool?.let { binaryPool ->
|
database?.attachmentPool?.let { binaryPool ->
|
||||||
entryInfo.attachments = getAttachments(binaryPool)
|
entryInfo.attachments = getAttachments(binaryPool).toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!raw)
|
if (!raw)
|
||||||
database?.stopManageEntry(this)
|
database?.stopManageEntry(this)
|
||||||
|
}
|
||||||
return entryInfo
|
return entryInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,16 +481,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
companion object CREATOR : Parcelable.Creator<Entry> {
|
|
||||||
override fun createFromParcel(parcel: Parcel): Entry {
|
|
||||||
return Entry(parcel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<Entry?> {
|
|
||||||
return arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
const val PMS_TAN_ENTRY = "<TAN>"
|
const val PMS_TAN_ENTRY = "<TAN>"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -484,5 +490,16 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
fun newExtraFieldNameAllowed(field: Field): Boolean {
|
fun newExtraFieldNameAllowed(field: Field): Boolean {
|
||||||
return EntryKDBX.newCustomNameAllowed(field.name)
|
return EntryKDBX.newCustomNameAllowed(field.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val CREATOR: Parcelable.Creator<Entry> = object : Parcelable.Creator<Entry> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): Entry {
|
||||||
|
return Entry(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<Entry?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.model
|
package com.kunzisoft.keepass.database.element
|
||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.element
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
|
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
|
||||||
@@ -43,14 +44,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
// Virtual group is used to defined a detached database group
|
// Virtual group is used to defined a detached database group
|
||||||
var isVirtual = false
|
var isVirtual = false
|
||||||
|
|
||||||
fun updateWith(group: Group) {
|
var numberOfChildEntries: Int = 0
|
||||||
group.groupKDB?.let {
|
|
||||||
this.groupKDB?.updateWith(it)
|
|
||||||
}
|
|
||||||
group.groupKDBX?.let {
|
|
||||||
this.groupKDBX?.updateWith(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use this constructor to copy a Group
|
* Use this constructor to copy a Group
|
||||||
@@ -64,7 +58,12 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
if (this.groupKDBX == null)
|
if (this.groupKDBX == null)
|
||||||
this.groupKDBX = GroupKDBX()
|
this.groupKDBX = GroupKDBX()
|
||||||
}
|
}
|
||||||
updateWith(group)
|
group.groupKDB?.let {
|
||||||
|
this.groupKDB?.updateWith(it)
|
||||||
|
}
|
||||||
|
group.groupKDBX?.let {
|
||||||
|
this.groupKDBX?.updateWith(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(group: GroupKDB) {
|
constructor(group: GroupKDB) {
|
||||||
@@ -117,8 +116,8 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
dest.writeByte((if (isVirtual) 1 else 0).toByte())
|
dest.writeByte((if (isVirtual) 1 else 0).toByte())
|
||||||
}
|
}
|
||||||
|
|
||||||
override val nodeId: NodeId<*>?
|
override val nodeId: NodeId<*>
|
||||||
get() = groupKDBX?.nodeId ?: groupKDB?.nodeId
|
get() = groupKDBX?.nodeId ?: groupKDB?.nodeId ?: NodeIdUUID()
|
||||||
|
|
||||||
override var title: String
|
override var title: String
|
||||||
get() = groupKDB?.title ?: groupKDBX?.title ?: ""
|
get() = groupKDB?.title ?: groupKDBX?.title ?: ""
|
||||||
@@ -134,6 +133,20 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
groupKDBX?.icon = value
|
groupKDBX?.icon = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var tags: Tags
|
||||||
|
get() = groupKDBX?.tags ?: Tags()
|
||||||
|
set(value) {
|
||||||
|
groupKDBX?.tags = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO
|
||||||
|
get() = groupKDBX?.previousParentGroup ?: DatabaseVersioned.UUID_ZERO
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun setPreviousParentGroup(previousParent: Group?) {
|
||||||
|
groupKDBX?.previousParentGroup = previousParent?.groupKDBX?.id ?: DatabaseVersioned.UUID_ZERO
|
||||||
|
}
|
||||||
|
|
||||||
override val type: Type
|
override val type: Type
|
||||||
get() = Type.GROUP
|
get() = Type.GROUP
|
||||||
|
|
||||||
@@ -255,6 +268,20 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
ArrayList()
|
ArrayList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getFilteredChildGroups(filters: Array<ChildFilter>): List<Group> {
|
||||||
|
return groupKDB?.getChildGroups()?.map {
|
||||||
|
Group(it).apply {
|
||||||
|
this.refreshNumberOfChildEntries(filters)
|
||||||
|
}
|
||||||
|
} ?:
|
||||||
|
groupKDBX?.getChildGroups()?.map {
|
||||||
|
Group(it).apply {
|
||||||
|
this.refreshNumberOfChildEntries(filters)
|
||||||
|
}
|
||||||
|
} ?:
|
||||||
|
ArrayList()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getChildEntries(): List<Entry> {
|
override fun getChildEntries(): List<Entry> {
|
||||||
return groupKDB?.getChildEntries()?.map {
|
return groupKDB?.getChildEntries()?.map {
|
||||||
Entry(it)
|
Entry(it)
|
||||||
@@ -291,8 +318,8 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
ArrayList()
|
ArrayList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNumberOfChildEntries(filters: Array<ChildFilter> = emptyArray()): Int {
|
fun refreshNumberOfChildEntries(filters: Array<ChildFilter> = emptyArray()) {
|
||||||
return getFilteredChildEntries(filters).size
|
this.numberOfChildEntries = getFilteredChildEntries(filters).size
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -304,7 +331,9 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getFilteredChildren(filters: Array<ChildFilter>): List<Node> {
|
fun getFilteredChildren(filters: Array<ChildFilter>): List<Node> {
|
||||||
return getChildGroups() + getFilteredChildEntries(filters)
|
val nodes = getFilteredChildGroups(filters) + getFilteredChildEntries(filters)
|
||||||
|
refreshNumberOfChildEntries(filters)
|
||||||
|
return nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addChildGroup(group: Group) {
|
override fun addChildGroup(group: Group) {
|
||||||
@@ -325,6 +354,24 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateChildGroup(group: Group) {
|
||||||
|
group.groupKDB?.let {
|
||||||
|
groupKDB?.updateChildGroup(it)
|
||||||
|
}
|
||||||
|
group.groupKDBX?.let {
|
||||||
|
groupKDBX?.updateChildGroup(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateChildEntry(entry: Entry) {
|
||||||
|
entry.entryKDB?.let {
|
||||||
|
groupKDB?.updateChildEntry(it)
|
||||||
|
}
|
||||||
|
entry.entryKDBX?.let {
|
||||||
|
groupKDBX?.updateChildEntry(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun removeChildGroup(group: Group) {
|
override fun removeChildGroup(group: Group) {
|
||||||
group.groupKDB?.let {
|
group.groupKDB?.let {
|
||||||
groupKDB?.removeChildGroup(it)
|
groupKDB?.removeChildGroup(it)
|
||||||
@@ -368,14 +415,6 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
groupKDB?.nodeId = id
|
groupKDB?.nodeId = id
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLevel(): Int {
|
|
||||||
return groupKDB?.level ?: -1
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setLevel(level: Int) {
|
|
||||||
groupKDB?.level = level
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
------------
|
------------
|
||||||
KDBX Methods
|
KDBX Methods
|
||||||
@@ -402,10 +441,6 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
groupKDBX?.isExpanded = expanded
|
groupKDBX?.isExpanded = expanded
|
||||||
}
|
}
|
||||||
|
|
||||||
fun containsCustomData(): Boolean {
|
|
||||||
return groupKDBX?.containsCustomData() ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
------------
|
------------
|
||||||
Converter
|
Converter
|
||||||
@@ -452,4 +487,10 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
result = 31 * result + (groupKDBX?.hashCode() ?: 0)
|
result = 31 * result + (groupKDBX?.hashCode() ?: 0)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return groupKDB?.toString() ?: groupKDBX?.toString() ?: "Undefined"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,15 +28,17 @@ import java.util.*
|
|||||||
enum class SortNodeEnum {
|
enum class SortNodeEnum {
|
||||||
DB, TITLE, USERNAME, CREATION_TIME, LAST_MODIFY_TIME, LAST_ACCESS_TIME;
|
DB, TITLE, USERNAME, CREATION_TIME, LAST_MODIFY_TIME, LAST_ACCESS_TIME;
|
||||||
|
|
||||||
fun <G: GroupVersionedInterface<G, *>> getNodeComparator(sortNodeParameters: SortNodeParameters)
|
fun <G: GroupVersionedInterface<G, *>> getNodeComparator(
|
||||||
|
database: Database,
|
||||||
|
sortNodeParameters: SortNodeParameters)
|
||||||
: Comparator<NodeVersionedInterface<G>> {
|
: Comparator<NodeVersionedInterface<G>> {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
DB -> NodeNaturalComparator(sortNodeParameters) // Force false because natural order contains recycle bin
|
DB -> NodeNaturalComparator(database, sortNodeParameters) // Force false because natural order contains recycle bin
|
||||||
TITLE -> NodeTitleComparator(sortNodeParameters)
|
TITLE -> NodeTitleComparator(database, sortNodeParameters)
|
||||||
USERNAME -> NodeUsernameComparator(sortNodeParameters)
|
USERNAME -> NodeUsernameComparator(database, sortNodeParameters)
|
||||||
CREATION_TIME -> NodeCreationComparator(sortNodeParameters)
|
CREATION_TIME -> NodeCreationComparator(database, sortNodeParameters)
|
||||||
LAST_MODIFY_TIME -> NodeLastModificationComparator(sortNodeParameters)
|
LAST_MODIFY_TIME -> NodeLastModificationComparator(database, sortNodeParameters)
|
||||||
LAST_ACCESS_TIME -> NodeLastAccessComparator(sortNodeParameters)
|
LAST_ACCESS_TIME -> NodeLastAccessComparator(database, sortNodeParameters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,11 +50,9 @@ enum class SortNodeEnum {
|
|||||||
<
|
<
|
||||||
G: GroupVersionedInterface<*, *>,
|
G: GroupVersionedInterface<*, *>,
|
||||||
T: NodeVersionedInterface<G>
|
T: NodeVersionedInterface<G>
|
||||||
>(var sortNodeParameters: SortNodeParameters)
|
>(var database: Database, var sortNodeParameters: SortNodeParameters)
|
||||||
: Comparator<T> {
|
: Comparator<T> {
|
||||||
|
|
||||||
val database = Database.getInstance()
|
|
||||||
|
|
||||||
abstract fun compareBySpecificOrder(object1: T, object2: T): Int
|
abstract fun compareBySpecificOrder(object1: T, object2: T): Int
|
||||||
|
|
||||||
private fun specificOrderOrHashIfEquals(object1: T, object2: T): Int {
|
private fun specificOrderOrHashIfEquals(object1: T, object2: T): Int {
|
||||||
@@ -110,8 +110,9 @@ enum class SortNodeEnum {
|
|||||||
* Comparator of node by natural database placement
|
* Comparator of node by natural database placement
|
||||||
*/
|
*/
|
||||||
class NodeNaturalComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
class NodeNaturalComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||||
|
database: Database,
|
||||||
sortNodeParameters: SortNodeParameters)
|
sortNodeParameters: SortNodeParameters)
|
||||||
: NodeComparator<G, T>(sortNodeParameters) {
|
: NodeComparator<G, T>(database, sortNodeParameters) {
|
||||||
|
|
||||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||||
return object1.nodeIndexInParentForNaturalOrder()
|
return object1.nodeIndexInParentForNaturalOrder()
|
||||||
@@ -123,13 +124,14 @@ enum class SortNodeEnum {
|
|||||||
* Comparator of Node by Title
|
* Comparator of Node by Title
|
||||||
*/
|
*/
|
||||||
class NodeTitleComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
class NodeTitleComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||||
|
database: Database,
|
||||||
sortNodeParameters: SortNodeParameters)
|
sortNodeParameters: SortNodeParameters)
|
||||||
: NodeComparator<G, T>(sortNodeParameters) {
|
: NodeComparator<G, T>(database, sortNodeParameters) {
|
||||||
|
|
||||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||||
val titleCompare = object1.title.compareTo(object2.title, ignoreCase = true)
|
val titleCompare = object1.title.compareTo(object2.title, ignoreCase = true)
|
||||||
return if (titleCompare == 0)
|
return if (titleCompare == 0)
|
||||||
NodeNaturalComparator<G, T>(sortNodeParameters)
|
NodeNaturalComparator<G, T>(database, sortNodeParameters)
|
||||||
.compare(object1, object2)
|
.compare(object1, object2)
|
||||||
else
|
else
|
||||||
titleCompare
|
titleCompare
|
||||||
@@ -140,8 +142,9 @@ enum class SortNodeEnum {
|
|||||||
* Comparator of Node by Username, Groups by title
|
* Comparator of Node by Username, Groups by title
|
||||||
*/
|
*/
|
||||||
class NodeUsernameComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
class NodeUsernameComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||||
|
database: Database,
|
||||||
sortNodeParameters: SortNodeParameters)
|
sortNodeParameters: SortNodeParameters)
|
||||||
: NodeComparator<G, T>(sortNodeParameters) {
|
: NodeComparator<G, T>(database, sortNodeParameters) {
|
||||||
|
|
||||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||||
return if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) {
|
return if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) {
|
||||||
@@ -150,12 +153,12 @@ enum class SortNodeEnum {
|
|||||||
.compareTo((object2 as Entry).getEntryInfo(database).username,
|
.compareTo((object2 as Entry).getEntryInfo(database).username,
|
||||||
ignoreCase = true)
|
ignoreCase = true)
|
||||||
if (usernameCompare == 0)
|
if (usernameCompare == 0)
|
||||||
NodeTitleComparator<G, T>(sortNodeParameters)
|
NodeTitleComparator<G, T>(database, sortNodeParameters)
|
||||||
.compare(object1, object2)
|
.compare(object1, object2)
|
||||||
else
|
else
|
||||||
usernameCompare
|
usernameCompare
|
||||||
} else {
|
} else {
|
||||||
NodeTitleComparator<G, T>(sortNodeParameters)
|
NodeTitleComparator<G, T>(database, sortNodeParameters)
|
||||||
.compare(object1, object2)
|
.compare(object1, object2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,14 +168,15 @@ enum class SortNodeEnum {
|
|||||||
* Comparator of node by creation
|
* Comparator of node by creation
|
||||||
*/
|
*/
|
||||||
class NodeCreationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
class NodeCreationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||||
|
database: Database,
|
||||||
sortNodeParameters: SortNodeParameters)
|
sortNodeParameters: SortNodeParameters)
|
||||||
: NodeComparator<G, T>(sortNodeParameters) {
|
: NodeComparator<G, T>(database, sortNodeParameters) {
|
||||||
|
|
||||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||||
val creationCompare = object1.creationTime.date
|
val creationCompare = object1.creationTime.date
|
||||||
.compareTo(object2.creationTime.date)
|
.compareTo(object2.creationTime.date)
|
||||||
return if (creationCompare == 0)
|
return if (creationCompare == 0)
|
||||||
NodeNaturalComparator<G, T>(sortNodeParameters)
|
NodeNaturalComparator<G, T>(database, sortNodeParameters)
|
||||||
.compare(object1, object2)
|
.compare(object1, object2)
|
||||||
else
|
else
|
||||||
creationCompare
|
creationCompare
|
||||||
@@ -183,14 +187,15 @@ enum class SortNodeEnum {
|
|||||||
* Comparator of node by last modification
|
* Comparator of node by last modification
|
||||||
*/
|
*/
|
||||||
class NodeLastModificationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
class NodeLastModificationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||||
|
database: Database,
|
||||||
sortNodeParameters: SortNodeParameters)
|
sortNodeParameters: SortNodeParameters)
|
||||||
: NodeComparator<G, T>(sortNodeParameters) {
|
: NodeComparator<G, T>(database, sortNodeParameters) {
|
||||||
|
|
||||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||||
val lastModificationCompare = object1.lastModificationTime.date
|
val lastModificationCompare = object1.lastModificationTime.date
|
||||||
.compareTo(object2.lastModificationTime.date)
|
.compareTo(object2.lastModificationTime.date)
|
||||||
return if (lastModificationCompare == 0)
|
return if (lastModificationCompare == 0)
|
||||||
NodeNaturalComparator<G, T>(sortNodeParameters)
|
NodeNaturalComparator<G, T>(database, sortNodeParameters)
|
||||||
.compare(object1, object2)
|
.compare(object1, object2)
|
||||||
else
|
else
|
||||||
lastModificationCompare
|
lastModificationCompare
|
||||||
@@ -201,14 +206,15 @@ enum class SortNodeEnum {
|
|||||||
* Comparator of node by last access
|
* Comparator of node by last access
|
||||||
*/
|
*/
|
||||||
class NodeLastAccessComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
class NodeLastAccessComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||||
|
database: Database,
|
||||||
sortNodeParameters: SortNodeParameters)
|
sortNodeParameters: SortNodeParameters)
|
||||||
: NodeComparator<G, T>(sortNodeParameters) {
|
: NodeComparator<G, T>(database, sortNodeParameters) {
|
||||||
|
|
||||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||||
val lastAccessCompare = object1.lastAccessTime.date
|
val lastAccessCompare = object1.lastAccessTime.date
|
||||||
.compareTo(object2.lastAccessTime.date)
|
.compareTo(object2.lastAccessTime.date)
|
||||||
return if (lastAccessCompare == 0)
|
return if (lastAccessCompare == 0)
|
||||||
NodeNaturalComparator<G, T>(sortNodeParameters)
|
NodeNaturalComparator<G, T>(database, sortNodeParameters)
|
||||||
.compare(object1, object2)
|
.compare(object1, object2)
|
||||||
else
|
else
|
||||||
lastAccessCompare
|
lastAccessCompare
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.kunzisoft.keepass.database.element
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
class Tags: Parcelable {
|
||||||
|
|
||||||
|
private val mTags = ArrayList<String>()
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
|
||||||
|
constructor(values: String): this() {
|
||||||
|
mTags.addAll(values.split(';'))
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) : this() {
|
||||||
|
parcel.readStringList(mTags)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeStringList(mTags)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isEmpty(): Boolean {
|
||||||
|
return mTags.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return mTags.joinToString(";")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<Tags> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): Tags {
|
||||||
|
return Tags(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<Tags?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ class BinaryCache {
|
|||||||
*/
|
*/
|
||||||
var loadedCipherKey: LoadedKey = LoadedKey.generateNewCipherKey()
|
var loadedCipherKey: LoadedKey = LoadedKey.generateNewCipherKey()
|
||||||
|
|
||||||
lateinit var cacheDirectory: File
|
var cacheDirectory: File? = null
|
||||||
|
|
||||||
private val voidBinary = KeyByteArray(UNKNOWN, ByteArray(0))
|
private val voidBinary = KeyByteArray(UNKNOWN, ByteArray(0))
|
||||||
|
|
||||||
@@ -19,15 +19,16 @@ class BinaryCache {
|
|||||||
smallSize: Boolean = false,
|
smallSize: Boolean = false,
|
||||||
compression: Boolean = false,
|
compression: Boolean = false,
|
||||||
protection: Boolean = false): BinaryData {
|
protection: Boolean = false): BinaryData {
|
||||||
return if (smallSize) {
|
val cacheDir = cacheDirectory
|
||||||
|
return if (smallSize || cacheDir == null) {
|
||||||
BinaryByte(binaryId, compression, protection)
|
BinaryByte(binaryId, compression, protection)
|
||||||
} else {
|
} else {
|
||||||
val fileInCache = File(cacheDirectory, binaryId)
|
val fileInCache = File(cacheDir, binaryId)
|
||||||
return BinaryFile(fileInCache, compression, protection)
|
BinaryFile(fileInCache, compression, protection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Similar to file storage but much faster
|
// Similar to file storage but much faster TODO SparseArray
|
||||||
private val byteArrayList = HashMap<String, ByteArray>()
|
private val byteArrayList = HashMap<String, ByteArray>()
|
||||||
|
|
||||||
fun getByteArray(key: String): KeyByteArray {
|
fun getByteArray(key: String): KeyByteArray {
|
||||||
|
|||||||
@@ -1,8 +1,27 @@
|
|||||||
package com.kunzisoft.keepass.database.element.binary
|
package com.kunzisoft.keepass.database.element.binary
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class CustomIconPool(binaryCache: BinaryCache) : BinaryPool<UUID>(binaryCache) {
|
class CustomIconPool(private val binaryCache: BinaryCache) : BinaryPool<UUID>(binaryCache) {
|
||||||
|
|
||||||
|
private val customIcons = HashMap<UUID, IconImageCustom>()
|
||||||
|
|
||||||
|
fun put(key: UUID? = null,
|
||||||
|
name: String,
|
||||||
|
lastModificationTime: DateInstant?,
|
||||||
|
smallSize: Boolean,
|
||||||
|
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||||
|
val keyBinary = super.put(key) { uniqueBinaryId ->
|
||||||
|
// Create a byte array for better performance with small data
|
||||||
|
binaryCache.getBinaryData(uniqueBinaryId, smallSize)
|
||||||
|
}
|
||||||
|
val uuid = keyBinary.keys.first()
|
||||||
|
val customIcon = IconImageCustom(uuid, name, lastModificationTime)
|
||||||
|
customIcons[uuid] = customIcon
|
||||||
|
result.invoke(customIcon, keyBinary.binary)
|
||||||
|
}
|
||||||
|
|
||||||
override fun findUnusedKey(): UUID {
|
override fun findUnusedKey(): UUID {
|
||||||
var newUUID = UUID.randomUUID()
|
var newUUID = UUID.randomUUID()
|
||||||
@@ -11,4 +30,14 @@ class CustomIconPool(binaryCache: BinaryCache) : BinaryPool<UUID>(binaryCache) {
|
|||||||
}
|
}
|
||||||
return newUUID
|
return newUUID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun any(predicate: (IconImageCustom)-> Boolean): Boolean {
|
||||||
|
return customIcons.any { predicate(it.value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doForEachCustomIcon(action: (customIcon: IconImageCustom, binary: BinaryData) -> Unit) {
|
||||||
|
doForEachBinary { key, binary ->
|
||||||
|
action.invoke(customIcons[key] ?: IconImageCustom(key), binary)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -38,8 +38,6 @@ import kotlin.collections.ArrayList
|
|||||||
|
|
||||||
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||||
|
|
||||||
private var backupGroupId: Int = BACKUP_FOLDER_UNDEFINED_ID
|
|
||||||
|
|
||||||
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
||||||
|
|
||||||
override val version: String
|
override val version: String
|
||||||
@@ -55,13 +53,14 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
return getGroupById(NodeIdInt(groupId))
|
return getGroupById(NodeIdInt(groupId))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve backup group in index
|
|
||||||
val backupGroup: GroupKDB?
|
val backupGroup: GroupKDB?
|
||||||
get() {
|
get() {
|
||||||
return if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
|
return retrieveBackup()
|
||||||
null
|
}
|
||||||
else
|
|
||||||
getGroupById(backupGroupId)
|
val groupNamesNotAllowed: List<String>
|
||||||
|
get() {
|
||||||
|
return listOf(BACKUP_FOLDER_TITLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val kdfEngine: KdfEngine
|
override val kdfEngine: KdfEngine
|
||||||
@@ -80,12 +79,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
|
|
||||||
val rootGroups: List<GroupKDB>
|
val rootGroups: List<GroupKDB>
|
||||||
get() {
|
get() {
|
||||||
val kids = ArrayList<GroupKDB>()
|
return rootGroup?.getChildGroups() ?: ArrayList()
|
||||||
doForEachGroupInIndex { group ->
|
|
||||||
if (group.level == 0)
|
|
||||||
kids.add(group)
|
|
||||||
}
|
|
||||||
return kids
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val passwordEncoding: String
|
override val passwordEncoding: String
|
||||||
@@ -163,27 +157,16 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
return this.iconsManager.getIcon(iconId)
|
return this.iconsManager.getIcon(iconId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun containsCustomData(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isInRecycleBin(group: GroupKDB): Boolean {
|
override fun isInRecycleBin(group: GroupKDB): Boolean {
|
||||||
var currentGroup: GroupKDB? = group
|
var currentGroup: GroupKDB? = group
|
||||||
|
val currentBackupGroup = backupGroup ?: return false
|
||||||
|
|
||||||
// Init backup group variable
|
if (currentGroup == currentBackupGroup)
|
||||||
if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
|
|
||||||
findBackupGroupId()
|
|
||||||
|
|
||||||
if (backupGroup == null)
|
|
||||||
return false
|
|
||||||
|
|
||||||
if (currentGroup == backupGroup)
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
val backupGroupId = currentBackupGroup.id
|
||||||
while (currentGroup != null) {
|
while (currentGroup != null) {
|
||||||
if (currentGroup.level == 0
|
if (backupGroupId == currentGroup.id) {
|
||||||
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
|
|
||||||
backupGroupId = currentGroup.id
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
currentGroup = currentGroup.parent
|
currentGroup = currentGroup.parent
|
||||||
@@ -191,12 +174,12 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findBackupGroupId() {
|
/**
|
||||||
rootGroups.forEach { currentGroup ->
|
* Retrieve backup group with his name
|
||||||
if (currentGroup.level == 0
|
*/
|
||||||
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
|
private fun retrieveBackup(): GroupKDB? {
|
||||||
backupGroupId = currentGroup.id
|
return rootGroup?.searchChildGroup {
|
||||||
}
|
it.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,8 +188,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
* if it doesn't exist
|
* if it doesn't exist
|
||||||
*/
|
*/
|
||||||
fun ensureBackupExists() {
|
fun ensureBackupExists() {
|
||||||
findBackupGroupId()
|
|
||||||
|
|
||||||
if (backupGroup == null) {
|
if (backupGroup == null) {
|
||||||
// Create recycle bin
|
// Create recycle bin
|
||||||
val recycleBinGroup = createGroup().apply {
|
val recycleBinGroup = createGroup().apply {
|
||||||
@@ -214,7 +195,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
|
icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
|
||||||
}
|
}
|
||||||
addGroupTo(recycleBinGroup, rootGroup)
|
addGroupTo(recycleBinGroup, rootGroup)
|
||||||
backupGroupId = recycleBinGroup.id
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +248,5 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
val TYPE = DatabaseKDB::class.java
|
val TYPE = DatabaseKDB::class.java
|
||||||
|
|
||||||
const val BACKUP_FOLDER_TITLE = "Backup"
|
const val BACKUP_FOLDER_TITLE = "Backup"
|
||||||
private const val BACKUP_FOLDER_UNDEFINED_ID = -1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ import android.content.res.Resources
|
|||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.encrypt.HashManager
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
|
||||||
import com.kunzisoft.keepass.utils.longTo8Bytes
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
import com.kunzisoft.keepass.database.crypto.AesEngine
|
import com.kunzisoft.keepass.database.crypto.AesEngine
|
||||||
@@ -34,22 +32,29 @@ import com.kunzisoft.keepass.database.crypto.VariantDictionary
|
|||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
||||||
|
import com.kunzisoft.keepass.database.element.CustomData
|
||||||
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.binary.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
|
import com.kunzisoft.keepass.database.element.entry.FieldReferencesEngine
|
||||||
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.icon.IconImageStandard
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
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.node.NodeVersioned
|
||||||
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
||||||
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible
|
||||||
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_31
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
|
||||||
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
|
||||||
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
|
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
|
||||||
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
import com.kunzisoft.keepass.utils.longTo8Bytes
|
||||||
import org.apache.commons.codec.binary.Hex
|
import org.apache.commons.codec.binary.Hex
|
||||||
import org.w3c.dom.Node
|
import org.w3c.dom.Node
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -75,6 +80,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
private var kdfList: MutableList<KdfEngine> = ArrayList()
|
private var kdfList: MutableList<KdfEngine> = ArrayList()
|
||||||
private var numKeyEncRounds: Long = 0
|
private var numKeyEncRounds: Long = 0
|
||||||
var publicCustomData = VariantDictionary()
|
var publicCustomData = VariantDictionary()
|
||||||
|
private val mFieldReferenceEngine = FieldReferencesEngine(this)
|
||||||
|
private val mTemplateEngine = TemplateEngineCompatible(this)
|
||||||
|
|
||||||
var kdbxVersion = UnsignedInt(0)
|
var kdbxVersion = UnsignedInt(0)
|
||||||
var name = ""
|
var name = ""
|
||||||
@@ -100,7 +107,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
*/
|
*/
|
||||||
var isRecycleBinEnabled = true
|
var isRecycleBinEnabled = true
|
||||||
var recycleBinUUID: UUID = UUID_ZERO
|
var recycleBinUUID: UUID = UUID_ZERO
|
||||||
var recycleBinChanged = Date()
|
var recycleBinChanged = DateInstant()
|
||||||
var entryTemplatesGroup = UUID_ZERO
|
var entryTemplatesGroup = UUID_ZERO
|
||||||
var entryTemplatesGroupChanged = DateInstant()
|
var entryTemplatesGroupChanged = DateInstant()
|
||||||
var historyMaxItems = DEFAULT_HISTORY_MAX_ITEMS
|
var historyMaxItems = DEFAULT_HISTORY_MAX_ITEMS
|
||||||
@@ -109,7 +116,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
var lastTopVisibleGroupUUID = UUID_ZERO
|
var lastTopVisibleGroupUUID = UUID_ZERO
|
||||||
var memoryProtection = MemoryProtectionConfig()
|
var memoryProtection = MemoryProtectionConfig()
|
||||||
val deletedObjects = ArrayList<DeletedObject>()
|
val deletedObjects = ArrayList<DeletedObject>()
|
||||||
val customData = HashMap<String, String>()
|
val customData = CustomData()
|
||||||
|
|
||||||
var localizedAppName = "KeePassDX"
|
var localizedAppName = "KeePassDX"
|
||||||
|
|
||||||
@@ -124,22 +131,29 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
/**
|
/**
|
||||||
* Create a new database with a root group
|
* Create a new database with a root group
|
||||||
*/
|
*/
|
||||||
constructor(databaseName: String, rootName: String) {
|
constructor(databaseName: String,
|
||||||
|
rootName: String,
|
||||||
|
templatesGroupName: String? = null) {
|
||||||
name = databaseName
|
name = databaseName
|
||||||
kdbxVersion = FILE_VERSION_32_3
|
kdbxVersion = FILE_VERSION_31
|
||||||
val group = createGroup().apply {
|
val group = createGroup().apply {
|
||||||
title = rootName
|
title = rootName
|
||||||
icon.standard = getStandardIcon(IconImageStandard.FOLDER_ID)
|
icon.standard = getStandardIcon(IconImageStandard.FOLDER_ID)
|
||||||
}
|
}
|
||||||
rootGroup = group
|
rootGroup = group
|
||||||
addGroupIndex(group)
|
if (templatesGroupName != null) {
|
||||||
|
val templatesGroup = mTemplateEngine.createNewTemplatesGroup(templatesGroupName)
|
||||||
|
entryTemplatesGroup = templatesGroup.id
|
||||||
|
entryTemplatesGroupChanged = templatesGroup.lastModificationTime
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val version: String
|
override val version: String
|
||||||
get() {
|
get() {
|
||||||
val kdbxStringVersion = when(kdbxVersion) {
|
val kdbxStringVersion = when(kdbxVersion) {
|
||||||
FILE_VERSION_32_3 -> "3.1"
|
FILE_VERSION_31 -> "3.1"
|
||||||
FILE_VERSION_32_4 -> "4.0"
|
FILE_VERSION_40 -> "4.0"
|
||||||
|
FILE_VERSION_41 -> "4.1"
|
||||||
else -> "UNKNOWN"
|
else -> "UNKNOWN"
|
||||||
}
|
}
|
||||||
return "KeePass 2 - KDBX$kdbxStringVersion"
|
return "KeePass 2 - KDBX$kdbxStringVersion"
|
||||||
@@ -187,7 +201,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
CompressionAlgorithm.GZip -> {
|
CompressionAlgorithm.GZip -> {
|
||||||
// Only in databaseV3.1, in databaseV4 the header is zipped during the save
|
// Only in databaseV3.1, in databaseV4 the header is zipped during the save
|
||||||
if (kdbxVersion.isBefore(FILE_VERSION_32_4)) {
|
if (kdbxVersion.isBefore(FILE_VERSION_40)) {
|
||||||
compressAllBinaries()
|
compressAllBinaries()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,7 +209,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
CompressionAlgorithm.GZip -> {
|
CompressionAlgorithm.GZip -> {
|
||||||
// In databaseV4 the header is zipped during the save, so not necessary here
|
// In databaseV4 the header is zipped during the save, so not necessary here
|
||||||
if (kdbxVersion.isBefore(FILE_VERSION_32_4)) {
|
if (kdbxVersion.isBefore(FILE_VERSION_40)) {
|
||||||
when (newCompression) {
|
when (newCompression) {
|
||||||
CompressionAlgorithm.None -> {
|
CompressionAlgorithm.None -> {
|
||||||
decompressAllBinaries()
|
decompressAllBinaries()
|
||||||
@@ -313,9 +327,11 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addCustomIcon(customIconId: UUID? = null,
|
fun addCustomIcon(customIconId: UUID? = null,
|
||||||
|
name: String,
|
||||||
|
lastModificationTime: DateInstant?,
|
||||||
smallSize: Boolean,
|
smallSize: Boolean,
|
||||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||||
iconsManager.addCustomIcon(customIconId, smallSize, result)
|
iconsManager.addCustomIcon(customIconId, name, lastModificationTime, smallSize, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean {
|
fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean {
|
||||||
@@ -326,12 +342,118 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
return this.iconsManager.getIcon(iconUuid)
|
return this.iconsManager.getIcon(iconUuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putCustomData(label: String, value: String) {
|
fun isTemplatesGroupEnabled(): Boolean {
|
||||||
this.customData[label] = value
|
return entryTemplatesGroup != UUID_ZERO
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun containsCustomData(): Boolean {
|
fun enableTemplatesGroup(enable: Boolean, templatesGroupName: String) {
|
||||||
return customData.isNotEmpty()
|
// Create templates group only if a group with a valid name don't already exists
|
||||||
|
val firstGroupWithValidName = getGroupIndexes().firstOrNull {
|
||||||
|
it.title == templatesGroupName
|
||||||
|
}
|
||||||
|
if (enable) {
|
||||||
|
val templatesGroup = firstGroupWithValidName
|
||||||
|
?: mTemplateEngine.createNewTemplatesGroup(templatesGroupName)
|
||||||
|
entryTemplatesGroup = templatesGroup.id
|
||||||
|
entryTemplatesGroupChanged = templatesGroup.lastModificationTime
|
||||||
|
} else {
|
||||||
|
removeTemplatesGroup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeTemplatesGroup() {
|
||||||
|
entryTemplatesGroup = UUID_ZERO
|
||||||
|
entryTemplatesGroupChanged = DateInstant()
|
||||||
|
mTemplateEngine.clearCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTemplatesGroup(): GroupKDBX? {
|
||||||
|
if (isTemplatesGroupEnabled()) {
|
||||||
|
return getGroupById(entryTemplatesGroup)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTemplates(templateCreation: Boolean): List<Template> {
|
||||||
|
return if (templateCreation)
|
||||||
|
listOf(mTemplateEngine.getTemplateCreation())
|
||||||
|
else
|
||||||
|
mTemplateEngine.getTemplates()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTemplate(entry: EntryKDBX): Template? {
|
||||||
|
return mTemplateEngine.getTemplate(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeEntryWithTemplateConfiguration(entryKDBX: EntryKDBX, entryIsTemplate: Boolean): EntryKDBX {
|
||||||
|
return if (entryIsTemplate) {
|
||||||
|
mTemplateEngine.decodeTemplateEntry(entryKDBX)
|
||||||
|
} else {
|
||||||
|
mTemplateEngine.removeMetaTemplateRecognitionFromEntry(entryKDBX)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encodeEntryWithTemplateConfiguration(entryKDBX: EntryKDBX, entryIsTemplate: Boolean, template: Template): EntryKDBX {
|
||||||
|
return if (entryIsTemplate) {
|
||||||
|
mTemplateEngine.encodeTemplateEntry(entryKDBX)
|
||||||
|
} else {
|
||||||
|
mTemplateEngine.addMetaTemplateRecognitionToEntry(template, entryKDBX)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Search methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun getGroupById(id: UUID): GroupKDBX? {
|
||||||
|
return this.getGroupById(NodeIdUUID(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryById(id: UUID): EntryKDBX? {
|
||||||
|
return this.getEntryById(NodeIdUUID(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryByTitle(title: String, recursionLevel: Int): EntryKDBX? {
|
||||||
|
return this.entryIndexes.values.find { entry ->
|
||||||
|
entry.decodeTitleKey(recursionLevel).equals(title, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryByUsername(username: String, recursionLevel: Int): EntryKDBX? {
|
||||||
|
return this.entryIndexes.values.find { entry ->
|
||||||
|
entry.decodeUsernameKey(recursionLevel).equals(username, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryByURL(url: String, recursionLevel: Int): EntryKDBX? {
|
||||||
|
return this.entryIndexes.values.find { entry ->
|
||||||
|
entry.decodeUrlKey(recursionLevel).equals(url, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryByPassword(password: String, recursionLevel: Int): EntryKDBX? {
|
||||||
|
return this.entryIndexes.values.find { entry ->
|
||||||
|
entry.decodePasswordKey(recursionLevel).equals(password, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryByNotes(notes: String, recursionLevel: Int): EntryKDBX? {
|
||||||
|
return this.entryIndexes.values.find { entry ->
|
||||||
|
entry.decodeNotesKey(recursionLevel).equals(notes, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryByCustomData(customDataValue: String): EntryKDBX? {
|
||||||
|
return entryIndexes.values.find { entry ->
|
||||||
|
entry.customData.containsItemWithValue(customDataValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the value of a field reference
|
||||||
|
*/
|
||||||
|
fun getFieldReferenceValue(textReference: String, recursionLevel: Int): String {
|
||||||
|
return mFieldReferenceEngine.compile(textReference, recursionLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
@@ -584,24 +706,32 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
*/
|
*/
|
||||||
fun ensureRecycleBinExists(resources: Resources) {
|
fun ensureRecycleBinExists(resources: Resources) {
|
||||||
if (recycleBin == null) {
|
if (recycleBin == null) {
|
||||||
// Create recycle bin
|
// Create recycle bin only if a group with a valid name don't already exists
|
||||||
val recycleBinGroup = createGroup().apply {
|
val firstGroupWithValidName = getGroupIndexes().firstOrNull {
|
||||||
title = resources.getString(R.string.recycle_bin)
|
it.title == resources.getString(R.string.recycle_bin)
|
||||||
icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
|
}
|
||||||
enableAutoType = false
|
val recycleBinGroup = if (firstGroupWithValidName == null) {
|
||||||
enableSearching = false
|
val newRecycleBinGroup = createGroup().apply {
|
||||||
isExpanded = false
|
title = resources.getString(R.string.recycle_bin)
|
||||||
|
icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
|
||||||
|
enableAutoType = false
|
||||||
|
enableSearching = false
|
||||||
|
isExpanded = false
|
||||||
|
}
|
||||||
|
addGroupTo(newRecycleBinGroup, rootGroup)
|
||||||
|
newRecycleBinGroup
|
||||||
|
} else {
|
||||||
|
firstGroupWithValidName
|
||||||
}
|
}
|
||||||
addGroupTo(recycleBinGroup, rootGroup)
|
|
||||||
recycleBinUUID = recycleBinGroup.id
|
recycleBinUUID = recycleBinGroup.id
|
||||||
recycleBinChanged = recycleBinGroup.lastModificationTime.date
|
recycleBinChanged = recycleBinGroup.lastModificationTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeRecycleBin() {
|
fun removeRecycleBin() {
|
||||||
if (recycleBin != null) {
|
if (recycleBin != null) {
|
||||||
recycleBinUUID = UUID_ZERO
|
recycleBinUUID = UUID_ZERO
|
||||||
recycleBinChanged = DateInstant().date
|
recycleBinChanged = DateInstant()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,6 +745,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
return false
|
return false
|
||||||
if (recycleBin == null)
|
if (recycleBin == null)
|
||||||
return false
|
return false
|
||||||
|
if (node is GroupKDBX
|
||||||
|
&& recycleBin!!.isContainedIn(node))
|
||||||
|
return false
|
||||||
if (!node.isContainedIn(recycleBin!!))
|
if (!node.isContainedIn(recycleBin!!))
|
||||||
return true
|
return true
|
||||||
return false
|
return false
|
||||||
@@ -652,9 +785,20 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
this.deletedObjects.add(deletedObject)
|
this.deletedObjects.add(deletedObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun addEntryTo(newEntry: EntryKDBX, parent: GroupKDBX?) {
|
||||||
|
super.addEntryTo(newEntry, parent)
|
||||||
|
mFieldReferenceEngine.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateEntry(entry: EntryKDBX) {
|
||||||
|
super.updateEntry(entry)
|
||||||
|
mFieldReferenceEngine.clear()
|
||||||
|
}
|
||||||
|
|
||||||
override fun removeEntryFrom(entryToRemove: EntryKDBX, parent: GroupKDBX?) {
|
override fun removeEntryFrom(entryToRemove: EntryKDBX, parent: GroupKDBX?) {
|
||||||
super.removeEntryFrom(entryToRemove, parent)
|
super.removeEntryFrom(entryToRemove, parent)
|
||||||
deletedObjects.add(DeletedObject(entryToRemove.id))
|
deletedObjects.add(DeletedObject(entryToRemove.id))
|
||||||
|
mFieldReferenceEngine.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun undoDeleteEntryFrom(entry: EntryKDBX, origParent: GroupKDBX?) {
|
override fun undoDeleteEntryFrom(entry: EntryKDBX, origParent: GroupKDBX?) {
|
||||||
@@ -725,6 +869,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
override fun clearCache() {
|
override fun clearCache() {
|
||||||
try {
|
try {
|
||||||
super.clearCache()
|
super.clearCache()
|
||||||
|
mFieldReferenceEngine.clear()
|
||||||
attachmentPool.clear()
|
attachmentPool.clear()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to clear cache", e)
|
Log.e(TAG, "Unable to clear cache", e)
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import java.io.ByteArrayInputStream
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.UnsupportedEncodingException
|
import java.io.UnsupportedEncodingException
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
abstract class DatabaseVersioned<
|
abstract class DatabaseVersioned<
|
||||||
@@ -68,7 +67,7 @@ abstract class DatabaseVersioned<
|
|||||||
var changeDuplicateId = false
|
var changeDuplicateId = false
|
||||||
|
|
||||||
private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>()
|
private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>()
|
||||||
private var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
|
protected var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
|
||||||
|
|
||||||
abstract val version: String
|
abstract val version: String
|
||||||
|
|
||||||
@@ -87,6 +86,16 @@ abstract class DatabaseVersioned<
|
|||||||
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
|
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
|
||||||
|
|
||||||
var rootGroup: Group? = null
|
var rootGroup: Group? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
value?.let {
|
||||||
|
addGroupIndex(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllGroupsWithoutRoot(): List<Group> {
|
||||||
|
return getGroupIndexes().filter { it != rootGroup }
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
|
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
|
||||||
@@ -233,13 +242,6 @@ abstract class DatabaseVersioned<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateGroupIndex(group: Group) {
|
|
||||||
val groupId = group.nodeId
|
|
||||||
if (groupIndexes.containsKey(groupId)) {
|
|
||||||
groupIndexes[groupId] = group
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeGroupIndex(group: Group) {
|
fun removeGroupIndex(group: Group) {
|
||||||
this.groupIndexes.remove(group.nodeId)
|
this.groupIndexes.remove(group.nodeId)
|
||||||
}
|
}
|
||||||
@@ -282,13 +284,6 @@ abstract class DatabaseVersioned<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateEntryIndex(entry: Entry) {
|
|
||||||
val entryId = entry.nodeId
|
|
||||||
if (entryIndexes.containsKey(entryId)) {
|
|
||||||
entryIndexes[entryId] = entry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeEntryIndex(entry: Entry) {
|
fun removeEntryIndex(entry: Entry) {
|
||||||
this.entryIndexes.remove(entry.nodeId)
|
this.entryIndexes.remove(entry.nodeId)
|
||||||
}
|
}
|
||||||
@@ -312,8 +307,6 @@ abstract class DatabaseVersioned<
|
|||||||
|
|
||||||
abstract fun getStandardIcon(iconId: Int): IconImageStandard
|
abstract fun getStandardIcon(iconId: Int): IconImageStandard
|
||||||
|
|
||||||
abstract fun containsCustomData(): Boolean
|
|
||||||
|
|
||||||
fun addGroupTo(newGroup: Group, parent: Group?) {
|
fun addGroupTo(newGroup: Group, parent: Group?) {
|
||||||
// Add tree to parent tree
|
// Add tree to parent tree
|
||||||
parent?.addChildGroup(newGroup)
|
parent?.addChildGroup(newGroup)
|
||||||
@@ -322,7 +315,11 @@ abstract class DatabaseVersioned<
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateGroup(group: Group) {
|
fun updateGroup(group: Group) {
|
||||||
updateGroupIndex(group)
|
group.parent?.updateChildGroup(group)
|
||||||
|
val groupId = group.nodeId
|
||||||
|
if (groupIndexes.containsKey(groupId)) {
|
||||||
|
groupIndexes[groupId] = group
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeGroupFrom(groupToRemove: Group, parent: Group?) {
|
fun removeGroupFrom(groupToRemove: Group, parent: Group?) {
|
||||||
@@ -331,15 +328,19 @@ abstract class DatabaseVersioned<
|
|||||||
removeGroupIndex(groupToRemove)
|
removeGroupIndex(groupToRemove)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addEntryTo(newEntry: Entry, parent: Group?) {
|
open fun addEntryTo(newEntry: Entry, parent: Group?) {
|
||||||
// Add entry to parent
|
// Add entry to parent
|
||||||
parent?.addChildEntry(newEntry)
|
parent?.addChildEntry(newEntry)
|
||||||
newEntry.parent = parent
|
newEntry.parent = parent
|
||||||
addEntryIndex(newEntry)
|
addEntryIndex(newEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateEntry(entry: Entry) {
|
open fun updateEntry(entry: Entry) {
|
||||||
updateEntryIndex(entry)
|
entry.parent?.updateChildEntry(entry)
|
||||||
|
val entryId = entry.nodeId
|
||||||
|
if (entryIndexes.containsKey(entryId)) {
|
||||||
|
entryIndexes[entryId] = entry
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun removeEntryFrom(entryToRemove: Entry, parent: Group?) {
|
open fun removeEntryFrom(entryToRemove: Entry, parent: Group?) {
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ 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.utils.ParcelableUtil
|
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
|
||||||
class AutoType : Parcelable {
|
class AutoType : Parcelable {
|
||||||
@@ -30,7 +28,7 @@ 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 = LinkedHashMap<String, String>()
|
private var windowSeqPairs = ArrayList<AutoTypeItem>()
|
||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
|
|
||||||
@@ -38,16 +36,15 @@ class AutoType : Parcelable {
|
|||||||
this.enabled = autoType.enabled
|
this.enabled = autoType.enabled
|
||||||
this.obfuscationOptions = autoType.obfuscationOptions
|
this.obfuscationOptions = autoType.obfuscationOptions
|
||||||
this.defaultSequence = autoType.defaultSequence
|
this.defaultSequence = autoType.defaultSequence
|
||||||
for ((key, value) in autoType.windowSeqPairs) {
|
this.windowSeqPairs.clear()
|
||||||
this.windowSeqPairs[key] = value
|
this.windowSeqPairs.addAll(autoType.windowSeqPairs)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(parcel: Parcel) {
|
constructor(parcel: Parcel) {
|
||||||
this.enabled = parcel.readByte().toInt() != 0
|
this.enabled = parcel.readByte().toInt() != 0
|
||||||
this.obfuscationOptions = UnsignedInt(parcel.readInt())
|
this.obfuscationOptions = UnsignedInt(parcel.readInt())
|
||||||
this.defaultSequence = parcel.readString() ?: defaultSequence
|
this.defaultSequence = parcel.readString() ?: defaultSequence
|
||||||
this.windowSeqPairs = ParcelableUtil.readStringParcelableMap(parcel)
|
parcel.readTypedList(this.windowSeqPairs, AutoTypeItem.CREATOR)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int {
|
||||||
@@ -58,15 +55,43 @@ class AutoType : Parcelable {
|
|||||||
dest.writeByte((if (enabled) 1 else 0).toByte())
|
dest.writeByte((if (enabled) 1 else 0).toByte())
|
||||||
dest.writeInt(obfuscationOptions.toKotlinInt())
|
dest.writeInt(obfuscationOptions.toKotlinInt())
|
||||||
dest.writeString(defaultSequence)
|
dest.writeString(defaultSequence)
|
||||||
ParcelableUtil.writeStringParcelableMap(dest, windowSeqPairs)
|
dest.writeTypedList(windowSeqPairs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun put(key: String, value: String) {
|
fun add(key: String, value: String) {
|
||||||
windowSeqPairs[key] = value
|
windowSeqPairs.add(AutoTypeItem(key, value))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun entrySet(): Set<MutableMap.MutableEntry<String, String>> {
|
fun doForEachAutoTypeItem(action: (key: String, value: String) -> Unit) {
|
||||||
return windowSeqPairs.entries
|
windowSeqPairs.forEach {
|
||||||
|
action.invoke(it.key, it.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class AutoTypeItem(var key: String, var value: String): Parcelable {
|
||||||
|
constructor(parcel: Parcel) : this(
|
||||||
|
parcel.readString() ?: "",
|
||||||
|
parcel.readString() ?: "") {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeString(key)
|
||||||
|
parcel.writeString(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<AutoTypeItem> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): AutoTypeItem {
|
||||||
|
return AutoTypeItem(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<AutoTypeItem?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
url = parcel.readString() ?: url
|
url = parcel.readString() ?: url
|
||||||
notes = parcel.readString() ?: notes
|
notes = parcel.readString() ?: notes
|
||||||
binaryDescription = parcel.readString() ?: binaryDescription
|
binaryDescription = parcel.readString() ?: binaryDescription
|
||||||
binaryDataId = parcel.readInt()
|
val rawBinaryDataId = parcel.readInt()
|
||||||
|
binaryDataId = if (rawBinaryDataId == -1) null else rawBinaryDataId
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun readParentParcelable(parcel: Parcel): GroupKDB? {
|
override fun readParentParcelable(parcel: Parcel): GroupKDB? {
|
||||||
@@ -109,9 +110,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
dest.writeString(url)
|
dest.writeString(url)
|
||||||
dest.writeString(notes)
|
dest.writeString(notes)
|
||||||
dest.writeString(binaryDescription)
|
dest.writeString(binaryDescription)
|
||||||
binaryDataId?.let {
|
dest.writeInt(binaryDataId ?: -1)
|
||||||
dest.writeInt(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateWith(source: EntryKDB) {
|
fun updateWith(source: EntryKDB) {
|
||||||
|
|||||||
@@ -20,12 +20,12 @@
|
|||||||
package com.kunzisoft.keepass.database.element.entry
|
package com.kunzisoft.keepass.database.element.entry
|
||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
|
import android.os.ParcelUuid
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
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.binary.AttachmentPool
|
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
|
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
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
|
||||||
@@ -33,6 +33,7 @@ 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.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 java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.collections.LinkedHashMap
|
import kotlin.collections.LinkedHashMap
|
||||||
@@ -45,42 +46,20 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
@Transient
|
@Transient
|
||||||
private var mDecodeRef = false
|
private var mDecodeRef = false
|
||||||
|
|
||||||
var customData = LinkedHashMap<String, String>()
|
override var usageCount = UnsignedLong(0)
|
||||||
var fields = LinkedHashMap<String, ProtectedString>()
|
override var locationChanged = DateInstant()
|
||||||
|
override var customData = CustomData()
|
||||||
|
private var fields = LinkedHashMap<String, ProtectedString>()
|
||||||
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
|
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
|
||||||
var foregroundColor = ""
|
var foregroundColor = ""
|
||||||
var backgroundColor = ""
|
var backgroundColor = ""
|
||||||
var overrideURL = ""
|
var overrideURL = ""
|
||||||
|
override var tags = Tags()
|
||||||
|
override var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO
|
||||||
|
var qualityCheck = true
|
||||||
var autoType = AutoType()
|
var autoType = AutoType()
|
||||||
var history = ArrayList<EntryKDBX>()
|
var history = ArrayList<EntryKDBX>()
|
||||||
var additional = ""
|
var additional = ""
|
||||||
var tags = ""
|
|
||||||
|
|
||||||
fun getSize(attachmentPool: AttachmentPool): Long {
|
|
||||||
var size = FIXED_LENGTH_SIZE
|
|
||||||
|
|
||||||
for (entry in fields.entries) {
|
|
||||||
size += entry.key.length.toLong()
|
|
||||||
size += entry.value.length().toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
size += getAttachmentsSize(attachmentPool)
|
|
||||||
|
|
||||||
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(attachmentPool)
|
|
||||||
}
|
|
||||||
|
|
||||||
size += overrideURL.length.toLong()
|
|
||||||
size += tags.length.toLong()
|
|
||||||
|
|
||||||
return size
|
|
||||||
}
|
|
||||||
|
|
||||||
override var expires: Boolean = false
|
override var expires: Boolean = false
|
||||||
|
|
||||||
@@ -89,34 +68,42 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
constructor(parcel: Parcel) : super(parcel) {
|
constructor(parcel: Parcel) : super(parcel) {
|
||||||
usageCount = UnsignedLong(parcel.readLong())
|
usageCount = UnsignedLong(parcel.readLong())
|
||||||
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
|
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
|
||||||
customData = ParcelableUtil.readStringParcelableMap(parcel)
|
customData = parcel.readParcelable(CustomData::class.java.classLoader) ?: CustomData()
|
||||||
fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java)
|
fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java)
|
||||||
binaries = ParcelableUtil.readStringIntMap(parcel)
|
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
|
||||||
|
tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags
|
||||||
|
previousParentGroup = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
|
||||||
autoType = parcel.readParcelable(AutoType::class.java.classLoader) ?: autoType
|
autoType = parcel.readParcelable(AutoType::class.java.classLoader) ?: autoType
|
||||||
parcel.readTypedList(history, CREATOR)
|
parcel.readTypedList(history, CREATOR)
|
||||||
url = parcel.readString() ?: url
|
|
||||||
additional = parcel.readString() ?: additional
|
additional = parcel.readString() ?: additional
|
||||||
tags = parcel.readString() ?: tags
|
}
|
||||||
|
|
||||||
|
override fun readParentParcelable(parcel: Parcel): GroupKDBX? {
|
||||||
|
return parcel.readParcelable(GroupKDBX::class.java.classLoader)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeParentParcelable(parent: GroupKDBX?, parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeParcelable(parent, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
super.writeToParcel(dest, flags)
|
super.writeToParcel(dest, flags)
|
||||||
dest.writeLong(usageCount.toKotlinLong())
|
dest.writeLong(usageCount.toKotlinLong())
|
||||||
dest.writeParcelable(locationChanged, flags)
|
dest.writeParcelable(locationChanged, flags)
|
||||||
ParcelableUtil.writeStringParcelableMap(dest, customData)
|
dest.writeParcelable(customData, flags)
|
||||||
ParcelableUtil.writeStringParcelableMap(dest, flags, fields)
|
ParcelableUtil.writeStringParcelableMap(dest, flags, fields)
|
||||||
ParcelableUtil.writeStringIntMap(dest, binaries)
|
ParcelableUtil.writeStringIntMap(dest, binaries)
|
||||||
dest.writeString(foregroundColor)
|
dest.writeString(foregroundColor)
|
||||||
dest.writeString(backgroundColor)
|
dest.writeString(backgroundColor)
|
||||||
dest.writeString(overrideURL)
|
dest.writeString(overrideURL)
|
||||||
|
dest.writeParcelable(tags, flags)
|
||||||
|
dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
|
||||||
dest.writeParcelable(autoType, flags)
|
dest.writeParcelable(autoType, flags)
|
||||||
dest.writeTypedList(history)
|
dest.writeTypedList(history)
|
||||||
dest.writeString(url)
|
|
||||||
dest.writeString(additional)
|
dest.writeString(additional)
|
||||||
dest.writeString(tags)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -127,9 +114,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
super.updateWith(source)
|
super.updateWith(source)
|
||||||
usageCount = source.usageCount
|
usageCount = source.usageCount
|
||||||
locationChanged = DateInstant(source.locationChanged)
|
locationChanged = DateInstant(source.locationChanged)
|
||||||
// Add all custom elements in map
|
customData = CustomData(source.customData)
|
||||||
customData.clear()
|
|
||||||
customData.putAll(source.customData)
|
|
||||||
fields.clear()
|
fields.clear()
|
||||||
fields.putAll(source.fields)
|
fields.putAll(source.fields)
|
||||||
binaries.clear()
|
binaries.clear()
|
||||||
@@ -137,13 +122,13 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
foregroundColor = source.foregroundColor
|
foregroundColor = source.foregroundColor
|
||||||
backgroundColor = source.backgroundColor
|
backgroundColor = source.backgroundColor
|
||||||
overrideURL = source.overrideURL
|
overrideURL = source.overrideURL
|
||||||
|
tags = source.tags
|
||||||
|
previousParentGroup = source.previousParentGroup
|
||||||
autoType = AutoType(source.autoType)
|
autoType = AutoType(source.autoType)
|
||||||
history.clear()
|
history.clear()
|
||||||
if (copyHistory)
|
if (copyHistory)
|
||||||
history.addAll(source.history)
|
history.addAll(source.history)
|
||||||
url = source.url
|
|
||||||
additional = source.additional
|
additional = source.additional
|
||||||
tags = source.tags
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startToManageFieldReferences(database: DatabaseKDBX) {
|
fun startToManageFieldReferences(database: DatabaseKDBX) {
|
||||||
@@ -164,13 +149,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
return NodeIdUUID(nodeId.id)
|
return NodeIdUUID(nodeId.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun readParentParcelable(parcel: Parcel): GroupKDBX? {
|
override val type: Type
|
||||||
return parcel.readParcelable(GroupKDBX::class.java.classLoader)
|
get() = Type.ENTRY
|
||||||
}
|
|
||||||
|
|
||||||
override fun writeParentParcelable(parent: GroupKDBX?, parcel: Parcel, flags: Int) {
|
|
||||||
parcel.writeParcelable(parent, flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode a reference key with the FieldReferencesEngine
|
* Decode a reference key with the FieldReferencesEngine
|
||||||
@@ -178,55 +158,98 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
* @param key
|
* @param key
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private fun decodeRefKey(decodeRef: Boolean, key: String): String {
|
private fun decodeRefKey(decodeRef: Boolean, key: String, recursionLevel: Int): String {
|
||||||
return fields[key]?.toString()?.let { text ->
|
return fields[key]?.toString()?.let { text ->
|
||||||
return if (decodeRef) {
|
return if (decodeRef) {
|
||||||
if (mDatabase == null) text else FieldReferencesEngine().compile(text, this, mDatabase!!)
|
mDatabase?.getFieldReferenceValue(text, recursionLevel) ?: text
|
||||||
} else text
|
} else text
|
||||||
} ?: ""
|
} ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun decodeTitleKey(recursionLevel: Int): String {
|
||||||
|
return decodeRefKey(mDecodeRef, STR_TITLE, recursionLevel)
|
||||||
|
}
|
||||||
|
|
||||||
override var title: String
|
override var title: String
|
||||||
get() = decodeRefKey(mDecodeRef, STR_TITLE)
|
get() = decodeTitleKey(0)
|
||||||
set(value) {
|
set(value) {
|
||||||
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectTitle
|
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectTitle
|
||||||
fields[STR_TITLE] = ProtectedString(protect, value)
|
fields[STR_TITLE] = ProtectedString(protect, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val type: Type
|
fun decodeUsernameKey(recursionLevel: Int): String {
|
||||||
get() = Type.ENTRY
|
return decodeRefKey(mDecodeRef, STR_USERNAME, recursionLevel)
|
||||||
|
}
|
||||||
|
|
||||||
override var username: String
|
override var username: String
|
||||||
get() = decodeRefKey(mDecodeRef, STR_USERNAME)
|
get() = decodeUsernameKey(0)
|
||||||
set(value) {
|
set(value) {
|
||||||
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUserName
|
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUserName
|
||||||
fields[STR_USERNAME] = ProtectedString(protect, value)
|
fields[STR_USERNAME] = ProtectedString(protect, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun decodePasswordKey(recursionLevel: Int): String {
|
||||||
|
return decodeRefKey(mDecodeRef, STR_PASSWORD, recursionLevel)
|
||||||
|
}
|
||||||
|
|
||||||
override var password: String
|
override var password: String
|
||||||
get() = decodeRefKey(mDecodeRef, STR_PASSWORD)
|
get() = decodePasswordKey(0)
|
||||||
set(value) {
|
set(value) {
|
||||||
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectPassword
|
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectPassword
|
||||||
fields[STR_PASSWORD] = ProtectedString(protect, value)
|
fields[STR_PASSWORD] = ProtectedString(protect, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun decodeUrlKey(recursionLevel: Int): String {
|
||||||
|
return decodeRefKey(mDecodeRef, STR_URL, recursionLevel)
|
||||||
|
}
|
||||||
|
|
||||||
override var url
|
override var url
|
||||||
get() = decodeRefKey(mDecodeRef, STR_URL)
|
get() = decodeUrlKey(0)
|
||||||
set(value) {
|
set(value) {
|
||||||
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUrl
|
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUrl
|
||||||
fields[STR_URL] = ProtectedString(protect, value)
|
fields[STR_URL] = ProtectedString(protect, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun decodeNotesKey(recursionLevel: Int): String {
|
||||||
|
return decodeRefKey(mDecodeRef, STR_NOTES, recursionLevel)
|
||||||
|
}
|
||||||
|
|
||||||
override var notes: String
|
override var notes: String
|
||||||
get() = decodeRefKey(mDecodeRef, STR_NOTES)
|
get() = decodeNotesKey(0)
|
||||||
set(value) {
|
set(value) {
|
||||||
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectNotes
|
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectNotes
|
||||||
fields[STR_NOTES] = ProtectedString(protect, value)
|
fields[STR_NOTES] = ProtectedString(protect, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override var usageCount = UnsignedLong(0)
|
fun getCustomFieldValue(label: String): String {
|
||||||
|
return decodeRefKey(mDecodeRef, label, 0)
|
||||||
|
}
|
||||||
|
|
||||||
override var locationChanged = DateInstant()
|
fun getSize(attachmentPool: AttachmentPool): Long {
|
||||||
|
var size = FIXED_LENGTH_SIZE
|
||||||
|
|
||||||
|
for (entry in fields.entries) {
|
||||||
|
size += entry.key.length.toLong()
|
||||||
|
size += entry.value.length().toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
size += getAttachmentsSize(attachmentPool)
|
||||||
|
|
||||||
|
size += autoType.defaultSequence.length.toLong()
|
||||||
|
autoType.doForEachAutoTypeItem { key, value ->
|
||||||
|
size += key.length.toLong()
|
||||||
|
size += value.length.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (entry in history) {
|
||||||
|
size += entry.getSize(attachmentPool)
|
||||||
|
}
|
||||||
|
|
||||||
|
size += overrideURL.length.toLong()
|
||||||
|
size += tags.toString().length
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
fun afterChangeParent() {
|
fun afterChangeParent() {
|
||||||
locationChanged = DateInstant()
|
locationChanged = DateInstant()
|
||||||
@@ -240,25 +263,45 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
|| key == STR_NOTES)
|
|| key == STR_NOTES)
|
||||||
}
|
}
|
||||||
|
|
||||||
var customFields = LinkedHashMap<String, ProtectedString>()
|
fun doForEachDecodedCustomField(action: (field: Field) -> Unit) {
|
||||||
get() {
|
val iterator = fields.entries.iterator()
|
||||||
field.clear()
|
while (iterator.hasNext()) {
|
||||||
for ((key, value) in fields) {
|
val mapEntry = iterator.next()
|
||||||
if (!isStandardField(key)) {
|
if (!isStandardField(mapEntry.key)) {
|
||||||
field[key] = ProtectedString(value.isProtected, decodeRefKey(mDecodeRef, key))
|
action.invoke(Field(mapEntry.key,
|
||||||
}
|
ProtectedString(mapEntry.value.isProtected,
|
||||||
|
decodeRefKey(mDecodeRef, mapEntry.key, 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return field
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFieldValue(label: String): ProtectedString? {
|
||||||
|
return fields[label]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFields(): List<Field> {
|
||||||
|
return fields.map { Field(it.key, it.value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putField(field: Field) {
|
||||||
|
putField(field.name, field.protectedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putField(label: String, value: ProtectedString) {
|
||||||
|
fields[label] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeField(name: String) {
|
||||||
|
fields.remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
fun removeAllFields() {
|
fun removeAllFields() {
|
||||||
fields.clear()
|
fields.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putExtraField(label: String, value: ProtectedString) {
|
|
||||||
fields[label] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It's a list because history labels can be defined multiple times
|
* It's a list because history labels can be defined multiple times
|
||||||
*/
|
*/
|
||||||
@@ -302,14 +345,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putCustomData(key: String, value: String) {
|
|
||||||
customData[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun containsCustomData(): Boolean {
|
|
||||||
return customData.isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addEntryToHistory(entry: EntryKDBX) {
|
fun addEntryToHistory(entry: EntryKDBX) {
|
||||||
history.add(entry)
|
history.add(entry)
|
||||||
}
|
}
|
||||||
@@ -349,6 +384,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
const val STR_URL = "URL"
|
const val STR_URL = "URL"
|
||||||
const val STR_NOTES = "Notes"
|
const val STR_NOTES = "Notes"
|
||||||
|
|
||||||
|
private const val FIXED_LENGTH_SIZE: Long = 128 // Approximate fixed length size
|
||||||
|
|
||||||
fun newCustomNameAllowed(name: String): Boolean {
|
fun newCustomNameAllowed(name: String): Boolean {
|
||||||
return !(name.equals(STR_TITLE, true)
|
return !(name.equals(STR_TITLE, true)
|
||||||
|| name.equals(STR_USERNAME, true)
|
|| name.equals(STR_USERNAME, true)
|
||||||
@@ -367,7 +404,5 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
return arrayOfNulls(size)
|
return arrayOfNulls(size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val FIXED_LENGTH_SIZE: Long = 128 // Approximate fixed length size
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ abstract class EntryVersioned
|
|||||||
|
|
||||||
constructor(parcel: Parcel) : super(parcel)
|
constructor(parcel: Parcel) : super(parcel)
|
||||||
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
super.writeToParcel(dest, flags)
|
||||||
|
}
|
||||||
|
|
||||||
override fun nodeIndexInParentForNaturalOrder(): Int {
|
override fun nodeIndexInParentForNaturalOrder(): Int {
|
||||||
if (nodeIndexInParentForNaturalOrder == -1) {
|
if (nodeIndexInParentForNaturalOrder == -1) {
|
||||||
val numberOfGroups = parent?.getChildGroups()?.size
|
val numberOfGroups = parent?.getChildGroups()?.size
|
||||||
|
|||||||
@@ -19,269 +19,132 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element.entry
|
package com.kunzisoft.keepass.database.element.entry
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
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.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.search.EntryKDBXSearchHandler
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class FieldReferencesEngine {
|
class FieldReferencesEngine(private val mDatabase: DatabaseKDBX) {
|
||||||
|
|
||||||
inner class TargetResult(var entry: EntryKDBX?, var wanted: Char)
|
// Key : <WantedField>@<SearchIn>:<Text>
|
||||||
|
// Value : content
|
||||||
|
private var refsCache = ConcurrentHashMap<String, String?>()
|
||||||
|
|
||||||
private inner class SprContextV4 {
|
fun clear() {
|
||||||
|
refsCache.clear()
|
||||||
var databaseV4: DatabaseKDBX? = null
|
|
||||||
var entry: EntryKDBX
|
|
||||||
var refsCache: MutableMap<String, String> = HashMap()
|
|
||||||
|
|
||||||
internal constructor(db: DatabaseKDBX, entry: EntryKDBX) {
|
|
||||||
this.databaseV4 = db
|
|
||||||
this.entry = entry
|
|
||||||
}
|
|
||||||
|
|
||||||
internal constructor(source: SprContextV4) {
|
|
||||||
this.databaseV4 = source.databaseV4
|
|
||||||
this.entry = source.entry
|
|
||||||
this.refsCache = source.refsCache
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun compile(text: String, entry: EntryKDBX, database: DatabaseKDBX): String {
|
fun compile(textReference: String, recursionLevel: Int): String {
|
||||||
return compileInternal(text, SprContextV4(database, entry), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun compileInternal(text: String?, sprContextV4: SprContextV4?, recursionLevel: Int): String {
|
|
||||||
if (text == null) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if (sprContextV4 == null) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return if (recursionLevel >= MAX_RECURSION_DEPTH) {
|
return if (recursionLevel >= MAX_RECURSION_DEPTH) {
|
||||||
""
|
""
|
||||||
} else fillRefPlaceholders(text, sprContextV4, recursionLevel)
|
} else
|
||||||
|
fillReferencesPlaceholders(textReference, recursionLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fillRefPlaceholders(textReference: String, contextV4: SprContextV4, recursionLevel: Int): String {
|
/**
|
||||||
var text = textReference
|
* Manage placeholders with {REF:<WantedField>@<SearchIn>:<Text>}
|
||||||
|
*/
|
||||||
if (contextV4.databaseV4 == null) {
|
private fun fillReferencesPlaceholders(textReference: String, recursionLevel: Int): String {
|
||||||
return text
|
var textValue = textReference
|
||||||
}
|
|
||||||
|
|
||||||
var offset = 0
|
var offset = 0
|
||||||
for (i in 0..19) {
|
var numberInlineRef = 0
|
||||||
text = fillRefsUsingCache(text, contextV4)
|
while (textValue.contains(STR_REF_START)
|
||||||
|
&& numberInlineRef <= MAX_INLINE_REF) {
|
||||||
|
numberInlineRef++
|
||||||
|
|
||||||
val start = text.indexOf(STR_REF_START, offset, true)
|
try {
|
||||||
if (start < 0) {
|
textValue = fillReferencesUsingCache(textValue)
|
||||||
break
|
|
||||||
}
|
|
||||||
val end = text.indexOf(STR_REF_END, start + 1, true)
|
|
||||||
if (end <= start) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
val fullRef = text.substring(start, end + 1)
|
val start = textValue.indexOf(STR_REF_START, offset, true)
|
||||||
val result = findRefTarget(fullRef, contextV4)
|
if (start < 0) {
|
||||||
|
break
|
||||||
if (result != null) {
|
}
|
||||||
val found = result.entry
|
val end = textValue.indexOf(STR_REF_END, offset, true)
|
||||||
found?.stopToManageFieldReferences()
|
if (end <= start) {
|
||||||
val wanted = result.wanted
|
break
|
||||||
|
|
||||||
var data: String? = null
|
|
||||||
when (wanted) {
|
|
||||||
'T' -> data = found?.title
|
|
||||||
'U' -> data = found?.username
|
|
||||||
'A' -> data = found?.url
|
|
||||||
'P' -> data = found?.password
|
|
||||||
'N' -> data = found?.notes
|
|
||||||
'I' -> data = found?.nodeId.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data != null && found != null) {
|
val reference = textValue.substring(start + STR_REF_START.length, end)
|
||||||
val subCtx = SprContextV4(contextV4)
|
val fullReference = "$STR_REF_START$reference$STR_REF_END"
|
||||||
subCtx.entry = found
|
|
||||||
|
|
||||||
val innerContent = compileInternal(data, subCtx, recursionLevel + 1)
|
if (!refsCache.containsKey(fullReference)) {
|
||||||
addRefsToCache(fullRef, innerContent, contextV4)
|
val newRecursionLevel = recursionLevel + 1
|
||||||
text = fillRefsUsingCache(text, contextV4)
|
val result = findReferenceTarget(reference, newRecursionLevel)
|
||||||
} else {
|
val entryFound = result.entry
|
||||||
offset = start + 1
|
val data: String? = when (result.wanted) {
|
||||||
|
'T' -> entryFound?.decodeTitleKey(newRecursionLevel)
|
||||||
|
'U' -> entryFound?.decodeUsernameKey(newRecursionLevel)
|
||||||
|
'A' -> entryFound?.decodeUrlKey(newRecursionLevel)
|
||||||
|
'P' -> entryFound?.decodePasswordKey(newRecursionLevel)
|
||||||
|
'N' -> entryFound?.decodeNotesKey(newRecursionLevel)
|
||||||
|
'I' -> UuidUtil.toHexString(entryFound?.nodeId?.id)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
refsCache[fullReference] = data
|
||||||
|
textValue = fillReferencesUsingCache(textValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
offset = end
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error when fill placeholders by reference", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
return textValue
|
||||||
return text
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findRefTarget(fullReference: String?, contextV4: SprContextV4): TargetResult? {
|
private fun fillReferencesUsingCache(text: String): String {
|
||||||
var fullRef: String? = fullReference ?: return null
|
|
||||||
|
|
||||||
fullRef = fullRef!!.toUpperCase(Locale.ENGLISH)
|
|
||||||
if (!fullRef.startsWith(STR_REF_START) || !fullRef.endsWith(STR_REF_END)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val ref = fullRef.substring(STR_REF_START.length, fullRef.length - STR_REF_END.length)
|
|
||||||
if (ref.length <= 4) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (ref[1] != '@') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (ref[3] != ':') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val scan = Character.toUpperCase(ref[2])
|
|
||||||
val wanted = Character.toUpperCase(ref[0])
|
|
||||||
|
|
||||||
val searchParameters = SearchParameters()
|
|
||||||
searchParameters.setupNone()
|
|
||||||
|
|
||||||
searchParameters.searchString = ref.substring(4)
|
|
||||||
when (scan) {
|
|
||||||
'T' -> searchParameters.searchInTitles = true
|
|
||||||
'U' -> searchParameters.searchInUserNames = true
|
|
||||||
'A' -> searchParameters.searchInUrls = true
|
|
||||||
'P' -> searchParameters.searchInPasswords = true
|
|
||||||
'N' -> searchParameters.searchInNotes = true
|
|
||||||
'I' -> searchParameters.searchInUUIDs = true
|
|
||||||
'O' -> searchParameters.searchInOther = true
|
|
||||||
else -> return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val list = ArrayList<EntryKDBX>()
|
|
||||||
searchEntries(contextV4.databaseV4?.rootGroup, searchParameters, list)
|
|
||||||
|
|
||||||
return if (list.size > 0) {
|
|
||||||
TargetResult(list[0], wanted)
|
|
||||||
} else null
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addRefsToCache(ref: String?, value: String?, ctx: SprContextV4?) {
|
|
||||||
if (ref == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (value == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (ctx == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctx.refsCache.containsKey(ref)) {
|
|
||||||
ctx.refsCache[ref] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fillRefsUsingCache(text: String, sprContextV4: SprContextV4): String {
|
|
||||||
var newText = text
|
var newText = text
|
||||||
for ((key, value) in sprContextV4.refsCache) {
|
refsCache.keys.forEach { key ->
|
||||||
newText = text.replace(key, value, true)
|
// Replace by key if value not found
|
||||||
|
newText = newText.replace(key, refsCache[key] ?: key, true)
|
||||||
}
|
}
|
||||||
return newText
|
return newText
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchEntries(root: GroupKDBX?, searchParameters: SearchParameters?, listStorage: MutableList<EntryKDBX>?) {
|
private fun findReferenceTarget(reference: String, recursionLevel: Int): TargetResult {
|
||||||
if (searchParameters == null) {
|
|
||||||
return
|
val targetResult = TargetResult(null, 'J')
|
||||||
|
|
||||||
|
if (reference.length <= 4) {
|
||||||
|
return targetResult
|
||||||
}
|
}
|
||||||
if (listStorage == null) {
|
if (reference[1] != '@') {
|
||||||
return
|
return targetResult
|
||||||
|
}
|
||||||
|
if (reference[3] != ':') {
|
||||||
|
return targetResult
|
||||||
}
|
}
|
||||||
|
|
||||||
val terms = splitStringTerms(searchParameters.searchString)
|
targetResult.wanted = Character.toUpperCase(reference[0])
|
||||||
if (terms.size <= 1 || searchParameters.regularExpression) {
|
val searchIn = Character.toUpperCase(reference[2])
|
||||||
root!!.doForEachChild(EntryKDBXSearchHandler(searchParameters, listStorage), null)
|
val searchQuery = reference.substring(4)
|
||||||
return
|
targetResult.entry = when (searchIn) {
|
||||||
}
|
'T' -> mDatabase.getEntryByTitle(searchQuery, recursionLevel)
|
||||||
|
'U' -> mDatabase.getEntryByUsername(searchQuery, recursionLevel)
|
||||||
// Search longest term first
|
'A' -> mDatabase.getEntryByURL(searchQuery, recursionLevel)
|
||||||
val stringLengthComparator = Comparator<String> { lhs, rhs -> lhs.length - rhs.length }
|
'P' -> mDatabase.getEntryByPassword(searchQuery, recursionLevel)
|
||||||
Collections.sort(terms, stringLengthComparator)
|
'N' -> mDatabase.getEntryByNotes(searchQuery, recursionLevel)
|
||||||
|
'I' -> {
|
||||||
val fullSearch = searchParameters.searchString
|
UuidUtil.fromHexString(searchQuery)?.let { uuid ->
|
||||||
var childEntries: List<EntryKDBX>? = root!!.getChildEntries()
|
mDatabase.getEntryById(NodeIdUUID(uuid))
|
||||||
for (i in terms.indices) {
|
|
||||||
val pgNew = ArrayList<EntryKDBX>()
|
|
||||||
|
|
||||||
searchParameters.searchString = terms[i]
|
|
||||||
|
|
||||||
var negate = false
|
|
||||||
if (searchParameters.searchString.startsWith("-")) {
|
|
||||||
searchParameters.searchString = searchParameters.searchString.substring(1)
|
|
||||||
negate = searchParameters.searchString.isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!root.doForEachChild(EntryKDBXSearchHandler(searchParameters, pgNew), null)) {
|
|
||||||
childEntries = null
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
childEntries = if (negate) {
|
|
||||||
val complement = ArrayList<EntryKDBX>()
|
|
||||||
for (entry in childEntries!!) {
|
|
||||||
if (!pgNew.contains(entry)) {
|
|
||||||
complement.add(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
complement
|
|
||||||
} else {
|
|
||||||
pgNew
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (childEntries != null) {
|
|
||||||
listStorage.addAll(childEntries)
|
|
||||||
}
|
|
||||||
searchParameters.searchString = fullSearch
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a list of String by split text when ' ', '\t', '\r' or '\n' is found
|
|
||||||
*/
|
|
||||||
private fun splitStringTerms(text: String?): List<String> {
|
|
||||||
val list = ArrayList<String>()
|
|
||||||
if (text == null) {
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
val stringBuilder = StringBuilder()
|
|
||||||
var quoted = false
|
|
||||||
|
|
||||||
for (element in text) {
|
|
||||||
|
|
||||||
if ((element == ' ' || element == '\t' || element == '\r' || element == '\n') && !quoted) {
|
|
||||||
|
|
||||||
val len = stringBuilder.length
|
|
||||||
when {
|
|
||||||
len > 0 -> {
|
|
||||||
list.add(stringBuilder.toString())
|
|
||||||
stringBuilder.delete(0, len)
|
|
||||||
}
|
|
||||||
element == '\"' -> quoted = !quoted
|
|
||||||
else -> stringBuilder.append(element)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
'O' -> mDatabase.getEntryByCustomData(searchQuery)
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
|
return targetResult
|
||||||
if (stringBuilder.isNotEmpty()) {
|
|
||||||
list.add(stringBuilder.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
return list
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class TargetResult(var entry: EntryKDBX?, var wanted: Char)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val MAX_RECURSION_DEPTH = 12
|
private const val MAX_RECURSION_DEPTH = 10
|
||||||
|
private const val MAX_INLINE_REF = 10
|
||||||
private const val STR_REF_START = "{REF:"
|
private const val STR_REF_START = "{REF:"
|
||||||
private const val STR_REF_END = "}"
|
private const val STR_REF_END = "}"
|
||||||
|
|
||||||
|
private val TAG = FieldReferencesEngine::class.java.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,14 +31,12 @@ import java.util.*
|
|||||||
|
|
||||||
class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface {
|
class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface {
|
||||||
|
|
||||||
var level = 0 // short
|
|
||||||
// Used by KeePass internally, don't use
|
// Used by KeePass internally, don't use
|
||||||
var groupFlags = 0
|
var groupFlags = 0
|
||||||
|
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
|
|
||||||
constructor(parcel: Parcel) : super(parcel) {
|
constructor(parcel: Parcel) : super(parcel) {
|
||||||
level = parcel.readInt()
|
|
||||||
groupFlags = parcel.readInt()
|
groupFlags = parcel.readInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,13 +50,11 @@ class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
super.writeToParcel(dest, flags)
|
super.writeToParcel(dest, flags)
|
||||||
dest.writeInt(level)
|
|
||||||
dest.writeInt(groupFlags)
|
dest.writeInt(groupFlags)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateWith(source: GroupKDB) {
|
fun updateWith(source: GroupKDB) {
|
||||||
super.updateWith(source)
|
super.updateWith(source)
|
||||||
level = source.level
|
|
||||||
groupFlags = source.groupFlags
|
groupFlags = source.groupFlags
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,15 +69,12 @@ class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
return NodeIdInt(nodeId.id)
|
return NodeIdInt(nodeId.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun afterAssignNewParent() {
|
|
||||||
if (parent != null)
|
|
||||||
level = parent!!.level + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setGroupId(groupId: Int) {
|
fun setGroupId(groupId: Int) {
|
||||||
this.nodeId = NodeIdInt(groupId)
|
this.nodeId = NodeIdInt(groupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun afterAssignNewParent() {}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
|
|||||||
@@ -20,8 +20,11 @@
|
|||||||
package com.kunzisoft.keepass.database.element.group
|
package com.kunzisoft.keepass.database.element.group
|
||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
|
import android.os.ParcelUuid
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.database.element.CustomData
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.Tags
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
@@ -33,14 +36,17 @@ import java.util.*
|
|||||||
|
|
||||||
class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
|
class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
|
||||||
|
|
||||||
private val customData = HashMap<String, String>()
|
override var usageCount = UnsignedLong(0)
|
||||||
|
override var locationChanged = DateInstant()
|
||||||
|
override var customData = CustomData()
|
||||||
var notes = ""
|
var notes = ""
|
||||||
|
|
||||||
var isExpanded = true
|
var isExpanded = true
|
||||||
var defaultAutoTypeSequence = ""
|
var defaultAutoTypeSequence = ""
|
||||||
var enableAutoType: Boolean? = null
|
var enableAutoType: Boolean? = null
|
||||||
var enableSearching: Boolean? = null
|
var enableSearching: Boolean? = null
|
||||||
var lastTopVisibleEntry: UUID = DatabaseVersioned.UUID_ZERO
|
var lastTopVisibleEntry: UUID = DatabaseVersioned.UUID_ZERO
|
||||||
|
override var tags = Tags()
|
||||||
|
override var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO
|
||||||
|
|
||||||
override var expires: Boolean = false
|
override var expires: Boolean = false
|
||||||
|
|
||||||
@@ -60,7 +66,7 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
constructor(parcel: Parcel) : super(parcel) {
|
constructor(parcel: Parcel) : super(parcel) {
|
||||||
usageCount = UnsignedLong(parcel.readLong())
|
usageCount = UnsignedLong(parcel.readLong())
|
||||||
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
|
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
|
||||||
// TODO customData = ParcelableUtil.readStringParcelableMap(parcel);
|
customData = parcel.readParcelable(CustomData::class.java.classLoader) ?: CustomData()
|
||||||
notes = parcel.readString() ?: notes
|
notes = parcel.readString() ?: notes
|
||||||
isExpanded = parcel.readByte().toInt() != 0
|
isExpanded = parcel.readByte().toInt() != 0
|
||||||
defaultAutoTypeSequence = parcel.readString() ?: defaultAutoTypeSequence
|
defaultAutoTypeSequence = parcel.readString() ?: defaultAutoTypeSequence
|
||||||
@@ -69,6 +75,8 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
val isSearchingEnabled = parcel.readInt()
|
val isSearchingEnabled = parcel.readInt()
|
||||||
enableSearching = if (isSearchingEnabled == -1) null else isSearchingEnabled == 1
|
enableSearching = if (isSearchingEnabled == -1) null else isSearchingEnabled == 1
|
||||||
lastTopVisibleEntry = parcel.readSerializable() as UUID
|
lastTopVisibleEntry = parcel.readSerializable() as UUID
|
||||||
|
tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags
|
||||||
|
previousParentGroup = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun readParentParcelable(parcel: Parcel): GroupKDBX? {
|
override fun readParentParcelable(parcel: Parcel): GroupKDBX? {
|
||||||
@@ -83,13 +91,15 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
super.writeToParcel(dest, flags)
|
super.writeToParcel(dest, flags)
|
||||||
dest.writeLong(usageCount.toKotlinLong())
|
dest.writeLong(usageCount.toKotlinLong())
|
||||||
dest.writeParcelable(locationChanged, flags)
|
dest.writeParcelable(locationChanged, flags)
|
||||||
// TODO ParcelableUtil.writeStringParcelableMap(dest, customData);
|
dest.writeParcelable(customData, flags)
|
||||||
dest.writeString(notes)
|
dest.writeString(notes)
|
||||||
dest.writeByte((if (isExpanded) 1 else 0).toByte())
|
dest.writeByte((if (isExpanded) 1 else 0).toByte())
|
||||||
dest.writeString(defaultAutoTypeSequence)
|
dest.writeString(defaultAutoTypeSequence)
|
||||||
dest.writeInt(if (enableAutoType == null) -1 else if (enableAutoType!!) 1 else 0)
|
dest.writeInt(if (enableAutoType == null) -1 else if (enableAutoType!!) 1 else 0)
|
||||||
dest.writeInt(if (enableSearching == null) -1 else if (enableSearching!!) 1 else 0)
|
dest.writeInt(if (enableSearching == null) -1 else if (enableSearching!!) 1 else 0)
|
||||||
dest.writeSerializable(lastTopVisibleEntry)
|
dest.writeSerializable(lastTopVisibleEntry)
|
||||||
|
dest.writeParcelable(tags, flags)
|
||||||
|
dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateWith(source: GroupKDBX) {
|
fun updateWith(source: GroupKDBX) {
|
||||||
@@ -97,34 +107,21 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
usageCount = source.usageCount
|
usageCount = source.usageCount
|
||||||
locationChanged = DateInstant(source.locationChanged)
|
locationChanged = DateInstant(source.locationChanged)
|
||||||
// Add all custom elements in map
|
// Add all custom elements in map
|
||||||
customData.clear()
|
customData = CustomData(source.customData)
|
||||||
for ((key, value) in source.customData) {
|
|
||||||
customData[key] = value
|
|
||||||
}
|
|
||||||
notes = source.notes
|
notes = source.notes
|
||||||
isExpanded = source.isExpanded
|
isExpanded = source.isExpanded
|
||||||
defaultAutoTypeSequence = source.defaultAutoTypeSequence
|
defaultAutoTypeSequence = source.defaultAutoTypeSequence
|
||||||
enableAutoType = source.enableAutoType
|
enableAutoType = source.enableAutoType
|
||||||
enableSearching = source.enableSearching
|
enableSearching = source.enableSearching
|
||||||
lastTopVisibleEntry = source.lastTopVisibleEntry
|
lastTopVisibleEntry = source.lastTopVisibleEntry
|
||||||
|
tags = source.tags
|
||||||
|
previousParentGroup = source.previousParentGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
override var usageCount = UnsignedLong(0)
|
|
||||||
|
|
||||||
override var locationChanged = DateInstant()
|
|
||||||
|
|
||||||
override fun afterAssignNewParent() {
|
override fun afterAssignNewParent() {
|
||||||
locationChanged = DateInstant()
|
locationChanged = DateInstant()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putCustomData(key: String, value: String) {
|
|
||||||
customData[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun containsCustomData(): Boolean {
|
|
||||||
return customData.isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
|
|||||||
@@ -63,6 +63,17 @@ abstract class GroupVersioned
|
|||||||
get() = titleGroup
|
get() = titleGroup
|
||||||
set(value) { titleGroup = value }
|
set(value) { titleGroup = value }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To determine the level from the root group (root group level is -1)
|
||||||
|
*/
|
||||||
|
fun getLevel(): Int {
|
||||||
|
var level = -1
|
||||||
|
parent?.let { parent ->
|
||||||
|
level = parent.getLevel() + 1
|
||||||
|
}
|
||||||
|
return level
|
||||||
|
}
|
||||||
|
|
||||||
override fun getChildGroups(): List<Group> {
|
override fun getChildGroups(): List<Group> {
|
||||||
return childGroups
|
return childGroups
|
||||||
}
|
}
|
||||||
@@ -87,6 +98,24 @@ abstract class GroupVersioned
|
|||||||
this.childEntries.add(entry)
|
this.childEntries.add(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateChildGroup(group: Group) {
|
||||||
|
val index = this.childGroups.indexOfFirst { it.nodeId == group.nodeId }
|
||||||
|
if (index >= 0) {
|
||||||
|
val oldGroup = this.childGroups.removeAt(index)
|
||||||
|
group.nodeIndexInParentForNaturalOrder = oldGroup.nodeIndexInParentForNaturalOrder
|
||||||
|
this.childGroups.add(index, group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateChildEntry(entry: Entry) {
|
||||||
|
val index = this.childEntries.indexOfFirst { it.nodeId == entry.nodeId }
|
||||||
|
if (index >= 0) {
|
||||||
|
val oldEntry = this.childEntries.removeAt(index)
|
||||||
|
entry.nodeIndexInParentForNaturalOrder = oldEntry.nodeIndexInParentForNaturalOrder
|
||||||
|
this.childEntries.add(index, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun removeChildGroup(group: Group) {
|
override fun removeChildGroup(group: Group) {
|
||||||
this.childGroups.remove(group)
|
this.childGroups.remove(group)
|
||||||
}
|
}
|
||||||
@@ -106,8 +135,4 @@ abstract class GroupVersioned
|
|||||||
else
|
else
|
||||||
nodeIndexInParentForNaturalOrder
|
nodeIndexInParentForNaturalOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return titleGroup
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>,
|
|||||||
|
|
||||||
fun addChildEntry(entry: Entry)
|
fun addChildEntry(entry: Entry)
|
||||||
|
|
||||||
|
fun updateChildGroup(group: Group)
|
||||||
|
|
||||||
|
fun updateChildEntry(entry: Entry)
|
||||||
|
|
||||||
fun removeChildGroup(group: Group)
|
fun removeChildGroup(group: Group)
|
||||||
|
|
||||||
fun removeChildEntry(entry: Entry)
|
fun removeChildEntry(entry: Entry)
|
||||||
@@ -45,23 +49,64 @@ interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>,
|
|||||||
groupHandler.operate(this as Group)
|
groupHandler.operate(this as Group)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun doForEachChild(entryHandler: NodeHandler<Entry>,
|
fun doForEachChild(entryHandler: NodeHandler<Entry>?,
|
||||||
groupHandler: NodeHandler<Group>?,
|
groupHandler: NodeHandler<Group>?,
|
||||||
stopIterationWhenGroupHandlerFails: Boolean = true): Boolean {
|
stopIterationWhenGroupHandlerOperateFalse: Boolean = true): Boolean {
|
||||||
for (entry in this.getChildEntries()) {
|
if (entryHandler != null) {
|
||||||
if (!entryHandler.operate(entry))
|
for (entry in this.getChildEntries()) {
|
||||||
return false
|
if (!entryHandler.operate(entry))
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (group in this.getChildGroups()) {
|
for (group in this.getChildGroups()) {
|
||||||
var doActionForChild = true
|
var doActionForChild = true
|
||||||
if (groupHandler != null && !groupHandler.operate(group)) {
|
if (groupHandler != null && !groupHandler.operate(group)) {
|
||||||
doActionForChild = false
|
doActionForChild = false
|
||||||
if (stopIterationWhenGroupHandlerFails)
|
if (stopIterationWhenGroupHandlerOperateFalse)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (doActionForChild)
|
if (doActionForChild)
|
||||||
group.doForEachChild(entryHandler, groupHandler)
|
group.doForEachChild(entryHandler, groupHandler, stopIterationWhenGroupHandlerOperateFalse)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun searchChildEntry(criteria: (entry: Entry) -> Boolean): Entry? {
|
||||||
|
return searchChildEntry(this, criteria)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchChildEntry(rootGroup: GroupVersionedInterface<Group, Entry>,
|
||||||
|
criteria: (entry: Entry) -> Boolean): Entry? {
|
||||||
|
for (childEntry in rootGroup.getChildEntries()) {
|
||||||
|
if (criteria.invoke(childEntry)) {
|
||||||
|
return childEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (group in rootGroup.getChildGroups()) {
|
||||||
|
val searchChildEntry = searchChildEntry(group, criteria)
|
||||||
|
if (searchChildEntry != null) {
|
||||||
|
return searchChildEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun searchChildGroup(criteria: (group: Group) -> Boolean): Group? {
|
||||||
|
return searchChildGroup(this, criteria)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchChildGroup(rootGroup: GroupVersionedInterface<Group, Entry>,
|
||||||
|
criteria: (group: Group) -> Boolean): Group? {
|
||||||
|
for (childGroup in rootGroup.getChildGroups()) {
|
||||||
|
if (criteria.invoke(childGroup)) {
|
||||||
|
return childGroup
|
||||||
|
} else {
|
||||||
|
val subGroup = searchChildGroup(childGroup, criteria)
|
||||||
|
if (subGroup != null) {
|
||||||
|
return subGroup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.element.icon
|
|||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
|
||||||
class IconImage() : IconImageDraw(), Parcelable {
|
class IconImage() : IconImageDraw() {
|
||||||
|
|
||||||
var standard: IconImageStandard = IconImageStandard()
|
var standard: IconImageStandard = IconImageStandard()
|
||||||
var custom: IconImageCustom = IconImageCustom()
|
var custom: IconImageCustom = IconImageCustom()
|
||||||
|
|||||||
@@ -20,34 +20,46 @@
|
|||||||
package com.kunzisoft.keepass.database.element.icon
|
package com.kunzisoft.keepass.database.element.icon
|
||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
|
import android.os.ParcelUuid
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class IconImageCustom : Parcelable, IconImageDraw {
|
class IconImageCustom : IconImageDraw {
|
||||||
|
|
||||||
var uuid: UUID
|
val uuid: UUID
|
||||||
|
var name: String = ""
|
||||||
|
var lastModificationTime: DateInstant? = null
|
||||||
|
|
||||||
constructor() {
|
constructor(name: String = "", lastModificationTime: DateInstant? = null) {
|
||||||
uuid = DatabaseVersioned.UUID_ZERO
|
this.uuid = DatabaseVersioned.UUID_ZERO
|
||||||
|
this.name = name
|
||||||
|
this.lastModificationTime = lastModificationTime
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(uuid: UUID) {
|
constructor(uuid: UUID, name: String = "", lastModificationTime: DateInstant? = null) {
|
||||||
this.uuid = uuid
|
this.uuid = uuid
|
||||||
|
this.name = name
|
||||||
|
this.lastModificationTime = lastModificationTime
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(parcel: Parcel) {
|
constructor(parcel: Parcel) {
|
||||||
uuid = parcel.readSerializable() as UUID
|
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
|
||||||
|
name = parcel.readString() ?: name
|
||||||
|
lastModificationTime = parcel.readParcelable(DateInstant::class.java.classLoader)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
dest.writeParcelable(ParcelUuid(uuid), flags)
|
||||||
|
dest.writeString(name)
|
||||||
|
dest.writeParcelable(lastModificationTime, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
|
||||||
dest.writeSerializable(uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
val prime = 31
|
val prime = 31
|
||||||
var result = 1
|
var result = 1
|
||||||
|
|||||||
@@ -19,7 +19,9 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element.icon
|
package com.kunzisoft.keepass.database.element.icon
|
||||||
|
|
||||||
abstract class IconImageDraw {
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
abstract class IconImageDraw : Parcelable {
|
||||||
|
|
||||||
var selected = false
|
var selected = false
|
||||||
/**
|
/**
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user